最近在调一个多奖励RL训练任务时,我又撞上了那个熟悉到让人头疼的现象:GRPO一路跑得很稳,训练loss唰唰往下掉,可模型的实际能力却像被卡住了脖子,格式得分一路狂飙,真实正确率反而原地打转甚至往下滑。代码没有写错,超参也反复调过,真正的问题出在GRPO计算组内优势的时候,多奖励信号发生了“优势崩溃”。周五上午9点,我跟几位同行正好约了一场线上讨论,聊的就是如何用GDPO这类优化思路来解这个问题。这篇文章把我踩过的坑、读过的论文和跑过的实验整理出来,给正在做多奖励RL训练的朋友一份能直接上手的参考。
1. GRPO为什么成了多奖励训练的主流基底
在谈优势崩溃之前,先对齐一下GRPO到底做了什么,以及它为什么能在推理任务训练里流行起来。很多刚接触RL训练的同学容易把GRPO当成一个普通的强化学习算法,但其实它的设计动机非常明确:去掉PPO里那个又大又不稳定的Critic。
1.1 从PPO到GRPO:去掉Critic反而更稳
PPO要估计优势函数,一般得训练一个价值网络来预测状态值。但在大语言模型这种场景里,“状态”是整个上下文和已经生成的token序列,空间巨大,分布还随时间漂移,价值网络很难拟合准。估值一偏,优势就偏,策略更新方向跟着偏,训练就抖。GRPO干脆不训练Critic,改为对同一个prompt采样一组输出,然后在这一组内部做相对比较。简单说,从“找一个绝对打分老师”变成“让一组学生互相比一比”,两者都可以估计相对好坏,但后者不需要额外网络,对奖励绝对数值的依赖也小很多。常见的做法是算组内优势:A_i=(r_i-mean(r))/std(r),再用这个优势去套裁剪后的策略梯度损失。
1.2 多奖励组合:推理训练绕不开的刚需
现在训练一个能做复杂推理的模型,光靠一个奖励函数很难覆盖全面。最典型的是三类:格式奖励,用来约束模型是否按照约定结构输出;答案正确性奖励,用来判断最终结果对不对;过程质量或风格奖励,用来约束推理链是否清晰、语言是否自然。有些场景还会加入安全约束和任务完成度指标。奖励一多,就得设计组合方式。最常见的是线性加权相加,也有用乘积或log-sum-exp让多个奖励互相抑制的。线性加权实现起来最简单,但隐含一个假设:所有奖励都能换算到同一把尺子。现实里这个假设几乎不成立,奖励的尺度、饱和点、噪声水平都不一样,靠手工调权重等于凭感觉踩钢丝。
1.3 多奖励+GRPO:优势开始悄悄失控
照理说,GRPO的组内相对化已经能处理奖励绝对尺度的漂移,多奖励训练为什么会翻车?我跑过不少实验,典型曲线是:前几千步模型学得飞快,因为格式奖励从低分涨到几乎满分,优势信号很充足;等格式奖励接近饱和,训练loss还在波动,但验证集指标不再提升,再跑几轮甚至能看到正确率倒退。换随机种子试,结果差别很大。最反直觉的是,我在某个任务里加大正确率奖励的权重,正确率反而掉了。问题不在奖励函数本身,而在奖励进入优化器之前的那个环节:优势信号已经崩了。这就是我们要聊的“优势崩溃”。
2. 优势崩溃的四种典型形态
优势崩溃不是一个单一bug,它对应几种不同的失效模式。我把自己实际遇到的情况梳理成四类,每一类在实际训练里都可能单独出现,也可能叠加出现。
2.1 尺度失衡:弱信号被标准差淹没
假设总奖励是w1r1+w2r2这样简单相加。如果r1在0.98上下波动,组内标准差只有0.01,r2在0.5附近波动,标准差有0.2,那么无论w1设多大,r_total的优势几乎都由r2驱动。原因在于组内z-score计算会除以总奖励的标准差,总奖励的标准差基本由波动大的那一路奖励决定。波动小的维度即使有价值,也会被压缩成零附近的一点点残差,最终约等于没有参加梯度更新。用生活里的话说,一个班的综合排名如果体育分标准差远大于文化课,那这个排名实际上只看体育。这就是尺度失衡:不是某个奖励没用,而是它在优势层面的信号被另一个维度淹没了。
2.2 信号饱和:方差归零带来的假性失效
当某个奖励维度在组内所有样本上的取值完全一样,这维度的组内标准差就是0。GRPO实现里通常会对除以std做防零保护,也就是std加上一个极小值。归一化之后,原来全相等的奖励会变成全0,对优势没有任何贡献。我见过最典型的场景是格式奖励:训练中期模型已经保证100%按格式输出,格式奖励的方差基本消失,这个维度就永久退出了学习信号。问题是,奖励饱和是分批发生的。先是格式,再是安全约束,再是语言风格,最后可能只剩一个维度还在跳。这种情况下模型不是被某个错误方向带偏,而是被噪声牵着走,训练自然停滞。
2.3 维度冲突:偏好方向在组内互相拉扯
维度冲突比尺度失衡更隐蔽。假设两个奖励,一个鼓励推理步骤充分展开,一个鼓励回答简洁,它们在同一个采样组里对两个样本给出的排序可能完全相反。GRPO把多维奖励合并成一个标量后再算优势,等于每次更新前先裁决一次哪一维说了算。由于采样是随机的,裁决结果也随机,策略梯度方向一会往东一会往西,最终体现为训练loss震荡、模型学不动。如果其中一维恰好接近饱和,冲突会更难察觉:饱和维度在优势里约等于0,剩下那一维赢的次数变多,但它赢的时候往往只是噪声在投票。这也是为什么“加大正确率权重但正确率下降”这种反直觉现象会出现。
2.4 基线偏差:矮子里拔将军
GRPO的优势是以组内均值为中心的,它隐含一个假设:组内平均是零基准。但采样是随机的,某一组可能整体都偏离较好水平。如果一组输出全是差的,那么其中一个“相对最好”的样本也会拿到正优势,被模型模仿。这在单奖励下偶尔发生,问题不大;在多奖励下,由于某个维度很容易达到,好坏样本在该维度的得分几乎一致,剩下可区分维度噪声又大,于是“矮子里拔将军”的效应被成倍放大。模型不断被一些随机亮点带走,训练轨迹就像喝了酒一样摇摆。这个形态提醒我们,“相对优势”不等于“真实优势”,组平均并不是一个可靠基线。
3. GDPO的思路拆解
聊完崩溃形态,再来看GDPO为什么能解这个问题。说明一下,GDPO这个缩写目前业内没有完全统一的展开,有人叫Group-wise Direct Preference Optimization,有人叫Group Direct Policy Optimization。我按照自己实践采用“分组直接偏好优化”的理解,核心思想是用组内偏好替代组内绝对优势。
3.1 核心思想:从“离平均多远”变成“比谁更好”
GRPO的做法是给每个样本算一个离组平均的标准化距离,然后把这个距离当作更新幅度。GDPO换了一个思路:同样每轮对一个prompt采样一组输出,保留组内比较的骨架,但不再计算标量优势。它把组内样本按照多奖励规则排出先后顺序,选出一对或多对chosen和rejected,然后用DPO式的偏好优化损失更新策略。优化器要做的事情从“拉大样本与组平均的距离”变成了“让模型更愿意输出组内偏序靠前的样本,更不愿意输出靠后的样本”。听起来只是换了个损失函数,但它随手解决了很多GRPO隐性问题。
3.2 为什么偏好排序免疫优势崩溃
对着四种崩溃逐一解释。尺度失衡:排序只关心次序,不关心差值,一个奖励维度只要在边界样本上改变了先后顺序,它就有发言权;如果它从头到尾不改变排序,那它也不会像z-score那样制造噪声。信号饱和:饱和维度的取值全相同,对排序完全无影响,不会产生除零或放大噪声;GRPO却会在这里把噪声放大。维度冲突:可以用字典序或分层规则显式定义优先级,不必经线性加权强行裁决。基线偏差:偏好对只要求chosen整体优于rejected,不需要组平均作基准,整组质量差的时候我们可以用置信度门槛滤掉模糊对,避免随机方向被当成正信号。除此之外,DPO类目标自带与参考策略的KL约束,天然抑制策略漂移,这对长程训练很友好。
3.3 多奖励下如何构造偏好排序
最简单方案是对多个奖励加权求一个总分,再按总分排序。这样做比GRPO对尺度的要求宽松,因为只保留序信息,但权重仍然需要手工标定。我推荐的做法是字典序分层:先按答案正确性排序,再按格式排序,最后按风格排序。这一步看着简单,实测非常稳,尤其是推理任务里正确性优先级通常远高于格式。更精细的做法是用Pareto占优:只有样本A在至少一个维度上严格不劣、且在所有维度上都不差于B时,才判A胜出;否则判为平局,不进入loss。Pareto方式能过滤掉冲突对的噪声,但样本利用率低,组要采得大一些。无论哪种,核心都是把“多维奖励”这块难啃的骨头转移到偏好定义阶段。
3.4 在线训练流程:GDPO和GRPO的对比
GDPO不是离线方法,它完全能在训练循环里跑,每轮采样、打分、构偏好对、更新策略,一步都不少。跟GRPO的差别主要在信号构建上。我列一个对照表:
| 环节 | GRPO | GDPO(我理解的) |
|---|---|---|
| 采样方式 | 每prompt采G条输出 | 每prompt采G条输出 |
| 奖励使用 | 合并为标量后z-score | 保持多维,用于构造排序 |
| 有效信号 | 组内优势A_i | 偏好对/排序 |
| 更新目标 | Clipped策略梯度 | 偏好优化loss(DPO式) |
| Critic依赖 | 无 | 无 |
| 对多奖励鲁棒性 | 易出现优势崩溃 | 对尺度与饱和不敏感 |
实际工程里,GDPO流程比GRPO多一个步骤:设计偏好规则。这个步骤的性价比很高,因为排序规则是显式的,出了任何问题都可以直接在日志里看到是哪一维在改变排序。GRPO那边则是一团合并端到端的数值,排查起来困难得多。
4. 实操落地与避坑
理论讲完了,直接给可以抄作业的流程和参数。
4.1 一个可直接参考的在线训练循环
用Python伪代码描述完整的GDPO式训练循环:
# 伪代码:仅用于描述训练结构,省略框架细节 for step in range(total_steps): prompts = sample_prompts(batch_size) ref_policy_logps = get_ref_logps(prompts) # 提前存好或在线推理 all_outputs = [] all_rewards = [] for prompt in prompts: outputs = [policy.sample(prompt) for _ in range(G)] rewards = multi_reward_model(outputs) # shape: [G, D] all_outputs.append(outputs) all_rewards.append(rewards) pair_list = [] for b in range(batch_size): ranked_ids = rank_by_rules(all_rewards[b]) # 字典序或加权总分 winner_id, loser_id = ranked_ids[0], ranked_ids[-1] # 置信度门槛:两样本奖励距离太小时不进loss if score_gap(all_rewards[b], winner_id, loser_id) < gap_thresh: continue pair_list.append((prompts[b], all_outputs[b][winner_id], all_outputs[b][loser_id])) loss = dpo_loss(pair_list, policy, ref_policy, beta) loss.backward() optimizer.step()解释:为什么要用参考策略的logp。DPO损失里出现log(πθ(chosen)/πref(chosen))这种比值,如果ref用的是当前策略自身,这个比值恒等于1,loss恒等于0,KL约束也就没了。工程上常用一个冻结的旧checkpoint当ref,或者维护一个滑动平均版本。滑动平均对训练早期格式学习的适应更好,我倾向用它。
4.2 超参与参数选择的经验值
Group size G我建议8到16。太小的话,组内最好样本也未必真的可靠;太大,两两比较引入的噪声和采样开销都会上涨。DPO里的温度β建议从0.1起步,最高不要超过0.5。β越小,策略更新越猛,但对偏好噪声的容错也越差。偏好置信度门槛建议设置在组内奖励总差的0.1到0.2之间,也就是chosen与rejected的综合奖励差至少要大于这个值,否则这对数据不进loss。学习率一般取GRPO配置的1/2到1/3,因为DPO式loss收敛很快,加速容易把策略推离参考分布。另外我会每隔500步跑一次饱和检测,把当前组内每个奖励维度的方差打印出来,那些方差已经低于阈值的维度会从排序规则里临时摘除,防止它们干扰排序。
4.3 多维奖励预处理的三条实用技巧
第一优先做分位数归一化,不要用z-score。z-score会放大极端值,分位数把每个维度的分数均匀映射到0到1之间,排序更稳定。第二是饱和检测逻辑要用unique值数量而不是单纯看std。我遇到过std不为0但实际只有一个样本数值不同、其余完全相同的情况,用std阈值会漏掉。第三,给每个prompt记录一份“维度冲突计数器”,统计有多少对样本被正确性维度判A胜、却被格式维度判B胜,如果冲突比例超过20%,就不要用线性加权总分排序,改成字典序。这套预处理看着繁琐,但能省掉后面大量猜权重的调试时间。
4.4 组内多对样本的利用方式
前面伪代码里只用第一名和最后一名构造一对chosen和rejected,这是最保守的做法。实际上组内排序信息比一对多,我们可以取前k名和后k名分别做chosen和rejected,构造多对参与loss更新;也可以对每个相邻名次构造一对。我的经验是,前k后k能显著增加数据利用率,k取2或3就够了。还有一个更稳的做法:把组内两两比较的胜率矩阵统计出来,用胜率作为软标签,而不是二元的1和0。这样即使某两个样本非常接近,也不会被强行拉成“一个该学一个该拒”,对连续奖励分布更友好。代价是胜率计算复杂度是O(G^2),G=16时还能接受,G太大就要小心。
4.5 常见问题速查表
把我在实验里反复踩到的坑整理成表:
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| DPO loss长期不降 | 偏好对噪声太大,置信度门槛太低 | 提高gap_thresh,降低G |
| 评估正确率下降 | 排序规则里正确性没有真正优先 | 检查奖励模型的打分日志,确认正确率维度的判序权 |
| 策略快速坍缩 | β太小或学习率太大 | 增大β,降低学习率 |
| 输出多样性下降 | 组内样本过于相似,偏好对信息量低 | 提高采样温度,或加熵bonus |
| 某些奖励维度失去作用 | 该维度已经饱和或奖励模型退化 | 重新校准奖励模型,或从排序中摘除该维度 |
这些问题里最难排查的是第一条。loss不降不代表没有学习,也可能在反复横跳。建议在训练机上固定一个prompt池,每隔一段时间看一下池内chosen和rejected的奖励结构,判断排序规则是否真的把更好的样本挑了出来。有时候问题根本不在优化器,而在奖励模型本身,你再加再多的算法也救不回来。
5. 实际观察与延伸讨论
前面聊了不少实现细节,这一章说点更宏观的观察和周五讨论时的延伸话题。
5.1 一组对比实验的观察
我在模拟项目X上做过小规模对照,任务包含格式、答案正确、简洁度三个奖励维度。GRPO基线早期格式分快速爬到97%,但训练到20k步时正确答案率从62%掉到58%;然后换成GDPO式的偏好优化,同样的采样预算下,正确答案率跑到69%,格式分稳定在93%附近。这个结果不代表GDPO一定超过GRPO的每一个指标,但很能说明问题:GDPO的关键收益不是单点冲刺更强,而是多个指标同时出现在可接受区间内的稳定性。多奖励训练里真正怕的,就是模型为了一两个指标把其他维度全部牺牲掉。
5.2 GDPO不是银弹
也得泼盆冷水。GDPO把矛盾从优势计算转移到了偏好构造,如果我们的偏好规则本身跟真实目标不一致,优化一个错误排序只会更快走向错误。举个例子,如果奖励模型对正确性的打分本身就带有偏见,那么无论用加权还是字典序,模型都会沿着这个偏见走。另外,错误排序被当成真实信号的问题依然存在,只是不再表现为优势发病。哪怕加上置信度门槛,在长尾样本上依然可能出现噪声偏好。所以我建议每跑完一个阶段就用独立的评估集核对一次,不要只看训练loss和组内指标。
5.3 周五讨论还可以延伸哪些方向
GDPO和多奖励训练留给我的开放问题挺多。一是自适应权重:能否像多任务学习里的梯度均衡那样,根据每个奖励维度贡献的梯度幅度动态调整排序权重,而不是手工指定。二是混合策略:保留GRPO的连续优势信息,同时只在偏好冲突明显时切换到偏好排序,相当于给两种优化器加开关。三是从偏好到概率校准:把组内两两比较的次数统计成胜率,用胜率替换二元的chosen/rejected,这样能够更细腻地利用组内信息。周五上午聊的时候,我大概率会把这几个方向都拿出来晾一晾,看看有没有更好的解法。
最后说一个我个人养成的习惯。不管跑GRPO还是GDPO,我都会在训练机上固定几十条prompt,每跑一段就把组内奖励的散点图拉出来看看,顺手算一下每个维度的标准差和排序一致性。多奖励RL训练里最坑的一点是,你很难直观察觉某个信号已经瘫痪,而恶魔往往就藏在你看不见的那一维。周五上午聊完GDPO之后,我大概率还会把这个思路继续往前推,后面有新的实验结论再回来更新这篇笔记。