周五上午九点的技术分享,我差不多复盘了整整一个周末。题目是“GDPO 与 GRPO 在多奖励 RL 训练中的优势崩溃问题”,原本只是团队内部例会的一个议题,结果讨论得太深,大家干脆拉了个线上旁听频道。会后不少朋友私信我把思路整理成文,这便有了这一篇。
先说结论:GRPO 在多奖励(multi-reward)场景下出现的“优势崩溃”(advantage collapse),不是调几个权重就能糊弄过去的简单 bug,而是优势估计机制与奖励拼接方式之间的结构性冲突。GDPO 这类把优势按奖励维度先分解、再动态融合的方法,是目前我在实践中验证过最稳定、也最好解释的解法之一。这篇文章会从原理、复现、解法、参数配置到边界条件,把整条链路完整展开。
1. 从多奖励训练说起:GRPO 的优点与它的“暗坑”
1.1 GRPO 为什么能省下 critic:组内相对优势的原理
先快速对齐一下 GRPO 的基本逻辑。GRPO 最早出现在开源推理模型相关的强化学习论文里,核心动机是省掉 PPO 中那个与策略模型同等规模的价值网络(critic)。PPO 的优势函数通常写成:
A = returns - V(s)
也就是用“实际回报”减去“价值网络预测的期望回报”,差值就是某个动作比预期好多少或差多少。可价值网络本身训练起来又慢又贵,存储和推理开销也不小。
GRPO 换了一条路:同一个 prompt 下,采样一组(group)response,然后直接在这组 response 内部做归一化。假设某组有 n 条 response,奖励分别是 r1 到 rn,那么第 i 条 response 的优势是:
A_i = (r_i - mean(r_group)) / std(r_group)
这个公式的意思是:我们不关心绝对分数,只看这组兄弟里谁相对更好。省掉 critic 的前提是“组内相对排序能替代绝对值”,这在单个奖励信号、且采样组内方差适中的情况下,效果确实很好。省显存、省训练时间,还避免了价值网络偏差对策略更新的污染。
1.2 多奖励拼接的诱惑与隐患:三个保证的连环崩
当训练目标从单一奖励变成多奖励——例如格式奖励、正确答案奖励、安全性奖励、风格对齐奖励同时存在——最常见、最省事的做法就是线性加权求和:
R_total = w1 * r1 + w2 * r2 + w3 * r3 ...
接着再对 R_total 做组内归一化,得到优势。很多团队一开始都会走这条路,毕竟代码改动最小。但问题恰恰出在这里。
多奖励拼接会破坏 GRPO 正常工作的三个隐含保证。
第一个保证是“同一组内,奖励差异主要由模型的真实改进驱动”。可多个奖励同时存在时,组内差异可能来自某个奖励的固有噪声,而噪声恰恰主导了排序。
第二个保证是“奖励的量纲和方差在一个数量级”。现实中根本不是这样——格式奖励通常是 0/1 的二值信号,正确答案奖励可能是一个 0 到 1 的连续分但极端值很多,风格奖励可能是模型打出的 0 到 5 的分数。方差一差就是好几倍。
第三个保证是“所有奖励与策略改进是同方向的”。多奖励之间经常相互制约,例如“格式正确但答案错误”和“答案正确但格式错误”,这两类样本在组内的排序会被完全不同的维度支配。
当这三个保证被破坏,组内归一化就变得不稳定。于是,GRPO 在多奖励场景下的“优势崩溃”出现了。
2. 亲手复现一次“优势崩溃”:现象、脚本与训练日志
2.1 复现设置:两个奖励通道的极端对照
我说“崩溃”之前,最好先有可复盘的证据。我这边在内部做了一次极简复现实验:任务是一个带格式约束的数学问答,奖励只保留两路——格式分(format,二值判定)和答案正确分(correct,逐步评分,0到1)。
训练配置如下:
- 模型:一个 7B 量级的开源底座
- 每 prompt 组大小:8
- 奖励加权:w_format=1.0,w_correct=1.0
- 训练步长:每批 128 个 prompts
- 优化器与学习率:AdamW,lr=5e-6
- 采样温度:0.8
表面上看,权重比例是 1:1,非常合理。但在训练到大约 300 步左右时,我发现几个关键指标开始出现明显异常。当时第一反应不是“崩溃”,而是“怎么收益曲线长这样”。
2.2 崩溃的三阶段特征曲线与权重诊断
把训练日志拉出来,优势崩溃基本会经历三个阶段。
第一阶段:初期正常,平均优势值大致在 0.5 到 1.0 之间,策略熵也在缓慢下降。此时格式奖励还处于“多数样本能拿到 0/1、偶尔有极端值”的状态,correct 奖励则显得平滑。
第二阶段:大概在 200 步之后,模型快速学会了“把格式搞对”,format 奖励的组内方差急剧下降。可 format 奖励仍带着 1.0 的权重参与求和,所以 R_total 的组内差异就只剩下 correct 那一路在贡献。此时公式分母 std(R_total) 如果校正不足,某些组里所有 response 的 correct 分都很接近,优势值会整体坍缩到 0 周围的窄带。策略梯度几乎失去方向,loss 看起来在走平,实际是训练在空转。
第三阶段:一旦组内出现一个“格式对但答案全错”的离群样本,它的奖励会被极端放大。我见过单个样本的优势值瞬间跳高到 8 倍标准差以上,随后策略猛地把对正确性的权重降下来,开始往回抽风。训练日志里最直接的体现是:advantage_std 从约 0.6 突然飙升到 4.0 以上,然后 KL 项剧烈震荡,回答分布开始两极分化。
我用一个排查脚本把每个奖励通道的归一化贡献单独打出来,可以说是证据确凿:
stats = {} for rew_name in reward_names: r = train_batch[rew_name] # shape: [batch_size, group_size] stats[f'{rew_name}_mean'] = r.mean().item() stats[f'{rew_name}_std'] = r.std().item() stats[f'{rew_name}_rel_gain'] = (r.max(dim=-1).values - r.min(dim=-1).values).mean().item() print(stats)结果显示,format 通道的rel_gain从 0.9 掉到 0.05,几乎不再提供排序信息;而 correct 通道的rel_gain仍然在 0.5 上下。可由于求和后再归一化,format 那一把“死掉的权重”还是挂在总奖励里,拉低了有效信号的信噪比。
2.3 关于崩溃的判定,给几个务实的检验方法
以后你遇到训练曲线异常,不要凭感觉说“崩了”。我这边总结出三个可靠判定方式。
第一个是看“组内优势分布”的形状。正常 GRPO 的优势分布近似对称、单峰,标准差稳定在 0.3 到 1.5 之间;崩溃时会出现明显的双峰或重尾,标准差剧烈波动。
第二个是单样本优势与最终回报的散点关系。如果单样本优势高、但它所在的组内总奖励并不高,说明优势空间里混入了和真实目标不一致的排序。
第三个是分奖励维度的“组内相对增益”变化。上面那个脚本已经写了,因为它是按 channel 统计的。如果一个奖励通道的相对增益持续走低,优势矩阵主要由另一个通道驱动,而混合优势仍是按原权重算出来的,那么崩溃只是时间问题。
3. GDPO 的核心解法:按奖励维度分离优势,再动态融合
3.1 第一刀:将总奖励拆成独立优势通道
GDPO 的做法用一句话说,就是不先去加总奖励,而是让每一个奖励维度都单独经历一次组内归一化,得到各自的“优势分量”:
A_i^jm = (r_i^jm - mean(r^jm_group)) / std(r^jm_group)
这样做的理由很直接:每个奖励维度先在自己的坐标系里对齐,量纲影响被清除了。格式奖励即使组内差异很小,它的优势也只反映“通过格式而拉开差距的部分”,不会被其他维度的噪声淹没。
之后才是融合。但 GDPO 的融合不是固定权重外挂一个超参,而是设计成动态权重。基础形式可以写成:
A_i_final = sum_j w_j * A_i^j
其中 w_j 的更新往往和每个奖励通道的“有效信噪比”挂钩,而不是由我们手动固定。
我在第一次实验里就把这个改动套了上去,训练很快稳定下来。关键是:即使 format 通道的组内方差变小,它的 w_j 会自动下调,原来挂在总奖励里的“死权重”被丢掉了。
3.2 第二刀:动态权重与方差保护机制
动态权重的具体计算方法,我见过两种比较实用的路线。
一种基于最近一段训练窗口内的通道方差估计。比如维护一个指数滑动平均(EMA),对每个通道的优势标准差做统计:
ema_std = {name: 1.0 for name in reward_names} alpha = 0.05 for name in reward_names: cur_std = batch_advantages[name].std().item() ema_std[name] = (1 - alpha) * ema_std[name] + alpha * cur_std inv_var = {name: 1.0 / (ema_std[name] + 1e-4) for name in reward_names} softmax_w = softmax(inv_var, temperature=0.5)核心直觉是:优势方差越大的通道,包含的排序信息可能更多,理应获得更高权重;优势方差趋于零的通道则表示这个信号已经饱和,权重自动下调。但要注意,直接用原始方差做 softmax 也会有问题——一个离群样本会把整条通道的方差拉高,权重的波动也随之剧烈。所以对原始方差做 EMA 平滑是必要的,温度系数也需要单独调。
另一种路线是对每个通道的动态权重做“下限保护和归一化”:所有通道权重保持在 0.1 到 0.5 之间,防止某个通道权重归零后,策略完全无视某类约束。这个下限保护在实践中对安全性奖励尤其重要。
3.3 近端约束:让策略更新重新安全
只有优势重标定还不够。GRPO 组的更新本质上是策略梯度,近端约束(clip)决定了单步更新幅度上限。多奖励场景下,由于不同优势通道的尺度已经被重标定,clip 的语义也要与原来不同。
经典 PPO clip 的做法是对概率比做截断:
ratio_i = π_new(x_i) / π_old(x_i) loss_i = -min(ratio_i * A_i, clip(ratio_i, 1-ε, 1+ε) * A_i)
GDPO 在这部分的实践是:对优势做分解后,还要把 clip 范围与优势分布的标准差做联动。我这边常用的一种做法是,当某个通道的组内优势标准差超过阈值(比如 2.0)时,强制对该通道的优势做裁剪到 [-c, c]:
max_adv = 2.0 clipped_adv = torch.clamp(A_i_j, -max_adv, max_adv)这个操作的直观理解是:即便某一批数据里出现了极端优势,也不允许它单枪匹马把模型参数推得太远。抗住这一步,策略更新就市场稳定很多。
4. 落地训练时的完整配置与稳定参数
4.1 可复制的训练配置骨架
理论讲清楚后,关键在于能复现。下面是一个经过验证的 GDPO 风格训练配置骨架,基于我这边常驻的多奖励强化学习训练框架。它不绑定具体代码库,但核心逻辑是通用的。
{ "group_size": 8, "reward_mode": "decompose_and_dynamic_weight", "reward_weights_ema_alpha": 0.05, "reward_weight_temperature": 0.5, "reward_weight_min": 0.1, "reward_weight_max": 0.5, "advantage_clip": 2.0, "policy_clip_epsilon": 0.2, "kl_target": 0.05, "learning_rate": 5e-6, "warmup_steps": 100, "batch_size_prompts": 128 }配置里有个细节值得说清楚:reward_weight_temperature直接控制动态权重的平滑程度。温度越高,权重越接近均匀;温度越低,权重越极端。前期我建议温度高一点,比如 0.7,让各路奖励先雨露均沾;等训练进入中段,模型对各路信号相对稳定后,再把温度降到 0.3 到 0.5。
4.2 关键超参的调法和我踩过的坑
最坑的一个参数是advantage_clip。我一开始设成 3.0,觉得“反正优势分布正常情况下不会超太多,clip 留足余量”。结果训练到 500 步左右,format 通道方差变小,correct 通道又出现高离群值,优势被推到 3.0 上限多次触发。策略确实没有大幅震荡,但 KL 一直压不下来。
后来我改成 2.0,同时把动态权重的 EMA 窗口拉大,问题立刻缓解。经验是:clip 值宁可偏小,也不要偏大;因为分解后的优势通道各不相同,之后再融合时,多通道的峰值效应可能叠加。
另一个容易踩的坑是:奖励通道之间的刷新频率不同步。比如 correct 奖励由一个规则判定器产生,格式奖励由模板判定器产生,它们的计算延迟不一样。如果你的 reward 信息是异步到达的,那在做组内归一化时,不同通道会站在不同的数据窗口上,统计口径不一致会直接污染优势估计。我们的做法是让所有通道先同步到同一个 recent buffer,再进行统计。
4.3 多奖励信号的分析工具
训练稳定后,我并不只看整体 reward 均值。我会同时跟踪四个指标:
- per-channel 组内相对增益(判断每个通道是否还在提供有效排序信息)
- per-channel 权重的 EMA(判断动态权重是否合理收敛)
- 优势分布的标准差与峰度(判断是否重新出现崩溃前兆)
- 每条 response 在最终优势中的“主导通道标签”(判断策略当前主要被谁牵着走)
我习惯在训练日志里加一行主导通道统计:
dominant = torch.argmax(torch.stack(advs_per_channel, dim=-1), dim=-1) dominant_channel_counts = torch.bincount(dominant.flatten(), minlength=len(reward_names))如果训练后期“主导通道”长期只有某一个,而其他通道的权重也一直偏低,那说明模型正在过度拟合单一奖励,下一步就要考虑给其他通道加约束或补充新样本了。
5. GDPO 的边界在哪里:什么情况下依然需要保守方案
5.1 奖励器本身相关时,分解也可能失灵
GDPO 不是银弹。最明显的边界是:多个奖励器高度相关的时候,按通道分解并不能真正“去重”。
举个例子,如果一个奖励器是另一个奖励器的线性变换,或者说两个奖励器都基于同一套特征模型给出分数,那么它们各自的组内优势会高度共线。动态权重即使能调,也只是在重复强调同一个信息源。此时模型容易在表面指标上刷高分,实际上并没有学到更本质的规律。
我遇到过一个典型案例:格式奖励与相关性奖励都由同一个打分器产出,相关性分数越高格式分数也越高,两者共线度超过了 0.85。用 GDPO 跑虽然不会出现优势崩溃,但训练收敛后的输出多样性极差。最后这个场景不得不退回“单奖励”或“奖励降维”,先把冗余通道去掉再训练。
5.2 折中方案:混合模式与渐进式奖励接管
还有一类情况:奖励通道中等相关且方向大体一致。此时完全分解会丢掉通道间的共享信息,完全拼接又可能崩溃。折中方案是采用混合模式——前半段用拼接奖励做粗对齐,后半段切换为 GDPO 式分解。这个顺序有讲究:训练早期模型还在学习基础行为,通道间共享信息多一点反而有帮助;训练后期需要精细化优化,分解和动态权重才能保住各奖励维度的独立性。
“渐进式奖励接管”是另一个实用技巧。不要一开始就把所有奖励都接入训练,先只开一个最重要的通道,比如“答案正确率”,跑几百步让模型先学会主干行为;再逐步加入格式、风格、安全性通道。每新增一个通道,都观察优势分布 48 小时左右,确认稳定后再加下一步。这在多目标 RL 工程上属于最稳妥的迭代策略。
我在一套实际项目里,正是用“先单通道、后多通道、再切换分解融合”的节奏,把原本一加多奖励就崩的训练曲线,稳定收敛到了 KL 与正确率双达标的水平。整个过程没有什么灵丹妙药,就是反复验证优势分布、动态权重、clip 边界的相互作用。
最后分享一个我个人的习惯:不管用什么算法,训练日志里一定要把 per-channel 的优势统计放进可视化面板,而不是只看总奖励。优势崩溃这种事,等到总奖励曲线难看的时候再排查,往往已经浪费了好几天算力。把这个维度提前亮出来,很多问题在萌芽阶段就能被看到——这比任何花哨的新算法都更值得投入精力。