train_rl 在一个进程内采样 64 个回合,等全部结束后更新,再开始下一批。采样和训练不重叠,采样器与训练器共用模型;二者的对数概率误差不超过 (第 8.6 节)。扩大到真实编程任务后,这些条件往往不再成立:回合耗时差异很大,推理和训练各自使用不同的设备、并行方式与 kernel。本章讨论怎样提高资源利用率,同时记录清楚每批数据由哪个行为策略生成。
本章用四个实验逐步检查这些问题。一个 rollout 流水线的离散事件模拟器,测量同步、提前一步和完全异步(asynchronous)三种设计在吞吐量(throughput)上换来了什么、在陈旧度(staleness)上付出了什么。一个用落后 次更新的策略(policy)采样的训练循环,测量陈旧度在有无重要性校正时对 TinyAgent 的影响。一次在真实 Qwen3-0.6B 上的测量,表明即使权重完全相同,推理端和训练端的前向计算给出的对数概率(log-probability)也不一致。最后,针对这种不一致的两种校正,即截断重要性采样(importance sampling)和掩码重要性采样,我们会推导、模拟,并在一个故意制造不一致的采样器上用 TinyAgent 进行测试。
13.1 RL 系统的组成部分
面向智能体的生产级 RL 系统有四类组件。各框架对它们的命名不同,但职责相同。- 推理引擎(vLLM、SGLang)负责生成 token。它管理 KV cache,用连续批处理(continuous batching)同时批量处理数千条并发序列,返回 token id,以及每个被采样 token 在其采样分布下的对数概率。它的 kernel 针对解码吞吐量调优:分页注意力(paged attention)、融合采样,权重常用 bf16 或 FP8。*故障模式:*长回合中 KV cache 增长导致显存溢出,内存压力下序列被抢占并重新计算,以及对数概率与训练器不一致(第 13.6 节)。
- 训练器(FSDP 或 Megatron,嵌在 verl、slime、OpenRLHF 或 AReaL 等框架中)重新计算对数概率、计算优势(advantage)并执行梯度步。它把参数、梯度和优化器状态切分到多个加速器上。*故障模式:*长序列导致内存暴涨(第 14 章),跨微批次(microbatch)的梯度归一化 bug,以及当数据来自另一个策略而非它所假定的策略时出现的无声发散。
- 环境 worker 运行工具:每个回合一个容器,其中检出了代码仓库,有一个 shell 和一个测试运行器。它们受 CPU 限制、速度慢且不可靠。*故障模式:*容器启动延迟、测试进程挂起、不稳定(flaky)测试、worker 在回合中途崩溃,以及数千个回合同时运行时的资源耗尽。
- 权重同步(weight sync)在每次更新后把新参数从训练器搬到引擎:可以在两个进程组之间做 NCCL 广播,可以在两者共用加速器时(“共置”,colocated)原地重切分,也可以写一个检查点让引擎重新加载。*故障模式:*同步要花几秒到几分钟,期间生成停滞;不完整的同步让引擎混用新旧层;如果版本计数器没有附在每个样本上,陈旧度就无法测量。
rollout_batch 是引擎,BugFixEnv 是环境 worker,rl_update 是训练器,而权重同步没有成本,因为采样器和训练器是同一个 nn.Module。真实的同步搬运的是参数。下面的代码块为 TinyAgent 统计参数量,并在标明的带宽假设下,对真实模型规模做估算。
13.2 时间花在哪里:长尾回合
第二个输入是回合时长。智能体回合在生成和工具调用之间交替。每一轮生成几百个 token,单条序列的解码速率是每秒几十个 token(引擎的总吞吐量很高,但每条序列每个解码步只前进一个 token),然后等待工具:读文件在毫秒级返回,跑测试套件需要几秒到几分钟。轮数随任务和尝试而变化。agent_episode_durations 按这种结构采样时长:几何分布的轮数,每轮均匀分布的 token 数,以及对数正态分布的工具延迟。默认参数是为了模仿编程智能体而选的假设,不是对编程智能体的测量。
13.3 同步、提前一步与异步流水线
三种设计以不同方式应对拖尾问题。- 同步(on-policy)。用版本 采样一个批次,在其上训练,同步到 ,重复。每个回合都由生成它的版本来训练,因此梯度正是第 6 章的 on-policy 估计量(estimator)。步长时间为 ,生成与训练从不重叠。
- 提前一步(一步 off-policy,one-step off-policy)。训练器在批次 上训练时,引擎已经在用版本 采样批次 。训练与生成重叠,每个回合恰好陈旧一个版本。verl 的文档描述了这种模式(第 20 章)。
- **完全异步。**生成槽位从不等待:每当一个回合结束,该槽位就用最新权重启动另一个回合。训练器只要空闲,就取最早完成的 个回合。陈旧度上界 对生成进行节流,使其领先训练不超过 个批次;第 20 章引用的 AReaL 条件是,只有当已启动的回合数小于 时才允许启动新回合。当 时,这一设计退化为同步设计。
n_slots 个生成槽位(引擎中的并发回合),一个训练器,每步消耗 batch_size 个回合、耗时 train_time 秒,以及一次耗时 sync_time 秒、会暂停所有生成的同步。这是一个离散事件模拟:时间从一个事件(某个回合结束、某个训练步结束、某次同步结束)跳到下一个事件。回合时长从同一个张量中按顺序取用,因此每种设计看到的是相同的回合。它记录所模拟的每个时间区间,以便绘制时间线,并报告吞吐量、槽位利用率,以及每个被训练回合的陈旧度:训练器在该步开始时的版本减去启动该回合的版本。
batch_size 个回合已完成,它就启动一个训练步,并记录每个回合的陈旧度。它把时间推进到最早的待处理事件。然后处理该事件:同步结束则发布下一个版本,训练步结束则请求一次同步,回合结束则加入缓冲区。在部分 rollout 模式下,请求的同步立即开始,并把每个在途回合的结束时间推后 sync_time;在排空模式下,同步只在没有在途回合时才开始。槽位利用率把被中断回合的暂停时间算作忙碌时间,这会略微高估。
我们在相同配置下运行五种设计:64 个槽位,每步 64 个回合,训练 60 秒,同步 10 秒,共 20 步。
**证据。**本节的所有内容都是在明确假设下的模拟:第 13.2 节的时长模型、固定的训练时间,以及一次会暂停所有生成的同步。它展示的是机制(等待拖尾回合、重叠、排空的代价、基于计数的节流无法限制的陈旧度)。真实系统报告的加速比(AReaL 为 2.77x,ROLL Flash 在智能体任务上为 2.72x;见第 20 章)来自不同的工作负载,这里没有复现。
13.4 针对行为策略的重要性校正
异步改变了估计量。令 为行为策略(behavior policy),即实际生成这些 token 的分布,可能来自:落后 次更新的权重版本、数值行为不同的推理引擎,或一个被中断回合内多个版本的混合。训练器持有 。第 6 章的策略梯度(policy gradient)是 下的期望,而数据来自 。采用 第 8.1 节的逐 token 近似,对每个 token 有 忽略这个比率(rl_update 用训练器重新计算 old_logp 时正是这么做的),估计的其实是 。这是我们想要的梯度的有偏估计,偏差(bias)随 与 之间的距离增长。对固定前缀,引入该比率可校正动作分布;要完全校正轨迹分布仍需第 8 章的比率乘积。此外,大权重又带回方差问题:比率没有上界,当 在 偏好的 token 上概率质量很小时,其方差(variance)会爆炸。
PPO 的裁剪(clipping)解决不了这个问题。裁剪限制的是 ,其中 是训练器在本次更新开始时持有的策略副本。当 时存在两个比率,AReaL 的解耦(decoupled)目标(第 20 章)把它们分开:行为权重 校正数据由哪个分布采样,PPO 的裁剪比率 限制本次更新走多远。把 视为常数,
由于 ,,因此解耦损失就是第 8 章的 ppo_policy_loss,只是逐 token 优势换成了 。下面的代码正是这样实现的。实践中常用两种方式限制 的影响:
- 截断重要性采样(truncated importance sampling,TIS):。TIS 博文(Yao et al., 2025;第 20 章)针对采样器–训练器不一致提出了它,verl 的 rollout 校正预设使用 。截断把每个加权项的二阶矩限制在未加权项的 倍以内,但留下了偏差: 的 token 权重偏低,这意味着采样器相对训练器采样不足的 token 仍然代表性不足。
- 掩码重要性采样(masked importance sampling,MIS),即 Ring-1T 报告中的 IcePop:在区间 内 ,区间外为 ,报告中取 、。两个概率的差异超出该区间的 token 被视为噪声,从梯度中移除,而不是以封顶的权重保留。
13.5 TinyAgent 上的陈旧策略
模拟器告诉我们数据会变得多陈旧,却无法告诉我们陈旧度对学习有什么影响。为此,我们用一个落后训练器固定次数更新的行为策略来训练 TinyAgent。offpolicy_update 就是第 9 章的 rl_update,只改了一处:批次的 sample_logp,即行为策略在采样时记录的对数概率,通过解耦权重进入计算。当 correction='none' 时,权重就是动作掩码,更新与 rl_update 完全相同:训练器重新计算 old_logp,从不看 。它还报告 与 之间对数比的平均绝对值(mismatch),以及比率超过上限的策略 token 所占比例(over_cap)。
train_offpolicy 保留最近 lag + 1 个权重版本,每一步都用最老的版本采样,因此在前 lag 步之后,每个批次都恰好陈旧 lag 次更新:lag=1 时对应提前一步流水线,一般情况对应陈旧度固定的异步流水线。可选的 sampler 函数在采样前变换行为模型;第 13.6 节用它构造一个数值上不同的采样器。
lag=0 且不校正时,它必须与 train_rl 逐步一致,因为两者以相同顺序消耗相同的随机数流。在相信任何比较之前先检查这一点,只需五步。
**证据。**本书测得,每种配置一个随机种子,在一个 157k 参数的策略上训练 40 步。其中的排序(不校正时随 lag 退化;TIS 和 MIS 挽回了其中很大一部分)就是本节讨论的机制。TinyAgent 在哪个 lag 崩溃,具体取决于它的学习率和策略变化速度;以 学习率训练的大模型每次更新的移动要小得多,这就是公开的异步系统能容忍 4 到 8 的陈旧度上界的原因(AReaL 报告的默认值,第 20 章)。
13.6 训推不一致
陈旧度是我们主动选择的不一致。第二种不一致则不请自来。在生产系统中,行为策略就是推理引擎,即使权重相同,它的概率也和训练器的不同。引擎在一个解码步中计算每个被采样 token 的分布:每条序列一个新 token,基于 KV cache 做注意力,批次的组成随着序列的开始和结束而逐步变化,kernel 也针对该形状选择。训练器在对整条序列做的一次前向传播中计算相同的分布,批次不同、填充(padding)不同,往往并行方式和 kernel 也不同。浮点加法不满足结合律,所以不同的归约顺序给出不同的结果,而 bf16 的 8 位尾数让这些差异变得可见。第 3.6 节在一条序列上测量了精度这一部分;这里我们在真实的 Qwen3-0.6B 上测量整条路径,模型从检查主机的缓存加载,有 GPU 时在 GPU 上运行。engine_sample 模仿推理引擎,但并不是推理引擎:左填充的批次、KV cache、每个 token 一个解码步、温度为 1 的采样,被采样 token 的对数概率取自该步的 logits(像 selected_log_probs 那样在 fp32 中归一化)。vLLM 和 SGLang 还加入了分页注意力、连续批处理和融合采样 kernel,这会进一步改变数值;这里是同一计算路径的最小版本。
batch_size 决定多少条序列共享一次前向传播。
TinyAgent 上的不一致采样器
这种量级的不一致会损害训练吗?校正有帮助吗?TinyAgent 的采样器和训练器在同一个进程中,所以我们故意让采样器不同。FP8 rollout 是 TIS 博文的动机之一:引擎为了速度使用低精度权重,而训练器保持全精度。fp8_copy 把副本的每个权重四舍五入到 float8_e4m3fn 再转回来,即 3 位尾数。
13.7 实践中的设计问题
问题:拖尾回合。 现象: 推理集群的加速器利用率随批次增大而下降;步长时间跟随最慢的回合;每次任务组合变化,回合时长直方图的尾部都会移动。 原因: 同步步骤要等批次上的 ,它的增长类似时长分布的高分位数(第 13.2 节)。 可选方案: (a) 使用部分 rollout 的异步生成,消除等待,但增加陈旧度。(b) 按实际耗时或轮数设置超时以截断尾部,这会改变任务:被截断的回合需要一个处理策略(第 12 章),而把它们从损失中掩掉(DeepSWE 的紧凑过滤)会让训练偏向能快速完成的任务。(c) 过采样:启动多于 个回合,用最先完成的 个训练,这是同一种长度偏差的另一种形式。 建议做法: 本书推荐 (a),同时记录被训练回合与已启动回合的时长,并设置一个宽松的超时作为安全网。下面的代码块测量在第 95 百分位处设置超时能为同步流水线带来多少收益。
问题:选择陈旧度上限。 现象: 异步运行每小时更快,但按样本数计比同步基线更差;训练器的old_logp与采样器对数概率之间的mismatch随吞吐量上升;最坏情况下策略崩溃。 原因: 梯度是基于 估计的。第 13.5 节在 TinyAgent 上测得了剂量–反应关系:陈旧度 2 无害,4 在不校正时损失了约三分之一的留出通过率,8 摧毁了技能。 可选方案: (a) 紧的上界(1 或 2),这会限制吞吐量(图 13.2 显示大部分收益此时已经获得)。(b) 更宽松的上界,配合解耦的 TIS 或 MIS。(c) 在消耗时也执行上界,丢弃陈旧度超过 的回合,因为基于计数的节流只限制平均陈旧度,不限制最大值。 建议做法: 本书推荐 ,从一开始就使用 TIS,并记录每个被训练批次的陈旧度直方图,而不只是均值。策略变化速度和上界同样重要:陈旧度以更新次数计量,而更大的学习率让每次更新迈出更大的一步。
问题:权重同步的开销。 现象: 步长时间有一个固定下限,不随推理容量增加而缩小;引擎的吞吐量曲线出现周期性缺口。 原因: 每次更新都必须到达每个引擎副本,同步期间生成暂停(第 13.1 节估算 32B 模型每次同步需要几秒到一分钟)。 可选方案: (a) 引擎与训练器共置并原地重切分,避免了网络传输,但让生成和训练串行化。(b) 从训练器通过 NCCL 广播到分离部署的引擎。(c) 降低同步频率(多次更新同步一次),这会提高陈旧度。(d) 让广播与生成重叠并原子地切换权重,代价是需要两份权重的内存。 建议做法: (b),并把同步时间作为独立阶段记录。下面的代码块展示同步设计和异步设计对同步开销的反应。
问题:环境 worker 故障与重试。 现象: 百分之几的回合以 worker 错误结束(容器崩溃、测试进程挂起、内存溢出);幸存回合的奖励看起来比应有的更好,或者被训练的批次向短任务漂移。 原因: 故障与回合并不独立。如果 worker 以每秒恒定的风险率发生故障,长回合更可能失败,因此丢弃失败回合会优先移除长回合,而静默重试会让它们的成本翻倍。 可选方案: (a) 丢弃失败回合并接受偏差。(b) 在新的 worker 上重试,从头开始或从检查点状态开始,并把重试成本计算在内。(c) 把该回合标记为环境故障,不给梯度,并把它的任务保留到下一个批次,以保持任务组合不变。 建议做法: (c),把基础设施故障与智能体失败分开记录,绝不记为奖励 0:给崩溃的容器奖励 0,会教策略去回避任何导致容器崩溃的行为。下面的代码块在每 2,000 秒一次故障的风险率下,测量丢弃故障回合造成的长度偏差。
问题:非确定性。
现象: 同一随机种子在重跑时给出不同的 rollout;在第 0 步,比率 就不严格等于 1;bug 无法复现。
原因: 第 13.6 节测得:在 bf16 下,同一个训练器使用相同权重,仅仅因为批次中有哪些其他序列,给出的对数概率每个 token 就相差约 0.03 nats,而在 fp32 下两者在这块 GPU 上完全一致。推理引擎在每个解码步都改变批次组成。
可选方案: (a) 接受它,在训练器中重新计算 old_logp,并用 TIS 或 MIS 校正剩余的采样器不一致。(b) 批不变 kernel,让结果与批次组成无关,代价是吞吐量;Thinking Machines 的博文报告了逐位一致的采样器和训练器,KL 恰好为 0(第 20 章)。(c) 对最重要的部分使用更高精度(如 MiniMax-M1 所报告的,LM head 和 log-softmax 用 fp32)。
建议做法: (a) 加 (c),并把第 0 步的不一致分布( 的均值、p99、最大值)作为运行指标进行监控:它突然上升意味着 kernel、精度或版本 bug,而不是学习。
小结
- 生产级 RL 系统包含推理引擎、训练器、环境 worker 和权重同步。每个组件都有自己的故障模式,而且必须记录生成每个样本的策略版本,否则无法测量陈旧度。
- 智能体回合时长呈长尾分布,因此同步步骤要等最慢的回合:在模拟器中,每步 64 个回合时,这一等待是平均时长的 4.3 倍,槽位只有 19% 的时间处于忙碌状态。使用部分 rollout 的异步生成把模拟吞吐量提高了 4.1 到 4.6 倍;每次同步前排空则几乎抵消了全部收益。基于计数的陈旧度上界限制了平均陈旧度,但没有限制最大值。
- off-policy 数据需要解耦视角:用行为权重 处理采样分布,用 PPO 的裁剪处理更新。在单步模拟中,TIS 和 MIS 在不增加方差的情况下消除了小的不一致带来的偏差,但修复不了大的不一致;完整 IS 修复了偏差,代价是方差爆炸。
- 在 TinyAgent 上,用落后 8 次更新的策略采样,使留出通过率崩溃到 0.00;TIS 恢复到 0.55,MIS 恢复到 0.64(各一个随机种子)。陈旧度 2 没有造成损害。
- 在权重相同的 Qwen3-0.6B 上,推理端与训练端 bf16 对数概率平均每个 token 相差 0.03 nats(第 99 百分位数 0.13),集中在低概率 token 上;两次 bf16 训练器前向传播仅因批次组成不同就相差同样多;fp32 前向传播的一致性在 以内。token 级比率保持在 内。
练习
- 对均值为 的指数分布时长,推导同步步骤的期望时间 ,并证明它像 那样增长。用指数分布时长运行
simulate_pipeline,检验这一预测。 - 扩展
simulate_pipeline,让训练器在消耗时丢弃陈旧度超过max_staleness的回合。画出吞吐量和被丢弃回合比例随 的变化。上界取多大时,丢弃不再损失吞吐量? - 改变一个假设:用
RLConfig(lr=3e-4)重跑第 13.5 节的陈旧度实验。lag 8 还会崩溃吗?用mismatch列解释结果。 - 实现序列级 TIS:每个回合一个权重 ,广播到该回合的所有 token。在 lag 8 下将它与 token 级 TIS 比较。哪一种有更多权重处于上限,为什么?
- 测量 LM head 和 log-softmax 使用 fp32、模型其余部分使用 bf16(两侧都如此)时 Qwen3-0.6B 的不一致。它消除了多少不一致?把你的结果与第 20 章引用的 MiniMax-M1 报告联系起来。
- 在
engine_sample中,把同样的 30 个提示分别按批大小 1、10 和 30 解码。引擎自身的批次组成不一致有多大?这对比较同一引擎两次运行的对数概率意味着什么? - 设计评审:你的异步智能体运行使用 ,每小时训练速度是同步基线的 3 倍,但在相同样本数下留出通过率更低。列出你会要求的测量(陈旧度直方图、不一致分布、被训练回合与已启动回合的长度、处于上限的比率所占比例),以及每项测量为哪个决策提供依据。