用直觉理解 RL 系统
用直觉读懂 LLM RL:GRPO、PPO Clipping、DAPO/Dr.GRPO/GSPO、Rollout、Async RL 与训推数值一致性。
理解整个 RL 系统
我过去主要做大模型 Pretraining Infrastructure。在过去学习 LLM Reinforcement Learning 时,我花了大量时间研究 PPO、GRPO 的 Loss 公式:Advantage 为什么要归一化?Importance Ratio 的分子和分母是什么?为什么需要 min 和 clip?这些数学符号看起来都能逐个解释,但我始终没有真正建立起对 RL 训练过程的完整认识。
事实上,更有效的思路 turns out 恰好相反:先理解 模型究竟生成了什么数据、谁在计算概率、什么时候更新了权重、更新后的模型又如何改变下一轮数据,再从这些具体问题推出 loss。我发现遵循这样的学习路径,能够以最 intuitive 的方式理解 RL:
- Pretraining Cross-Entropy 与 RL 的 objective 有什么区别?
- REINFORCE、Baseline、Advantage。
- Importance Sampling、PPO Clipping(到底在 clip 什么?) 和 GRPO。
- 理解 DAPO、Dr.GRPO、GSPO 修改了什么。
- 什么导致了训推不一致?区分 Policy Staleness 与 Numerical Mismatch。
- 通过 Qwen3.5-0.8B 在 GSM8K 上的两次 Slime Run,学习真正的 RL 指标。
贯穿全文的关键思想是:RL 把模型本身变成了训练数据的生成器。
Pretraining 与 RL 的 objective
Pretraining:样本已经给定
Pretraining 的目标通常写成:
读这条公式时,不妨从最里面向外看: 代表模型中所有需要学习的参数, 表示第 个目标 token, 表示它前面的目标 token,也就是模型在预测当前位置时已经知道的前缀。这个公式里最关键的一项是 :它表示在给定上下文 和前缀 时,参数为 的模型给正确的下一个 token 分配的概率。 把这一序列各位置的 logprob 加起来, 再对训练样本求期望(实际训练用 batch 平均近似)。由于概率越大,logprob 越大,最前面的负号使“提高目标 token 的概率”对应于“降低 Loss”。
是要最小化的 Pretraining Loss。 是训练数据的分布; 表示从中取一条样本,其中 是已给定的上下文, 是要预测的目标 token 序列, 是目标序列长度。
你可能会疑惑:Pretraining 的 Objective 通常被称之为交叉熵。然而,交叉熵衡量的是两个分布之间的距离,为什么这个公式看起来只有一个条件概率?事实上,这里依然存在两个分布——模型预测的概率分布,以及真实标签对应的 One-hot 分布(即 概率为 ,其余全为 )。由于概率为 的项在求和中全部消去,公式最终就退化成了只包含 条件概率的形式。此事在 3B1B 讲解交叉熵的视频中亦有记载。
目标 token 来自相对固定的数据集 。模型更新会改变它对这些 token 的概率,却不会立即改变下一个 batch 里的真实 token 是什么。总而言之,Pretraining 的 objective 决定了我们让模型去拟合一个已有数据的分布。
RL:模型生成训练样本
对数学 RLVR(Reinforcement Learning with Verifiable Rewards),我们给模型一道题 ,由模型自己生成回答 ,再通过答案检查器、单元测试等给它一个奖励 :
这里的 不是需要最小化的 Loss,而是要最大化的 RL Objective。 表示题目(prompt)的采样分布, 表示先抽一道题; 表示模型给定 这道题时所有可能回答的概率分布,其中的“”可以理解成一个通配符; 表示再按这个分布实际采样一条回答。 是 verifier 对“题目 + 回答 ”给出的 reward, 表示对抽到的题目和回答求期望。换句话说:希望模型自己采样出来的回答,平均能拿到更高的奖励。
这一目标是在 最大化期望 reward。关键变化是 不再来自静态数据,而来自当前策略 。所以每次更新后,下一轮 rollout 的分布也会变化:
注意:这里的 rollout 引擎你可以看到是 vLLM / SGLang,因为我们需要采样 ,也即,调用推理引擎,让模型真正去解题。另外,你可以发现这是一条反馈回路。某个错误倾向、过长的推理模式、采样偏差或者训练—推理数值偏差,又或者说,rollout 使用的模型的分布和训练的模型分布有区别,都有可能影响下一轮数据,继而被 放大 。这也是 RL 非常容易训崩的原因之一。因此,你可能会听到大家讨论 Off-policy,Async RL 的稳定性之类的名词。这也是本文稍后会讨论的内容。
Rollout 不是 Training Forward
在大模型 RL 中,Rollout(推理采样)、Training Forward(训练前向) 和 Training Backward(训练反向) 实际上跑在三套不同的 Kernel 上:
- RL Rollout 调用的是 SGLang / vLLM 等推理引擎:走的是 Prefill + Autoregressive Decode 路径,依赖 Paged KV Cache、Continuous Batching 等专门面向低延迟或高吞吐推理优化的算子;
- Training Forward 调用的是 Megatron / FSDP 等训练引擎:拿到已经生成的完整 token 序列后,像 Pretraining 一样做整段序列并行的前向计算,来重算 logprob 并构造 Loss;
- Training Backward 则是第三套专门用于求导、激活重算与梯度归约的反向传播算子。(当然,如果 Forward 和 Backward 都由 torch 代劳的话,那会方便很多)
因为底层算子、分块策略与归约顺序完全不同,这三套 Kernel 中任何一个环节出现非确定性或数值偏差,都会导致计算出的概率或梯度发生精度漂移。这意味着:哪怕是完全同步的 RL——即推理和训练使用的是完全相同的模型权重 ——训练前向算出来的 token 概率,也往往不等于当时推理引擎采样它时的真实概率。 这种天然的 Train-Inference Mismatch(训推不一致),正是本文后半部分要深挖的主线。
从 REINFORCE 到 Advantage
Policy Gradient 的最简推导
为了最大化期望奖励 ,我们需要对参数 求梯度 。这里我们先固定一道题目 ,把期望按定义展开成对所有可能的完整回答 求和:
但直接对这个式子求导并计算会遇到两个麻烦:
- 奖励函数 不可导:判题器(正则表达式、单元测试等)是个黑盒,只针对离散文本打分,没法直接对 反向传播;
- 求和空间 无法穷举:一条几百 token 的回答有天文数字般的组合可能,工程上不可能遍历所有 ,只能通过采样几条回答来估计梯度。
为了把梯度重新写回“可以通过采样 来估计的期望形式”,REINFORCE 用到了对数求导恒等式 (即 ):
看中间那一行:通过把 拆成 ,我们重新凑出了概率分布 ,于是对所有 的加权求和 就顺理成章地变回了期望 。
这个变形一举解决了上面的两个难题:
- 不需要对 求导:梯度算子 只作用在模型自身的生成概率 (称为 score function)上, 只是一个标量权重;
- 可以用采样代替穷举:既然是期望 ,我们只需要让模型自己 rollout 采样几条回答 ,算出它们的 并乘上对应的奖励 ,就能无偏地估计出梯度方向。
完整回答的 logprob 可以分解为:
这里左边的 是整条回答的联合概率,右边把它展开成每次生成下一个 token 的条件概率;这是概率的链式分解。后面为了书写简洁,我们会把“prompt 加上前 个生成 token”统称为 ,于是 就是同一个条件概率。对比 Pretraining 的 objective
这两者似乎惊人的相似,差别在于两点:(1)REINFORCE 看起来就像对模型自己生成的 token 做 reward-weighted Cross-Entropy;(2)这里我们是用采样估计 policy gradient,不是对不可微的抽样操作直接反向传播。
错误回答会被惩罚吗?
如果 correctness reward 为 ,直接用单样本目标 ,当回答错误、 时:
错误的答案没有直接梯度。 它不是“奖励为零,所以模型仍然会降低错误答案的概率”。我们更想要的信号是:比预期好的回答应该被鼓励,比预期差的应该被压低。所以这自然引出了我们后面要讲的 Advantage 概念。
Advantage 的由来
定义 ,其中 只依赖 prompt,不能依赖当前抽样回答本身。则: 是 Advantage(优势值),衡量回答的 reward 比参考水平高还是低; 就是这个参考水平,即 Baseline。它不必等于 0,也不一定来自另一个模型;例如可以是对这道题的预期 reward。写成 而不是 ,是为了强调:固定题目后,不能随当前 sampled response 的取值而改变,否则下面“减掉它不改变期望梯度”的推导不成立(你会看到,如果 和分布有关,则不能把它提出去)
所以 。合适的 baseline 不改变期望 policy gradient,却能降低估计方差。
- :提高这条采样回答的 logprob。
- :降低这条回答的 logprob。
- :该样本没有 policy-gradient 信号。
GRPO 估计 Advantage 的方法
Actor-Critic 可以训练 当作 baseline(PPO 是这样做的),但 GRPO 不需要额外的 Critic。对每个 prompt 采样 条回答,使用 group reward 的均值和标准差:
这里 是同一道 prompt 采样的回答条数, 是回答编号; 是第 条回答的 reward。 是这个 group 的平均 reward, 是这 个 reward 的标准差;分子 表示比同组平均水平好多少。 是防止 时除零的一个很小常数(注意:它和后面 PPO clipping 的 虽然同名,但用途完全不同)。所以这条公式做了两步:先求相对表现,再用组内波动大小调整尺度。
例如 8 条回答中 2 条正确、6 条错误:
| 回答 | 仅减均值 | 再除标准差后的 GRPO Advantage |
|---|---|---|
| 正确 | ||
| 错误 |
所以我们既能鼓励正确回答,也能直接抑制错误回答。
值得注意的是,对二值 reward 的全对/全错 group, 且所有 centered advantage 都是零;实现中要妥善处理分母的 ,但它不能制造学习信号,所以你可以在后续看到 DAPO 是怎么处理这件事的。
Importance Ratio 和 PPO Clipping
为什么“旧 Policy 采样,新 Policy 训练”需要修正?
目标是当前 Policy 下的期望,但一个 batch 的样本往往来自采样时的旧 Policy 。对于固定 prefix 和 token :
这次只考虑某个固定的生成前缀 : 是接下来可能生成的一个 token(或 action), 是采样时的旧策略给它的概率, 是当前策略给它的概率。 是我们想对它求期望的任意量; 是把词表中所有可能 token 的贡献加起来。 就是 Importance Ratio:新策略认为该 token 有多可能,相对于旧策略有多可能。等式的关键是“乘上旧概率、再除以旧概率”,从而能用旧策略的样本估计新策略下的期望;它要求分母在相关位置非零。
举例:旧 Policy 给一个 token 的概率是 ,当前 Policy 是 ,那么 ,旧分布中的该样本在估计新分布期望时需要被放大两倍。前提是旧分布覆盖了新分布的支持集。
但注意:整条自回归 trajectory 的严格重要性比率是所有 token ratio 的乘积:
是连乘(对应前面 的连加);每个 都是该 token 之前的真实 prefix。由于整条回答的概率等于各 token 条件概率的乘积,整条 trajectory 的重要性比率也要将各 token 的比率相乘。
为什么实际训练不直接用这个连乘比率?想象一条长 个 token 的推理回答:哪怕每个 token 的新旧概率比只波动 ,连乘之后也会变成 或 ,导致梯度方差爆炸或直接消失。因此,PPO/GRPO 放弃了严格的整条轨迹连乘,退而求其次,把重要性采样近似到了单个 token 级别——每个位置只看自己的单步比率 。
PPO 的 Clipping 到底裁剪什么?
即使改成了单 token 比率 ,如果我们直接最大化未裁剪的目标 ,依然会出问题:只要某个 token 的优势 ,模型为了拿高分就会拼命把 往无穷大推,换言之,模型有动力尽可能远离原始的分布;但 一旦偏离 太远,基于旧样本估计出的梯度就不再可信了。
因此,我们需要一个见好就收的刹车机制:当 偏离 超过容忍幅度 (例如 ,即合法区间为 )时,就停止继续给它提供梯度。这就是 PPO 最大化的 clipped surrogate objective:
先理解一下符号:
- 是第 个 token 的新旧策略概率比;
- 是分配给该 token 的 Advantage(在 outcome-level GRPO 中,同一条回答里的所有 token 通常共享同一个 );
- 会把超出 的 强行截平为边界常数。注意一旦函数被截平为常数,它对参数 的梯度就变成了 ;
- 是要最大化的每 token 目标(代码里用梯度下降实现时,会在最外面加个负号变成 Loss)。注意:这是最大化!因为观察式子,是 ,也即每个 token 加权后的 advantage.
初看这个公式,最容易产生两个疑问:
- 为什么要把
clip套在 上,而不是直接截断整项 ? 因为我们要限制的是新旧策略的偏离幅度(即比率 不能离 太远),而不是限制 Advantage 本身的大小。 - 既然已经有了 ,为什么外面还要套一层 ? 如果只用 ,那么无论 是往“好方向”改过了头,还是往“坏方向”跑偏了,只要超出 梯度都会变成 ——这意味着一旦模型不小心把好 token 的概率降得太低(比如 但 ),梯度也没了,模型再也无法纠错。 外层套上 (取悲观下界)后,就实现了一个精巧的单向刹车:只有当策略朝着有利方向越界时,才触发截断(梯度归零);如果策略朝着错误方向越界,则保留原项 的梯度把它拉回来。 没听懂?没关系,看一下这个例子,取 :
| Advantage | Ratio 越过哪里 | 发生什么 |
|---|---|---|
| 偏离太多,停止继续鼓励该 token | ||
| 概率错误地下降了,仍需鼓励提高 | ||
| 已经降低太多,停止继续惩罚该 token | ||
| 错误 token 反而更常见,仍需推动下降 |
Understand?对于正负 Advantage, clip 控制的方向是相反的!对于正的 Advantage, 我们要避免让 过大,而对于负的 Advantage, 我们要避免让 过小。
PPO 为什么要用 Clipping?
Okay 学到这里你可能会想,PPO 为什么要引入 clipping?我们回想一下我们是为什么要引入 ?是因为“旧 Policy 采样,新 Policy 训练”,导致两个分布不完全一致。如果训练和采样的 Policy 完全一致,那就根本不需要引入 啊?难道那个时候大家已经开始研究 Async RL 了?事实上并不是。
PPO 典型的设计允许在同一批 rollout 数据上反复做多个 optimization epochs / minibatch updates,即使训练流程完全同步:
分母始终是采样时的 ,所以第二次开始就已经是 了,因为参数已经经过了一次更新。这时 clipping 防止旧样本连续推动 Policy 偏离得太远。
其中 是采样时的模型参数快照, 是后续 optimizer updates 后得到的参数。即使分子变成 ,分母也仍然应该保持为 ,否则 ratio 衡量的就不是“当前策略相对生成这些样本的旧策略变化了多少”。
但是如果我们保证严格同步、训推一致、每 batch 只做一次更新。 更新前 ,clipping 不发挥作用。但梯度不是零:
所以:
这里竖线下标 表示:先对参数求导,再把参数代入采样时的 。虽然这时 的数值是 1,但它对 的导数并不是零,右边仍然有 倍的 logprob 梯度。因此首次单步更新的梯度局部等价于 Advantage-weighted Policy Gradient。
GRPO 之后:DAPO、Dr.GRPO、GSPO 在修改什么?
读完 GRPO 和 PPO Clipping,再看 2025 年之后涌现的 DAPO、Dr.GRPO、GSPO 等一众论文,我们可以从“他们解决了 GRPO 中的什么痛点”的角度去理解。把前面几节的组件拼起来,GRPO 实际优化的完整目标是:
其中 就是 group, 是第 个 sample 的 token 数, 是组内相对 Advantage, 是逐 token 的新旧策略比率。稍微细细想一下,对着这条公式逐项检查,有四个隐患:
- 对称的 clip 区间 :探索空间过早坍塌。 一条罕见但正确的推理路径被正 Advantage 强化时,token 概率快速上升, 很快碰到 的上界、梯度归零——模型刚探索到的新能力被过早刹车,策略容易集中到少数固定套路。
- 的分子 :全对/全错的 group 没有信号。 组内 reward 完全相同时 、所有 ,整组 rollout 烧掉的算力不产生任何梯度,训练被白白浪费。
- 归约里的 (以及 ):梯度权重失衡。 长回答的每个 token 被自己的长度稀释,短回答的单 token 权重反而被放大;除以 还会悄悄改变不同难度题目的相对权重。
- 逐 token 的 :长序列下 Token 级概率比率的剧烈噪声。 单步比率方差大,缺乏序列级的协调性。
DAPO、Dr.GRPO、GSPO 的各项改动,正好分别落在这四个位置上。
DAPO:Clip-Higher
回顾前面的 PPO,裁剪区间 是上下对称的(例如统一取 )。DAPO 做的第一个改动,就是把这个对称裁剪拆成了上下两个独立的边界 和 :
这里的 和 分别控制比率 可以向下、向上偏离 多远。二者不必相等:DAPO 保持 不变,单独把上界 调大(即所谓的 Clip-Higher)。注意,这并没有直接放大正 Advantage 的数值,而是延后了“当 且 向上增长时,触碰上界并失去梯度”的时刻。
举个具体的例子,取 ,:
- 对于正优势样本(),当 时:传统 PPO 的上界是 ,此时梯度已经归零;而 DAPO 的上界放宽到了 ,仍然允许模型继续强化这个 token。
- 对于负优势样本(),当 时:两者的下界都是 ,都已经触发刹车、停止继续向下打压。
为什么要对正向路径“网开一面”?模型在解一道难题时,好不容易探索出一条罕见但正确的推理路径,这往往代表着模型刚萌芽的新能力(这种轨迹往往和之前偏移更大)。如果因为它的新旧概率比 涨得快,就按老规矩在 处早早把梯度截断掉,就浪费了极其珍贵的学习信号。放宽上界能给这些不常见的正确路径留出更大的上调空间,从而缓解策略过早集中到少数固定套路(即 Entropy Collapse)。
不过,理解 Clip-Higher 时有2个细节:
- Clip-Higher 不会在 时放大正向初始梯度:它没有给正优势乘更大的系数,仅仅是拓宽了后续多次更新时正向梯度不被截断的跑道。
- 放宽上界并不保证 Entropy 一定上升:虽然抬高稀有优质路径的概率有助于增加多样性,但如果被继续强化的恰好是本就占主导地位的高概率正确路径(注意: 大只代表偏移大,并不代表这条轨迹一定很罕见。倘若 , 此时 也是 1.2),分布反而会变得更尖锐,使 Entropy 下降。此外,一条正确的长回答里往往也夹杂着冗余步骤,Trajectory-level 的正 Advantage 会连带把这些废话一起强化。
DAPO:Dynamic Sampling
我们在前面讲 GRPO 的 Advantage 时提到过一个情况:如果同一个 Prompt 采样出的 条回答全部正确或全部错误,组内标准差 ,居中后的分子 也全为 ——整组样本算完没有任何 Policy Gradient 信号。
我们可以算一个简单的概率题:对某道题,假设每次采样相互独立、单次答对的概率为 ,每组采样 条。那么这组采样“至少有一条对、且至少有一条错”(即能产生有效梯度)的概率是:
取 画出 随 的变化:当题目太难()或太简单()时,绝大多数 Group 都是全错或全对,只有难度适中的题目最容易产生有效信号:
DAPO 的 Dynamic Sampling 做的事情非常直接:在收集训练 Batch 时,直接过滤掉这些全对或全错、Reward 毫无差异的 Group,让推理引擎继续采样新题,直到凑够指定数量的有效 Group 才启动一次训练更新。这样做相当于把训练步预算,全部分配给了当前模型“跳一跳够得着”、有时回答正确、有时错误的题目。随着模型能力提升,原本全错的难题 逐渐上升,自然会进入有效区间,从而形成了隐式的题目难度 scheduler,题目的分布自然变成了:初始简单,而后续简单题变少,难题增多。
但我们也要看清它的局限与代价:
- 它不是显式的“由易到难”调度器:如果一道超难题对当前模型来说始终是 (每次都全错),它就会被永远过滤掉,模型永远学不到它。对于这类题目,往往还需要配合难度自适应采样、过程奖励(Partial Reward)、任务拆解或中间过渡任务。
- 提升样本效率,不等于省了 Rollout 算力:这是系统视角下最关键的一点。假设当前题库里只有 的 Group 有效,为了凑齐 组有效样本送去训练,推理侧期望需要采样 个 Group;按每组 条算,就是整整 次回答生成,而不是平时的 次。训练步的样本效率提升了,但推理侧的 GPU 算力消耗可能成倍增加——我们只是达成了“相同 step 数量下,我的样本效率更高” 而不是 “相同时间 budget 下,我的效率更高”。
DAPO: Loss Reduction
在 RL 里,你在做 Loss Reduction(归约求平均)时先除以什么、后除以什么,会直接改变不同长度回答在总梯度中的话语权。 回看开头 的归约部分 : 是回答条数, 是第 条回答实际生成的有效 token 数(不含 Prompt 和 Padding)。这种原始 GRPO 常见的 Response-level Reduction 分两步平均:先算 ,把每条回答压成一个均值;再算 ,让每条回答平起平坐。
这会带来什么后果?假设一个 的组里有两条回答:回答 A 很精炼,只有 个 tokens;回答 B 极其啰嗦,有 个 tokens。在 中,回答 A 里每个 token 前面的系数是 ,而回答 B 里每个 token 的系数是 。也就是说,短回答 A 中单个 token 的归一化权重是长回答 B 的 100 倍,尽管两条回答作为整体的总归一化权重是一样的。
为了消除这种“按回答长度稀释单 token 权重”的现象,DAPO 改成了与预训练更接近的 Token-level Reduction:
这里分母不再是回答数 ,而是整个 Batch 里所有有效生成 token 的总数 。换句话说,DAPO 先把所有回答的 token 倒进同一个大池子里,再让每个 token 平分权重。
这样一来,A 和 B 里每个有效 token 的权重完全相等;但代价是,长达 个 token 的回答 B,其整条轨迹贡献的总权重变成了短回答 A 的 倍。这很容易得出一个粗糙的结论:“GRPO 偏袒短回答,DAPO 偏袒长回答”。其实远没有这么简单:Advantage 有正有负,加权方式会同时作用于好的和坏的轨迹:
- 在 GRPO(Response-level) 下,如果模型生成了一条又臭又长的错误回答(,且 很大),由于分母除以了巨大的 ,里面每个错误 token 受到的惩罚反而被严重稀释了。结果就是:模型写错并不可怕,只要写得足够长,每个坏 token 挨的板子就很轻——这反而可能导致错误回答越训越冗长。
- 反过来,在 DAPO(Token-level) 下,如果模型生成了一条很长、虽然最终答对但中间充斥着大量废话的回答(),这条长回答就会在 Batch 总梯度中占据压倒性的比重,这同样有鼓励冗余的隐患。
此外,从跨 Batch 角度看,如果两个 Batch 的有效 token 总数分别是 8K 和 64K,DAPO 因为除以了各自的 ,只要两批 token 的平均梯度向量大致相近,算出来的总梯度尺度就会保持稳定,不会因为某个 Batch 吐出的字数多就凭空放大 8 倍(当然,真实梯度在各维度上还会互相抵消,这里只是说明量级关系)。
DAPO: Overlong Reward Shaping
在长思维链训练中,经常遇到一种情况:模型其实走在正确的推理方向上,推导过程也很有价值,但因为思考得太细致,还没来得及写出最终答案就撞上了 max_tokens 被强行截断。如果因为提取不到最终答案,就直接按“完全答错”给它记 0 分甚至重罚,给模型的信号未免过于粗暴。
为了缓解这种硬截断带来的奖励突变,DAPO 引入了平滑的超长惩罚(Overlong Reward Shaping)。例如原论文在上限 之前,设置了一个从 到 (宽度为 tokens)的缓冲区间:
这里 是回答生成的 token 数, 是一个附加的长度惩罚项(取值非正):在 以内完全不罚;进入缓冲区后,惩罚随着长度线性加重(比如到了缓冲区中点 时,附加惩罚为 );超过 则封顶为 。这项惩罚会直接加到任务本身的 Correctness Reward 上,而不是另起炉灶搞一个新的 Policy Loss。
要注意,Overlong Shaping 的目的不是无差别打压长推理,而是避免模型在跨过最大长度红线的那一刻遭遇断崖式的奖励跳变。至于 和 这两个具体数字,只是原论文实验里的一套超参 Recipe,实际用时需要根据你自己的任务长度上限来调整。
Dr.GRPO:剔除归一化
如果说 DAPO 是在工程与采样策略上做加法,那么 Dr.GRPO 的核心主张就是做减法:尽可能剔除人为引入的归一化项,还原最接近无偏 Policy Gradient 的优化目标。
它对标准 GRPO 动了两刀。第一刀,砍掉了 Advantage 分母上的组内标准差 ,只保留减均值(居中):
第二刀,砍掉了 Loss 归约时依赖当前采样长度的动态分母(无论是每条回答自己的 ,还是当前 Batch 的总长度 ),统一改成与实际生成多长无关的固定常数(例如 ):
这里的 是预先设定的最大生成长度上限(或其他固定长度常数),注意不要和前面的代理目标 混淆。对于给定的训练配置, 是一个彻头彻尾的常数,分子则只对实际生成的有效 token 求和。
为什么要把 std 拿掉?对比两个二值 reward(对为 、错为 )的 Group,设 :
- 难题 Prompt A:8 条回答里只有 1 条正确。均值 ,总体标准差 。代入标准 GRPO 公式后,那条唯一正确的回答拿到的 Advantage 高达 ,其余错误回答则是 。
- 中等题 Prompt B:8 条回答里有 4 条正确。均值 ,标准差 。此时正确和错误回答的 Advantage 分别是温和的 与 。
由于除以了 ,标准 GRPO 在难题( 接近 )上会给罕见的成功赋予极大的正向权重;对称地,在极简单的题( 接近 )上,它也会对罕见的失误施以重罚。从代数上看,只要一个 Group 不是全对或全错,用总体标准差归一化后,组内所有 Advantage 的平方和恒为定值 ——也就是说,无论这道题原本处于什么难度区间,它产生梯度的整体模长都被强行拉平了。
如果像 Dr.GRPO 一样去掉 呢?此时二值奖励下的平方和变成了 :上面难题 A 组的值是 ,而中等题 B 组是 ,权重自然向中等难度的题目倾斜。我们可以用一个极简的 Toy Model 来看看为什么这是“原始”的梯度尺度:假设模型只有一个可学习的标量 logit ,用来控制“答对”这一动作的概率 ( 为 Sigmoid 函数),那么期望奖励就是 。它对参数 的真实导数恰好是:
(这里 是对标量参数 求偏导,和前面针对整个参数向量的 方向一致。)如果在估计梯度时强行除以 ,期望梯度的尺度就会从 被扭曲成大约 ,从而人为改变了不同难度题目之间的相对权重——这就是 Dr.GRPO 所批评的 Question-level Difficulty Bias(题目难度偏差)。当然,这只是用来说明直觉的简化模型,有限采样下的样本标准差和真实深度网络的梯度向量还要复杂得多。
不过,我个人并不认为“给难题里的罕见成功更高权重”就一定是件坏事。如果你的训练目标是突破能力边界,难题里撞大运试出来的一次成功本来就价值连城。问题不在于能不能加权,而在于这种加权应该是我们心知肚明的,而不是在套用公式时被分母里的 不知不觉地代劳了。 甚至,我们可以把这两种选择统一写成一个带超参数 的谱系:
这里的 是人为调节归一化强度的旋钮:取 就退化为 Dr.GRPO,取 就是标准 GRPO,取在 之间则是折中方案。(声明一下,这个式子只是为了帮你看清设计空间,并不是一个已经过大规模验证的新算法;而且对于 、根本采不出正确答案的极难题, 调得再大也无济于事。)
同样地,Dr.GRPO 改用固定分母 也不是没有代价的:假设 Batch B 的有效生成 token 总数是 Batch A 的 倍,且每个 token 的平均梯度方向差不多,那么在固定分母 下,Batch B 算出来的整体梯度就会是 Batch A 的 倍;而 DAPO 的 动态分母反而会让这两个 Batch 的梯度尺度保持接近。所以,它们本质上是在调整不同长度样本的权重分配与跨 Batch 的有效学习率尺度,并不是非黑即白的对错之分。所以 Dr.GRPO (他们称之为 GRPO Done Right) 到底有没有真正的 done right? 其实我觉得也不好说。
GSPO:Sequence Ratio
我们在前面讲 Importance Sampling 时留了一个伏笔:理论上,整条自回归轨迹的重要性比率应该是所有 token 比率的连乘 ;因为连乘在长序列下必然数值爆炸或归零,PPO 和 GRPO 才妥协成了每个位置只看自己的单步比率:
但逐 token 独立计算比率并做裁剪,在长序列尤其是 MoE 模型训练中又会带来新的麻烦:单个 token 的概率比噪声很大,同一条回答里有的 token 被截断、有的没被截断,容易破坏整条序列更新的协调性。
于是,GSPO(Group Sequence Policy Optimization) 提出把视角重新拉回整条序列——但它不是用会爆炸的连乘,而是先对整条回答所有 token 的 求算术平均,再取指数还原:
拆开来看,因为对数的平均等于乘积开 次方,所以 ——它其实就是整条回答所有 token 概率比的几何平均数。这样一来,不管是多长的回答, 都稳定在和单步比率相同的数值量级(接近 的正数),GSPO 再用这个序列级比率去统一衡量并裁剪整段回答的相对变化。原论文表明,这种做法有效降低了长序列下逐 token 比率的噪声,显著改善了 Qwen 系列(尤其是 MoE 模型)RL 训练的稳定性。
但这里要注意:开 次方后的几何平均比率 ,在数学上绝不等于严格无偏的完整轨迹重要性比率 !两者的方差特性与对应的代理目标完全不同,千万不要把 GSPO 误解成“终于实现了严格无偏的序列级 Importance Sampling”。
沿着这个脉络往外看,2025 年还有不少有意思的探索:比如 CISPO 选择直接对 Importance Weight 本身做裁剪,而不是裁剪整个 PPO Surrogate 的梯度;VAPO 则选择重拾带 Value/Critic 网络的 PPO 路线,针对长思维链做专门的偏差与方差优化。此外,GSPO 在部分实现下还能减少对训练侧逐 token 重算旧策略 logprob 的依赖(具体取决于训练 Recipe)。这些工作共同说明了一件事:GRPO 并没有终结 RL 算法的设计,大家依然在“偏差—方差—长序列稳定性”的不可能三角里寻找更优的工程折中。
同步与异步 RL
四个容易混淆的 Policy
去读 RL 训练框架的代码时,你大概率很快就会发现:同样是“算某个策略的 logprob”,代码里居然有四套不同的变量名:
| 名称 | 含义 | 用在哪里 |
|---|---|---|
/ rollout_logprobs | 真正采样生成 token 的 Behavior Policy | Importance Weight 分母、训推一致性监控 |
/ old_logprobs | 本轮 PPO 更新的旧策略基线(常由训练引擎重算) | PPO / GRPO ratio 的分母 |
/ current_logprobs | 正在做前向和反向求导的当前 Training Policy | PPO / GRPO ratio 的分子 |
/ ref_logprobs | 冻结不动的 Reference Policy(通常是 RL 初始模型) | Reference KL 可以提供“相比初始”偏移多少的指标 |
看下标其实就能分清它们的身份: 代表 behavior(真正在推理引擎里采样出数据的那个策略), 是本轮 PPO 优化周期固定下来的比较基线, 是当前正在被优化器更新的参数,而 则是作为锚点的 reference 模型。当然,它们在模型结构上完全一样——除了 ,其余几个往往只是不同时间点的权重快照,或者是同一份权重在不同引擎(推理 vs 训练)上算出来的结果,并不需要真的在显存里同时塞下四个模型。
我们重点需要搞清楚这两件事:这两个概率用的到底是不是同一个版本的模型权重? 以及 它们到底是不是在同一个 Kernel 上算出来的? 搞不清这两件事,看训练日志里的各种 “KL” 就会张冠李戴:Reference KL(比较 和 )、PPO KL(比较 和 )、以及 Train–Inference KL(同一份权重,比较训练引擎和推理引擎的输出),虽然在日志里可能都顶着 kl 的名字,但它们衡量的其实是三对完全不同的分布。
Synchronous RL
很多人直觉上觉得:只要我是严格 Synchronous RL——也就是老老实实等一整个 Batch 的数据全部生成完,再开始训练——那么训练时的概率比 就应该等于 1。
其实不然。我们在前面讲 PPO 时已经提到过,标准的 PPO 设计允许在同一批 Rollout 数据上反复做多个 Epoch 或 Minibatch 的更新。只要第一次梯度更新把参数从 推到了 ,从第二次更新开始,你就已经处于“用旧策略 采样、拿新策略 训练”的 Off-policy 状态了,PPO Clipping 也就会立刻开始工作。另外更有可能发生的是,由于训推不一致(浮点数的 non-associativity),rollout 的 policy 和训练 forward 计算得到的 policy 不一致,导致 不等于 1。
换句话说,只有满足“权重版本完全相同、训推数值完全一致、且每批数据只做一次 Optimizer Update”这三条更苛刻的条件时,计算梯度那一刻的 ratio 才会严格等于 1。而且我们马上就会看到,哪怕你把这三条全满足了,工程实现上依然有别的坑等着你。
Fully Async 的收益与代价
为什么大家现在都在卷 Async RL?因为长思维链和 Agent 环境下的 Rollout 有着极其严重的长尾效应。在同步模式下,如果你想凑齐 1024 条轨迹再开始训练,只要其中有一小撮题目生成了 256K tokens 甚至 1M 的超长回答,或者某几个代码沙箱(Sandbox)卡住了,整个集群的 GPU 都得干等着这几条慢样本跑完,算力利用率非常低。
异步系统的核心思路,就是把 Rollout、Reward/Verifier 判题、Training 以及权重同步拆成流水线,让各个环节各跑各的、互不空等:
这样做能显著拉高 GPU 利用率、缩短端到端的训练时间,但天下没有免费的午餐,它必然会引入 Policy Staleness(策略滞后):你看图里,某条样本明明是旧权重 生成出来的,等它在队列里排上号、真正被训练端消费时,训练侧的模型权重可能已经一路更新到 了。更极端一点,如果系统允许在生成一条回答的中途热更新推理权重,那么同一条 Trajectory 的前半截和后半截甚至是由不同版本的模型拼出来的!这时候你如果还想把重要性采样算对,就必须按 token 粒度去记录当时真正的 Behavior Policy,连前半截老权重留下的 KV Cache 能不能直接给新权重接着用,在语义上都得重新打个问号。
从系统调度范式上看,目前主流方案大致分为三档:Synchronous(生成完一批再训一批)、Pipelined(训推分池、按批次做流水线重叠)和 Fully Async(推理端持续生成、训练端持续消费,显式管理版本滞后)。顺便提一句,“训推是否分卡部署(Disaggregated vs. Colocated)”和“调度上是同步还是异步”是两件独立的事,在实际系统里完全可以自由组合。
Numerical Mismatch 与 Policy Staleness
聊到这里,我们终于可以把“RL 训练为什么会变成 Off-policy”拆解成两个正交的根源了。对于固定前缀 下采样出的同一个 token ,我们不妨定义三个概率值:
怎么读这三个量?看上标和下标就够了:上标 和 区分的是在哪个引擎上算(比如 SGLang/vLLM 还是 Megatron/FSDP),下标 和 区分的是用哪个版本的模型权重( 是当时生成这条数据的 Behavior 权重, 是现在训练消费它时的 Current 权重)。所以, 是推理引擎采样时算出来的真实概率, 是把同一份旧权重 拿到训练引擎里重算出来的概率,而 则是新权重 在训练引擎下算出的概率。注意,这三个数评估的始终是同一个已采样 token 在相同上下文下的条件概率——它们是三个标量,不是重新采样了三次。
有了这三个定义,问题瞬间就清晰了:
- 哪怕推理和训练用的是完全同一份权重 ,仅仅因为两边底层 Kernel、量化精度、浮点舍入顺序、Attention 实现或 MoE 路由的细微差异,算出来的概率也会出现 。这部分纯粹由软硬件执行路径不同带来的偏差,就是 Numerical Mismatch(训推数值不一致);
- 而在同一个训练引擎内部,由于模型权重从 更新到了 导致的 ,则是纯粹的 Policy Staleness(策略版本滞后)。
于是,当前训练策略相对真实采样策略的总概率比 ,就可以干脆利落地拆成两项的乘积:
其中, 专门衡量同权重、跨引擎的数值偏差,而 专门衡量同引擎、跨版本的策略漂移。如果对两边取对数,乘法就变成了加法:
但这里有个数学细节:上面相加的是带正负号的 logprob 差值,而不是绝对值!因为三角不等式的存在, 一般并不等于 。我们在分析实验日志时,绝不能简单粗暴地拿“异步训练的平均绝对误差”减去“同步训练的平均绝对误差”,去倒推 Staleness 有多大。
理解了这个拆分,我们就能摆正两者的关系:在异步训练里,Staleness 是追求高吞吐必然要付出的代价,但同权重下的 Numerical Mismatch 却是工程上可以消除的噪音。 只有先在底层把数值一致性做好,我们才能拿到一个干净准确的旧策略基线(让 ),然后再把真正的版本滞后交给上层的 TIS / MIS 等算法去修正(我们稍后就会提到);反过来,即使你做到了极致的逐位一致,也并不能替你消除异步训练带来的 。
一个例子
你可能会觉得:“训练和推理算出来的概率差一点点有什么关系?反正大概方向对就行了吧?” 我们不妨算一个非常具体的例子。
假设我们在做严格同步的 RL,模型权重还是初始的 ,一步都没更新过。但是因为训推不一致,Rollout 引擎和 Training 引擎对某两个 token 算出来的概率出现了偏差。此时如果我们直接把 Rollout 记录的 rollout_logprobs 当作 PPO ratio 的分母:
| Token | Rollout Engine () | Training Engine () |
|---|---|---|
| Correct Token () | 0.4 | 0.6 |
| Wrong Token () | 0.6 | 0.4 |
假设好 token 的优势 ,坏 token 的优势 ,PPO 的裁剪区间设为标准的 。我们来算一下训练第一步的 Clipped Surrogate Objective:
- 对 Correct Token:比率 ,套入公式得到 —— 被 Clip,梯度归零!
- 对 Wrong Token:比率 ,套入公式得到 —— 被 Clip,梯度也归零!
模型明明连一次梯度更新都还没做过,仅仅因为训练和推理两个引擎算出的概率对不上,这两个 token 在第一步就双双触发了 PPO 的 clip,彻底丧失了学习信号! 如果这时候再叠加异步训练(Async RL)呢?假设 ,那么:
你看,真实的策略滞后其实只有温和的 ,本来完全在 内,根本不该被截断;纯粹是因为前面乘了一个 的数值偏差,才冤枉地让其被 clip 了。
在工程实践中,当然还有一种更隐蔽的“掩耳盗铃”做法:有些框架在训练开始时,会先用训练引擎跑一次前向重算出 old_logprobs(即 ),并把它当作 PPO 分母。这样一来,PPO 看到的比率就变成了 ,TensorBoard 日志里的 ppo_kl 和 clipfrac 看起来岁月静好、健康无比。但实际上呢?真正的采样比率明明是 !这种做法并没有消除偏差,它只是把真实的采样分布偏差偷偷藏到了 PPO ratio 的监控视野之外。哪怕你做的是“严格同步 + 每批只更新一次”,只要训练侧用自己重算的 当分母,表面上 ratio 恒等于 ,背地里模型优化的依然不是实际吐出这些样本的那个分布。
顺着上面的拆分再多想一步:既然 ,那如果某一个 token 恰好遇到 ,同时因为版本滞后又有 呢?
两者一乘,总比率 !如果你在监控面板上只看最终合成的 Total Ratio,你会误以为当前训练和采样策略高度一致(只差了 );可实际上,底层的数值偏差和版本滞后都已经偏离得很厉害了,只是碰巧一正一负抵消了而已。这就是为什么做 RL 系统诊断时,必须把同版本的训推数值差、策略版本滞后步数、新旧策略漂移拆开来单独记录,绝不能糊里糊涂揉成一个平均数了事。
TIS 与 MIS
既然在真实系统里,训推偏差和异步滞后很难彻底归零,那我们在算法层面能怎么补救?
目前最常见的手段是 TIS(Truncated Importance Sampling,截断重要性采样) 与 MIS(Masked / Filtered Importance Sampling)。
- TIS 的思路很直接:既然真实生成样本的是 ,那我就显式算出训练策略相对真实采样策略的比率 ,把它作为重要性权重乘到第 个 token 的梯度上;同时为了防止某些离群的 把梯度方差搞炸,再给 设一个截断上限——本质上是引入一点点截断偏差,来换取方差的稳定。
- MIS 则更干脆:如果某条样本或某个 token 的偏离程度超过了阈值,直接把它 Mask 掉(过滤掉),不让它参与梯度更新。在异步 Agentic RL 系统里,大家还会进一步结合轨迹级信任域(Trajectory-level Trust Region)、最大版本滞后限制(Staleness Limit)以及过期样本丢弃等策略。
读代码或看日志时,一定要注意区分 TIS 的权重 和前面讲的 PPO Ratio:在构建 时,分母 必须是推理引擎当时实际生成该 token 的概率; 代表当前训练策略觉得这个 token 比采样时更有可能出现,因此需要放大它的权重, 则代表需要缩小。而且,TIS 的截断(直接给权重 封顶以压低方差)和 PPO 的截断(结合 Advantage 正负号在 Surrogate Objective 里做单向刹车)完全是两码事,日志里的 tis_clipfrac 和 pg_clipfrac 千万不能混为一谈。
因此,现代 RL 基础设施最推崇的是让每一层只解决自己该管的问题:
- 数值与算子:尽量消除同一份权重在训练和推理引擎之间的 Numerical Mismatch;
- 系统调度:控制异步流水线的版本 staleness,并准确记录每个 token 对应的版本;
- 算法:用 TIS / MIS 和 PPO Clipping 兜底,控制 Off-policy 更新的偏差与方差;
更广义的系统进展与 Agentic RL
如果我们把目光从单个 Loss 公式挪开,放眼整个 2025–2026 年的 LLM RL Infra 演进,会发现大家发力的核心方向非常明确:异步训练框架(如 AReaL、Slime、SkyRL)、长思维链的 Rollout 投机采样加速(如在严格保持采样分布无损的前提下引入 Speculative Decoding / MTP)、MoE 模型的训推路由一致性(如 R3,Rollout Routing Replay),以及多轮工具调用与 Agentic RL。
我们可以看几个有代表性的工程数据(当然,这些加速比高度依赖具体的模型规模、硬件配置与任务负载):比如 AReaL 在其 2025 年论文的实验环境下,相比同步系统拿到了最高约 2.57× 的训练吞吐提升(不过,我在去年使用 AReaL 的时候体验相当相当的糟糕);2026 年 NVIDIA NeMo RL 的投机采样工作,在特定的 8B 推理模型配置下实现了 1.5–1.8× 的 Rollout 吞吐提升,并将同步端到端 RL 训练加速了最高 1.4×。
而在 Agentic RL 场景下,连训练数据本身的形态都发生了质变。在传统的数学题或单轮对话 RL 里,一条数据只是简单的 Prompt → Response → Reward;但在 Agent 任务中,一条轨迹变成了多轮交互的链条:
在这里,State 是当前的外部环境与上下文状态,Action 是模型生成的推理片段或发出的工具调用指令,Observation 是外部工具、代码解释器或沙箱环境执行后返回的真实结果,如此循环往复多轮(),直到任务结束才由验证器给出一个最终(或分步)的 Reward。比如一个 Coding Agent 在修 Bug 时,可能会先读文件、跑一次 Shell 命令、看一眼报错日志,再去修改代码并重跑单测。
这种形态直接给底层系统抛出了一连串全新的工程难题:
- 信用分配:如果一个 Agent 连续调用了 30 次工具,最后一步因为超时失败拿了 0 分,难道前面 29 步排查操作也全都是错的吗?
- 环境确定性与资源瓶颈:如何保证代码沙箱和外部环境的可复现性?当成千上万个沙箱并发跑测试时,瓶颈往往从 GPU 转移到了 CPU、网络 I/O 和长尾沙箱调度上。
- 轨迹极长:对于 Agentic tasks, 轨迹长度会从传统的数学任务的 32k 增长到 256K 甚至 1M. 此时 rollout 的压力将会非常大,若不加控制,也会进一步放大 staleness.
这也再次印证了前面的观点:评价一套 RL Infra 的好坏,绝不能只看静态的 tokens/sec 吞吐,而要看每个 GPU-hour 到底能换来多少实打实的模型能力提升。“GPU Hours 对应多少性能提升” 事实上已经成为了大家所公认的优化标准,例如我后面要讲的 vLLM 团队 “Finish RL Training Faster” 以及前阵子腾讯混元团队 https://hunyuan.tencent.com/research/100116 ↗ 的这篇文章。
vLLM 的案例分析
在上一节提到的四层架构里,最底层的地基就是“在数值层消除同版本的训推 Mismatch”。这一层具体在工程上要怎么落地?我们可以拿 vLLM 团队 2026 年发表的《Finish RL Training Faster — Bitwise-Consistent, High-Performance, Agent-Driven》作为绝佳的案例来剖析。如果你想阅读原文,点这里 ↗。
浮点运算的 non-associativity
一切问题的起点,都是计算机科学里最基础的一条常识:浮点数加法不满足结合律。
我们可以举一个一眼就能看懂的 FP32 例子:取 。如果你先算 ,正负 先抵消成 ,再加上 ,最终结果是 ;但如果你换个顺序先算 ,由于 FP32 的尾数精度只有 24 位, 里的那个 会直接因为精度不够被舍入掉( 恰好卡在 与 正中间,会按最近的偶数尾数舍入到 ),结果还是 ,最后再加上 就变成了 !
而在矩阵乘法 里,底层干的恰恰全都是这种大规模累加:设输入 的形状为 ( 是当前处理的 token 行数),权重 为 ( 是输出特征维度),那么输出矩阵 (形状为 )里的每一个元素,都是一次长度为 的内积求和 。这就意味着:哪怕两个 Kernel 喂进去的是完全相同的 FP8 输入、用的是完全相同的权重、甚至内部都用高精度的 FP32 寄存器做累加,只要它们把这 项加起来的先后顺序 有一丁点不同,算出来的浮点数在最后几个比特(ULP,Unit in the Last Place)上就可能不一样。
在普通的推理服务里,末位差几个 ULP 根本无伤大雅。但在 RL 训练里,这些算出来的 logprob 是要拿去相除算 Importance Ratio 的;更重要的是,现代大模型里到处都是 MoE Top-K 路由、Sparse Attention 的 Top-K 索引(例如,DSA, CSA, CSA v2 等)这样的离散选择——一旦刚好卡在排序的临界点上,就可能导致选出来的 Expert 或 Attention 位置直接换了一批,下游输出的差异瞬间就会被放大。
Bitwise Consistency
正因如此,vLLM 团队在文章里把“真正的训推逐位一致”拆解成了层层递进的五条要求:
- Kernel Determinism:同一个 Kernel 喂入完全相同的输入,反复跑多少次,输出都必须一模一样。
- Batch Invariance:一条请求算出来的结果,绝不能因为“和它拼在同一个 Batch 里的其他请求是谁、有多少条”而发生改变。
- Stage Consistency:同一个 token 无论是走 Prefill 路径还是走 Decode 路径,算出来的结果必须完全一致。
- Distributed Consistency:跨卡通信的归约顺序,或者 TP / EP / CP 等并行切分方式的改变,不能影响被比较路径上的数值结果。
- 共同的 Numerical Contract:训推两端的量化方式、舍入规则、中间计算精度、算子分块、离散排序的 Tie-breaking 以及归约合并顺序,必须全部对齐。
Bitwise Consistency 指的是在每次参数更新前,固定同一份权重版本和相同的 token 前缀,把推理引擎采样时记录的 rollout_logprob 与训练端重算出来的 old_logprob 相减都必须精确等于 0,而不是传统意义上“误差小于 就算对齐”。(当然,等做完 Minibatch 更新把权重改了之后,策略本身已经变了,自然不在这个定义范围内。)
你可以看到,上面这五条是环环相扣、谁也替代不了谁的:单算子有确定性,不代表换了 Batch 大小结果还一样;做到了 Batch-invariant,也不代表 Prefill、Decode 和训练前向能对齐;哪怕前向做到了逐位一致,也更不代表反向传播的梯度就天然精确。
Batch Invariance
在 vLLM 原本的默认实现里,遇到 的小 Batch 时会调用针对低延迟优化的 FlashInfer Kernel,而遇到较大的 Batch 时则会切换到高吞吐的 DeepGEMM;除此之外,MoE Router、小矩阵乘法以及 mHC 切分也都有各自基于形状的动态分支(Heuristics)。在推理的 Continuous Batching 机制下,同一条请求在每一个 Decode Step 里和多少个请求拼在一起是随时在变的;而到了训练阶段,我们又习惯把所有序列拼成一个巨大的 Packed Batch 一次性做前向计算,这通常是不变的。结果就是,同一个 token 在推理和训练时根本没有跑在同一个 GEMM Kernel 上,累加顺序自然千差万别。
vLLM 团队的改法:强制开启 DeepGEMM 的 Batch-invariant 模式,固定每一行内积的累加切分方式;同时把 Router 统一固定为输出 FP32 的 GEMM,固定小矩阵的归约策略,并将 mHC 的 split 固定为 1。当然,这样做的代价是牺牲了一部分极小 Batch 下的推理延迟优化,而且训练端每次跑 Forward 时都得重新对主权重做一次量化。
Stage Consistency
搞定了矩阵乘法,接下来就是 Attention。虽然我们在纸上写的 Attention 公式永远只有一个:
(复习一下符号:左边是大家熟悉的矩阵形式, 分别是 Query、Key、Value 矩阵, 是每个 Head 的向量维度;右边则是把目光聚焦到某一个具体 Query 的输出向量 上,其中 是它对第 个历史位置打出的注意力分数, 是为了防止指数溢出减掉的最大值, 是未归一化的权重,分子对 Value 加权求和,分母做归一化。)
公式看着简单,但在 GPU 上执行时,FlashAttention 内部用的是分块的 Online Softmax(如果你不知道 FlashAttn 是怎么实现的话,TL;DR 就是:为了避免上述计算时的 Softmax 要反复存取 HBM,所以使用的是在线的算法):怎么切块、要不要做 Split-KV 并行归约、乃至底层用哪条指令近似计算 ,都会直接影响中间的浮点舍入。更麻烦的是,在推理引擎里,Prefill 阶段是一口气并行处理一整段序列的许多 Query,而 Decode 阶段则是每次只拿着当前新生成的 1 个 Query 去 Paged KV Cache 里找——这两个阶段在主流框架里压根就跑在两个完全不同的 Kernel 上!所以,就算你的 KV Cache 存储本身没有任何精度损失,同一个 token 走 Prefill 算出来的输出(对应训练时的整序列并行 Forward)和走 Decode 算出来的输出(对应推理时的逐 token 生成),也照样对不上。
在 DeepSeek-V4-Flash 的实现里,这个矛盾尤为突出:它的 Prefill 路径是先把 FP8 的 KV Cache 反量化回 BF16,再调稀疏 Attention 算子;而 Decode 路径为了省带宽,直接调用了一个原生的 FP8 FlashMLA Kernel。为了强行对齐这两条路径,vLLM 一致性版本的做法是:让 Decode 也放弃原生 FP8 算子,统一改成 从 FP8 Cache 读取 → Gather 并反量化到临时的 BF16 Workspace → 调用与 Prefill 完全相同的 flash_mla_sparse_fwd 算子。
这样做的好处是保住了 FP8 KV Cache 省显存的核心收益(文章实测显存占用依然只有 BF16 KV 的 57% 左右),代价则是 Decode 阶段多付出了一步 Gather 和反量化的开销。而训练端也需要配合:明明训练 forward 是一次性算完的,根本不需要自回归的 KV Cache,但训练端还是人为开辟了一块私有的 FP8 Cache 缓冲区,强行让训练时的 和 也经历一次一模一样的“量化成 FP8 再反量化回 BF16”的操作,只为复刻推理时的舍入逻辑。
不过,要把前向的量化算子和反向的求导拼接起来,对于经历了一次“量化再反量化”的近似张量 ,由于量化是离散的阶梯函数,它在数学上的雅可比矩阵几乎处处为 ,反向传播时梯度到这里就会直接断掉。为了把梯度传过去,文章采用了量化训练里经典的 STE(Straight-Through Estimator,直通估计器),在反向传播时直接近似令 ,相当于假装量化和反量化不存在,把上游梯度原封不动地透传给 。对于少数无法直接自动求导的融合算子,他们则在 PyTorch 里单独搭了一条平行的可微计算图专门用来算梯度,而前向激活值则替换为推理 Kernel 的输出。
Distributed Consistency
到了 MoE 层,训推两端的不一致会变得更加棘手。首先,两边在内存里组织 Expert Token 的布局就完全不同。要想让同一个 token 在这两种截然不同的内存布局下算出逐位相同的结果,必须同时固定三个细节:
- 每一行输入在做 FP8 动态量化时,缩放因子与舍入规则必须完全一致;
- 专家层的 DeepGEMM 必须开启 Batch-invariant 模式,确保某一行 token 的计算结果不受它上下紧挨着哪些邻居 token 的影响;
- 激活函数层计算
SiLU(gate) * up时,必须在 FP32 精度下一口气算完乘法,最后只做一次向 BF16 的舍入,绝不能先在SiLU输出时转一次 BF16、乘完up再转一次。
解决了单卡算子,还有跨卡通信的归约难题。在 DeepSeek-V4-Flash 的实验配置里,Rollout 端用的是 EP8,而 Training 端用的则是 EP32 + CP4。两边连并行切分度数都不一样,怎么可能算出逐位相同的结果?他们的实验结果以及解决办法是:
- 只要不开 TP,就避开了最容易因为跨卡归约顺序不同而引入浮点误差的
TP All-Reduce; - 专家并行的
EP All-to-All通信只负责把 token 原封不动地搬运到目标 GPU 上,中间不做任何跨卡加法归约;等各个 Expert 算完输出、通过 All-to-All 传回该 token 原本所属的 GPU 后,再由本地 GPU 严格按照固定的 Top-K 顺序在本地用 FP32 完成累加; - 上下文并行
CP在这里也只负责传递和拼接 KV 状态,不涉及改变比较结果的跨卡求和。
跑两个极简的 RL
前面建立的系统直觉,我们现在来看看在真实的 run 当中,staleness 如何,数值误差有多少,以及“什么样的 run 是健康的”。我用 slime 跑了两个实验:同样的 Qwen3.5-0.8B 在 GSM8K 上做 GRPO,一次 Sync、一次 Async,各 120 个 step。
超参数设置
相同的配置:
- Qwen3.5-0.8B / GSM8K / GRPO,Slime 框架,temperature 1.0,每步 rollout 1024 条回答;
num_steps_per_rollout=1(每批数据只做一次 optimizer update);use_rollout_logprobs=false(PPO ratio 的分母不用推理引擎给的 logprob,而是训练引擎用同版本权重重算一遍);use_kl_loss=true但kl_loss_coef=0.0:Reference KL 只计算,不进梯度。
不同的配置:
- Sync Run:单节点,同一批 GPU 交替跑推理和训练,单条回答上限 1024 tokens;
- Async Run:同样单节点,4 张专职 Rollout、4 张专职 Training,回答上限放宽到 2048 tokens,并启用了 TIS;
注意:这两次 Run 不是单变量对照。 长度上限 Async run 高于 Sync run,所以 Async 后期 reward 更高是正常的。
Reward, Eval
先看 Reward:
总体而言,这两次 run 的 reward 都还算健康,总体保持上升,没有出现 reward 崩的情况。
- Sync 的 reward 前 20 步有明显涨幅(0.54 → 0.63),之后 100 步都停在 0.61–0.63 的平台期。这是“前期学得快、后期边际收益递减”,而不是训崩(训崩应当表现为 reward 一直保持在较低水平使得模型无法得到奖励信号,或者 reward 下降)。事实上,eval 的结果可以看到后期仍然在继续爬升。这部分可以解释成为模型 pass@1 更加稳定带来的收益。
- Async 的 reward 从 0.48 一路爬到 0.82,没有平台期。但细线的散布一眼就能看出噪声大:最后 20 步的单步标准差有 0.074(Sync 只有 0.021),单步在 0.63–0.95 之间震荡。rollout reward 的噪声较大,这也进一步说明仍然需要下游评测任务来观测模型的走势。
Entropy, KL
- Entropy 下降是模型对推理步骤从迷茫走向笃定,属正常下降。判断 Entropy Collapse 要联合几个指标看:如果 Entropy 断崖式归零、以及 reward 同步崩溃、组内解法千篇一律,那大概就是训崩了。
- Reference KL 缓慢升到 0.25 后走平。RL 的目标本来就是偏离初始模型,KL 拉开是必然,所以 reference KL 有增长是没太大问题的。(这不代表训推不一致!)
Length, Truncation
截断率都在收敛。在 GSM8K 里撞上长度天花板,答案没写出来就被判 0 分,所以截断率太高是不好的。但是 GSM8K 是非常非常简单的题目,所以可以看到设置当前的最大长度是合理值,模型最终大都能在最大长度内把答案做出来。
除了 reward, 还值得看的是全对/全错组数的走势。因为 reward 只能反映出模型在所有题目上表现如何,但是注意:GRPO 里一个组全对或全错,意味着组内 ,整组不产生任何梯度信号(回忆 DAPO 的 Dynamic Sampling 一节)。左图里 Sync 的对应是 dynamic_filter/drop_zero_std_1(每步因全对而被丢弃的组数,Sync 开了 Dynamic Sampling 过滤),从 14 涨到 400:训练后期为了凑够一批有效的组,推理端每步要扔掉几百个全对组——这正是 Dynamic Sampling 在难题不够多后需要 rollout 显著更多 sample 的情况。这道曲线持续走高时,就该考虑换更难的题库或调整采样分布了。
另外,老生常谈的 grad_norm 也应当关注下,不过我这次两边 grad_norm 都平稳,无尖刺、无 NaN。
Staleness 与 TIS
Async 的日志里多出一组 Sync 没有的指标,正好对应前面的 Staleness 与 TIS 理论:
rollout/staleness/mean≈ 3–3.5:每条数据从被采样到被训练消费,训练端平均已经多更新了 3 个版本;单步最高 4.7,staleness/max峰值到过 7。这就是 Fully Async 用吞吐换来的版本滞后。train/tis/train/tis_abs/train/tis_clipfrac:TIS 权重 的均值几乎精确等于 1.0000。但均值等于 1 绝不代表没有漂移——0.9 和 1.1 的均值同样是 1。真实的漂移幅度要看tis_abs(即 ,全程约 2%–3.6%);tis_clipfrac峰值也只有 0.5%,说明极端离群的权重很少。- 左图里可以看到 Async 相比 Sync 的 logprob_abs_diff 更高。这个值在 Async 下是 Numerical Mismatch 与 Policy Staleness 的叠加。
效率:Async 到底快在哪
单步快了 ,训练本身几乎没变快(13.8s → 12.2s),省下来的几乎全是等待:训练端消化上一批数据时,推理端在持续生产下一批。注意到,Async 的生成长度甚至比 Sync 更长,然而整体的 Step 还更快,说明确实还是有很大收益。
结语
把整套 RL 流程还原为真实的系统闭环,就是这样一条清晰的流水线: