在大模型后训练进入 Agent 阶段之后,资源分配从“怎么把模型训练完”变成了“怎么让多个训练任务在一个集群里都按时跑完”。Agentic RL 后训练和普通 SFT 最大的区别是 workload 不稳定:策略模型要反复调用工具、查询知识库、和环境交互,轨迹长度从几百 token 到几万 token 都可能出现。同一个任务在 rollout 阶段可能只需要少量 GPU,在 trainer 阶段又突然需要整卡。如果按传统方式给每个任务固定分 8 卡、固定显存额度,集群会出现排队区堆满任务,运行区却大量 GPU 空转的情况。香港中文大学和恒生大学提出的 Libra,正是把焦点放在后训练资源分配上,公开描述中吞吐最高提升 3 倍。下文不会逐条复述论文公式,而是把“为什么要动态分配、资源分配器怎么设计、怎么用一个最小模拟器验证效果”讲清楚。
1. 先理解 Agentic RL 后训练为什么这么不稳定
1.1 后训练不再只是把文本样本喂进去
传统 SFT 的后训练过程是:准备一批指令和回答,计算交叉熵损失,更新模型参数。每个 batch 的输入输出长度基本可控,显存占用、计算时间、数据读取速度都相对平稳,资源分配可以按“一个 job 固定占几卡”来规划。
Agentic RL 后训练不同。模型不再是简单生成一段文本,而是在一个回合制环境里反复决策:它要决定调用哪个工具,读取工具返回结果,再决定下一步动作。每一步都会产生新的文本和状态,整条轨迹的长度由环境反馈决定。比如一个 agent 在调用搜索接口时返回 8000 token,在下一轮又只返回 50 token,同一个 job 内的显存需求和推理延迟会出现很大毛刺。
训练目标也不再是直接对文本求损失,而是把采样到的轨迹放入策略梯度目标里更新。为了算 advantage,还需要 critic 或 reward model 对每个状态打分;如果使用 PPO 这类方法,还要维护旧策略、做 clip、做 GAE。这些环节都会额外消耗显存、CPU 和内存,而且消耗量跟着轨迹内容变化。
1.2 四个环节的资源画像会按分钟级变化
在一个典型的 Agentic RL 后训练循环里,至少有四个环节在抢资源,而它们各自的资源敏感度差异很大。
| 环节 | 主要资源 | 波动来源 | 最容易出现的问题 |
|---|---|---|---|
| 轨迹采样 rollout | GPU/CPU 推理、KV cache、网络 | 工具调用次数、环境响应长度、并发度 | GPU 空转或推理排队 |
| 奖励/验证模型 | GPU/CPU 推理 | 外部服务延迟、评估 prompt 长度 | 单个慢请求拖慢整批结果 |
| 策略更新 | GPU 计算、显存带宽 | batch 内轨迹长度方差、是否用梯度检查点 | 显存超限或训练吞吐下降 |
| 经验缓存 replay buffer | CPU 内存、磁盘 | 长轨迹积压、日志量 | 内存被打满、日志写入慢 |
因此,只看 GPU 利用率是不够的。一个 job 可能 GPU 利用率很低,但 CPU 上已经有几千条 rollout 在排队;另一个 job 可能 GPU 计算很忙,但显存因为某一条超长轨迹突然被打爆。资源调度必须能同时感知这些维度,否则任何单一指标都会误导决策。
1.3 多任务并发时,静态配额会放大不确定性
单任务训练就算波动大,只要资源够,也能靠排队慢慢跑完。真正让问题恶化的是多个训练 job 共享同一个集群。
假设集群有 8 张卡,Job A 正在做 Agentic RL 后训练,当前处于 rollout 阶段,8 张卡里实际只有 1 张卡在做推理,其余 7 张处于空闲或低负载。Job B 在后面对列里等着申请 8 张卡。静态调度下,Job B 必须等到 Job A 整体结束才能获得资源,即使 Job A 在未来五分钟内都用不到那 7 张卡,它们也不能转给 Job B。结果就是:集群总 GPU 利用率不高,任务排队时间却很长。
Libra 这类资源分配方案要解决的,就是这种“固定配额和实际需求错配”的问题。它把后训练 job 当作一个动态负载,而不是一个提交后就不会变化的黑盒。
2. Libra 的资源分配核心:把任务当成动态可调度负载
2.1 静态预留为什么在 Agentic RL 上失灵
静态预留最典型的做法是:每个 job 申请固定数量的 GPU、固定显存、固定 CPU,调度器只在开始时刻做一次决策,之后不再调整。
这种模型适合计算量均匀的批处理任务。它的优点是简单、可预期、不容易互相影响。但 Agentic RL 后训练有两个特征会破坏这个假设:
- 需求随时间变化:rollout 和 training 交替出现,工具调用和长文本又会引入尖峰。
- 需求随内容变化:同样一个模型,在不同环境、不同用户 prompt 下产生的轨迹长度差异很大。
于是固定 8 卡的 job 可能在 60% 时间内只用到 2 卡,却把另外 6 卡锁死;其他 job 想用却申请不到。这属于典型的资源碎片化,也会让队列里的任务越积越多。
2.2 动态调度器只需要四个核心组件
Libra 这类方法的具体实现可以各有不同,但核心思路通常是四件事:观测、预测、规划、反馈。
- 观测:实时采集每个 job 的 GPU 使用率、显存水位、CPU 排队长度、rollout 平均步数、tool call 次数。
- 预测:根据当前轨迹和历史窗口,预测下一个时间段内这个 job 对资源的需求。
- 规划:在总资源有限、每个 job 有最小配额和最大配额约束下,把空闲资源动态分配给需求较高的 job。
- 反馈:执行新分配方案后,继续观测指标,如果发现任务被频繁抢占或队列延迟变大,就回调参数。
可以把这四个组件理解成一个闭环控制。调度器不是每秒钟都去抢资源,而是每隔一个固定周期做一次再平衡。这个周期不能太短,否则调度本身会成为瓶颈;也不能太长,否则无法跟上工具调用带来的负载尖峰。
2.3 GPU 卡之外,还要分配显存、CPU、内存和网络
很多集群调度器只认“卡数”,这在实际 Agentic RL 后训练里远远不够。
| 资源维度 | 单位 | 典型瓶颈 | 不纳入调度的后果 |
|---|---|---|---|
| GPU 算力 | TFLOPS/SM 占用 | 策略更新和 rollout 推理 | 算力不足,训练步数变慢 |
| GPU 显存 | GB | KV cache、模型权重、梯度 | 显存超限,job OOM |
| CPU 核数 | vCPU | rollout 数据预处理、tokenizer | CPU 排队,GPU 空转 |
| 主机内存 | GB | replay buffer、采样结果暂存 | 内存不足,进程被杀 |
| 网络带宽 | GB/s | 模型并行通信、工具调用 | 通信开销拖慢训练 |
实际调度中,最好把每个 job 抽象成一组多维资源需求,例如gpu: 8, gpu_mem: 80Gi, cpu: 32, mem: 128Gi,而不是只写gpu: 8。否则会出现显存明明够,但卡数被人占满,导致小显存任务无法调度;或者 CPU 已经排队到几千条,但 GPU 还空在那里等数据。
2.4 如何理解“吞吐最高提升 3 倍”
“吞吐提升 3 倍”在标题里是一个很引人注意的数字,但要正确理解它的适用范围。它通常不是指所有任务、所有集群配置下都能提升 3 倍,而是指在资源竞争明显、job 之间需求互补、静态分配造成大量空闲的场景下,动态分配让集群整体完成工作量更快。
吞吐指标本身也分很多种:
- 单位时间完成的训练 step 数。
- 单位时间完成的 rollout 轨迹数。
- 单位时间完成的 job 总数。
- 单位时间完成的有效训练量,例如处理了多少 token。
不同指标下的提升幅度会不一样。因此在对比方案时,先明确“吞吐”到底指什么,再看这个指标是否和业务目标一致。Libra 的价值不在于把单卡算力提升,而在于把集群空闲资源利用起来,减少排队和等待。
3. 用最小模拟器验证“固定分卡”和“动态分配”的差距
3.1 最小可行模型:需求曲线加工作量
为了理解资源分配策略,不一定要先搭大规模集群。可以用一个 Python 模拟器建模:每个 job 有一条需求曲线,表示每个时间片内它能用掉多少资源;还有一个总工作量,表示完成它需要消耗多少资源单位。调度器在每时刻决定给每个 job 分配多少资源,实际推进量是分配量和需求量的较小值。
下面的模型只用于说明思路,不包含显存、带宽、抢占成本。要放到真实集群,需要再叠加这些约束。
from dataclasses import dataclass @dataclass class Job: name: str demand_curve: list[float] total_work: float JOBS = [ Job("tool-call-heavy", [0.2, 0.1, 0.9, 0.9, 0.9, 0.1], 2.6), Job("long-context", [0.8, 0.8, 0.5, 0.5], 2.0), Job("rag-light", [0.5, 0.5, 0.3, 0.3, 0.3], 1.7), Job("interleaved", [0.4, 0.9, 0.1, 0.9, 0.4], 2.4), ]这里的demand_curve是每个时间片的最大可用资源,取值在 0 到 1 之间。total_work是完成该 job 需要的总资源单位。如果 job 没完成,曲线会循环复用,用来模拟周期性出现的 rollout 高峰。
3.2 静态平分调度
静态调度的规则最简单:不管需求是多少,始终给每个 job 平均分配资源。
def static_equal(demands): n = len(demands) return [1.0 / n] * n这种策略的问题是:当一个 job 的需求低于平均份额时,多出来的份额不会被其他 job 使用;当一个 job 的需求远高于平均份额时,它又会因为拿不到更多资源而迟迟无法完成。在真实集群里,这就对应着 GPU 空转和任务排队并存。
3.3 按需求动态分配调度
动态调度可以先保证