Skip to content
传衡博客
返回

【九】在线RL:GRPO 与 DAPO 的推导与代码实现

参考资料
  1. Proximal Policy Optimization Algorithms
  2. DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models
  3. DAPO: An Open-Source LLM Reinforcement Learning System at Scale

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 估计每个状态的价值 V(s)V(s),再用 GAE 计算 advantage。GRPO 放弃这条路径,改用组内相对排名

同一个 prompt qq,从旧策略采样 GG 条回答 {oi}i=1G\{o_i\}_{i=1}^G。每条回答得到一个 reward rir_i(来自 verifier 或 rule-based 判分)。组内做 z-score 归一化:

A^i,t=A^i=rimean({rj}j=1G)std({rj}j=1G)\hat{A}_{i,t} = \hat{A}_i = \frac{r_i - \text{mean}(\{r_j\}_{j=1}^G)}{\text{std}(\{r_j\}_{j=1}^G)}

关键洞察:advantage 不再依赖 critic 的绝对估计,而是依赖同组回答的相对好坏。

回顾 PPO Loss

PPO 的 clipped loss 是 GRPO 的基础。PPO 通过限制 importance sampling ratio 的更新幅度,防止策略崩溃。PPO 的 loss 要最小化

LCLIP(θ)=Et[min(rt(θ)A^t,clip(rt(θ),1ϵ,1+ϵ)A^t)]\mathcal{L}^{\text{CLIP}}(\theta) = -\mathbb{E}_t\left[ \min\left( r_t(\theta)\hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_t \right) \right]

其中 rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)} 是新旧策略的比值,A^t\hat{A}_t 是 advantage。前面加负号是因为我们要最大化 clip objective,转成 loss 就要最小化。

PPO 的问题A^t\hat{A}_t 来自 critic + GAE,critic 在 LLM 场景难学且占显存。GRPO 保留 PPO 的 clip 机制,但用组内归一化替代 critic 估计 advantage。

GRPO Loss

有了 advantage,代入 PPO 的 clipped loss 形式。先说明符号:

符号含义
qqprompt(输入问题)
oio_iii 条回答(output),i=1,,Gi = 1, \dots, G
oi,to_{i,t}ii 条回答的第 tt 个 token
oi,<to_{i,<t}ii 条回答的前 t1t-1 个 token(上下文)
$o_i
ρi,t\rho_{i,t}importance sampling ratio(重要性采样比率)

Importance Sampling Ratio 是什么

ρi,t(θ)=πθ(oi,tq,oi,<t)πθold(oi,tq,oi,<t)\rho_{i,t}(\theta) = \frac{\pi_\theta(o_{i,t} \mid q,o_{i,<t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q,o_{i,<t})}

分子是新策略(当前要优化的模型)生成该 token 的概率,分母是旧策略(上一轮采样用的模型)生成该 token 的概率。

GRPO 的 loss[2]最小化

LGRPO(θ)=E[1Gi=1G1oit=1oimin(ρi,tA^i,t,clip(ρi,t,1ϵ,1+ϵ)A^i,t)]+βDKL(πθπref)\mathcal{L}_{\text{GRPO}}(\theta) = -\mathbb{E}\left[ \frac{1}{G}\sum_{i=1}^{G}\frac{1}{|o_i|}\sum_{t=1}^{|o_i|} \min\left( \rho_{i,t}\hat{A}_{i,t}, \text{clip}(\rho_{i,t},1-\epsilon,1+\epsilon)\hat{A}_{i,t} \right) \right] + \beta D_{\text{KL}}(\pi_\theta \| \pi_{\text{ref}})

直觉理解

注意:公式里写的是 A^i,t\hat{A}_{i,t},但因为 reward 只在序列末尾给出(答案对不对),同一条回答的所有 token 共享同一个 advantage(即 A^i,t=A^i\hat{A}_{i,t} = \hat{A}_i)。这就是 Outcome Supervision:不知道具体哪个 token 贡献大,整条回答一起奖一起罚。

GRPO Loss 由两部分组成:负的 PPO clip 项(期望最大化,所以 loss 最小化)和 KL 惩罚项。

PPO clip 项在做什么

KL 惩罚项在做什么

整体效果

PPO 和 GRPO 对比:

部件PPOGRPO
advantage 来源critic + GAE组内 reward 归一化
是否需要 value model需要不需要
数据形态单条 rollout同 prompt 的 GG 条回答
适合的 reward通用 reward model可验证、可比较的 reward

GRPO 的适用场景:数学推理、代码生成等有明确对错判定的任务。verifier 给出 0/1 或连续分数,组内对比即可得到 advantage。

GRPO 训练过程拆解

GRPO 的计算流程比 DPO 复杂,需要在线采样、组内对比、PPO clip。用具体数值演示整个计算循环。

输入示例

流程概览

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回答内容长度
o1o_12x = 13 - 5 = 8, so x = 412
o2o_2x = (13 - 5) / 2 = 410
o3o_32x = 8, x = 8/2 = 411
o4o_4x = 13 - 5 / 2 = 9(错误)9

Step 2: Reward 计算

# Verifier 判分(数学问题:答案正确得 1 分,错误得 0 分)
rewards = verifier(responses)  # [G]

Reward 结果

rollout答案reward
o1o_141.0
o2o_241.0
o3o_341.0
o4o_490.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]

数值计算

rolloutrewardadvantage含义
o1o_11.0+0.5高于平均水平,应该鼓励
o2o_21.0+0.5高于平均水平,应该鼓励
o3o_31.0+0.5高于平均水平,应该鼓励
o4o_40.0-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 的作用

训练效果(本轮更新后):

GRPO 总结

Loss 总结

根据上述推导,GRPO 的 Loss 形式与 PPO 几乎相同,都是”负的 clip objective + KL 惩罚”。两者的核心差异在于 advantage 的来源和计算方式

对比项PPOGRPO
Advantage 来源Critic 网络估计 V(s)V(s) + GAE组内 reward 的 z-score 归一化
Advantage 粒度每个时间步不同 A^t\hat{A}_t整条回答共享同一个 A^i\hat{A}_i
平均方式对时间步平均GG 条回答平均,每条内所有 token 均分
需要模型Policy + Critic + ReferencePolicy + Reference(无 Critic)

关键理解:GRPO 中 A^i,t=A^i\hat{A}_{i,t} = \hat{A}_i 对所有 tt 成立,即同一个回答的所有 token 被赋予相同的 advantage。这是因为 reward 只在序列末尾给出(答案对不对),无法判断中间每个 token 的具体贡献,只能让整条回答”一起奖、一起罚”。这种设计牺牲了细粒度的 credit assignment,换来了无需训练 critic 的简洁性。

完整张量形状总结

张量形状说明
responses[G, T_resp]GG 条回答,最大长度 TrespT_{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 clip(r,1ϵ,1+ϵ)\text{clip}(r, 1-\epsilon, 1+\epsilon) 上下界对称,低概率的探索 token 很容易被 clip 住,导致 entropy collapse(分布快速变尖,回答互相复制)。

DAPO 修正:上下界解耦

clip(r,1ϵlow,1+ϵhigh)\text{clip}(r, 1-\epsilon_{\text{low}}, 1+\epsilon_{\text{high}})

通常 ϵhigh>ϵlow\epsilon_{\text{high}} > \epsilon_{\text{low}},在保持 trust region 的前提下,放宽”往上长”的一侧,鼓励探索。

Dynamic Sampling

问题:GRPO 的组归一化有一个死角。如果某个 prompt 的 GG 条回答全对或全错,advantage 全为 0,没有有效梯度。

DAPO 修正:只保留”既有对也有错”的 prompt

0<#{oiis_equivalent(a,oi)}<G0 < \#\{o_i \mid \text{is\_equivalent}(a,o_i)\} < G

过滤掉全对或全错的样本,确保每个 prompt 都有有效梯度信号。

Token-Level Loss

问题:原始 GRPO 先对每条序列平均,再对组平均:

1Gi=1G1oit=1oilossi,t\frac{1}{G}\sum_{i=1}^{G}\frac{1}{|o_i|}\sum_{t=1}^{|o_i|} \text{loss}_{i,t}

长回答里的单个 token 权重被稀释(除以更长的 oi|o_i|)。

DAPO 修正:全局 token 平均

1ioii=1Gt=1oilossi,t\frac{1}{\sum_i |o_i|}\sum_{i=1}^{G}\sum_{t=1}^{|o_i|} \text{loss}_{i,t}

所有 token 平等对待,长回答里的 token 不会被天然稀释,这对 long-CoT 的场景很关键[3]

Overlong Reward Shaping

问题:超长样本直接截断再硬惩罚,会把”推理过程正确但因写太长被截断”的样本也当成负例,引入 reward noise。

DAPO 修正:软惩罚函数

Rlength(y)={0,yLmaxLcache(LmaxLcache)yLcache,LmaxLcache<yLmax1,Lmax<yR_{\text{length}}(y) = \begin{cases} 0, & |y| \le L_{\max}-L_{\text{cache}} \\ \frac{(L_{\max}-L_{\text{cache}})-|y|}{L_{\text{cache}}}, & L_{\max}-L_{\text{cache}} < |y| \le L_{\max} \\ -1, & L_{\max} < |y| \end{cases}

在长度窗口内线性衰减,超过最大长度才给 -1,减少误判[3]

DAPO 四项修正对比

技术naive GRPODAPO 改法解决的问题
Clip-Higherclip(r,1ϵ,1+ϵ)\text{clip}(r, 1-\epsilon, 1+\epsilon)clip(r,1ϵlow,1+ϵhigh)\text{clip}(r, 1-\epsilon_{\text{low}}, 1+\epsilon_{\text{high}})entropy collapse
Dynamic Sampling所有 prompt 都进 batch只保留 0<correct<G0 < \text{correct} < G 的 prompt零梯度样本
Token-Level Loss先序列平均再组平均全局 token 平均长序列 token 稀释
Overlong Reward截断后直接硬惩罚长度窗口内线性衰减reward noise

DAPO 递进增益

DAPO 在 Qwen2.5-32B 上的消融实验[3]

配置AIME 2024 avg@32
DeepSeek-R1-Zero-Qwen-32B47
Naive GRPO30
+ Overlong Filtering36
+ Clip-Higher38
+ Soft Overlong Punishment41
+ Token-level Loss42
+ 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 数值示例

G=4G=4 的完整计算流程:

rollout回答rewardadvantageratioclippedloss contribution
o1o_1正确1+0.51.11.1-0.55
o2o_2正确1+0.50.950.95-0.475
o3o_3正确1+0.51.21.15 (clip)-0.575
o4o_4错误0-1.50.80.8+1.2

小结

方法需要模型关键改进适用场景
PPOPolicy, Reference, RM, Critic在线 RL + GAE通用 RLHF
GRPOPolicy, Reference组采样替代 critic可验证任务
DAPOPolicy, Reference四项工程修正long-CoT 推理

演化逻辑:

离线分支(DPO/SimPO)和在线分支(GRPO/DAPO)的分界:前者适合已有偏好对,后者适合有 verifier、能在线 rollout、愿意为更强的 reasoning 行为付训练成本的场景。



Previous Post
FlashAttention2 原理与数值推导
Next Post
【八】离线RL:DPO 与 SimPO 的推导与代码实现