1. 从奖励到优势:GRPO到底在解决什么问题
第一次接触GRPO(Group Relative Policy Optimization,组相对策略优化)是在做一批对话模型的偏好对齐实验时。当时手头只有若干张消费级显卡,跑PPO(Proximal Policy Optimization)时显存和吞吐都卡得很难受,尤其是Critic网络和Actor网络同时驻留显存,动不动就OOM。后来翻到GRPO的思路,核心一句话就能概括:用一组采样样本的相对奖励代替独立的价值网络来估计优势值。这一下就把Critic网络整个砍掉了,显存占用直接降下来一大截。
先把概念摆清楚。强化学习里,策略优化的目标是让模型在给定状态下选择能带来更高长期回报的动作。PPO的做法是训练一个价值网络(Critic)去估计状态价值V(s),然后用实际回报减去V(s)得到优势值A(s,a),再用这个优势值去加权策略梯度。问题在于,Critic本身也要训练,它和Actor共享或并行占用显存,而且Critic估计不准的时候,优势值就是噪声,策略更新方向会跑偏。
GRPO的破局点在于:不训练Critic,而是对同一个prompt采样一组(Group)输出,用这组输出的奖励均值作为基线(baseline),每个样本的奖励减去这个均值,就得到了相对优势。数学上非常干净:
A_i = (r_i - mean(r_group)) / std(r_group)其中r_i是第i个样本的奖励,mean和std是这一组样本奖励的均值和标准差。这个式子就是GRPO里优势值的核心计算方式。它和传统优势函数的区别在于,基线不是学出来的,而是从同组样本里统计出来的。
为什么这个设计合理?因为对于同一个问题,模型采样出的多个回答,它们的奖励差异本身就反映了哪些回答更好、哪些更差。用组内均值做基线,相当于问:“在这个问题的所有尝试里,这个回答比平均水平好多少?”这比用一个独立网络去猜“这个状态本身值多少分”要稳定得多,尤其是在奖励信号稀疏或者奖励模型本身有噪声的场景下。
适合谁来参考这篇内容?如果你正在做LLM的偏好对齐、数学推理、代码生成这类需要奖励信号驱动的任务,并且手头算力有限、不想维护Critic网络,GRPO是非常值得上手的方向。如果你只是想理解强化学习里奖励值和优势值的关系,这篇也会把这条链路讲透。下面我会从整体设计、核心细节、实操流程、问题排查四个层面展开,把踩过的坑和能直接抄的配置都放出来。
2. 整体设计与思路拆解:为什么是“组相对”而不是“价值估计”
2.1 奖励值、优势值与策略梯度的三角关系
要理解GRPO,得先把奖励值(Reward)和优势值(Advantage)这两个概念的关系理清楚。很多人初学时会混淆,觉得奖励高就直接更新策略,其实不是。
奖励值r是环境或奖励模型给单次交互的即时反馈,它回答的是“这个动作/这个输出好不好”。但策略梯度真正需要的是优势值A,它回答的是“这个动作比当前策略的平均水平好多少”。为什么不能直接用奖励?因为奖励的绝对值没有可比性。比如一道数学题,奖励模型给所有回答都打了0.8分,那这0.8分说明不了任何问题,模型不知道哪个回答更值得强化。优势值通过减去基线,把“绝对好坏”变成了“相对好坏”,梯度信号才有意义。
传统PPO里,基线由Critic网络估计的V(s)提供:
A(s,a) = r + gamma * V(s') - V(s)这个式子依赖Critic的准确性。Critic训练需要大量交互数据,而且在LLM场景下,状态空间是token序列,价值估计非常困难。GRPO换了个思路:既然同一个prompt下采样多个回答,那这些回答面对的是同一个“状态”(同一个prompt),直接用组内奖励统计量做基线,就绕开了价值估计。
2.2 GRPO的组采样机制与基线选择
GRPO的“组”体现在对每个prompt采样G个输出(通常G取4到16)。这G个输出共享同一个prompt,但生成过程有随机性,所以奖励会有差异。基线就是这G个奖励的均值。
这里有个关键设计选择:为什么要除以标准差。如果不除标准差,当一组奖励的方差很小时,优势值也会很小,梯度更新几乎没效果;方差大时优势值又过大,更新不稳定。除以标准差相当于做了一次归一化,让不同组之间的优势值尺度一致。这个操作和Batch Normalization的思路很像,都是把信号标准化到可训练的范围内。
但要注意,标准差归一化在组内奖励完全相同时会导致除零。实际实现里会加一个极小值epsilon,比如1e-4。这个细节后面在实操部分会再展开。
2.3 与PPO、DPO的横向对比
把GRPO放在算法谱系里看会更清楚。PPO是Actor-Critic架构,需要Critic网络,显存占用高,训练链路长。DPO(Direct Preference Optimization)直接跳过奖励模型和强化学习,用偏好数据做监督式微调,简单但依赖成对偏好数据,且无法在线采样探索。GRPO介于两者之间:它保留了在线采样和奖励驱动的强化学习框架,但去掉了Critic,用组相对基线替代。
| 维度 | PPO | DPO | GRPO |
|---|---|---|---|
| 是否需要Critic | 是 | 否 | 否 |
| 是否需要奖励模型 | 是 | 否(用偏好对) | 是 |
| 是否在线采样 | 是 | 否 | 是 |
| 显存占用 | 高 | 低 | 中 |
| 优势值来源 | Critic估计 | 无 | 组内相对奖励 |
| 适合场景 | 通用RLHF | 静态偏好对齐 | 算力受限的在线对齐 |
从表里能看出来,GRPO的定位很明确:要在线采样的探索能力,但不要Critic的显存和训练负担。这也是它在数学推理、代码生成这类需要大量采样探索的任务里特别受欢迎的原因。
2.4 奖励值设计对优势值质量的决定性影响
GRPO的优势值完全由奖励值推导而来,所以奖励函数的设计直接决定了训练效果。这里有个容易被忽略的点:奖励的尺度和分布比奖励的绝对值更重要。
举个例子,如果奖励模型对一组回答给出[0.9, 0.91, 0.92, 0.93]这样的分数,组内均值0.915,标准差很小,归一化后优势值会被放大,但原始差异其实很微弱,放大后可能引入噪声。反过来,如果奖励是[0.1, 0.5, 0.9, 0.3],差异明显,归一化后的优势值就能清晰区分好坏。
所以在实操中,我通常会对奖励做一次预处理:对于规则可验证的任务(如数学题答案对错),直接用0/1奖励;对于奖励模型打分的任务,先在小批量上统计奖励分布,如果方差过小,考虑调整奖励模型的prompt或加入长度惩罚、格式惩罚等辅助信号来拉开差距。
3. 核心细节解析与实操要点:优势值计算的每一步
3.1 组采样数量G的选择与显存权衡
G是GRPO里最核心的超参数之一。G越大,组内统计量越稳定,优势值估计越准,但采样成本线性增长。我实测下来的经验是:
- G=4:最低可用配置,优势值噪声较大,适合显存极度受限或快速验证。
- G=8:比较平衡的选择,大多数任务上表现稳定。
- G=16:适合奖励信号噪声大、需要更稳定基线的场景,但吞吐会明显下降。
显存方面,采样阶段的主要开销是KV Cache。以7B模型、序列长度2048为例,单条序列的KV Cache大约几百MB,G=8时如果串行采样,峰值显存和G=1差不多,只是时间变长;如果并行采样,显存会成倍增长。我的做法是串行采样、批量前向:一次只生成一个样本,但把G个样本的奖励计算批量做,这样显存可控,速度也能接受。
3.2 优势值归一化的数值稳定性处理
前面提到标准差归一化可能除零,实际代码里要处理。标准做法是:
import torch def compute_advantages(rewards, eps=1e-4): # rewards: shape [G] mean_r = rewards.mean() std_r = rewards.std(unbiased=False) advantages = (rewards - mean_r) / (std_r + eps) return advantages这里有几个细节。第一,用unbiased=False,因为我们是把这一组样本当作完整总体来算统计量,不是从更大总体里抽样。第二,eps加在分母上,防止std为0。第三,如果组内奖励完全一致,优势值全为0,这时候策略梯度为0,相当于这个prompt不产生更新信号,这是合理的——所有回答一样好,没什么可学的。
还有一个进阶技巧:对优势值做裁剪。和PPO一样,GRPO也会对策略比率做clip,但优势值本身如果过大,也会导致更新过猛。我通常会把归一化后的优势值再clip到[-5, 5]范围,防止极端值主导更新。
3.3 奖励值的来源与预处理
GRPO的奖励可以来自多个渠道,常见的有:
- 规则奖励:数学题答案匹配、代码单元测试通过、格式校验。这类奖励确定性强,噪声低,是GRPO最理想的奖励来源。
- 奖励模型:训练一个打分模型,对回答质量打分。这类奖励灵活但有噪声,需要配合归一化使用。
- 混合奖励:规则奖励为主,奖励模型为辅,加权求和。
我踩过的一个坑是:奖励模型打分范围不统一。有的样本奖励在0到1之间,有的在-1到1之间,混在一起算组内均值时,尺度不一致会导致优势值失真。解决办法是在计算优势值之前,先对奖励做一次全局或批次内的标准化,确保所有奖励在同一尺度上。
另一个坑是长度偏差。奖励模型往往偏好长回答,导致组内长回答奖励普遍偏高,优势值把模型往“写更长”的方向推,而不是往“写得更好”的方向推。缓解方法是在奖励里加入长度惩罚项,或者用长度归一化的奖励。
3.4 策略比率裁剪与KL惩罚的配合
GRPO虽然去掉了Critic,但PPO的另一个核心机制——策略比率裁剪——还是保留的。策略比率是当前策略和旧策略在同一个动作上的概率比:
ratio = pi_new(a|s) / pi_old(a|s)裁剪的目的是防止单次更新步子太大,把策略带偏。GRPO的损失函数大致是:
L = -min(ratio * A, clip(ratio, 1-eps, 1+eps) * A) + beta * KL(pi_new || pi_ref)其中A是组相对优势值,KL项是当前策略和参考策略的散度惩罚,防止模型偏离太远。beta通常取0.01到0.1之间。这里要注意,KL惩罚和优势值是两个独立的正则化手段:优势值控制更新方向,KL控制更新幅度。
实操中我发现,KL系数不宜过大。如果beta太大,模型几乎不更新,训练停滞;太小则容易过拟合奖励模型。我的经验值是先从0.04开始,观察KL散度的变化,如果KL持续增长超过初始值的10倍,就调大beta。
4. 实操过程与核心环节实现:从采样到更新的完整链路
4.1 环境准备与依赖配置
先列一下我用的环境,这套配置在单卡24G显存上跑7B模型没问题:
# 基础环境 python=3.10 torch=2.1.0+cu121 transformers=4.36.0 trl=0.7.1 peft=0.7.0 accelerate=0.25.0TRL库里有GRPOTrainer,但早期版本功能不全,我后来是基于它改的。如果你要从零实现,核心模块就四个:采样器、奖励计算、优势计算、策略更新。下面按顺序讲。
4.2 组采样与奖励计算的具体实现
采样阶段的关键是保证同一个prompt的G个输出是独立采样的,且采样时的策略是旧策略。代码骨架:
def sample_group(policy_model, prompt, G, max_new_tokens=512, temperature=0.8): outputs = [] rewards = [] for _ in range(G): with torch.no_grad(): output = policy_model.generate( prompt, max_new_tokens=max_new_tokens, temperature=temperature, do_sample=True, top_p=0.95 ) outputs.append(output) rewards.append(compute_reward(prompt, output)) return outputs, torch.tensor(rewards)这里temperature设0.8是为了保证组内有足够的多样性。如果temperature太低,G个输出几乎一样,奖励方差为0,优势值全为0,训练没信号。如果太高,输出质量下降,奖励普遍偏低。0.7到1.0之间是比较好的区间。
奖励计算函数根据任务不同而不同。以数学题为例:
def compute_reward(prompt, output): # 提取答案 pred = extract_answer(output) gold = extract_gold(prompt) if pred is None: return 0.0 if pred == gold: return 1.0 else: return 0.0这是最简版本。实际中我会加格式奖励(比如答案必须放在指定标记里)和部分正确奖励(比如数值接近给0.3分),让奖励信号更稠密。
4.3 优势值计算与策略更新的代码细节
拿到一组奖励后,计算优势值并更新策略:
def grpo_update(policy_model, ref_model, optimizer, prompts, G=8, eps=0.2, beta=0.04): for prompt in prompts: outputs, rewards = sample_group(policy_model, prompt, G) advantages = compute_advantages(rewards) # 计算旧策略下的log概率 with torch.no_grad(): old_log_probs = compute_log_probs(policy_model, prompt, outputs) ref_log_probs = compute_log_probs(ref_model, prompt, outputs) # 多轮更新 for _ in range(4): new_log_probs = compute_log_probs(policy_model, prompt, outputs) ratio = torch.exp(new_log_probs - old_log_probs) # 裁剪 surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1-eps, 1+eps) * advantages policy_loss = -torch.min(surr1, surr2).mean() # KL惩罚 kl = (new_log_probs - ref_log_probs).mean() loss = policy_loss + beta * kl optimizer.zero_grad() loss.backward() optimizer.step()这段代码里有几个关键点。第一,old_log_probs是在采样时计算的,更新过程中不变,这是PPO系算法的标准做法。第二,同一个prompt的样本可以做多轮更新(这里4轮),提高样本利用率。第三,KL项用的是当前策略和参考策略的log概率差,参考策略通常是SFT后的模型,固定不动。
4.4 训练循环与关键参数配置
完整的训练循环大概是这个结构:
for epoch in range(num_epochs): for batch_prompts in dataloader: grpo_update(policy_model, ref_model, optimizer, batch_prompts) # 定期评估 if step % eval_interval == 0: eval_reward = evaluate(policy_model, eval_set) print(f"Step {step}, Eval Reward: {eval_reward}")关键参数我整理成表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| G(组大小) | 8 | 显存够可上16 |
| temperature | 0.8 | 保证组内多样性 |
| clip eps | 0.2 | PPO标准值 |
| KL beta | 0.04 | 根据KL散度调整 |
| 学习率 | 1e-6 | 比SFT小一个量级 |
| 每prompt更新轮数 | 4 | 提高样本利用率 |
| max_new_tokens | 512 | 根据任务调整 |
学习率这块要特别说。GRPO的学习率通常比SFT小,因为强化学习的更新方向噪声更大,步子大了容易崩。我试过5e-7到5e-6之间,1e-6是比较稳的起点。
5. 常见问题与排查技巧实录
5.1 奖励不涨或震荡的排查思路
训练GRPO最常见的问题就是奖励曲线不涨或者剧烈震荡。按我的排查顺序,先看三个地方:
第一,组内奖励方差。如果G个样本的奖励几乎一样,优势值接近0,策略没有更新信号。这时候要检查temperature是不是太低,或者奖励函数是不是太粗糙(比如只有0/1,且大多数样本都是0)。解决办法是提高temperature,或者设计更稠密的奖励。
第二,KL散度。如果KL散度快速增长,说明策略偏离参考模型太快,可能是beta太小或学习率太大。把beta调大、学习率调小,观察KL是否稳定。
第三,奖励尺度。如果奖励值范围很大(比如0到100),优势值归一化后虽然尺度统一了,但原始奖励的噪声也被放大。建议把奖励缩放到0到1之间再计算优势值。
5.2 显存不足时的降级方案
显存不够是常态,我整理了一套降级顺序:
- 减小G:从8降到4,显存和采样时间都减半。
- 减小max_new_tokens:如果任务不需要长输出,从512降到256。
- 开启梯度检查点:用时间换显存,通常能省30%到40%。
- 使用LoRA:只训练低秩适配器,显存占用大幅下降。
- 减小batch size:最后的手段,会降低训练稳定性。
我一般优先用LoRA加G=8的组合,在24G卡上跑7B模型比较舒服。
5.3 奖励黑客与过拟合的识别
奖励黑客是指模型找到了奖励函数的漏洞,拿到高分但实际质量很差。比如奖励模型偏好长回答,模型就疯狂输出重复内容来拉长长度。识别方法是人工抽查高分样本,如果高分样本读起来明显不对,就是奖励黑客。
缓解手段有三个:一是加入长度惩罚和重复惩罚;二是用多个奖励模型投票,取最低分或平均分;三是定期用人工评估校准奖励模型。我自己的习惯是每训练500步就抽20个样本人工看一遍,这个时间花得值。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 奖励不涨 | 组内方差太小 | 提高temperature,稠密化奖励 |
| 奖励震荡 | 学习率太大 | 降低学习率到5e-7 |
| KL爆炸 | beta太小 | 调大beta到0.1 |
| 显存OOM | G太大或序列太长 | 减小G,开启梯度检查点 |
| 输出重复 | 奖励偏好长度 | 加长度惩罚和重复惩罚 |
| 优势值全0 | 组内奖励相同 | 检查奖励函数和采样多样性 |
| 训练后期崩 | 过拟合奖励模型 | 早停,用KL监控 |
5.5 几个反直觉的实操心得
第一个心得:G不是越大越好。我试过G=32,优势值确实更稳,但采样时间太长,单位时间内的更新次数太少,整体训练效率反而下降。G=8在大多数任务上是性价比最高的。
第二个心得:奖励归一化比奖励设计更重要。一开始我花很多时间设计复杂的奖励函数,后来发现只要奖励能区分好坏,剩下的交给组内归一化就行。归一化做得好,简单奖励也能训出好效果。
第三个心得:KL惩罚要动态调。固定beta在训练初期合适,后期可能太松或太紧。我后来改成根据KL散度自适应调整beta,KL大就调大beta,KL小就调小,训练稳定很多。
第四个心得:参考模型的选择很关键。参考模型太强,KL惩罚会限制策略探索;参考模型太弱,KL惩罚形同虚设。通常用SFT后的模型做参考,如果SFT模型本身质量差,考虑用更强的基座模型。
6. 奖励值与优势值的进阶调优:从能跑到跑好
6.1 多奖励信号的加权与归一化
实际任务里往往有多个奖励信号,比如正确性奖励、格式奖励、长度奖励。加权求和是最直接的方式:
r_total = w1 * r_correct + w2 * r_format + w3 * r_length但权重怎么定?我的做法是先单独跑每个奖励,观察它们的数值范围和方差,然后让每个奖励在加权后的贡献大致相当。比如正确性奖励是0/1,方差0.25;格式奖励也是0/1,方差0.25;长度奖励是连续的,方差可能很大。这时候要给长度奖励一个小权重,防止它主导总奖励。
更精细的做法是对每个奖励分别做组内归一化,再加权求和。这样每个奖励的优势值尺度一致,权重才有可比性。代价是计算量增加,但效果通常更好。
6.2 优势值裁剪与梯度噪声控制
优势值裁剪前面提过,这里展开讲。归一化后的优势值理论上应该在-3到3之间(正态分布假设),但实际中可能出现极端值。比如一组奖励是[0, 0, 0, 1],均值0.25,标准差0.43,归一化后1对应的优势值是1.73,0对应的是-0.58。这个范围还好。但如果一组奖励是[0, 0, 0, 0, 0, 0, 0, 1],均值0.125,标准差0.33,1对应的优势值是2.65,还在可接受范围。
真正危险的是奖励模型给出异常高分的情况,比如一组奖励是[0.1, 0.1, 0.1, 0.9],归一化后0.9对应2.3,0.1对应-0.77。如果奖励模型抽风给出[0.1, 0.1, 0.1, 10],归一化后10对应1.73,反而因为标准差被拉大而缩小了。所以归一化本身有一定的抗异常值能力,但为了保险,我还是会clip到[-5, 5]。
6.3 课程学习与难度调度
GRPO训练后期容易遇到奖励饱和的问题:模型在简单题上已经满分,难题上还是零分,组内奖励方差变小,优势值信号减弱。解决办法是课程学习:先训练简单题,等奖励上来后逐步加入难题。
具体操作是把训练集按难度分层,每个epoch调整各层的采样比例。比如前1000步只用简单题,1000到2000步简单题和中等题各半,2000步后加入难题。这样模型始终有学习信号,不会因为全难题导致组内奖励全零。
难度怎么定义?对于数学题,可以用题目所需的推理步数或历史通过率来分层。对于代码题,可以用单元测试的通过率。没有明确难度指标的任务,可以用模型在当前策略下的平均奖励作为难度的代理指标。
6.4 评估指标与早停策略
GRPO训练不能只看训练奖励,因为奖励可能被黑客。我通常监控四个指标:
- 训练奖励均值:反映模型在当前奖励函数下的表现。
- 评估集奖励:用独立评估集,反映泛化能力。
- KL散度:反映策略偏离程度。
- 输出长度和重复率:反映是否出现退化。
早停策略是:如果评估集奖励连续3次评估不涨,或者KL散度超过初始值的5倍,就停止训练。我一般不会等到训练奖励饱和才停,因为那时候往往已经过拟合了。
7. 我踩过的坑与最后分享的几个技巧
第一个坑是采样时用了训练模式。早期实现时忘了在采样阶段加torch.no_grad()和model.eval(),导致dropout开启,组内样本差异过大,奖励方差爆炸,训练完全不稳定。后来固定了采样流程,这个问题再没出现过。
第二个坑是奖励函数里的字符串匹配太严格。数学题答案提取时,模型输出“答案是42”和“42”应该都算对,但早期实现只匹配纯数字,导致大量正确回答被误判为错误。后来加了正则提取和模糊匹配,奖励信号质量提升明显。
第三个坑是参考模型和策略模型共享参数。一开始为了省显存,参考模型直接指向策略模型,结果KL项恒为0,惩罚失效,模型很快过拟合奖励。后来老老实实复制一份参考模型,冻结参数,KL惩罚才起作用。
最后分享一个小技巧:用组内奖励的中位数代替均值做基线。均值容易受极端值影响,中位数更鲁棒。我试过在奖励噪声大的任务上用中位数,训练稳定性有提升。代价是中位数的梯度不如均值平滑,需要配合更小的学习率。这个技巧不是万能的,但在奖励模型质量一般的时候值得一试。
GRPO这套东西,核心就是把优势值的估计从“学一个网络”变成“算一组统计量”,思路简单但效果扎实。奖励值的设计和优势值的归一化是两条主线,把这两条线理顺了,训练基本不会出大问题。剩下的就是根据具体任务调G、调temperature、调KL系数,这些参数没有万能值,得靠实验手感。