
1. 从 PPO 到 GRPO 的演进逻辑与核心矛盾1.1 为什么 LLM 场景下 PPO 开始“水土不服”如果你在过去两年里做过大模型强化学习微调大概率经历过这样的场景用 PPO 训一个 7B 到 70B 的模型reward 曲线前几百步看着还行再往后就开始抽风——KL 爆炸、advantage 方差大得离谱、value loss 死活降不下去最后只能靠调 clip 系数和 learning rate 硬撑。这不是你调参不行而是 PPO 这套 2017 年为连续控制任务设计的算法搬到 LLM 这个离散、超高维、reward 稀疏且带主观性的场景里本身就存在结构性错配。PPO 的核心假设是策略和价值函数共享同一个输入表征critic 能比较准确地估计状态价值。在 Atari 或者 MuJoCo 里这个假设基本成立因为状态空间相对低维、reward 密集。但 LLM 的“状态”是一整段 token 序列动作空间是词表大小动辄 10 万reward 往往只在序列末尾给一个标量。这时候 critic 要学的东西极其困难——它得从一段文本里预测最终得分本质上是在做信用分配credit assignment而这个任务本身就接近“猜”。更麻烦的是显存。PPO 需要同时驻留 policy model、reference model、reward model、critic model 四份权重。70B 模型按 FP16 算光权重就 140GB四份就是 560GB还没算 optimizer state 和 activation。这就是为什么 DeepSeek 在 GRPO 论文里直接点出PPO 在 LLM 上的 critic 训练成本几乎和 policy 一样高但收益却不成正比。1.2 GRPO 的核心洞察用组内相对值替代绝对价值GRPOGroup Relative Policy Optimization的破局点非常干脆既然 critic 学不准那就不学了。它的做法是对同一个 prompt 采样一组group输出比如 G8 或 16 条然后用这组输出的 reward 均值作为 baseline用组内标准差做归一化直接算出每条输出的 advantage。用公式表达就是对每个 prompt q采样 {o_1, o_2, ..., o_G}得到 reward {r_1, ..., r_G}然后A_i (r_i - mean(r)) / std(r)这个 advantage 直接替代了 PPO 里 critic 输出的 value baseline。省掉了 critic 模型显存直接砍掉四分之一训练稳定性还提升了——因为组内归一化天然把 reward 尺度拉到了均值 0、方差 1 附近advantage 不会因为 reward model 的绝对尺度漂移而爆炸。但这里有个容易被忽略的代价GRPO 的方差比 PPO 大。因为 PPO 的 baseline 是 critic 学出来的、跨 batch 平滑的估计而 GRPO 的 baseline 只来自当前这一组样本。组小的时候比如 G4均值估计噪声很大advantage 抖动明显。这就是为什么 GRPO 论文里推荐 G 至少 8实践中 16 更稳。我自己的经验是G8 在 7B 模型上勉强能用但 32B 以上建议 G16 起步否则训练曲线会像心电图。1.3 九种修正的定位不是替代而是补丁标题里说的“九种修正”指的是从 PPO 到 GRPO 这条路上社区为了解决具体问题提出的九类改进思路。它们不是互相替代的关系而是各自针对一个痛点打补丁。我把它整理成下面这张表方便你快速定位自己该用哪个修正方向代表方法解决的核心问题适用场景去掉 criticGRPO、RLOO显存和训练成本大模型、显存受限改进 advantageGAE、V-trace方差与偏差权衡长序列、稀疏 reward组内归一化GRPO、Dr.GRPOreward 尺度漂移reward model 不稳定长度归一化Dr.GRPO、DAPO长度偏置输出长度差异大KL 约束重构GRPO 的 KL 项防止策略跑偏需要保持多样性采样策略RLOO 的 leave-one-outbaseline 偏差小 group 场景裁剪机制PPO clip、DAPO clip-higher更新幅度控制训练不稳定优势估计RLOO、Reinforce无 critic 的方差控制纯 RL 微调课程与过滤DAPO 的动态采样样本效率大规模训练这张表不是让你全用上而是让你在遇到具体问题时知道往哪个方向找。比如你发现模型输出越来越短那是长度偏置问题看 Dr.GRPO如果 reward 曲线震荡先看组大小和归一化方式。2. 九种修正的逐一拆解与实操要点2.1 修正一砍掉 Critic——GRPO 与 RLOO 的分野GRPO 和 RLOOREINFORCE Leave-One-Out都去掉了 critic但它们的 baseline 构造方式不同。GRPO 用整组均值RLOO 用 leave-one-out 均值——也就是对第 i 条输出baseline 是除它以外其他 G-1 条的均值。这个差别看起来很小但影响很大。GRPO 的 baseline 包含了第 i 条自己所以当 G 很小时baseline 会被自己“污染”导致 advantage 被低估。RLOO 的 leave-one-out 则保证了 baseline 和当前样本独立理论上无偏。代价是计算稍微麻烦一点但 G4 到 8 的小组场景下RLOO 的方差明显更小。实操上如果你用 TRL 或 OpenRLHF 这类框架GRPO 通常是默认选项RLOO 需要自己改 advantage 计算。我的建议是G≥16 用 GRPO 就行差别不大G≤8 优先试 RLOO尤其是 reward 信号本身噪声大的任务比如主观评分。注意去掉 critic 后value loss 这一项就没了但 KL 惩罚项还在。很多人第一次跑 GRPO 会发现 KL 涨得比 PPO 快这是因为没有 critic 平滑策略更新更“激进”。解决办法是把 KL 系数调大一点或者用自适应 KL。2.2 修正二GAE 在 LLM 场景的适配与局限GAEGeneralized Advantage Estimation是 PPO 的标配用 λ 参数在偏差和方差之间权衡。但在 LLM 里GAE 依赖 critic 的 value 估计而前面说了 critic 本身就学不准。所以 GAE 在 LLM RL 里的实际效果往往取决于 critic 的质量——critic 好GAE 锦上添花critic 差GAE 反而放大噪声。我试过在 7B 模型上对比 GAE(λ0.95) 和简单的 Monte Carlo advantage结果在 reward 密集的任务上 GAE 略好但在 reward 稀疏只有末尾给分的任务上两者差不多甚至 MC 更稳。原因是稀疏 reward 下GAE 的 bootstrap 项几乎没信息λ 再大也退化成 MC。所以如果你用的是 GRPO 这类无 critic 方法GAE 就用不上了。但如果你坚持 PPO 路线GAE 的 λ 建议设 0.9 到 0.95不要设 1.0纯 MC 方差太大也不要设太低偏差太大。2.3 修正三组内归一化的尺度陷阱GRPO 的组内归一化 A_i (r_i - mean) / std看起来简单但有个坑当组内 reward 全部相同比如全对或全错时std0除零会炸。代码里一般加一个 eps1e-8但这样算出来的 advantage 全是 0这一组样本就浪费了。更隐蔽的问题是 reward 尺度。如果 reward model 输出范围是 0 到 1组内 std 可能只有 0.1归一化后 advantage 被放大 10 倍如果 reward 范围是 -10 到 10std 可能 5归一化后 advantage 被压缩。这导致不同 batch 之间的有效学习率不一致。Dr.GRPO 的修正思路是不做除以 std只用 (r_i - mean)。这样 advantage 的尺度直接跟 reward 尺度挂钩配合固定的 learning rate 反而更稳。我实测下来在 reward model 输出范围稳定的情况下Dr.GRPO 的简化版确实比原版 GRPO 少了很多“突然崩掉”的情况。2.4 修正四长度归一化的必要性LLM 输出长度差异极大同一个 prompt 可能生成 50 token 也可能生成 500 token。PPO 和 GRPO 的原始 loss 都是对 token 求平均这导致长序列的每个 token 权重被稀释短序列的每个 token 权重被放大。结果就是模型倾向于生成短输出——因为短输出里每个 token 的梯度更大更容易被“记住”。Dr.GRPO 和 DAPO 都提出了长度归一化把 loss 除以序列长度或者用 token 级 advantage 除以长度。具体做法是在计算 policy loss 时对每个序列的 token loss 求和后除以该序列长度而不是除以总 token 数。这个改动看起来小但效果立竿见影。我在一个摘要任务上试过不加长度归一化时模型输出长度从平均 200 token 掉到 80 token加了之后稳定在 180 到 220 之间。如果你发现模型越训越“惜字如金”先检查这一项。2.5 修正五KL 约束的三种写法与选择KL 惩罚是防止策略偏离 reference model 太远的关键。PPO 里 KL 通常加在 reward 里r_total r - β * KL。GRPO 里 KL 直接加在 loss 里形式是 β * KL(π || π_ref)。但 KL 的估计方式有讲究。常见的有三种k1 估计直接用 log prob 差、k2 估计用 exp 的差、k3 估计用 (r-1) - log r 的形式。k3 是无偏且方差最小的但计算稍复杂。TRL 默认用 k1简单但方差大。我的经验是如果训练稳定k1 够用如果 KL 曲线抖动厉害换 k3β 可以设小一点比如 0.01 到 0.04。另外KL 系数不要固定死用自适应 KL目标 KL 设 0.01 到 0.05通常比固定值好调。2.6 修正六采样策略与 group 构造GRPO 的 group 是从同一个 prompt 采样多条输出。这里有个细节采样温度。温度太高输出多样性好但质量差温度太低输出几乎一样组内方差小advantage 没信息。实践中采样温度建议 0.7 到 1.0top-p 0.9 到 0.95。如果发现组内输出几乎相同把温度调高如果输出质量太差导致 reward 全低把温度调低。另外group 大小 G 和 batch 大小的关系是总样本数 batch_size * G。显存不够时优先减 batch_size保 G。2.7 修正七裁剪机制的变体PPO 的 clip 是 ratio 裁剪到 [1-ε, 1ε]ε 通常 0.1 到 0.2。GRPO 沿用这个机制但 DAPO 提出了 clip-higher把上界放宽到 1ε_high下界保持 1-ε_low且 ε_high ε_low。理由是LLM 训练中提升好动作的概率比压低坏动作更重要所以上界应该更宽松。我试过 ε_low0.2, ε_high0.28在数学推理任务上确实比对称 clip 收敛快一点。但这不是万能药如果 reward model 本身有噪声放宽上界会放大噪声导致策略过拟合到错误信号。2.8 修正八优势估计的 Reinforce 路线Reinforce 是另一条无 critic 路线它用 reward-to-go 的移动平均作为 baseline而不是组内均值。优点是可以用在在线采样场景不需要等一整组采完缺点是 baseline 的方差比组内均值大。如果你的训练框架支持流式采样Reinforce 的吞吐量比 GRPO 高因为不用等 group 凑齐。但代价是训练稳定性稍差需要更小的 learning rate 和更大的 batch。2.9 修正九动态采样与课程学习DAPO 提出的动态采样是指如果一个 prompt 的 group 内 reward 全对或全错advantage 全 0就跳过这个 prompt重新采样。这样避免浪费计算在“没有学习信号”的样本上。这个技巧在大规模训练里很实用。我算过一笔账如果 30% 的 prompt 是“全对”或“全错”跳过它们能省 30% 的计算而且不影响收敛。实现上就是在 advantage 计算后加一个 mask全 0 的组直接不参与 loss。3. 从零实现一个 GRPO 训练循环3.1 环境与依赖准备我用的是 PyTorch 2.1 TRL 0.8 transformers 4.38。硬件是 8 张 A100 80G训一个 7B 模型。如果你显存小可以用 LoRA 或者 QLoRA但注意 LoRA 会改变 KL 的计算基准需要把 reference model 也做同样的 LoRA 适配。依赖清单pip install torch2.1.0 transformers4.38.0 trl0.8.0 pip install accelerate0.27.0 deepspeed0.13.0 pip install wandb # 可选用于记录3.2 数据格式与 reward model 对接GRPO 的输入是 prompt 列表输出是 group 内多条 completion。数据格式建议用 jsonl每行一个 prompt{prompt: 请解释什么是强化学习中的信用分配问题, answer: ...}reward model 可以是规则比如数学题对答案、也可以是训练好的 RM。如果是规则 reward直接写函数返回 0 或 1如果是 RM注意它的输出范围最好做一下归一化到 [0, 1] 或 [-1, 1]。3.3 核心训练循环代码拆解下面是一个简化版的 GRPO 训练循环我删掉了分布式和混合精度的细节保留核心逻辑import torch from transformers import AutoModelForCausalLM, AutoTokenizer def grpo_step(policy, ref_model, prompts, reward_fn, G8, beta0.04, clip_eps0.2): # 1. 对每个 prompt 采样 G 条输出 all_completions [] all_log_probs [] for prompt in prompts: completions [] log_probs [] for _ in range(G): with torch.no_grad(): out policy.generate(prompt, max_new_tokens256, do_sampleTrue, temperature0.8) completions.append(out) log_probs.append(compute_log_prob(policy, prompt, out)) all_completions.append(completions) all_log_probs.append(torch.stack(log_probs)) # 2. 计算 reward rewards torch.tensor([[reward_fn(p, c) for c in comps] for p, comps in zip(prompts, all_completions)]) # 3. 组内归一化算 advantage mean_r rewards.mean(dim1, keepdimTrue) std_r rewards.std(dim1, keepdimTrue) 1e-8 advantages (rewards - mean_r) / std_r # shape: [batch, G] # 4. 计算 KL 和 policy loss total_loss 0 for i, prompt in enumerate(prompts): for j, comp in enumerate(all_completions[i]): old_log_prob all_log_probs[i][j] new_log_prob compute_log_prob(policy, prompt, comp) ratio torch.exp(new_log_prob - old_log_prob) adv advantages[i][j] # PPO clip surr1 ratio * adv surr2 torch.clamp(ratio, 1 - clip_eps, 1 clip_eps) * adv policy_loss -torch.min(surr1, surr2).mean() # KL 惩罚 ref_log_prob compute_log_prob(ref_model, prompt, comp) kl (new_log_prob - ref_log_prob).mean() total_loss policy_loss beta * kl total_loss / (len(prompts) * G) return total_loss这段代码里几个关键点采样时用torch.no_grad()省显存advantage 是组内归一化的KL 用的是 k1 估计。实际训练时compute_log_prob要对 completion 部分算不要算 prompt 部分。3.4 超参数配置与调参顺序我整理了一份 7B 模型的推荐配置你可以直接抄参数推荐值说明learning rate1e-6 到 5e-6比 SFT 小一个量级batch size64 到 128总样本数 batch * GG8 到 16显存够就 16temperature0.7 到 1.0太低没多样性top_p0.9 到 0.95不要用 top_kKL beta0.01 到 0.04自适应更好clip eps0.2对称即可max_new_tokens256 到 512看任务epochs1 到 3多了过拟合调参顺序建议先固定 lr2e-6, G8, beta0.04跑 100 步看 reward 和 KL 曲线。如果 reward 不涨先调大 lr 或 G如果 KL 爆炸调大 beta如果输出长度崩加长度归一化。3.5 训练监控与关键指标必须监控的指标reward 均值、KL、输出长度、advantage 的 std、clip fraction被裁剪的 ratio 比例。clip fraction 如果超过 0.3说明更新太激进调小 lr 或调大 clip eps。advantage std 如果接近 0说明组内没区分度调大 temperature 或换 prompt。4. 常见问题与排查技巧实录4.1 Reward 不涨或震荡的排查路径这是最常见的问题。我的排查顺序是先看 reward model 本身有没有区分度——拿几条明显好和明显差的输出喂给 RM看分数差多少。如果 RM 分不出好坏那 RL 再调也没用。然后看 advantage 的 std如果接近 0说明组内输出太像调 temperature。再看 KL如果 KL 一直涨说明策略跑偏调大 beta。最后看 lr如果以上都正常但 reward 还是震荡把 lr 减半。4.2 输出长度异常缩短或爆炸缩短的原因通常是长度偏置加长度归一化。爆炸的原因通常是 reward model 偏好长输出比如 RM 训练数据里长回答得分高这时候要么修 RM要么在 reward 里加长度惩罚。我试过加一个 -0.001 * length 的惩罚项效果立竿见影但惩罚系数要小心调太大模型就不说话了。4.3 显存不足的降级方案如果 8 张 A100 都跑不动 7B 的 GRPO降级顺序是先减 batch size 到 32再减 G 到 4再开 gradient checkpointing再上 LoRA。LoRA 的 rank 建议 16 到 64alpha 是 rank 的两倍。注意 LoRA 下 reference model 也要用同样的 LoRA 配置否则 KL 算出来是错的。4.4 训练崩溃的急救措施如果 loss 突然变 NaN先检查 reward 里有没有 inf 或 nan再检查 KL 有没有除零。急救方法是把 lr 降到 1e-7beta 调到 0.1跑几百步稳定后再慢慢恢复。如果还是崩回滚到上一个 checkpoint换一批 prompt 重跑。4.5 常见问题速查表现象可能原因解决reward 不涨RM 无区分度 / lr 太小换 RM / 调大 lrKL 爆炸beta 太小 / lr 太大调大 beta / 调小 lr输出变短长度偏置加长度归一化输出重复temperature 太低调高 temperatureloss NaNreward 有 nan / 除零检查 reward / 加 epsclip fraction 高更新激进调小 lr / 调大 clip eps显存 OOMbatch 或 G 太大减 batch / 减 G / LoRA4.6 几个我踩过的坑第一个坑reward model 的输出范围没归一化导致不同 batch 的 advantage 尺度差 10 倍训练极不稳定。后来统一把 RM 输出 clamp 到 [-1, 1] 才解决。第二个坑采样时忘了设do_sampleTrue结果 G 条输出完全一样advantage 全 0白跑一天。第三个坑KL 用 k1 估计时如果 new_log_prob 和 ref_log_prob 差太大exp 会溢出。后来换成 k3 估计稳多了。第四个坑多卡训练时每个卡的 group 是独立采样的如果不用 all_gather 同步 rewardadvantage 算出来是错的。这个坑最隐蔽因为 loss 看起来在降但实际是在各自为战。5. 适用边界什么时候该用 GRPO什么时候不该5.1 GRPO 的优势场景GRPO 最适合的场景是reward 可以比较稳定地评估规则或高质量 RM、显存受限、需要快速迭代。比如数学推理、代码生成、格式化输出这类任务reward 信号清晰GRPO 的组内归一化能很好地工作。7B 到 32B 模型上GRPO 的性价比明显高于 PPO。5.2 GRPO 的局限与不适用场景如果 reward 信号极其稀疏比如只有最终答案对错且正确率低于 5%GRPO 的组内可能全是 0advantage 全 0学不到东西。这时候要么用课程学习从简单样本开始要么用 PPO 的 critic 做 bootstrap。另外如果任务需要精细的信用分配比如多轮对话里每一步都重要GRPO 的序列级 advantage 太粗不如 PPO GAE。5.3 与 PPO、RLOO、Reinforce 的选型对照方法显存稳定性样本效率适用场景PPO高中高有 critic、reward 密集GRPO中高中大模型、reward 清晰RLOO中高中小 group、无偏 baselineReinforce低中低流式采样、吞吐优先选型逻辑很简单显存够且 reward 密集用 PPO显存紧或 reward 清晰用 GRPOgroup 小G≤8用 RLOO要流式吞吐用 Reinforce。5.4 我个人在实际操作中的体会跑了十几个 GRPO 项目后我最大的体会是GRPO 不是“更好的 PPO”而是“在特定约束下的更务实选择”。它的成功很大程度上依赖于 reward model 的质量和 group 的构造。如果 RM 本身有偏GRPO 会把偏差放大——因为组内归一化只关心相对排序不关心绝对正确性。所以我现在做 GRPO 之前会花大量时间在 RM 的评估和校准上而不是急着调 RL 超参。另一个体会是不要迷信论文里的默认值。GRPO 论文里的 G64 是在 70B 模型上跑的7B 模型用 G64 显存直接爆。我一般从 G8 开始根据 reward 曲线的方差决定要不要加到 16。KL beta 也是论文里 0.04 是个起点实际要根据 KL 曲线的斜率动态调。最后分享一个小技巧在训练初期先用一个很小的 lr比如 5e-7跑 50 步让模型“热身”再恢复到正常 lr。这样能避免一开始就因为 advantage 噪声太大而崩掉。这个技巧在多个项目上帮我省了不少重跑的时间。