ms-swift GRPO 进阶:REAL 损失——把奖励当作标签的组内分类视角
【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift
在 RLVR(可验证奖励强化学习)训练中,GRPO 的逐 token 梯度权重既可能"给错了对象",也可能被少数过度自信的负样本"劫持"。本文围绕 ms-swift 中loss_type=real的进阶研究实现,讲清楚 REAL(Rewards as Labels)如何用组内分类替代 advantage 估计来重构 GRPO 优化目标:先剖析 GRPO 梯度中"梯度错配(Gradient Misassignment)"与"梯度主导(Gradient Domination)"两个问题的数学来源,再给出 REAL 的分类 logit、损失函数与有界梯度权重的推导,最后结合仓库源码逐行印证其实现、参数配置(--loss_type real/--real_tau)、官方训练脚本 real.sh 以及必须满足的per_device_train_batch_size可被num_generations整除的约束条件。读完本文,你可以直接在 ms-swift 的swift rlhf流程中启用 REAL 训练,并理解其在源码层面与教师蒸馏、RLSD 等特性不兼容的原因。
背景与动机:GRPO 目标函数的两处"梯度病"
GRPO 的目标函数与逐 token 梯度权重
GRPO 的优化目标为:
$$ J_{\mathrm{GRPO}}(\theta)=\mathbb{E}{q,o\sim\pi{\mathrm{od}}(\cdot|q)}\left[\frac{1}{|o|}\sum_{t=1}^{|o|}\left(\min\left(\rho_tA_t,\mathrm{clip}(\rho_t,1-\epsilon,1+\epsilon)A_t\right)\right)\right] $$
其中:
- $\rho_t = \frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$ 为概率比(probability ratio);
- $A_t$ 为优势函数(advantage function)。
对应的梯度为:
$$ \nabla_{\theta} J_{\mathrm{GRPO}} = \mathbb { E } \left[ \frac { 1 } { | o | } \sum _ { t = 1 } ^ { | o | } \mathbb { I } _ { \mathrm { clip } } \cdot A _ { t } e ^ { s _ { t } } \nabla _ { \theta } \log \pi _ { \theta } \left( o _ { t } | q \right) \right] $$
其中:
- $s_t = \log \frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$ 为相对对数概率(relative log-probability);
- $\mathbb{I}_{\mathrm{clip}}$ 为裁剪指示函数。
因此 GRPO 中每个 token 的梯度权重(取绝对值)为:
$$ |\mathcal{W}{\mathrm{GRPO}}|=\left{ \begin{array} {ll}\left|A\cdot e^s\right|, & \mathrm{if~}\mathbb{I}{\mathrm{clip}}=1, \ 0, & \text{otherwise.} \end{array}\right. $$
可以看到,梯度权重由 advantage 幅度 $A$ 与指数项 $e^s$ 共同决定,后者正是问题的根源。
问题一:正样本的梯度错配(Gradient Misassignment)
对正样本($A>0$)而言,随着相对对数概率 $s$ 减小,梯度幅值 $|A\cdot e^s|$ 也随之减小。这与直觉相悖:模型当前把握较小($s$ 低)但作答正确的 token,本应获得更大的学习信号;而 GRPO 反而把权重给了模型已经很自信的 token,导致"欠训练"的 token 学习信号不足——这就是梯度错配。
问题二:负样本的梯度主导(Gradient Domination)
对负样本($A<0$)而言,随着 $s$ 减小,$|A\cdot e^s|$ 并不减小而是指数级增大(此时 $s$ 越负说明旧策略下该 token 概率被当前策略压得越低,对应 $e^s$ 越大)。结果是:组内少数几个"过度自信的坏 token"主导了整个梯度,淹没同组其他负样本的信号;且该权重没有上界,可能导致不稳定、过大的参数更新。
从 ms-swift 源码结构看,GRPO 的 token 级裁剪权重正体现在 grpo_trainer.py 中
coef_1/coef_2与advantages的乘积项上,即 $\rho \cdot A$ 的经典 PPO-clip 形式,与上述公式一一对应。
针对上述两个问题,REAL 的思路是:不再估计 advantage 再逐 token 加权,而是把奖励直接当作分类标签,做组内(in-group)分类训练。
REAL 的数学形式化:从 advantage 估计到组内分类
分类 logit:序列级相对对数概率
REAL 为每个样本 $k$ 定义如下分类 logit:
$$ \bar{s}^k=\frac{1}{|o^k|}\sum_{t=1}^{|o^k|}\left(\log\frac{\pi_\theta(o_t^k\mid q)}{\pi_{\mathrm{old}}(o_t^k\mid q)}\right) $$
即:该样本在当前策略下相对旧策略的逐 token 对数概率差在序列上的平均值。它的符号具有明确语义:
- $\bar{s}^k > 0$:该样本在当前策略下比在旧策略下更可能 → 模型倾向于提升(promote)该样本;
- $\bar{s}^k < 0$:该样本在当前策略下更不可能 → 模型倾向于抑制(suppress)该样本。
REAL 损失函数
$$ \mathcal{L}{REAL}=\log\left(1+\sum{\mathcal{O}+}e^{-\bar{s}^i/\tau}\right)+\log\left(1+\sum{\mathcal{O}_-}e^{\bar{s}^j/\tau}\right) $$
其中 $\mathcal{O}+$、$\mathcal{O}-$ 分别为组内奖励为正/负的样本集合,$\tau$ 为温度参数。两项都是 softplus/logsumexp 型结构:正样本项希望把 $\bar{s}^i$ 推正,负样本项希望把 $\bar{s}^j$ 推负,而常数 1 提供了"无样本时损失为 0"的自然基线。
梯度性质:单调且有界
REAL 的梯度权重绝对值为:
$$ |\mathcal{W}{\mathrm{REAL}}|= \begin{cases} \frac{1}{\tau}\frac{1}{1+C{+}e^{\bar{s}^{k}/\tau}}, & r=1 \ \ \frac{1}{\tau}\frac{1}{1+C_{-}e^{-\bar{s}^{k}/\tau}}, & r=0 \end{cases} $$
($r\in{0,1}$ 为样本奖励标签,$C_\pm$ 为组内同类样本数相关的常数。)由此带来两点关键改进,正好对应 GRPO 的两个病灶:
- 有界性:权重幅值上界为 $1/\tau$,负样本不再能产生指数级放大的梯度,消除了 Gradient Domination;
- 单调性:对正样本,$\bar{s}$ 越负(模型当前越不自信但答案正确),权重越大——学习信号被正确地分配给欠训练样本,解决了 Gradient Misassignment。
ms-swift 的参数文档 args_mixin.py 对该性质的描述与推导一致:"REAL induces monotonic and bounded gradient weighting with magnitude upper-bounded by 1/tau"。
参数配置
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--loss_type | str | - | 设置为real |
--real_tau | float | 0.5 | 温度参数,控制分类决策边界的锐度(决策边界越锐,权重区分度越强;同时决定权重上界 $1/\tau$) |
对应源码定义见 args_mixin.py:
# REAL https://arxiv.org/abs/2602.05630 real_tau: float = 0.5除这两个显式参数外,启用 REAL 还会触发一些隐式约束,源码中已做强制处理:
- 奖励归一化被强制关闭。在 rlhf_args.py 中,当
loss_type == 'real'时,scale_rewards会被强制置为'none'并打印警告,因为 REAL 的分组正/负划分直接使用奖励的原始符号(见后文group_rewards > 0的判断),组内标准化会破坏该语义; - 与教师蒸馏(OPD-RL)互斥。
real损失将 advantage 归约为序列级标量,与教师逐 token 的 signed log-ratio 冲突,rlhf_args.py 与 grpo_trainer.py 均会在参数校验和训练入口处直接抛出ValueError; - 与 RLSD 优势重加权互斥。
advantage_reweight=rlsd同样不支持loss_type=real,原因见 rlhf_args.py。
源码实现走读:REAL 损失在 grpo_trainer.py 中的落地
以下按 grpo_trainer.py 的执行顺序,逐段对照上面的数学公式。
第一步:计算序列级 logit $\bar{s}^k$(L1096-1102)
global_scores = (log_ratio * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) group_scores = global_scores.view(-1, self.num_generations) seq_advantages = (advantages * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) group_rewards = seq_advantages.view(-1, self.num_generations)log_ratio = per_token_logps - old_per_token_logps即逐 token 的 $\log(\pi_\theta/\pi_{\mathrm{old}})$,按completion_mask做有效 token 平均后得到 $\bar{s}^k$;随后通过view(-1, num_generations)把展平的 batch 重新切回"每个 prompt 一组"的形状——这正是组内分类的前提,也解释了后文"batch size 必须可被num_generations整除"的约束从何而来。
第二步:按奖励符号划分正负标签,并过滤无效组(L1104-1109)
pos_mask = (group_rewards > 0) neg_mask = (group_rewards <= 0) valid_mask = (pos_mask.sum(dim=1) != 0) & (neg_mask.sum(dim=1) != 0) if not valid_mask.any(): loss = torch.tensor(0., device=global_scores.device) * global_scores.mean()组内必须同时存在正、负样本才参与损失计算(单侧组没有对比信号);若当前 batch 所有组都无效,则损失置 0(保留计算图以便 backward)。
第三步:用 logsumexp 实现 $\log(1+\sum e^{\cdot/\tau})$(L1111-1126)
scaled_scores = batch_scores / self.real_tau zeros = torch.zeros(batch_scores.size(0), 1, device=batch_scores.device, dtype=batch_scores.dtype) # Negative Loss: log(1 + sum(e^{S_neg})) neg_input = scaled_scores.masked_fill(~batch_neg_mask, float('-inf')) neg_loss = torch.logsumexp(torch.cat([neg_input, zeros], dim=1), dim=1) # Positive Loss: log(1 + sum(e^{-S_pos})) pos_input = (-scaled_scores).masked_fill(~batch_pos_mask, float('-inf')) pos_loss = torch.logsumexp(torch.cat([pos_input, zeros], dim=1), dim=1) loss = (neg_loss + pos_loss).sum() / group_rewards.size(0)这里用一个巧妙的数值技巧实现公式中的常数项 1:把每行拼一个 0 元素再取logsumexp,$e^0=1$ 即成为和式中的"1";非目标类别用-inf掩掉($e^{-\infty}=0$),使 logsumexp 只累加对应类别。负样本项为 $\log(1+\sum_{\mathcal O_-} e^{\bar s^j/\tau})$,正样本项取 $-\bar s^i/\tau$ 后同理,最终按组数(group_rewards.size(0))归一。
第四步:KL 正则独立叠加(L1128-1130)
if self.beta != 0.0: kl_loss = (per_token_kl * completion_mask).sum() / completion_mask.sum().clamp(min=1.0) loss = loss + kl_loss * self.beta注意 REAL 与标准 GRPO 的一个重要结构差异:由于分类损失是序列级的,token 级策略损失在 grpo_trainer.py 中被直接置零(per_token_loss = torch.zeros_like(per_token_logps)),KL 项不再混入 token 级 loss,而是单独以序列级 token 平均的形式、乘以beta叠加到总损失上。这与 PPO/GRPO 分支中per_token_loss + self.beta * per_token_kl的写法不同,阅读指标日志时需注意kl的含义是叠加项而非权重内嵌项。
另外,REAL 分支不统计任何裁剪指标(grpo_trainer.py 中elif self.loss_type in ['sapo', 'real']: pass),因为分类损失本身没有 clip 机制——这也是"用有界权重替代裁剪"这一设计思想在工程上的自然体现。
训练脚本参考
仓库提供的完整参考脚本为 examples/train/grpo/internal/real.sh,采用 vLLM server 模式 rollout + 双卡全参训练 Qwen3-1.7B 的数学任务,核心片段如下(完整脚本含 rollout 服务启动部分):
CUDA_VISIBLE_DEVICES=2 \ swift rollout \ --model Qwen/Qwen3-1.7B NPROC_PER_NODE=2 \ CUDA_VISIBLE_DEVICES=0,1 \ swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen3-1.7B \ --dataset 'AI-MO/NuminaMath-TIR#5000' \ --enable_thinking false \ --reward_funcs accuracy \ --use_vllm true \ --vllm_mode server \ --vllm_server_host 127.0.0.1 \ --vllm_server_port 8000 \ --tuner_type full \ --torch_dtype bfloat16 \ --load_from_cache_file true \ --max_completion_length 4096 \ --num_train_epochs 1 \ --per_device_train_batch_size 8 \ --learning_rate 2e-6 \ --gradient_accumulation_steps 1 \ --save_total_limit 2 \ --save_steps 500 \ --logging_steps 1 \ --warmup_ratio 0.05 \ --dataloader_num_workers 4 \ --num_generations 8 \ --temperature 0.6 \ --system """You are a helpful math assistant. Solve the problem step by step and put your final answer within \\boxed{}.""" \ --log_completions true \ --num_iterations 1 \ --beta 0.001 \ --loss_type real \ --deepspeed zero2几个与 REAL 直接相关的取值值得注意:
--loss_type real是开关,配合默认--real_tau 0.5(脚本未显式设置即使用默认值);--num_generations 8与--per_device_train_batch_size 8恰好整除,满足下述组完整性要求(该脚本中--tuner_type full与多卡并行下,每个训练 batch 内的样本数需能按 8 个一组完整切分);--beta 0.001保留了一个较小的 KL 正则,对应源码中叠加的beta * kl_loss项;--reward_funcs accuracy提供环境奖励:REAL 的正负标签划分依赖奖励符号,因此必须配置 reward function(对数学题即为答案正确性)。
重要注意事项与适用限制
batch 必须包含完整组
配置训练参数时必须保证:
per_device_train_batch_size能被num_generations整除。
这是组内分类正确性的硬性前提:源码中global_scores.view(-1, self.num_generations)依赖"展平后的样本数 = 组数 × 每组生成数"才能正确还原组结构(grpo_trainer.py)。若不能整除,组边界会被错位切分,正负标签与 logit 的对应关系全部错乱。
单侧组被跳过
如前述实现细节,组内奖励全正或全负时该组不贡献损失(valid_mask过滤)。当奖励信号稀疏、大量组只出现单一符号时,有效梯度会比标准 GRPO 少,此时可考虑调高num_generations增大组内方差,或检查奖励函数区分度。
不兼容特性速查
从源码校验逻辑(rlhf_args.py、rlhf_args.py、grpo_trainer.py)确认,loss_type=real与以下特性互斥,配置时会被直接拒绝:
--teacher_model/--teacher_model_server(OPD-RL 教师蒸馏);--advantage_reweight rlsd;- 任何依赖奖励组内归一化的设置(
scale_rewards强制为'none')。
小结
REAL 用"奖励即标签"的组内二分类替换了 GRPO 的 advantage 估计与逐 token 加权:分类 logit $\bar{s}^k$ 只依赖新旧策略的序列级对数概率差,损失函数 $\log(1+\sum e^{\pm\bar s/\tau})$ 的梯度权重单调且上界为 $1/\tau$,从数学上同时消除了正样本梯度错配与负样本梯度主导。在 ms-swift 中,只需在swift rlhf --rlhf_type grpo基础上追加--loss_type real(可选--real_tau,默认 0.5),并注意 batch 组完整性与上述互斥约束,即可复现 examples/train/grpo/internal/real.sh 所示的完整训练流程;实现细节可进一步对照 swift/rlhf_trainers/grpo_trainer.py 与 swift/rlhf_trainers/args_mixin.py 深入阅读。
【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考