
1. 从奖励到优势GRPO到底在解决什么问题第一次接触GRPOGroup Relative Policy Optimization组相对策略优化是在做一批对话模型的偏好对齐实验时。当时手头只有若干张消费级显卡跑PPOProximal 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网络显存占用高训练链路长。DPODirect Preference Optimization直接跳过奖励模型和强化学习用偏好数据做监督式微调简单但依赖成对偏好数据且无法在线采样探索。GRPO介于两者之间它保留了在线采样和奖励驱动的强化学习框架但去掉了Critic用组相对基线替代。维度PPODPOGRPO是否需要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越大组内统计量越稳定优势值估计越准但采样成本线性增长。我实测下来的经验是G4最低可用配置优势值噪声较大适合显存极度受限或快速验证。G8比较平衡的选择大多数任务上表现稳定。G16适合奖励信号噪声大、需要更稳定基线的场景但吞吐会明显下降。显存方面采样阶段的主要开销是KV Cache。以7B模型、序列长度2048为例单条序列的KV Cache大约几百MBG8时如果串行采样峰值显存和G1差不多只是时间变长如果并行采样显存会成倍增长。我的做法是串行采样、批量前向一次只生成一个样本但把G个样本的奖励计算批量做这样显存可控速度也能接受。3.2 优势值归一化的数值稳定性处理前面提到标准差归一化可能除零实际代码里要处理。标准做法是import torch def compute_advantages(rewards, eps1e-4): # rewards: shape [G] mean_r rewards.mean() std_r rewards.std(unbiasedFalse) advantages (rewards - mean_r) / (std_r eps) return advantages这里有几个细节。第一用unbiasedFalse因为我们是把这一组样本当作完整总体来算统计量不是从更大总体里抽样。第二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, 1eps) * 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模型没问题# 基础环境 python3.10 torch2.1.0cu121 transformers4.36.0 trl0.7.1 peft0.7.0 accelerate0.25.0TRL库里有GRPOTrainer但早期版本功能不全我后来是基于它改的。如果你要从零实现核心模块就四个采样器、奖励计算、优势计算、策略更新。下面按顺序讲。4.2 组采样与奖励计算的具体实现采样阶段的关键是保证同一个prompt的G个输出是独立采样的且采样时的策略是旧策略。代码骨架def sample_group(policy_model, prompt, G, max_new_tokens512, temperature0.8): outputs [] rewards [] for _ in range(G): with torch.no_grad(): output policy_model.generate( prompt, max_new_tokensmax_new_tokens, temperaturetemperature, do_sampleTrue, top_p0.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, G8, eps0.2, beta0.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, 1eps) * 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(fStep {step}, Eval Reward: {eval_reward})关键参数我整理成表参数推荐值说明G组大小8显存够可上16temperature0.8保证组内多样性clip eps0.2PPO标准值KL beta0.04根据KL散度调整学习率1e-6比SFT小一个量级每prompt更新轮数4提高样本利用率max_new_tokens512根据任务调整学习率这块要特别说。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加G8的组合在24G卡上跑7B模型比较舒服。5.3 奖励黑客与过拟合的识别奖励黑客是指模型找到了奖励函数的漏洞拿到高分但实际质量很差。比如奖励模型偏好长回答模型就疯狂输出重复内容来拉长长度。识别方法是人工抽查高分样本如果高分样本读起来明显不对就是奖励黑客。缓解手段有三个一是加入长度惩罚和重复惩罚二是用多个奖励模型投票取最低分或平均分三是定期用人工评估校准奖励模型。我自己的习惯是每训练500步就抽20个样本人工看一遍这个时间花得值。5.4 常见问题速查表问题现象可能原因解决办法奖励不涨组内方差太小提高temperature稠密化奖励奖励震荡学习率太大降低学习率到5e-7KL爆炸beta太小调大beta到0.1显存OOMG太大或序列太长减小G开启梯度检查点输出重复奖励偏好长度加长度惩罚和重复惩罚优势值全0组内奖励相同检查奖励函数和采样多样性训练后期崩过拟合奖励模型早停用KL监控5.5 几个反直觉的实操心得第一个心得G不是越大越好。我试过G32优势值确实更稳但采样时间太长单位时间内的更新次数太少整体训练效率反而下降。G8在大多数任务上是性价比最高的。第二个心得奖励归一化比奖励设计更重要。一开始我花很多时间设计复杂的奖励函数后来发现只要奖励能区分好坏剩下的交给组内归一化就行。归一化做得好简单奖励也能训出好效果。第三个心得KL惩罚要动态调。固定beta在训练初期合适后期可能太松或太紧。我后来改成根据KL散度自适应调整betaKL大就调大betaKL小就调小训练稳定很多。第四个心得参考模型的选择很关键。参考模型太强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.730对应的是-0.58。这个范围还好。但如果一组奖励是[0, 0, 0, 0, 0, 0, 0, 1]均值0.125标准差0.331对应的优势值是2.65还在可接受范围。真正危险的是奖励模型给出异常高分的情况比如一组奖励是[0.1, 0.1, 0.1, 0.9]归一化后0.9对应2.30.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系数这些参数没有万能值得靠实验手感。