参考资料
GRPO 用组采样替代 PPO 的 critic model,DeepSeekMath 用它把 7B 模型的 MATH 准确率从 46.8% 提升到 51.7%[2]。但 naive GRPO 训练不稳定,容易出现 entropy collapse、长序列梯度爆炸等问题。DAPO 用四项工程修正(Clip-Higher、Dynamic Sampling、Token-Level Loss、Overlong Reward Shaping)了这个问题,并且将 Qwen2.5-32B 在 AIME 2024 上的分数从 30 分提升到 50 分[3]。
这篇文章先推导 GRPO 的原理:如何用组内 reward 归一化替代 critic 估计 advantage。然后用完整数值示例走一遍 GRPO 的训练流程。最后解释 DAPO 的四项修正各自解决了什么问题。
PPO 的 Critic 问题
PPO 的 actor-critic 结构在 Atari 游戏上运行良好,但在 LLM 场景有三个具体困难:
| 问题 | 在 LLM 场景的表现 |
|---|---|
| 模型大 | critic 和 policy 同量级,显存翻倍 |
| credit assignment 长 | 几千个 token,return 累积很长 |
| reward 稀疏 | 很多任务只在序列末尾给信号 |
critic 很难学。GRPO 的解决方式:不单独训练 value model,直接用同组 rollout 的相对好坏估计 advantage[2]。
GRPO 推导
核心思想
PPO 用 critic 估计每个状态的价值 ,再用 GAE 计算 advantage。GRPO 放弃这条路径,改用组内相对排名。
同一个 prompt ,从旧策略采样 条回答 。每条回答得到一个 reward (来自 verifier 或 rule-based 判分)。组内做 z-score 归一化:
关键洞察:advantage 不再依赖 critic 的绝对估计,而是依赖同组回答的相对好坏。
回顾 PPO Loss
PPO 的 clipped loss 是 GRPO 的基础。PPO 通过限制 importance sampling ratio 的更新幅度,防止策略崩溃。PPO 的 loss 要最小化:
其中 是新旧策略的比值, 是 advantage。前面加负号是因为我们要最大化 clip objective,转成 loss 就要最小化。
PPO 的问题: 来自 critic + GAE,critic 在 LLM 场景难学且占显存。GRPO 保留 PPO 的 clip 机制,但用组内归一化替代 critic 估计 advantage。
GRPO Loss
有了 advantage,代入 PPO 的 clipped loss 形式。先说明符号:
| 符号 | 含义 |
|---|---|
| prompt(输入问题) | |
| 第 条回答(output), | |
| 第 条回答的第 个 token | |
| 第 条回答的前 个 token(上下文) | |
| $ | o_i |
| importance sampling ratio(重要性采样比率) |
Importance Sampling Ratio 是什么:
分子是新策略(当前要优化的模型)生成该 token 的概率,分母是旧策略(上一轮采样用的模型)生成该 token 的概率。
GRPO 的 loss[2] 要最小化:
直觉理解
注意:公式里写的是 ,但因为 reward 只在序列末尾给出(答案对不对),同一条回答的所有 token 共享同一个 advantage(即 )。这就是 Outcome Supervision:不知道具体哪个 token 贡献大,整条回答一起奖一起罚。
GRPO Loss 由两部分组成:负的 PPO clip 项(期望最大化,所以 loss 最小化)和 KL 惩罚项。
PPO clip 项在做什么:
- :新策略比旧策略更倾向生成该 token
- :新策略比旧策略更抑制该 token
- :这整条回答比组内平均水平好,应该鼓励
- :这整条回答比组内平均水平差,应该抑制
- clip 限制 在 内:防止某条回答的概率更新过大
- min 选择较小的目标值:保守起见,避免激进的策略更新
KL 惩罚项在做什么:
- 防止新策略 偏离 reference 模型 太远
- 避免模型”学坏”(比如为了高分而生成乱码)
整体效果:
- 同一组内,reward 高的回答:,clip 项为正,加负号后降低 Loss,梯度会提升这些 token 的概率
- 同一组内,reward 低的回答:,clip 项为负,加负号后增加 Loss,梯度会降低这些 token 的概率
- 提升/降低的幅度被 clip 限制,且不能偏离 reference 太远
PPO 和 GRPO 对比:
| 部件 | PPO | GRPO |
|---|---|---|
| advantage 来源 | critic + GAE | 组内 reward 归一化 |
| 是否需要 value model | 需要 | 不需要 |
| 数据形态 | 单条 rollout | 同 prompt 的 条回答 |
| 适合的 reward | 通用 reward model | 可验证、可比较的 reward |
GRPO 的适用场景:数学推理、代码生成等有明确对错判定的任务。verifier 给出 0/1 或连续分数,组内对比即可得到 advantage。
GRPO 训练过程拆解
GRPO 的计算流程比 DPO 复杂,需要在线采样、组内对比、PPO clip。用具体数值演示整个计算循环。
输入示例:
- Prompt:
"求解方程:2x + 5 = 13" - 采样参数:(每组 4 条回答)
流程概览:
- Rollout 采样(Step 1):用旧策略生成 条回答
- Reward 计算(Step 2):verifier 给每条回答打分
- Advantage 估计(Step 3):组内 z-score 归一化
- PPO Loss 计算(Step 4):clip objective + KL 惩罚
Step 1: Rollout 采样
# 一个 prompt,采样 G=4 条回答
prompt = "求解方程:2x + 5 = 13"
G = 4
# 用旧策略 pi_old 采样
inputs = tokenizer(prompt, return_tensors="pt")
samples = policy.generate(
**inputs,
num_return_sequences=G,
max_new_tokens=128,
do_sample=True,
temperature=0.7
)
# 提取 response 部分(去掉 prompt)
prompt_len = inputs["input_ids"].shape[1]
responses = samples[:, prompt_len:] # [G, T_resp]
生成的 4 条回答示例:
| rollout | 回答内容 | 长度 |
|---|---|---|
2x = 13 - 5 = 8, so x = 4 | 12 | |
x = (13 - 5) / 2 = 4 | 10 | |
2x = 8, x = 8/2 = 4 | 11 | |
x = 13 - 5 / 2 = 9(错误) | 9 |
Step 2: Reward 计算
# Verifier 判分(数学问题:答案正确得 1 分,错误得 0 分)
rewards = verifier(responses) # [G]
Reward 结果:
| rollout | 答案 | reward |
|---|---|---|
| 4 | 1.0 | |
| 4 | 1.0 | |
| 4 | 1.0 | |
| 9 | 0.0 |
Step 3: Advantage 估计(组内归一化)
# 组内 z-score 归一化
mean_reward = rewards.mean() # 0.75
std_reward = rewards.std() # 0.5
advantages = (rewards - mean_reward) / (std_reward + 1e-6)
# advantages: [0.5, 0.5, 0.5, -1.5]
数值计算:
- mean =
- std =
| rollout | reward | advantage | 含义 |
|---|---|---|---|
| 1.0 | +0.5 | 高于平均水平,应该鼓励 | |
| 1.0 | +0.5 | 高于平均水平,应该鼓励 | |
| 1.0 | +0.5 | 高于平均水平,应该鼓励 | |
| 0.0 | -1.5 | 低于平均水平,应该抑制 |
关键理解:
- 没有 critic,advantage 完全来自同组对比
- 、、 虽然都答对了(reward=1),但 advantage 只有 +0.5(不是最高),因为组内还有其他正确答案
- 答错了,advantage 是 -1.5,会被抑制
Step 4: PPO Loss 计算
# 计算新旧策略的 log probs
old_logprobs = get_logprobs(old_policy, responses) # [G, T]
new_logprobs = get_logprobs(new_policy, responses) # [G, T]
# Importance sampling ratio
ratio = (new_logprobs - old_logprobs).exp() # [G, T]
# PPO clip
clipped_ratio = ratio.clamp(1 - eps, 1 + eps) # eps=0.2
# Advantage 广播到每个 token
adv = advantages.view(G, 1) # [G, 1]
# PPO objective: min(ratio * A, clipped_ratio * A)
ppo_loss = -torch.minimum(ratio * adv, clipped_ratio * adv)
# KL 惩罚
kl_penalty = beta * (new_logprobs - ref_logprobs).exp()
# 总 loss
loss = (ppo_loss + kl_penalty).mean()
loss.backward()
PPO Clip 的作用:
- ratio > 1:新策略比旧策略更倾向生成该 token
- ratio < 1:新策略比旧策略更抑制该 token
- clip 限制 ratio 在 内,防止策略更新过大
训练效果(本轮更新后):
- 、、 的生成概率略微提升(因为 advantage > 0)
- 的生成概率显著降低(因为 advantage < 0)
- 下一轮采样时,正确答案的占比会提高
GRPO 总结
Loss 总结
根据上述推导,GRPO 的 Loss 形式与 PPO 几乎相同,都是”负的 clip objective + KL 惩罚”。两者的核心差异在于 advantage 的来源和计算方式:
| 对比项 | PPO | GRPO |
|---|---|---|
| Advantage 来源 | Critic 网络估计 + GAE | 组内 reward 的 z-score 归一化 |
| Advantage 粒度 | 每个时间步不同 | 整条回答共享同一个 |
| 平均方式 | 对时间步平均 | 对 条回答平均,每条内所有 token 均分 |
| 需要模型 | Policy + Critic + Reference | Policy + Reference(无 Critic) |
关键理解:GRPO 中 对所有 成立,即同一个回答的所有 token 被赋予相同的 advantage。这是因为 reward 只在序列末尾给出(答案对不对),无法判断中间每个 token 的具体贡献,只能让整条回答”一起奖、一起罚”。这种设计牺牲了细粒度的 credit assignment,换来了无需训练 critic 的简洁性。
完整张量形状总结
| 张量 | 形状 | 说明 |
|---|---|---|
responses | [G, T_resp] | 条回答,最大长度 |
rewards | [G] | 每条回答的 reward |
advantages | [G] | z-score 归一化后的 advantage |
old_logprobs | [G, T_resp] | 旧策略的 log prob |
new_logprobs | [G, T_resp] | 新策略的 log prob |
ratio | [G, T_resp] | importance sampling ratio |
adv | [G, 1] | advantage 广播到每个 token |
DAPO:四项修正
Naive GRPO 在长思维链场景中效果不佳,DAPO 采用了四项工程技巧对其进行优化[3]。
Clip-Higher
问题:标准 PPO 上下界对称,低概率的探索 token 很容易被 clip 住,导致 entropy collapse(分布快速变尖,回答互相复制)。
DAPO 修正:上下界解耦
通常 ,在保持 trust region 的前提下,放宽”往上长”的一侧,鼓励探索。
Dynamic Sampling
问题:GRPO 的组归一化有一个死角。如果某个 prompt 的 条回答全对或全错,advantage 全为 0,没有有效梯度。
DAPO 修正:只保留”既有对也有错”的 prompt
过滤掉全对或全错的样本,确保每个 prompt 都有有效梯度信号。
Token-Level Loss
问题:原始 GRPO 先对每条序列平均,再对组平均:
长回答里的单个 token 权重被稀释(除以更长的 )。
DAPO 修正:全局 token 平均
所有 token 平等对待,长回答里的 token 不会被天然稀释,这对 long-CoT 的场景很关键[3]。
Overlong Reward Shaping
问题:超长样本直接截断再硬惩罚,会把”推理过程正确但因写太长被截断”的样本也当成负例,引入 reward noise。
DAPO 修正:软惩罚函数
在长度窗口内线性衰减,超过最大长度才给 -1,减少误判[3]。
DAPO 四项修正对比
| 技术 | naive GRPO | DAPO 改法 | 解决的问题 |
|---|---|---|---|
| Clip-Higher | entropy collapse | ||
| Dynamic Sampling | 所有 prompt 都进 batch | 只保留 的 prompt | 零梯度样本 |
| Token-Level Loss | 先序列平均再组平均 | 全局 token 平均 | 长序列 token 稀释 |
| Overlong Reward | 截断后直接硬惩罚 | 长度窗口内线性衰减 | reward noise |
DAPO 递进增益
DAPO 在 Qwen2.5-32B 上的消融实验[3]:
| 配置 | AIME 2024 avg@32 |
|---|---|
| DeepSeek-R1-Zero-Qwen-32B | 47 |
| Naive GRPO | 30 |
| + Overlong Filtering | 36 |
| + Clip-Higher | 38 |
| + Soft Overlong Punishment | 41 |
| + Token-level Loss | 42 |
| + Dynamic Sampling (DAPO) | 50 |
Naive GRPO 只拿到 30 分,甚至不如基座模型的 47 分。四项修正叠加后达到 50 分,超过基座模型。
long-CoT RL 跑不稳,通常不是单个超参的问题,而是采样、长度、loss reduction、探索约束一起拖后腿。
GRPO 代码实现
Rollout 与 Advantage 计算
# 输入: [B] 个 prompt
prompts = tokenizer(batch_prompts, return_tensors="pt", padding=True)
# 采样: 每个 prompt 生成 G 条回答
samples = policy.generate(
**prompts,
num_return_sequences=G,
max_new_tokens=256
) # [B*G, T_total]
# 提取 response 部分
responses = samples[:, prompts["input_ids"].shape[1]:] # [B*G, T_resp]
# Verifier 判分
rewards = verifier(responses).view(B, G) # [B, G]
# 组内 z-score 归一化
adv = (rewards - rewards.mean(dim=1, keepdim=True)) # [B, G]
adv = adv / rewards.std(dim=1, keepdim=True).clamp_min(1e-6)
adv = adv.reshape(B * G, 1) # [B*G, 1]examples/grpo_rollout.py
PPO Loss 计算
# 计算 log probs
new_logp = token_logprobs(new_logits, response_ids) # [B*G, T_resp]
old_logp = token_logprobs(old_logits, response_ids) # [B*G, T_resp]
ref_logp = token_logprobs(ref_logits, response_ids) # [B*G, T_resp]
# Importance sampling ratio
ratio = (new_logp - old_logp).exp() # [B*G, T_resp]
# PPO clip (DAPO: clip-higher)
clipped = ratio.clamp(1 - eps_low, 1 + eps_high)
# PPO objective
ppo_loss = -torch.minimum(ratio * adv, clipped * adv) # [B*G, T_resp]
# KL 惩罚
kl_penalty = beta * ((new_logp - ref_logp).exp() - 1)
# Token-level loss (DAPO: 全局平均)
loss = (ppo_loss + kl_penalty).sum() / (B * G * T_resp)
loss.backward()examples/grpo_loss.py
单 Prompt 数值示例
的完整计算流程:
| rollout | 回答 | reward | advantage | ratio | clipped | loss contribution |
|---|---|---|---|---|---|---|
| 正确 | 1 | +0.5 | 1.1 | 1.1 | -0.55 | |
| 正确 | 1 | +0.5 | 0.95 | 0.95 | -0.475 | |
| 正确 | 1 | +0.5 | 1.2 | 1.15 (clip) | -0.575 | |
| 错误 | 0 | -1.5 | 0.8 | 0.8 | +1.2 |
- 、、 的 advantage > 0,loss 为负(梯度提升概率)
- 的 advantage < 0,loss 为正(梯度降低概率)
- 的 ratio 被 clip 到 1.15,防止更新过大
小结
| 方法 | 需要模型 | 关键改进 | 适用场景 |
|---|---|---|---|
| PPO | Policy, Reference, RM, Critic | 在线 RL + GAE | 通用 RLHF |
| GRPO | Policy, Reference | 组采样替代 critic | 可验证任务 |
| DAPO | Policy, Reference | 四项工程修正 | long-CoT 推理 |
演化逻辑:
- PPO → GRPO:砍掉 critic,用组内相对分数估计 advantage
- GRPO → DAPO:用 Clip-Higher、Dynamic Sampling、Token-Level Loss、Overlong Reward 解决训练稳定性
离线分支(DPO/SimPO)和在线分支(GRPO/DAPO)的分界:前者适合已有偏好对,后者适合有 verifier、能在线 rollout、愿意为更强的 reasoning 行为付训练成本的场景。