GRPO / Dr.GRPO:不用 critic 的分组相对优势
GRPO 很适合 RLVR 场景:同一道题采样多条 response,用可验证 reward 判断谁更好。它不训练 critic,而是让同一个 prompt 下的多条 response 互相当参照系。
小白可以这样想:一道数学题采样 8 个答案,答对的 reward 高,答错的 reward 低。GRPO 不问“这个答案绝对值是多少”,而问“它比同题其他答案好多少”。
核心公式
对同一个 prompt 的一组 response,先得到每条 response 的总 reward:
原始 GRPO 使用组内均值和标准差:
其中 g 表示同一个 prompt 的 group。Dr.GRPO 常用的关键变化之一是关掉标准差归一:
在 verl 里,这个开关是:
algorithm.norm_adv_by_std_in_grpo=True # GRPO
algorithm.norm_adv_by_std_in_grpo=False # Dr.GRPO-styleGRPO 得到的是 response 级别的优势标量,最后会乘 response_mask 广播到每个有效 token:
源码实现怎么读
1. 分组 id 从哪里来
GRPO 的分组不是“batch 里连续 8 条就是一组”这么脆弱。RayPPOTrainer.fit() 会先给每个原始 prompt 写入 uid,然后 rollout 前后都按 actor_rollout_ref.rollout.n repeat:
raw prompts: uid = [u1, u2, ...]
repeat n times: [u1, u1, ..., u2, u2, ...]
rollout outputs: 每个 uid 对应多条 response后面 compute_grpo_outcome_advantage() 用 data.non_tensor_batch["uid"] 分组。所以你改数据流时,一旦丢了 uid,GRPO 的“同题比较”就坏了。
2. compute_grpo_outcome_advantage() 的变量表
| 源码变量 | 公式符号 | 含义 |
|---|---|---|
token_level_rewards | r_{i,t} | 每条 response 每个 token 的 reward |
scores = token_level_rewards.sum(-1) | R_i | 每条 response 的总 reward |
index | g(i) | 分组 key,也就是 uid |
id2mean[index[i]] | mu_g | 同 prompt 组内均值 |
id2std[index[i]] | sigma_g | 同 prompt 组内标准差 |
response_mask | mask | 有效 response token |
scores.unsqueeze(-1) * response_mask | A_{i,t} | 把 response 级优势广播到 token |
伪代码如下:
scores = sum(token_level_rewards over response tokens)
for each uid:
mean = average(scores in this uid)
std = std(scores in this uid)
for each response i:
if norm_adv_by_std_in_grpo:
adv_i = (score_i - mean_uid) / (std_uid + eps)
else:
adv_i = score_i - mean_uid
advantages = adv_i[:, None] * response_mask
returns = advantagescompute_grpo_vectorized_outcome_advantage() 做的是同一件事,只是用 group_mean_std() 向量化,适合大 batch。
3. trainer 入口:compute_advantage()
ray_trainer.compute_advantage() 中,GRPO 分支只传入:
token_level_rewards=data.batch["token_level_rewards"]
response_mask=data.batch["response_mask"]
index=data.non_tensor_batch["uid"]
norm_adv_by_std_in_grpo=...它不会传 values,也不会训练 critic。returns 直接等于 advantages,这就是 GRPO critic-free 的实现痕迹。
GRPO 和 PPO 的关系
GRPO 仍然常用 PPO-style clipped policy loss 更新 actor。区别主要在 advantage 来源:
| 维度 | PPO + GAE | GRPO |
|---|---|---|
| critic | 需要 value model | 通常不需要 |
| advantage | reward + value 递推 | 同 prompt 多 response 的相对 reward |
| reward 形态 | dense 或 outcome 都可 | 通常是 outcome reward |
| 关键配置 | adv_estimator=gae | adv_estimator=grpo |
| 工程重点 | critic 稳定性 | uid、rollout.n、组内 reward 方差 |
所以 GRPO 不是“没有 PPO loss”。更准确地说:GRPO 用 group-relative advantage,actor loss 仍可走 compute_policy_loss_vanilla() 或其他 policy loss。
Dr.GRPO 在 verl 里怎么看
官方 docs 建议 Dr.GRPO-style 关注三处:
algorithm.norm_adv_by_std_in_grpo=False
actor_rollout_ref.actor.loss_agg_mode=seq-mean-token-sum-norm
actor_rollout_ref.actor.use_kl_loss=False其中:
norm_adv_by_std_in_grpo=False:不除组内标准差,避免标准差归一带来的某些长度和难度偏置。seq-mean-token-sum-norm:先按 response 求和,再除固定 horizon 或loss_scale_factor,减少长短 response 因 token 数不同带来的权重偏差。use_kl_loss=False:一些 Dr.GRPO/R1-Zero-like recipe 会关闭 actor KL loss,但这不是“所有 GRPO 必须关闭 KL”。
配置里先看什么
GRPO 示例脚本集中在 examples/grpo_trainer/。常见关键项是:
algorithm.adv_estimator=grpo
algorithm.use_kl_in_reward=False
actor_rollout_ref.actor.use_kl_loss=True
actor_rollout_ref.rollout.n=${ROLLOUT_N}
actor_rollout_ref.actor.clip_ratio_low=0.2
actor_rollout_ref.actor.clip_ratio_high=0.28 # 一些大模型/DAPO-style 脚本会这样设actor_rollout_ref.rollout.n > 1 是算法条件,不只是吞吐参数。如果 n=1,组内比较几乎没有信息,源码里单样本 group 会退化成均值 0、标准差 1。
哪些地方不适合初学者硬啃
- 不要把
rollout.n当成普通并行采样参数。它决定每个 prompt 有多少 siblings 可以比较。 - 不要把
token_level_rewards.sum(-1)理解成“每个 token 都有 dense reward”。很多 RLVR reward 只有最后一个 token 非零,sum 只是取 outcome。 - 不要只看
adv_estimator=grpo就以为所有 GRPO 配置一样。KL 放 reward 侧还是 actor loss 侧、loss aggregation 怎么做,会显著改变训练行为。 - 不要忽略 reward 方差。如果一个 group 全对或全错,优势接近无学习信号,DAPO 的 dynamic sampling 就是在处理这个问题。
本节参考与延伸阅读
- verl 源码:
verl/trainer/ppo/core_algos.py的compute_grpo_outcome_advantage()、compute_grpo_vectorized_outcome_advantage()、agg_loss()。 - verl 源码:
verl/trainer/ppo/ray_trainer.py的compute_advantage()、RayPPOTrainer.fit()中uid、repeat(rollout.n)、reward、KL、advantage 相关上下文。 - verl 官方文档:
docs/algo/grpo.md,尤其 GRPO 配置和 Dr.GRPO 配置。 - 示例脚本:
examples/grpo_trainer/README.md、examples/grpo_trainer/run_qwen3_8b_fsdp.sh、run_qwen3_30b_a3b_megatron.sh、run_deepseek_v3_671b_megatron.sh。 - 论文:DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models。
- 论文:Understanding R1-Zero-Like Training: A Critical Perspective。