☰
从PPO到GRPO:九种修正与LLM强化学习微调实战
2026/10/1 4:54:20 网站建设 项目流程

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 的核心洞察:用组内相对值替代绝对价值

GRPO(Group Relative Policy Optimization)的破局点非常干脆:既然 critic 学不准,那就不学了。它的做法是对同一个 prompt 采样一组(group)输出,比如 G=8 或 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 只来自当前这一组样本。组小的时候(比如 G=4),均值估计噪声很大,advantage 抖动明显。这就是为什么 GRPO 论文里推荐 G 至少 8,实践中 16 更稳。我自己的经验是,G=8 在 7B 模型上勉强能用,但 32B 以上建议 G=16 起步,否则训练曲线会像心电图。

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 和 RLOO(REINFORCE 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 和当前样本独立,理论上无偏。代价是计算稍微麻烦一点,但 G=4 到 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 场景的适配与局限

GAE(Generalized 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 全部相同(比如全对或全错)时,std=0,除零会炸。代码里一般加一个 eps=1e-8,但这样算出来的 advantage 全是 0,这一组样本就浪费了。

更隐蔽的问题是 reward 尺度。如果 reward model 输出范围是 0 到 1,组内 std 可能只有 0.1,归一化后 advantage 被放大 10 倍;如果 reward 范围是 -10 到 10,std 可能 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.0,top-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 训练中,提升好动作的概率比压低坏动作更重要,所以上界应该更宽松。

我试过 ε_low=0.2, ε_high=0.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 torch==2.1.0 transformers==4.38.0 trl==0.8.0 pip install accelerate==0.27.0 deepspeed==0.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, G=8, beta=0.04, clip_eps=0.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_tokens=256, do_sample=True, temperature=0.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(dim=1, keepdim=True) std_r = rewards.std(dim=1, keepdim=True) + 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 * G
G8 到 16显存够就 16
temperature0.7 到 1.0太低没多样性
top_p0.9 到 0.95不要用 top_k
KL beta0.01 到 0.04自适应更好
clip eps0.2对称即可
max_new_tokens256 到 512看任务
epochs1 到 3多了过拟合

调参顺序建议:先固定 lr=2e-6, G=8, beta=0.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 到 64,alpha 是 rank 的两倍。注意 LoRA 下 reference model 也要用同样的 LoRA 配置,否则 KL 算出来是错的。

4.4 训练崩溃的急救措施

如果 loss 突然变 NaN,先检查 reward 里有没有 inf 或 nan,再检查 KL 有没有除零。急救方法是把 lr 降到 1e-7,beta 调到 0.1,跑几百步稳定后再慢慢恢复。如果还是崩,回滚到上一个 checkpoint,换一批 prompt 重跑。

4.5 常见问题速查表

现象可能原因解决
reward 不涨RM 无区分度 / lr 太小换 RM / 调大 lr
KL 爆炸beta 太小 / lr 太大调大 beta / 调小 lr
输出变短长度偏置加长度归一化
输出重复temperature 太低调高 temperature
loss NaNreward 有 nan / 除零检查 reward / 加 eps
clip fraction 高更新激进调小 lr / 调大 clip eps
显存 OOMbatch 或 G 太大减 batch / 减 G / LoRA

4.6 几个我踩过的坑

第一个坑:reward model 的输出范围没归一化,导致不同 batch 的 advantage 尺度差 10 倍,训练极不稳定。后来统一把 RM 输出 clamp 到 [-1, 1] 才解决。

第二个坑:采样时忘了设do_sample=True,结果 G 条输出完全一样,advantage 全 0,白跑一天。

第三个坑:KL 用 k1 估计时,如果 new_log_prob 和 ref_log_prob 差太大,exp 会溢出。后来换成 k3 估计,稳多了。

第四个坑:多卡训练时,每个卡的 group 是独立采样的,如果不用 all_gather 同步 reward,advantage 算出来是错的。这个坑最隐蔽,因为 loss 看起来在降,但实际是在各自为战。

5. 适用边界:什么时候该用 GRPO,什么时候不该

5.1 GRPO 的优势场景

GRPO 最适合的场景是:reward 可以比较稳定地评估(规则或高质量 RM)、显存受限、需要快速迭代。比如数学推理、代码生成、格式化输出这类任务,reward 信号清晰,GRPO 的组内归一化能很好地工作。7B 到 32B 模型上,GRPO 的性价比明显高于 PPO。

5.2 GRPO 的局限与不适用场景

如果 reward 信号极其稀疏(比如只有最终答案对错,且正确率低于 5%),GRPO 的组内可能全是 0,advantage 全 0,学不到东西。这时候要么用课程学习从简单样本开始,要么用 PPO 的 critic 做 bootstrap。另外,如果任务需要精细的信用分配(比如多轮对话里每一步都重要),GRPO 的序列级 advantage 太粗,不如 PPO + GAE。

5.3 与 PPO、RLOO、Reinforce++ 的选型对照

方法显存稳定性样本效率适用场景
PPO高中高有 critic、reward 密集
GRPO中高中大模型、reward 清晰
RLOO中高中小 group、无偏 baseline
Reinforce++低中低流式采样、吞吐优先

选型逻辑很简单:显存够且 reward 密集,用 PPO;显存紧或 reward 清晰,用 GRPO;group 小(G≤8),用 RLOO;要流式吞吐,用 Reinforce++。

5.4 我个人在实际操作中的体会

跑了十几个 GRPO 项目后,我最大的体会是:GRPO 不是“更好的 PPO”,而是“在特定约束下的更务实选择”。它的成功很大程度上依赖于 reward model 的质量和 group 的构造。如果 RM 本身有偏,GRPO 会把偏差放大——因为组内归一化只关心相对排序,不关心绝对正确性。所以我现在做 GRPO 之前,会花大量时间在 RM 的评估和校准上,而不是急着调 RL 超参。

另一个体会是:不要迷信论文里的默认值。GRPO 论文里的 G=64 是在 70B 模型上跑的,7B 模型用 G=64 显存直接爆。我一般从 G=8 开始,根据 reward 曲线的方差决定要不要加到 16。KL beta 也是,论文里 0.04 是个起点,实际要根据 KL 曲线的斜率动态调。

最后分享一个小技巧:在训练初期,先用一个很小的 lr(比如 5e-7)跑 50 步,让模型“热身”,再恢复到正常 lr。这样能避免一开始就因为 advantage 噪声太大而崩掉。这个技巧在多个项目上帮我省了不少重跑的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询