从PPO到SAPO:LLM强化学习策略优化算法推导与工程实践 1. 为什么这条RL算法链值得从头推一遍我最近花了不少时间把 PPO、GRPO、DAPO、GSPO、SAPO 这条链路的推导从头到尾完整走了一遍过程中最大感受是这条线根本不是网上很多人说的“GRPO 就是去掉评论家的 PPO”那样简单。每一次迭代背后都有一个非常具体的痛点而且这些痛点不会写在论文摘要里只有在真正去复现、去调崩训练时才会暴露出来。这篇内容就是把我的推导笔记和工程经验整理出来写给正在做 LLM 对齐训练、推理模型增强的工程师以及想从原理上理解这五条命名的学生。先说结论这五个算法在回答同一个核心问题——策略更新时如何限制“走太远”。PPO 用概率比裁剪GRPO 把基线改成组内比较并加入 KL 惩罚DAPO 修正了裁剪边界和 token 损失的细节GSPO 从理论上统一了这些做法SAPO 则重新设计了裁剪的对称性。表面上看只是公式变了几个符号实际上每一步都在处理训练稳定性、样本利用率、奖励信号质量这三件事之间的拉扯。对大多数读者来说直接跑一个 GRPO 最小实现再对照 DAPO、SAPO 的代码改动去理解是最快的方式。但如果不把每一步为什么这样改弄清楚很容易出现“代码能跑但训练永远不涨”或者“换成新算法后还不如原来”的情况。下面我从 PPO 开始一步步推。2. PPO限制更新步长的思想源头2.1 概率比与 clip 的几何意义PPO 的目标函数基础形式是这样的ρ_t(θ) π_θ(a_t|s_t) / π_old(a_t|s_t)其中 π_old 是采样时用的旧策略π_θ 是当前正在更新的策略。ρ_t 衡量当前策略在同一个状态动作对上相对旧策略的概率变化。如果不加任何约束直接最大化 E[ρ_t A_t]一次更新就能让 ρ_t 冲得很远策略瞬间崩掉。PPO 的做法是对 ρ_t 加上一个“护栏”L^CLIP(θ) E[min(ρ_t A_t, clip(ρ_t, 1-ε, 1ε) A_t)]这里 ε 通常取 0.2。先看 A_t 0 的情况这个 token 或动作带来正优势我们希望提升它的概率ρ_t 应该大于 1。但 clip 把它限制在 1ε 以内也就是说这步更新最多只能把概率比提高 20%。再看 A_t 0 的情况我们希望降低概率ρ_t 小于 1clip 下限是 1-ε防止概率被过度压低。这个 min 操作有意思的地方在于当 A_t 0如果 ρ_t 已经超过 1εmin 会选 clip 后的值梯度为 0更新停止当 A_t 0如果 ρ_t 已经低于 1-ε同样梯度为 0。换句话说PPO 只在你朝正确方向更新但更新过头时踩刹车。这种设计保证了每个 batch 的策略偏移都在可控范围内。2.2 GAE优势估计的前置条件PPO 的质量高度依赖优势估计 A_t。常见的做法是用 GAE泛化优势估计来降低方差δ_t r_t γ V(s_{t1}) - V(s_t)A_t^GAE Σ_{l0}^{T} (γλ)^l δ_{tl}GAE 里有个 λ 参数λ 越大优势估计越依赖长程奖励偏差更低但方差更高λ 越小估计越聚焦当前步偏差更高但更稳定。实践中 λ 取 0.95 左右是比较稳妥的起点。这个公式看起来只是计算中间变量但它决定了整个 PPO 的更新方向是否准确后面 GRPO 的改动其实就是在替换这一块。GAE 需要 V(s)也就是 critic 网络的价值输出。这就是 PPO 的隐性成本除了 actor 策略网络你还要同时训练一个价值网络。actor 和 critic 共享骨干网络时价值头会产生额外的梯度干扰分开训练时模型体积和显存开销几乎翻倍。这在 GPU 资源紧张时是非常现实的问题。2.3 数据使用与 on-policy 特性PPO 是典型的 on-policy 算法采完一批数据计算完 old_logprob更新一轮之后这批数据基本就不能再用了。因为 π_old 已经过期ρ_t 的分母对应的是更新前的策略。实践中你会发现同样一条样本如果你强行多更新几个 epoch训练曲线会开始震荡这是因为分子已经偏离分母太远clip 频繁触发有效梯度区域变小。这个特性是所有后续变体都继承的地基。GRPO、DAPO、SAPO 再怎么改目标函数采样机制始终是“用当前策略生成一批 response用这批 response 算优势并更新然后丢弃这批数据”。理解这一点后面看 Group 采样就顺了。3. 去掉评论家GRPO 的动机与 Group 优势3.1 评论家为什么成为瓶颈在 LLM 对齐场景里actor 本身就是一个大语言模型。如果你还给它配一个同规模的价值网络训练时的显存占用几乎是原来的两倍而且价值网络的训练信号来自稀疏的奖励本身很不稳定。我实际遇到过一种情况critic 的 loss 在正常下降但它的价值估计在对同一状态的不同采样之间漂移很严重导致 GAE 算出来的优势噪声特别大actor 的更新方向被反复拉扯。GRPO 的核心改动就是不再训练 critic。它把基线来源从“价值网络预测的状态价值”换成“同一问题下多个采样输出的平均奖励”。这样省掉的不仅是显存还有整个价值网络的调参负担。3.2 Group 优势的相对化GRPO 的做法是对每个问题 q用当前策略采样 G 个输出得到奖励 r_1, ..., r_G然后计算组内相对优势A_i (r_i - mean(r_i)) / std(r_i)当 std 为 0 时这一组所有输出奖励一样优势退化为 0不提供梯度。所以实际实现里要加一个小 epsilon 防止除零或者直接只用均值离差 A_i r_i - mean(r_i)。这个归一化为什么有效第一不同问题难度差异很大数学题和代码题天然奖励尺度不同跨问题比较会导致简单问题主导更新。组内比较天然排除了问题难度这个公共因子。第二除以 std 后奖励尺度差异被压平即使某个问题的奖励普遍偏高或偏低也不会在 batch 内造成过大影响。这就是 Group 基线比单纯减一个全局均值更稳的原因。3.3 组内对比的变化差异组大小 G 是第一个要调的超参。经验上 G 取 4 是一个性价比比较高的值G 越大组内相对优势的方差越小但内存消耗线性上升训练吞吐下降。在数学推理这类任务里正确和错误 response 的奖励差异非常明显所以即使 G4优势信号也足够强但在奖励很稠密的任务里可能需要更大的 G 才能把不同质量的输出区分开。这里需要注意采样温度。温度太高会产生大量随机噪音响应组内奖励方差变大但质量差温度太低则组内响应高度相似优势都趋近于 0。我在实际项目中的做法是训练初期用 1.0 的采样温度随着训练进行逐步降低到 0.6 附近这样既保证探索又不至于后期还在乱蹦。3.4 KL 惩罚与自适应系数GRPO 的目标函数里保留了裁剪但区别于 PPO它额外加入了一个逐 token 的 KL 惩罚项J_GRPO(θ) E[ 1/G Σ_i 1/|o_i| Σ_t min(ρ_{i,t} A_{i,t}, clip(ρ_{i,t}, 1-ε, 1ε) A_{i,t}) ] - β KL(π_θ || π_ref)这里的 π_ref 是训练开始前的冻结策略KL 惩罚让当前策略不要偏离参考策略太远。为什么要额外加这个惩罚因为去掉 critic 之后优势估计完全来自组内比较如果某个组整体都偏离参考策略组内相对优势无法察觉这种“整体漂移”。KL 惩罚提供了一个绝对的锚点防止模型在奖励噪声的驱动下飞出边界。β 是自适应的维护一个目标 KL 值比如 0.1。当前 batch 的 KL 小于目标就减小 β鼓励模型更大胆更新当前 KL 大于目标就加大 β收紧约束。这个机制比固定 β 好用的多因为训练中后期 KL 变化幅度很大固定值很难兼顾前后期。但这里有个坑GRPO 同时使用 clip 和 KL 惩罚两者可能产生方向冲突。clip 在 ρ_t 超过区间后就把梯度截断了KL 惩罚却还在往外推或往回拉。DAPO 要解决的核心问题之一就是这种冲突。4. DAPO 与 GSPO纠正 GRPO 的动态缺陷4.1 DAPO 的 Clip-Higher 与 Token-Level LossDAPO 是我认为最“工程感”的一组修正它完全是在复现 GRPO 时发现问题然后逐个解决。第一个问题是当优势为负时GRPO 的 clip 下界 1-ε 会在概率比降到 0.8 左右时就截断梯度导致该 token 的概率不再继续下降。但在大语言模型场景里一个 response 可能有几百上千个 token其中只有几个错误 token 需要被显著抑制。如果它们被提前裁剪错误行为就不会被充分惩罚模型学得慢甚至学不动。DAPO 的做法是 Clip-Higher优势为负时取消 clip 下界的限制或把下界设成 0让负优势 token 可以继续通过降低概率来获得梯度。这个改动的价值是正优势 token 和负优势 token 不再被同一个对称护栏约束模型能同时快速强化正确 token、压制错误 token。第二个问题是损失聚合方式。GRPO 的原始 loss 在 response 粒度上取平均意味着一条错误 response 里即使只有几个错误 token它们的梯度信号也会被大量无关 token 稀释。DAPO 建议在 token 级别聚合 loss也就是把所有 token 的损失直接求和平均而不是先在 response 内平均再对 response 平均。这样长 response 不会因为“归一化到单条”而稀释关键 token 的信号。我复现时做了一个小对比同样的数据、同样的 base 模型只把 loss 聚合从“response 级”改成“token 级”训练早期 response 长度和正确率的上升速度明显更快。这个改动几乎没有额外成本属于低成本高收益的优化。4.2 Stop Gradient 与动态梯度流的处理DAPO 的另一个重要细节是在计算优势时对 A 做 stop-gradient。也就是在 loss 表达式 ρ_t A_t 中A_t 参与数值计算但在反向传播时它的梯度不回流。原因是优势项如果参与梯度梯度流里会混入“奖励尺度的变化率”导致方差爆炸。你可以把这一步理解成我们想让策略面向一个固定的改进方向更新而不是让方向本身也在被梯度修正。实现上只需要一行weighted (rho * A.detach()).clamp(0, ???)但注意 detach 的位置很讲究。如果对整个 min 表达式的输入都 detach梯度完全消失正确的做法只是对优势项 detach。我见过有人直接把 A 当作常数然后用标准 PPO 公式结果训练完全不动排查半天发现是在计算 ρ 时把 π_old 的 logprob 也 detach 掉了——分母 detach 是对的分子必须保留梯度。4.3 GSPO把裁剪和 KL 惩罚装进同一个框架GSPO 来自对 GRPO 类目标的理论统一。这里的命名并不重要重要的是它给出的认知框架任何稳定的策略优化目标都可以写成“概率比上的凸约束 优势通道的缩放”的组合。PPO 的 clip 是一种约束形式GRPO 的 KL 惩罚是另一种GSPO 用一个广义目标函数把它们统一起来并证明了在一个统一的凸函数族下两者只是不同参数取值的结果。对我这种偏工程的人来说GSPO 最大的价值不是让我去换算法而是帮我理解调参的本质。比如你面对一个训练不稳定的 RL 任务真正要问的不是“要不要换成 GSPO”而是“当前约束是太硬还是太软”clip 太硬导致学习停滞KL 惩罚太软导致策略漂移快速崩坏。GSPO 的论文推导让我能对着训练曲线判断当前应该往哪个方向调整。GSPO 也解释了为什么 GRPO 的 group 基线有效它本质上是在做一种条件化的 baseline把不同问题之间的奖励尺度混在一起只会增加方差而 group 内比较天然消除了这部分。这个“baseline 应该跟随问题而非全局”的观点后续很多工作都在沿用。5. SAPO对称裁剪与规模化的稳定性设计5.1 对称裁剪的直觉SAPO 是针对裁剪函数本身的一个修正。前面 PPO 的 clip 是上下界都有限制的DAPO 的 Clip-Higher 实际上是把负优势方向的限制放松了相当于把护栏做成了不对称的形状。不对称会有副作用正优势路径和负优势路径对策略的推拉力度不一致长时间训练下来概率分布会逐渐偏向某个方向熵随之崩塌。SAPO 的做法是重新引入对称性对正优势方向设置一个有效上界同时对负优势方向设置一个对称的下界但又不像 PPO 那样让下界紧到 1-ε。它允许你在“利用正优势推动 token 概率上升”和“利用负优势压制 token 概率下降”之间保持平衡。从公式结构看SAPO 把 clip 函数替换成一组对称的边界函数在符号上更干净在梯度流上也更均衡。我不在这里展开 SAPO 的完整公式因为论文里的符号体系比较繁重这里只需要掌握三句话第一负优势不能被毫无限制地压否则策略会退化到只剩高频 token第二正优势也不能被无脑地放开否则会出现奖励 hacking 式的概率集中第三二者的边界应该对称地设计而不是独立调。这就是 SAPO 比 DAPO 更进一步的本质。5.2 为什么规模化训练更需要对称当并行规模变大、训练 step 数量变多时不对称裁剪的缺陷会被慢慢放大。你可以想象一个极端情况某个 batch 里负优势 token 占绝大多数正优势 token 占极少数。Clip-Higher 会导致绝大多数负优势 token 持续压低概率而少数正优势 token 冲高整体概率分布快速收窄模型输出多样性显著下降。反映在训练曲线上就是文本熵从 1.0 附近一路掉到 0.2 以下之后的训练几乎变成在模仿少数几条高奖励路径。SAPO 的对称设计让正负两方向都有明确边界大规模并行下不同 seed 的采样偏差不会累积成方向性漂移。这个特性在训练超过几千步后尤其明显前几百步可能看不出差别但中后期稳定性差距会拉开。5.3 工程落地上其实就是换个 clip 函数好消息是 SAPO 不需要重新写一套训练框架它改动的位置就是 clip 函数的实现。如果你已经有一套 GRPO 代码需要调整的只有 loss 组装的那一段。我建议在代码里把 clip 函数封装成一个独立模块参数包括上下界类型、是否 clip-higher、是否启用对称模式。这样你可以在同一个训练脚本里来回切换 PPO、DAPO、SAPO 的目标函数做消融实验而不是每次改需求就复制一份代码。我自己跑对比时的配置大概是PPO 用标准实现GRPO 用 clip 加 KL 惩罚DAPO 关闭 KL 惩罚并使用 token-level loss 和 stop-gradientSAPO 在这基础上启用对称裁剪并调 ε。每个配置至少跑 500 步再对比曲线前 100 步的波动不能说明问题。6. 复现这条链路的实战经验6.1 从 GRPO 最小闭环开始如果你之前完全没有跑过这套东西我建议不要上来就追求 SAPO先从一个最小 GRPO 闭环开始。整体流程分四步。第一步准备 prompt 池和奖励函数。prompt 池至少几千条覆盖你要优化的任务类型。奖励函数尽量先用规则奖励比如数学题答案匹配、代码题测试用例通过率。规则奖励的好处是稳定、可复现方便你判断训练是否真的在涨。第二步用当前策略采样。每个 prompt 采样 G 个 response保存 response 文本、每个 token 的 logprob、每个 response 的奖励。采样时要用 and old logprob 的模型 checkpoint不能更新完再把新旧 logprob 混在一起。第三步计算 group 优势。对每个 prompt 的 G 个 response减去组内均值并除以组内标准差。奖励相同的组直接给 0 优势不需要额外归一化。第四步更新策略。计算当前 logprob除以 old logprob 得到 ρ_t套用 GRPO 的目标函数加上 KL 惩罚项。更新完之后清空这批数据重新采样。下面是我常用的核心训练循环伪代码for batch in data_loader: responses, old_logp sampler.generate_and_log(batch) # 从旧策略采样 rewards reward_model(responses) advantages group_normalize(rewards) # 每个 group 内部减均值除 std # 当前模型重新计算 logprob new_logp model.compute_logp(responses) rho (new_logp - old_logp).exp() # 这里最好用 log 差再 exp数值更稳 pg_loss -torch.min(rho * advantages.detach(), rho.clamp(1-eps, 1eps) * advantages.detach()).mean() kl_penalty kl_penalty(model.logp, ref_model.logp) loss pg_loss beta * kl_penalty loss.backward() optimizer.step()6.2 调参踩坑KL 系数、温度与优势标准化β 初始值我给一个参考范围0.001 到 0.01 之间。如果你发现训练初期策略几乎没有变化检查是不是 β 太大KL 惩罚把梯度摁死了如果训练几百步后 ref 模型和当前模型的 KL 涨到 1.0 以上那说明 β 太小策略已经飞了。自适应 β 的实现很简单但要每几步更新一次系数不要每个 batch 都变否则系数震荡本身会引入噪声。采样温度我上面提到过训练后期如果发现熵掉得很快优先怀疑两类原因一是温度太低二是 clip 不对称导致方向漂移。先把温度调回 0.8 以上观察如果熵能恢复说明是探索问题如果还是继续掉再考虑工程上的 clip 设计。优势标准化这里有个细节除以 std 时如果某组全部正确或者全部错误std 是 0这时候 AE 会变成 NaN。实现上要么加一个很小的 epsilon 到分母要么当 std0 时把整个 group 的 advantage 设为 0。两种做法等价但第二种更干净因为奖励完全一致时本来就不应该产生更新。6.3 loss 聚合方式对比response 级还是 token 级这个选择很容易被忽略但影响非常直接。两种方式的公式差异在于求平均的范围response 级先对每个 response 内的 token loss 求平均再对 G 个 response 求平均。token 级把所有 token 的 loss 直接拼在一起求全局平均。在长 response 占比高的任务里response 级聚合会让长 response 的每个 token 权重变小短 response 的 token 权重变大模型的输出长度分布会被训练动态扭曲。token 级聚合则保证每个 token 对梯度的贡献相等但也意味着超长 response 可能会主导单个 batch 的梯度。我更推荐 token 级聚合同时加一个最大 token 数截断防止个别超长 response 撑爆显存。6.4 训练曲线的四个核心观察点最后说一句我判断算法是否有效的方法不要只看奖励曲线。至少同时看四个指标响应平均长度、输出文本的熵、当前策略与参考策略的 KL、正负优势占比。奖励涨但熵跌到 0 附近说明模型在走捷径KL 一直涨但奖励不涨说明约束失效负优势占比长期高过 70%说明采样质量太差应该降温度或提高奖励模型的精度响应长度剧烈震荡往往是 token level loss 和温度共同作用的信号。我个人把这一条链路推下来后的最大体会是这五个算法之间没有绝对的“谁取代谁”它们更像是在同一个策略优化问题上的不同取舍。PPO 稳但贵GRPO 便宜但需要小心的约束设计DAPO 把关键细节补上了GSPO 给了理论尺度SAPO 让大规模训练更不容易翻车。你在自己的任务里最该做的是找出当前训练曲线对应的问题然后选择那个能解决该问题的变体而不是盲目追最新名字。