周五上午的组会刚结束,我还在回味这次讨论的主角——GDPO。如果你最近也在做多奖励RL训练,又在为GRPO的"优势崩溃"头疼,那这篇复盘应该能帮上忙。事情说起来很简单:GRPO本身很强,省掉价值网络、显存友好、收敛也快,但在多个奖励模型同时参与打分的时候,优势函数会逐渐失去区分度,训练常常走着走着就废了。上周我们小组围绕GDPO的改法做了一轮完整拆解,今天我把当时的讨论、踩坑和实验记录整理成文,希望给你省点试错时间。
1. 先还原现场:GRPO 多奖励训练里的"优势崩溃"长什么样
1.1 GRPO 的简洁设计,隐藏了一个强假设
GRPO 的核心思路并不难理解。它对同一个 prompt 采样出 G 个响应,然后在这组响应内部把奖励转成标准化的优势值,再拿这个优势去做策略梯度更新。也就是说,它不需要像 PPO 那样额外训练一个价值网络去估计状态价值,完全依赖组内的相对比较来指示方向。
这个设计非常讨巧,因为少了一个 critic 模型,显存占用直接降一大截,训练吞吐也上去了。但代价是它隐含一个前提假设:组内样本的奖励分布能近似代表全局的好坏。如果只有一个奖励模型,而且这个模型的噪声可控,这个假设勉强站得住;可一旦进入多奖励 RL 训练,这个默认前提就会出问题。
我在做多奖励实验时,最开始用的是经典 GRPO 配线性加权奖励,四个奖励模型分别是正确性、格式约束、有害性规避、指令遵循度。训练到几百步时,日志里的优势均值还算正常,但优势分布肉眼可见地越收越窄,最后所有样本的优势都挤在同一个数值附近。这时候策略梯度几乎退化成均匀噪声,模型开始快速坍缩到一种固定模板输出。
1.2 多奖励不是"多加了几个打分器"这么简单
很多第一次接触多奖励训练的人都会觉得:不就是多加几个奖励模型,然后线性加权吗?权重调一调,跑就完事了。真正做进去就会发现,问题远不是"加权重"能解决的。
第一个问题是量纲不一致。每个奖励模型的输出范围、均值和方差完全不同。有的奖励在 0 到 1 之间,有的在 -2 到 2 之间,还有的可能高达 20。把它们直接相加,数值上占据绝对主导的往往是那个值域最大、噪声也最大的模型。第二个问题是相关性不对等。某些奖励之间存在天然的冲突,例如"正确性"和"格式约束"在一个响应上可能给出截然相反的信号,简单的加权求和根本没法表达这种矛盾关系。
这两个问题叠加到 GRPO 上会出大事。GRPO 在组内计算优势时,做的是先减均值再除以标准差,但这个标准化并不能消除多奖励融合带来的尺度污染。反而是一个噪声特别大的奖励模型会直接拉高组内标准差,把其他所有奖励的贡献给平均掉、稀释掉。你以为是四个奖励模型在共同训练模型,实际上是一个高方差模型在独裁。
1.3 优势崩溃的分阶段症状
我把当时实验里观察到的"崩溃三阶段"记下来,你之后跑实验可以对照一下:
阶段一是日志异常期。loss 曲线不再平滑下降,优势值的均值偏向一侧,组内标准差持续走低,trainable 位置的梯度范数开始剧烈抖动。阶段二是策略坍缩期。生成结果里模板化语句的占比显著上升,响应多样性指标下降,模型输出的句子越来越短,熵值掉得很快。阶段三是指标分家期。某个奖励分数确实在涨,比如格式得分完美,但真实正确率、安全指标全线下滑,这时候通常已经发生了 reward hacking。
我有一个很直观的体感:优势崩溃不是突然发生的,它是一个"温水煮青蛙"的过程。GRPO 的优势值在逐步失去区分能力,而你单看每一条日志曲线,很难判断到底哪里出了问题。我一开始还以为只是学习率太高,把学习率从 3e-6 调到 1e-6,甚至用了 20 次 warmup 步数的幅度去测,结果照样崩。后来把优势分布打印出来,才意识到问题根源根本不在优化器,而在奖励信号本身。
| 训练步数 | 平均优势 | 优势标准差 | 熵值 | 现象 |
|---|---|---|---|---|
| 100 | 0.31 | 0.78 | 5.2 | 正常 |
| 300 | 0.28 | 0.45 | 4.1 | 优势开始收缩 |
| 500 | 0.19 | 0.26 | 2.9 | 模板化输出增加 |
| 700 | 0.16 | 0.12 | 1.8 | 优势接近坍塌 |
2. GDPO 的解题思路:不优化"数值优势",改优化"组内偏好"
2.1 从 GRPO 到 GDPO:一个字的改动,本质换了信号源
我理解中的 GDPO 是 Group-wise Direct Preference Optimization,核心是把 GRPO 的连续优势信号替换成组内偏好信号。这是一个关键转向:GRPO 要算出一个具体的优势值,然后凭这个数值的大小决定更新幅度;而 GDPO 只关心一个响应比另一个响应好,至于好多少,并不需要精确衡量。
这个差异看着不大,实际影响是根本性的。偏好信号天然抗尺度漂移。不管奖励模型的分数整体抬高 100 还是压缩到 0.1 以下,只要两个样本之间的相对排序不变,偏好信号就不变。而 GRPO 的优势值会被这个尺度变化直接影响,这就是为什么在多奖励场景下 GRPO 容易崩、GDPO 思路更稳。
我把两种方法的训练信号流画出来比较好理解。GRPO 是:采样 G 个响应,拿多个奖励线性加权得到一个总奖励,然后在组内算优势,最后用优势更新策略。GDPO 是:采样 G 个响应,多个奖励模型各自给出对样本的喜好排序,再聚合出组内的成对偏好,直接用偏好优化目标更新策略。路径上少了一次"把奖励压成绝对数值"的操作,也就少了一个引入不稳定的环节。
2.2 为什么偏好比优势更稳:数学直觉
优势崩溃的本质是绝对数值的方差出了问题。GRPO 内部用的是 z-score 形式的归一化,计算方式是每个样本的奖励减去组均值、除以组标准差。这个操作假设了组内样本的奖励分布是相对可信的,但当多个奖励模型融合时,组内标准差会被某个噪声大、值域宽的奖励模型拉大,其他信号的贡献就相对稀释了。
偏好信号绕开了这个问题。它只需要知道"y_w 优于 y_l",通过 Bradley-Terry 模型配合 sigmoid 损失,数值上的异常点会被非线性压缩。比如某条响应的某个奖励模型给出了极端高分,如果它没有改变这条响应与其他响应的相对排序,那么对损失函数几乎没有影响。但换成 GRPO 的优势计算,这个极端高分一定会直接拉高组内均值,间接改变所有其他样本的优势值,造成一连串的扰动。
还有一点就是 KL 惩罚。GDPO 和 DPO 一样,优化目标里带着对参考模型的 KL 约束,能防止策略一轮更新就飞出太远。这一点在实践里极其重要,因为多奖励场景本来信号就乱,如果没有 KL 约束兜底,策略会更快地塌缩到某个局部最优模板。
2.3 多奖励聚合的新位置:从数值加权,变成偏好投票
GDPO 思路下,多奖励的融合方式也不再是简单的数值加法,而是偏好投票。我们可以把多个奖励模型看作多个裁判,每个裁判独立给出自己的偏好排序,最后汇总所有裁判的意见来得到最终偏好对。
这里有两种做法。第一种是先把多个奖励分数做加权,得到一个综合得分,再根据综合得分选赢家和输家。这种方式虽然加入了排序这一层,但本质上还是数值加权,单一高方差模型依然可以带偏综合分数。第二种是每个奖励模型单独构建偏好,然后做多路聚合。比如四个奖励模型里,有三个认为 A 好于 B,一个认为 B 好于 A,那就按多数意见来;甚至可以用更细的排序融合法,比如 Borda Count,把每个裁判给的排序换算成得分再汇总。
我比较推荐第二种。因为它在逻辑上直接把"奖励尺度一致性"的要求降到了最低,各路奖励只需要保证各自的排序可用,不需要被强行拉到一个统一的数值尺度上。这个改动对于一个信号冲突严重的多奖励场景来说,是本质性的。
| 对比维度 | GRPO 数值融合 | GDPO 偏好融合 |
|---|---|---|
| 信号类型 | 连续优势值 | 离散偏好排序 |
| 对量纲敏感度 | 高 | 低 |
| 高噪声奖励影响 | 直接污染标准差 | 影响单路偏好,可能被聚合抵消 |
| 奖励冲突表达 | 隐式、靠权重调和 | 显式、靠投票/排序融合 |
| 策略退化成模板的风险 | 高 | 低 |
3. 实操复盘:一场从崩溃到稳定的多奖励训练实验
3.1 实验配置和奖励健康度检查
我这里直接分享最近一次实验的完整配置,方便你对照。基座模型是一个 7B 规模的 SFT 模型,训练数据是 5000 条数学推理 prompt,每个 prompt 采样 8 个响应。奖励模型有四个:正确性奖励、格式奖励、安全性奖励、指令遵循奖励。优化器用 AdamW,初始学习率 2e-6,batch size 设为 16,序列最大长度 2048。
动手改算法前,我花了半天时间做奖励健康度检查。这个步骤强烈建议你千万别省。做法很简单:先从验证集里抽 200 条 prompt,用 SFT 模型各采样 8 个响应,然后统计每个奖励模型在这批样本上的均值、方差、分位数和奖励对之间的相关系数。跑完之后,我立刻发现了问题所在。
正确性奖励方差为 0.08,格式奖励方差是 0.5,安全性奖励方差达到了 3.2。也就是说安全性奖励自身的波动比其他奖励大一个量级以上。如果直接线性加权,安全性奖励会主导组内标准差的估算,其他三个奖励的信号在优势计算里就会变成可以被忽略的背景噪声。这一点不是猜出来的,是统计表直接告诉你的。
| 奖励模型 | 值域范围 | 均值 | 方差 | 与正确性奖励相关系数 |
|---|---|---|---|---|
| 正确性 | 0~1 | 0.62 | 0.08 | 1.0 |
| 格式约束 | -2~2 | 0.15 | 0.50 | 0.12 |
| 有害性规避 | 0~20 | 10.20 | 3.20 | -0.23 |
| 指令遵循 | 0~2 | 1.10 | 0.15 | 0.31 |
3.2 改造一:奖励分位数归一化
做任何算法替换之前,先做奖励归一化是性价比最高的一个动作。我用的不是简单的 z-score,而是分位数归一化。具体操作是:对每个奖励模型,统计它在一个滑窗内的奖励分布,然后把自己映射到 0 到 1 的分位数上。这样做的效果是,不管原始奖励的分布长什么形状,归一化之后都会变成一个接近均匀的 0 到 1 分布,离群点的影响会被压缩。
为什么要强调分位数而不是 z-score?因为 z-score 依赖均值和方差,而这两个统计量会被离群点严重污染。比如某个响应在安全性模型上因为一个罕见俚语拿到了极端低分,这会直接拉低整体均值,导致所有其他样本的 z-score 都变大。分位数虽然也会反映这个异常值,但其他样本经过分位数映射之后,相对位置不会被过度扭曲。
训练过程中,奖励模型给分的分布会随着策略模型的更新而漂移。所以我用了一个带滑动窗口的统计方式,只取最近 500 条 prompt 的奖励样本来计算分位数映射表,每 50 步更新一次映射函数。这个工程细节非常关键,否则训练到后期,早期统计的映射表已经不再匹配当前的奖励分布了。
3.3 改造二:GDPO 组内偏好数据构造与训练
奖励归一化做完之后,我把训练目标从 GRPO 切换到了 GDPO 的偏好优化思路。整个过程分四步走。
第一步,采样。对每个 prompt 采样 G=8 个响应。第二步,打分。四个奖励模型分别对这 8 个响应打分,得到 4 组分数。第三步,偏好聚合。针对这 8 个响应,我并行地使用两种偏好聚合方式:一是先用分位数归一化后的打分做加权融合,得到总排序;二是每个奖励模型单独给出一个排序,然后用 Borda Count 融合。当两个方法给出的胜者和负者不一致时,这组样本先丢弃不进训练,因为它大概率是标注冲突的样本。第四步,构造训练对。在最终排序里挑最高分的响应作为 y_w,最低分的作为 y_l,组成偏好对送入损失函数。
对应的训练逻辑可以简化成下面的伪代码:
# 组内偏好构造 + GDPO 训练 for batch_prompts in dataloader: for x in batch_prompts: resp = policy.sample(x, G=8) all_scores = [reward_model(r) for r in resp] # shape: [num_rm, G] # 方案A:归一化后加权融合排序 norm_scores = [quantile_normalize(s) for s in all_scores] fused_rank = argsort(weighted_sum(norm_scores))[::-1] # 方案B:每个奖励独立排序,再 Borda 融合 per_rm_ranks = [argsort(s)[::-1] for s in all_scores] borda_rank = borda_count(per_rm_ranks) if winner(fused_rank) != winner(borda_rank): continue # 冲突样本,跳过 y_w = resp[fused_rank[0]] y_l = resp[fused_rank[-1]] loss = dpo_loss(policy, ref_policy, x, y_w, y_l, beta=0.1) loss.backward()这里有一个我还挺想强调的经验:beta 的选择直接影响训练稳定性。beta 太小,KL 约束弱,策略容易一步就跑飞;beta 太大,更新幅度很小,收敛效率骤降。我用 0.1 作为初值,然后根据监控指标动态调整。如果你的场景里奖励噪声偏大,建议从 0.2 起步更稳。
3.4 改造三:给偏好融合加"温度"和"权重衰减"
偏好融合也不是简单地固定权重一路走到黑。多奖励之间会动态冲突,固定权重会导致某个阶段某一个模型被持续压制。
我做了两个调整。第一个是给每个奖励模型的打分加温度系数。具体说,Borda Count 在把排序转换成得分时,可以加上一个温度参数,排名靠前样本的得分更高、排名靠后样本的惩罚更大。温度低时,融合结果更激进,更看重排序第一的信号;温度高时,融合结果更平滑,更看重全局排序。第二个调整是给奖励权重做 EMA 动态衰减。当检测到两个奖励模型对同一批样本给出的排序冲突率超过 40% 时,就降低高冲突奖励的权重,同时提高低冲突奖励的权重。
这里有一个小细节值得注意:安全类奖励最好保留一票否决权。不管其他奖励模型怎么投票,如果某个响应在有害性指标上触发了阈值线,这一条响应直接降级为负样本,不参与赢家竞争。本质上多奖励训练里可以有一些不等权的设计,安全和合规的约束本身就应该比一般质量指标拥有更高的优先级。
3.5 对照结果
整个改造跑完之后,我直接跟原始的 GRPO 基线做了对比。对照组用的是经典 GRPO,奖励直接线性加权;实验组用的是 GDPO 思路加偏好融合。两者使用相同的采样 prompt、相同的采样数量、相同的学习率和训练步数。
结果差异非常明显。GRPO 基线在 500 到 700 步之间优势标准差已经退化到一个很小的值,生成多样性急剧下降;GDPO 实验组到 2000 步时,组内排序依然保持一致的区分度,优势间接反映出的策略梯度方差也稳定得多。最终在验证集上,正确率从基线最后的 51% 提升到实验组的 63%,格式约束达标率提升了 12 个百分点,安全性指标没有明显回退。
| GRPO 基线 | GDPO 改造 | |
|---|---|---|
| 优势方差退化步数 | 约 700 步 | 未退化 |
| 验证集正确率 | 51% | 63% |
| 格式达标率 | 76% | 88% |
| 安全性违规率 | 4.2% | 1.3% |
| 生成多样性(熵值) | 1.8 | 3.4 |
需要提醒的是,GDPO 训练在初期会比 GRPO 更慢,因为多了偏好聚合和冲突过滤的环节。但它的稳定性收益会把这个时间成本赚回来,至少在我跑的两个场景里都是这样。
4. 常见问题与排查技巧实录
4.1 一崩到底:loss 变成 NaN
多奖励场景里还有个很容易遇到的问题,就是训练到一半 loss 直接变成 NaN。这种现象常见于 DPO 家族的损失把概率值塞进 log 之后出现极端情况,比如某个响应在策略模型下的生成概率本就是 0,取了对数直接负无穷大。
遇到这种情况,先做三件事:第一,把所有 log 概率加上 clamp 下限,例如 log_prob.clamp(min=-100);第二,检查序列长度对应的 attention mask 有没有正确屏蔽 padding,padding 位上的 logits 会影响概率计算;第三,检查是否有样本的 KL 惩罚项被算成巨大负值,必要时提高 beta 或者降低学习率。
这些我都实际踩过,尤其是 padding 位带来的 NaN,排查起来非常隐蔽。如果你的 loss 在 200 步左右开始异常,优先看序列 padding 处理和 tokenizer 端的 EOS 停止逻辑,很多情况下问题不出在算法本身。
4.2 偏好对太假:在"矮子里拔将军"
一个很常见的坑是偏好对的质量问题。当模型的生成质量整体偏低时,组内排序的赢家和输家可能都不怎么样。拿一个 30 分的样本和一个 20 分的样本做偏好对,模型学到的只是"30 分的比 20 分的好",但这个 30 分本身远不及合格线。
我的处理方式是给偏好对加质量门槛。只有当最高分样本与最低分样本的归一化得分差超过某个阈值(我一般用 0.15)时,这对样本才进入训练;如果分差太小,说明这组响应本身没什么区分度,学它只会给策略引入噪声。另一个技巧是引入锚点样本,定期把一批人工标注的高质量答案混入采样池作为参照,这样偏好对中的赢家至少有一个是可信的锚点。
这个改动表面上减少了可用的训练样本量,但实际上减少了噪音数据的占比。我用下来之后,发现训练曲线更平滑,最终收敛指标反而更高,没有出现之前那样"奖励涨、真分数不涨"的局面。
4.3 多奖励互相打架:两个模型给出相反排序
多奖励场景里最让人头疼的问题是奖励冲突。同一个响应,正确性模型认为它是组内最优,安全性模型却认为它极度危险。这种冲突样本不做处理直接扔进偏好投票,会导致聚合结果很不稳定。
我建议先做冲突检测。在偏好聚合旁边并行统计两两奖励模型之间的排序冲突率。冲突率高于 40% 的奖励对,需要明确优先级。例如安全约束类奖励与格式奖励冲突时,记住可先做安全侧对齐,再叠加质量优化,比在同一个训练阶段里强行调和要简单得多,效果也更稳定。分阶段训练在这种情况下比任何精巧的融合公式都有效。
4.4 汇总速查表
| 现象 | 可能原因 | 处理动作 |
|---|---|---|
| 优势分布收缩、策略模板化 | 多奖励量纲不一致,高方差模型主导 | 分位数归一化 + 偏好投票整改 |
| loss 变 NaN | log 概率为 0、padding 位未屏蔽 | clamp + 检查 attention mask + 提高 beta |
| 奖励涨了但真实指标跌 | reward hacking、偏好对质量低 | 加质量门槛、混入锚点样本 |
| 多奖励排序冲突严重 | 奖励模型目标不一致 | 检测冲突率,高冲突奖励降权 |
| 训练初期收敛极慢 | 偏好聚合计算开销大、样本过滤多 | 先在小数据子集验证,G 从 8 起跑 |
最后再分享一个小经验。我踩过几次坑之后,基本上形成了一个习惯:不管换 GRPO 还是 GDPO,先在 500 条 prompt 的小集子上做一次完整的训练马拉松,看奖励健康度报告,再决定要不要上全量数据。你会发现很多时候"算法崩了"并不是算法本身的问题,而是奖励信号从源头就不健康。GDPO 的思路好,但它不是魔法,奖励分析这个前置步骤,才是整条链路里最值得花时间的部分。