1. 从PPO开始:旧路线的能力边界
PPO、GRPO、DAPO、GSPO、SAPO这条推导链,过去两年几乎成了策略优化算法升级的完整缩影。表面上它们只是换了一个目标函数,实际上每一次切换都涉及“优势估计”这件事的根本变化。这篇我会沿着这条线,把每一步的推导逻辑、改动动机和训练中真正影响收敛的地方拆开讲。适合正在做LLM对齐、奖励模型微调,或打算把RL引入生成任务的人。
1.1 为什么策略优化必须引入重要性采样
强化学习的最终目标是最大化累计奖励的期望,也就是:
$$J(\theta)=\mathbb{E}{\tau \sim \pi\theta}\left[\sum_{t=0}^{T} r_t\right]$$
如果直接对这个目标求梯度,会得到经典的策略梯度:
$$\nabla_\theta J(\theta)=\mathbb{E}{\tau \sim \pi\theta}\left[\sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot R_t\right]$$
问题在于这个期望是从当前策略 $\pi_\theta$ 采样的。训练时每更新一步策略,样本就“过期”了,想再采一批新样本又要重新和环境交互一轮。这在大模型场景下是不可接受的,因为一次 rollout 的成本可能是一整批生成序列,再加上奖励模型打分,时间和显存都吃不消。
于是有了重要性采样。我们把期望换成从旧策略 $\pi_{\theta_{old}}$ 采样得到的形式:
$$\mathbb{E}{\tau \sim \pi{\theta}}\left[\cdot\right]=\mathbb{E}{\tau \sim \pi{\theta_{old}}}\left[\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)} \cdot \cdot\right]$$
记策略比例:
$$\rho_t=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$$
这样我们就能用一批旧样本反复更新好几轮。但“反复使用”也带来问题:如果 $\rho_t$ 太大,说明新旧策略差异已经很大,这时再用旧样本估计新策略的梯度,偏差会非常离谱。TRPO 的做法是显式约束 KL 散度,PPO 则把这个约束变成了一个更廉价的裁剪目标。
1.2 PPO的裁剪目标函数是怎么推出来的
PPO 最终形式是:
$$J^{\text{CLIP}}(\theta)=\mathbb{E}\left[\min\left(\rho_t A_t,;\operatorname{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon) A_t\right)\right]$$
其中 $\operatorname{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon)$ 把 $\rho_t$ 限制在 $[1-\varepsilon, 1+\varepsilon]$ 区间内,$A_t$ 是优势函数。
如果只是用 $\rho_t A_t$ 作为目标,策略可以一步迈得非常大。加入 $\min$ 之后,这个目标会自动选择“更保守的一侧”。当 $A_t>0$ 时,如果 $\rho_t$ 超过 $1+\varepsilon$,$\min$ 会选择被裁剪后的常数值,梯度变为 0,防止策略继续激进放大这个动作的概率;当 $A_t<0$ 时,如果 $\rho_t$ 低于 $1-\varepsilon$,同样会被截住,防止策略继续疯狂压低这个动作的概率。
在传统 RL 场景里,$A_t$ 通常用 GAE 来估计:
$$A_t=\sum_{l=0}^{T-t}(\gamma\lambda)^l \delta_{t+l}$$
$$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$$
这里出现了一个关键角色:$V(s)$,也就是 Critic 价值网络。PPO 通常采用 Actor-Critic 架构,Actor 负责输出策略,Critic 负责估计状态价值,然后用两者的差值计算优势。这个设计在小规模环境里没什么问题,但放到 LLM 上,三个隐患会逐步放大。
1.3 LLM场景下PPO的三个隐痛
第一个是显存。Critic 网络如果和策略网络同规模,等于让训练显存直接翻一倍。哪怕只把 Critic 做成比 Actor 小几倍,整个优化器状态、梯度、中间激活也都得翻倍。
第二个是价值估计的噪声。LLM 的奖励往往是稀疏的,一条长回答只有最后打分,中间几千个 token 的“价值”全靠 Critic 去 bootstrap。Critic 预测不准,GAE 的 $\delta_t$ 就会带着噪声传播,最终优势估计的方差被放大。这个问题在短 horizon 的 gym 环境里不致命,但在动辄 1k token 的生成任务里很致命。
第三个是超参敏感性。PPO 对 reward scale、GAE 的 $\lambda$、KL 惩罚系数都非常敏感。同样的参数在小规模任务上调通了,换一个数据集或者换一个奖励模型,很可能直接崩。
正是这三个痛点,把推导链推向了去掉 Critic 的方向。GRPO 就是这条路上最关键的转折点。
2. GRPO:把Critic请出去,用“组内相对优势”代替价值估计
GRPO 的核心思想很直白:既然 Critic 又贵又不准,那就不用它,改为在一个 prompt 下采样多条候选回答,用“组内相对比较”来估计优势。
2.1 从单动作价值到组内相对优势
给定一个 query $q$,从当前旧策略中采样 $G$ 条回答,记作 ${a_1, a_2, ..., a_G}$。奖励模型给每条回答打分,得到 ${r_1, r_2, ..., r_G}$。
GRPO 不再计算绝对价值,而是计算这条回答在组内有多“突出”:
$$\mu = \frac{1}{G}\sum_{i=1}^{G} r_i$$
$$\sigma = \sqrt{\frac{1}{G}\sum_{i=1}^{G}(r_i - \mu)^2 + \epsilon}$$
$$A_i = \frac{r_i - \mu}{\sigma}$$
这个 $A_i$ 就是组内相对优势。它天然是近似零均值、单位方差的。如果某条回答比组内平均分高,$A_i>0$,策略会倾向提高它的概率;如果比平均分低,$A_i<0$,策略会降低它的概率。
这个设计的巧妙之处在于,它把“绝对分数”降级成了“组内相对位置”。奖励模型如果整体打分手松手紧,只要组内相对关系不变,优势估计就不会受太大影响。这在奖励尺度不稳定时非常有用。
2.2 GRPO的损失函数推导
GRPO 在 PPO 上的改动就是把 $A_t$ 换成 $A_i$,并去掉 Critic。损失函数写成:
$$L^{\text{GRPO}}(\theta)=-\mathbb{E}\left[\frac{1}{G}\sum_{i=1}^{G}\min\left(\rho_i A_i,;\operatorname{clip}(\rho_i, 1-\varepsilon, 1+\varepsilon)A_i\right)\right]+\beta, D_{\mathrm{KL}}\left(\pi_\theta | \pi_{\mathrm{ref}}\right)$$
这里的 $\rho_i$ 是当前策略和采样策略之间的概率比:
$$\rho_i=\frac{\pi_\theta(a_i|q)}{\pi_{\theta_{\mathrm{old}}}(a_i|q)}$$
KL 项通常加的是与参考策略 $\pi_{\mathrm{ref}}$ 的散度,用来防止模型在 RL 过程中把语言能力带偏,这也是 LLM 场景下和传统 PPO 不太一样的地方。
从梯度形式上看,忽略裁剪后,GRPO 相当于把每个样本的梯度加权为:
$$\nabla_\theta L \propto -\frac{1}{G}\sum_{i=1}^{G} A_i ,\nabla_\theta \log \pi_\theta(a_i|q)$$
因为 $A_i$ 在组内做了标准化,所有样本的梯度幅度被拉到了同一量级。这样即使某条回答的奖励绝对值特别大,也不会像 PPO 那样把单条样本的梯度放大到破坏整个策略的程度。
2.3 相比PPO的实际收益
第一,显存直接少了一大块。不需要维护 Critic 和它的优化器状态,同样的显存可以塞更大的策略模型或者更大的 batch。
第二,优势估计更稳定。组内归一化用的是一个 batch 内几十条真实采样结果,而不是一个可能没训练好的神经网络估值。规则奖励场景下,GRPO 的稳定性比 PPO 好非常多,这也是它很快成为 LLM 微调主流的原因。
第三,实现简单。不需要处理 GAE 的时序依赖,不需要训练价值网络,只需要一次 forward 拿到所有序列的 logprob,配合奖励分数做归一化就行。
但 GRPO 并不是终点。组内归一化依然有自身的毛病:一是 clip 机制会把策略比例和优势耦合在一起,二是组内统计量在小 batch 下噪声很大。DAPO 和后面的 GSPO,就是在分别处理这些问题。
3. DAPO:给GRPO打补丁
DAPO 我习惯把它理解成“把 GRPO 的动手脚空间拆得更干净”。它最核心的改动是 Decoupled Clip,也就是解耦裁剪。
3.1 Decoupled Clip:先把“比例”和“优势”拆开
GRPO 沿用了 PPO 的 $\min$ 目标:
$$L_i=\min\left(\rho_i A_i,;\operatorname{clip}(\rho_i, 1-\varepsilon, 1+\varepsilon)A_i\right)$$
这个形式虽然简洁,但存在一个隐蔽的问题:$\min$ 操作会让 clip 边界和优势的符号耦合在一起。当 $A_i>0$ 时,激活的是上限裁剪;当 $A_i<0$ 时,激活的是下限裁剪。也就是说,同一个 $\rho_i$,只要优势符号翻转一下,策略的更新方向和政策比例的有效范围都会跟着变。
问题在于,优势估计本身是有噪声的。一个本来应该为正的 advantage,可能因为奖励噪声变成负的,这时候策略更新就被迫走了另一个方向。这在组内优势近零均值的情况下尤其常见:一个样本今天在组内排正数第二,明天稍微波动就变负数第三,梯度方向来回抖。
DAPO 的做法是把“策略该不该动”和“往哪里动”拆开。一个典型表达是:
$$L_i=\operatorname{clip}(\rho_i,;1-\varepsilon_{\mathrm{low}},;1+\varepsilon_{\mathrm{high}}) \cdot A_i$$
这里的 clip 只负责限制策略比例,$A_i$ 只负责决定方向和幅度。更严格一点说,实现里常常把 $A_i$ 当成权重项,不让它去参与 $\min$ 的选择。这样即使某一时刻 advantage 符号翻转,也不会导致 clip 边界在上下限之间跳跃。
你可以这么理解:PPO/GRPO 的 clip 像是一个“既看速度又看方向的刹车”,而 DAPO 的 clip 是“只看速度的限速器”,方向交给 advantage 自己判断。这个解耦让策略更新在边界处更平滑,尤其是当 $A_i$ 在 0 附近抖动时,目标函数不会因为 $\min$ 切换而出现明显的不连续。
3.2 Token级Loss与动态采样:训练信号粒度之争
另一个重要改动是把 loss 从 sequence 级拆到 token 级。GRPO 里 $\rho_i$ 是整个序列的概率比,等于所有 token 概率比的乘积。假设一条回答有 500 个 token,其中 480 个 token 是正常表达,只有最后 20 个 token 决定了答案正确性。Sequence 级概率比会把 500 个 token 的联合变化混在一起,奖励信号很难回传到真正起作用的 token 上。
Token 级 loss 的做法是把概率比逐 token 拆开:
$$\rho_{i,t}=\frac{\pi_\theta(a_{i,t}|q,a_{i,<t})}{\pi_{\theta_{\mathrm{old}}}(a_{i,t}|q,a_{i,<t})}$$
然后对序列内所有 token 的 loss 取平均:
$$L_i=\frac{1}{T_i}\sum_{t=1}^{T_i} L(\rho_{i,t}, A_i)$$
这里的 $A_i$ 依然是整条序列的组内优势,但策略更新力度分摊到了每个 token 上。好处是长回答里真正导致成败的关键 token 更容易被识别,坏处是如果不对 padding 和太长序列做 mask,训练会被凑数的 token 稀释。所以实际实现里必须做长度归一化和 padding mask。
DAPO 还强调动态采样。简单说就是在一个 global batch 里不要把某个 prompt 的 $G$ 条回答全放进去,而是让每个 batch 尽量覆盖不同 prompt,并且根据 advantage 的分布动态调整正负样本占比。否则可能出现一个 batch 里 90% 样本都是负 advantage,梯度整体偏向压抑某个 token,最后模型很快退化。
4. GSPO:把“分组”这件事做得更平滑
GRPO 用组内均值和方差来归一化,听起来很合理,但有一个工程细节容易被忽略:组有多大?如果每个 prompt 只采样 4 到 8 条回答,那么这组样本的均值和方差本身波动很大。特别是奖励分数只有 0 和 1 时,一组样本可能全是 0,标准差为 0;下一组又全是 1,标准差还是 0。这种不连续的统计量会让 advantage 出现奇怪的跳变。
GSPO 在我这里的理解是 Group-wise Smoothed Policy Optimization,也就是把组内统计量换成平滑版本。它不是对 GRPO 的否定,而是对组内归一化的“降噪处理”。
4.1 从点估计到分布估计:把组内统计量换成平滑版本
GRPO 用的是当前这个 group 的局部均值和局部方差。GSPO 则引入跨 batch 的指数滑动平均,用全局平滑量代替局部统计量:
$$\mu_s \leftarrow \beta \mu_s + (1-\beta)\mu_{\text{batch}}$$
$$\sigma_s^2 \leftarrow \beta \sigma_s^2 + (1-\beta)\sigma_{\text{batch}}^2$$
然后计算优势:
$$A_i=\frac{r_i - \mu_s}{\sqrt{\sigma_s^2 + \epsilon}}$$
这里的 $\beta$ 是平滑系数,通常取 0.9 到 0.99。$\beta$ 越大,统计量更新越慢,越能反映整个训练阶段的大盘水平;$\beta$ 越小,统计量越跟随当前 batch,越接近原始 GRPO。
这么做的动机很朴素:如果组大小只有 4,那么一个组里的均值可能差出 0.5 以上,优势估计的置信区间非常大。用 EMA 统计量代替局部统计量后,baseline 更稳定,advantage 的波动更接近“奖励模型真实分布”,而不是“这一组采样运气”。
你可以把 GRPO 组内归一化想象成拿一屋子人的平均身高来判断一个人高不高,但这屋子只有 4 个人;GSPO 是把过去很多屋子的平均身高也记下来,再做判断。后者的参照系更稳。
4.2 一个统一的梯度方差视角
从梯度方差的角度看,策略梯度可以拆成两部分:
$$\mathrm{Var}\left[\nabla L\right]=\mathbb{E}\left[\mathrm{Var}\left(\nabla L \mid \text{group}\right)\right]+\mathrm{Var}\left(\mathbb{E}\left[\nabla L \mid \text{group}\right]\right)$$
前半部分是组内采样导致的随机梯度噪声,后半部分是组间 baseline 波动带来的额外方差。GRPO 主要降低了前半部分,因为它用了组内归一化让每个样本的梯度幅度相近。但组间的均值/方差波动依然在,组越小越明显。
GSPO 的 EMA 统计量直接压缩后半部分。它可以让同一批训练数据在不同 step 里获得更一致的 advantage,而不是每次因为组内运气不同而剧烈变化。实际训练中的一个表现是 loss 曲线更干净,early stopping 和 reward 波动更容易被分辨出来。
这个“平滑”思想到了 SAPO 会被进一步放大:不只是统计量平滑,连 clip 边界和采样策略都开始随训练状态自适应。
5. SAPO:自适应边界,最后的“长尾修正”
SAPO 我按 Self-Adaptive Policy Optimization 来理解。它要解决的问题是:所有前面算法都用固定的 $\varepsilon$,但训练不同阶段对 clip 的需求完全不同。训练早期策略探索空间大,模型应该允许更大的更新幅度;训练后期策略已经收敛到一个不错的区域,更新幅度必须快速收紧,否则会出现震荡。
5.1 从固定clip到自适应clip
PPO、GRPO、DAPO 的 $\varepsilon$ 是一个人工设定的常数,比如 0.2。SAPO 的基本思路是让 $\varepsilon$ 根据当前 advantage 的分布动态变化。
一个可用的自适公式是:
$$\varepsilon_t = \operatorname{clip}\left(\varepsilon_{\min} + \left(\varepsilon_{\max}-\varepsilon_{\min}\right)\cdot \frac{\mathrm{IQR}(A_t)}{\mathrm{IQR}{\text{ref}}},;\varepsilon{\min},;\varepsilon_{\max}\right)$$
其中 $\mathrm{IQR}(A_t)$ 是当前 batch 优势的四分位距,$\mathrm{IQR}_{\text{ref}}$ 是训练开始阶段的参考四分位距。如果当前优势分布发散,说明样本间差距大,策略需要更大自由度来区分好坏,$\varepsilon_t$ 会变大;如果优势分布集中,说明策略已经比较稳定,$\varepsilon_t$ 会变小,防止过度更新。
这个自适应过程不需要额外的神经网络,只需要在线维护 advantage 的分位数统计,计算代价非常低。它带来的直接好处是少调一个关键超参,而且能让算法在训练过程中自然地从“激进探索”过渡到“保守利用”。
5.2 自适应采样:把运气成分从组构造中降下来
SAPO 还会动态调整采样策略。GRPO 的 $G$ 如果固定不变,遇到奖励分布剧烈变化时就很被动。比如训练进入瓶颈期,所有 candidate 的 advantage 全都在 0 附近,这个 group 几乎没有提供任何有效梯度;过一会儿模型突然学到某个模式,优势分布又变得特别宽,固定 $G$ 又可能采样不够。
更实用的做法是基于策略熵或者 advantage 信号的方差来调整每个 prompt 的采样数量。策略熵高的时候,说明模型输出还很随机,可以多采样几条来稳定 baseline;策略熵低的时候,说明模型已经比较确定,可以少采样几条,省下算力去做更多 prompt 覆盖。这种采样调整同样不需要改动损失函数,直接作用在数据 pipeline 上,在长文本 RL 训练里收益非常明显。
到 SAPO 这一步,这条推导链的核心脉络已经清楚:PPO 用 Critic 估计价值,GRPO 用组内相对比较替代 Critic,DAPO 把 clip 和 advantage 解耦,GSPO 把组内统计量平滑化,SAPO 再把 clip 和采样变成自适应。每一步都不是另起炉灶,而是在前一步的短板上做定向修补。
6. 实操对照:从理论到训练配置
前面讲了大量公式推导,这里落回到实际训练配置。很多人在 PPO 到 GRPO 之间来回切换时,感觉“好像差异不大”,其实是因为没有理解优势估计的源头变了。下面这张表可以帮你快速对照。
6.1 关键差异速查表
| 算法 | 优势来源 | 是否依赖Critic | 核心机制 | 典型适用场景 |
|---|---|---|---|---|
| PPO | GAE,依赖价值网络 | 是 | 裁剪 + GAE | 传统控制任务、小规模RL |
| GRPO | 组内归一化 | 否 | 组内相对优势 | LLM微调、规则奖励 |
| DAPO | 组内归一化 + 解耦 | 否 | Decoupled Clip、Token级Loss | 长文本推理、稀疏奖励 |
| GSPO | 平滑组内归一化 | 否 | EMA统计量降噪 | 小组采样、噪声奖励 |
| SAPO | 自适应组内归一化 | 否 | 自适应Clip和采样 | 训练后期、奖励分布偏移明显 |
我实际跑 LLM 微调时,最常用的组合是 GRPO 起步,如果出现 loss 震荡或者单条回答的 reward 极端值太强,就切 DAPO 风格的目标函数;如果 reward 噪声大,则把组内统计量换成 EMA,也就是 GSPO 的做法;到了训练后半段,再打开自适应 clip。你完全不需要把它们当成五个互斥的算法,它们更像五个可以叠加的 patch。
训练循环的伪代码大概是这样的:
for step in range(max_steps): queries, responses, old_logprobs, rewards = sample_group(prompts, policy, group_size=8) # GRPO / GSPO: 组内优势归一化 if use_smoothed_stats: mu, sigma = update_ema(mu, sigma, rewards) advantages = (rewards - mu) / (sigma + eps) else: advantages = group_normalize(rewards) # 当前策略下重新计算 logprobs logprobs = policy.get_logprobs(queries, responses) ratio = (logprobs - old_logprobs).exp() if use_decoupled_clip: policy_loss = -decoupled_clip_loss(ratio, advantages, eps) else: policy_loss = -ppo_clip_loss(ratio, advantages, eps) kl_loss = beta * kl_divergence(policy, ref_policy) loss = policy_loss + kl_loss optimizer.backward(loss)6.2 训练中经常踩的坑
第一个坑是组内标准差为 0。规则奖励只有 0 和 1 时,如果一个组里所有样本都是同一个分数,GRPO 的 $\sigma$ 会直接变成 0。处理办法是加一个很小的 $\epsilon$,但这只是兜底。更推荐的做法是采样组里至少保留一条正样本和一条负样本,或者用 GSPO 的 EMA 统计量作为分母,这样即使当前组没有差异,也能参考历史分布给出稳定优势。
第二个坑是 clip 范围设得和 PPO 一样。PPO 的 0.2 在价值网络环境下很好用,但在 GRPO 这类组内相对优势里,优势的绝对量级已经完全变了,0.2 有时太紧,有时太松。我一般从 0.25 到 0.3 起步,然后根据训练 batch 中更新超过 clip 边界的样本比例做调整。如果超过 10% 的样本都到了 clip 边界,说明范围太紧;如果几乎碰不到边界,说明范围太松。
第三个坑是正负样本比例失衡。组内归一化后,正优势样本和负优势样本数量应该是大致均衡的,但因为采样方差,可能出现一个 batch 里大部分样本都是正优势。这时候策略会整体提高一堆候选的概率,等于在奖励噪声上过拟合。动态采样不只是 DAPO 的专利,我在 GRPO 里也会强制每个 batch 保证一定的正负比例。
第四个坑是 token 级 loss 的长度归一化。长回答的 token 数天然更多,如果不对每个序列做长度归一化,长回答会主导梯度,导致模型偏向生成更长但不一定更正确的输出。我在实现里会给每个 token loss 除以序列长度,并且对 padding 位置做 mask,这已经是标配了。
第五个坑是 KL 项的系数 $\beta$。在 LLM 强化学习里,$\beta$ 太小会让模型很快在格式上跑偏,生成一堆高分但语言质量糟糕的内容;$\beta$ 太大会让策略学不动,更新幅度被 KL 死死压住。一个相对实用的经验是先把 $\beta$ 设到和奖励尺度同量级,比如 0.01,然后看 KL 曲线的斜率。如果 KL 上涨过快,就调大;如果策略奖励不涨,就调小。
这些经验不是我拍脑袋想出来的,而是在多次调参过程中反复撞出来的。尤其是“组大小”这个超参,很多人喜欢设成 4,因为省显存。但在长文本任务里,组大小为 4 的组内均值和方差噪声非常大,训练曲线经常像锯齿一样。我后来改成 8 或 16,稳定性明显提升。如果你的显存不够,优先减少组大小而不是减少 batch,然后用 GSPO 的 EMA 统计量补救,这样保住多样性的同时,也能控制 advantage 的噪声。
这条推导链走到 SAPO,本质上是在回答一个问题:优势估计到底应该相信多少、信任到什么程度、如何随着训练阶段变化。PPO 相信价值网络,GRPO 相信群体比较,DAPO 相信解耦后的独立信号,GSPO 相信长期平滑的统计量,SAPO 则开始根据当前证据动态调整信任半径。每一步的推导都能落脚到训练时的具体现象,下次如果你的 loss 曲线又开始剧烈震荡,不妨沿着这条链往前找一找:是优势估计噪声太大,还是 clip 边界不合时宜,又或者是组内统计量太小。想清楚这一点,改代码会比盲目调超参有用得多。