def agg_loss(
loss_mat: torch.Tensor,
loss_mask: torch.Tensor,
loss_agg_mode: str,
dp_size: int = 1,
batch_num_tokens: Optional[int] = None,
global_batch_size: Optional[int] = None,
loss_scale_factor: Optional[int] = None,
):
"""
Aggregate the loss across global batch to ensure the loss is invariant to fsdp/megatron parallelism.
NOTE: The returned loss has different behaviors for different backend:
- FSDP: the loss is directly used for backward.
- Megatron: the loss should be scaled by `num_microbatches` and `cp_size` for pp schedule.
Args:
loss_mat: micro batch loss matrix, (bs, response_length)
loss_mask: micro batch loss mask, (bs, response_length)
loss_agg_mode: method to aggregate the loss matrix into a scalar
dp_size: data parallel size
batch_num_tokens: number of valid tokens in global batch
global_batch_size: global batch size
loss_scale_factor: scale factor for "seq-mean-token-sum-norm" mode. If None, uses loss_mask.shape[-1].
Set this to a constant value to ensure consistent normalization throughout training.
Returns:
loss: `a scalar torch.Tensor`
aggregated loss
"""
if loss_agg_mode == "token-mean":
if batch_num_tokens is None:
batch_num_tokens = loss_mask.sum()
loss = verl_F.masked_sum(loss_mat, loss_mask) / batch_num_tokens * dp_size
elif loss_agg_mode == "seq-mean-token-sum":
seq_losses = torch.sum(loss_mat * loss_mask, dim=-1) # token-sum
seq_mask = (torch.sum(loss_mask, dim=-1) > 0).float() # exclude fully masked sequences
if global_batch_size is None:
global_batch_size = seq_mask.sum()
loss = verl_F.masked_sum(seq_losses, seq_mask) / global_batch_size * dp_size # seq-mean
elif loss_agg_mode == "seq-mean-token-mean":
seq_mask = torch.sum(loss_mask, dim=-1) # per-sequence token count
seq_losses = torch.sum(loss_mat * loss_mask, dim=-1) / (seq_mask + 1e-8) # token-mean
seq_mask = (seq_mask > 0).float() # exclude fully masked sequences
if global_batch_size is None:
global_batch_size = seq_mask.sum()
loss = verl_F.masked_sum(seq_losses, seq_mask) / global_batch_size * dp_size # seq-mean
elif loss_agg_mode == "seq-mean-token-sum-norm":
seq_losses = torch.sum(loss_mat * loss_mask, dim=-1)
if loss_scale_factor is None:
loss_scale_factor = loss_mask.shape[-1]
loss = torch.sum(seq_losses) / loss_scale_factor
else:
raise ValueError(f"Invalid loss_agg_mode: {loss_agg_mode}")
return loss
这段代码定义了一个损失聚合函数 agg_loss,主要用于在大规模分布式训练(如 FSDP 或 Megatron-LM)中,将不同 GPU 上的微批次(micro-batch)损失转换成一个全局一致的标量损失。
根据 loss_agg_mode 的不同,损失的计算逻辑可以分为以下四种模式:
1. token-mean (按 Token 取平均)
这是语言模型训练中最常用的模式(例如标准的交叉熵损失)。
-
计算逻辑:将所有有效 token 的损失加总,然后除以全局总的 token 数量。
-
数学表达:
-
特点:每个 token 对梯度的贡献是平等的。如果一个序列很长,它对总损失的影响就比短序列大。
2. seq-mean-token-sum (序列间平均,序列内求和)
这种模式先计算每个序列的总损失,再对序列维度取平均。
-
计算逻辑:
-
对每个序列(row),把所有有效 token 的损失加在一起。
-
对所有序列的结果取平均值(除以全局 Batch Size)。
-
-
特点:它对 Batch 中的每个"样本"给予平等的权重,而不论序列的长短。如果序列 A 有 100 个 token,序列 B 有 10 个 token,它们在最终 loss 中的权重是一样的。
3. seq-mean-token-mean (序列间平均,序列内也平均)
这种模式在两个层面上都进行了归一化。
-
计算逻辑:
-
对每个序列,先计算其内部有效 token 的平均损失。
-
再对这些"平均后的序列损失"计算全局平均。
-
-
特点:这是一种双重平均。它不仅让每个序列对梯度的贡献平等,还消除了序列长度对单个序列内部损失数值大小的影响。通常用于某些特定的强化学习(RLHF)场景或序列长度差异极大的任务。
4. seq-mean-token-sum-norm (带固定缩放因子的序列求和)
这是一种变体,通常为了保持梯度在不同长度设置下的数值稳定性。
-
计算逻辑:
-
将当前 micro-batch 的序列损失求和。
-
除以一个固定 的缩放因子
loss_scale_factor(通常是最大序列长度)。
-
-
特点:它不像前几种模式那样动态地除以实际存在的 token 数或序列数,而是除以一个预设的常数。这能保证在训练过程中,损失函数的量级(Scale)不会随着 Batch 中 Padding 的多少而剧烈波动。
模式对比总结
| 模式 | 侧重点 | 适用场景 |
|---|---|---|
| token-mean | 每个 Token 权重平等 | 预训练(Pre-training)、指令微调(SFT) |
| seq-mean-token-sum | 每个序列权重平等 | 序列级别任务,不希望长序列主导梯度 |
| seq-mean-token-mean | 极度平滑序列长度差异 | 强化学习、或者对 loss 稳定性要求极高时 |
| seq-mean-token-sum-norm | 固定缩放,数值稳定 | 分布式并行优化,防止动态 mask 导致梯度跳变 |
代码中的 dp_size 说明
你会发现前三种模式都乘以了 dp_size (Data Parallel Size)。这是因为在分布式训练中,每个进程只计算了自己那部分数据的 sum / global_count。为了在反向传播时得到正确的全局梯度平均值,需要通过这个系数来抵消分布式环境下求和与平均的比例关系。
您是正在调试 verl 框架(一个常用的强化学习训练框架)的代码吗?如果是的话,通常 PPO 的策略损失(Policy Loss)会偏向使用 token-mean。
dapo用的哪一种?
在 verl 框架(以及更广泛的偏好对齐算法中),DAPO (Decoupled Clip and Dynamic Sampling Policy Optimization) 或类似的偏好优化算法默认通常使用:
token-mean (按 Token 取平均)
这是目前的主流推荐配置,原因如下:
-
细粒度学习 :DAPO 在设计上强调"Token-level Policy Graduate Loss"(Token 级别的策略梯度损失)。使用
token-mean能够确保每一个生成的 token 对梯度的贡献是平等的,这有助于模型更精准地学习哪些词导致了奖励的提升。 -
官方推荐 :根据
verl的官方文档和最佳实践,对于 DAPO 和最新的 GRPO 变体,建议使用token-mean。文档中明确提到:"Settingloss_agg_modetotoken-meanwill mean the loss across all the tokens in all the sequences in a mini-batch."(将模式设置为 token-mean 意味着损失将跨越微批次中所有序列的所有 token 进行计算)。 -
算法演进 :早期的 GRPO 或某些 RL 算法可能使用
seq-mean-token-mean来复现原始论文的行为(为了让不同长度的回复在 batch 中权重一致),但在实际大规模微调中,token-mean往往能带来更稳定的收敛效果,因为它不会因为某个序列特别短而人为放大该序列中单个 token 的梯度。
如何在配置中确认?
如果你正在查看 verl 的配置文件(如 .yaml 文件),通常可以在 actor 的部分看到类似设置:
YAML
actor_rollout_ref:
actor:
loss_agg_mode: "token-mean" # DAPO 默认推荐值
总结
-
DAPO 默认/推荐:
token-mean -
为什么要选它: 保证梯度在 Token 层面的一致性,符合 DAPO 细粒度优化的逻辑,且在大模型分布式训练中数值最稳定。
您是想调整 loss 计算方式来解决模型训练不收敛的问题,还是在做多机多卡的速度优化?
seq-mean-token-sum和token-mean感觉区别不大啊?
从数学公式上看,它们确实很像,但在分布式训练 和序列长度不均的情况下,它们的梯度行为有显著区别。
我们可以通过一个简单的例子对比一下。假设你的 Batch 里只有两个序列:
-
序列 A:100 个有效 token。
-
序列 B:10 个有效 token。
1. token-mean (权重随长度线性增长)
它把所有序列看作一个超长的 Token 串。
-
计算逻辑 :
-
实质:序列 A 的权重是 100/110,序列 B 的权重只有 10/110。
-
直观感受 :序列 A 的梯度比 B 强 10 倍。如果你的数据里有极长和极短的回复,长回复会主导整个模型的更新方向。
2. seq-mean-token-sum (序列权重平等)
它强制每个样本(序列)对梯度的贡献是一样的。
-
计算逻辑 :
(实际上代码里是先求和再除以全局
global_batch_size,效果一致)。 -
实质 :序列 A 和序列 B 各占总 Loss 的 50%。
-
直观感受 :虽然序列 B 只有 10 个词,但它和 100 个词的序列 A 平起平坐。
核心区别总结
| 特性 | token-mean | seq-mean-token-sum |
|---|---|---|
| 谁更重要? | 长序列更重要(Token 越多权重越大) | 每个样本都一样重要 |
| 梯度稳定性 | 比较稳定,因为分母是总 Token 数 | 如果某个序列特别短且 Loss 异常,可能会产生梯度爆炸 |
| 训练侧重 | 侧重于学习把每一个词预测对 | 侧重于把每一个完整的句子/回答写好 |
| 典型算法 | GPT 预训练、SFT | 强化学习(如 PPO/DAPO),因为每个完整回答对应一个 Reward |
为什么你会觉得"区别不大"?
如果你训练时的 Padding 很少 ,或者所有序列的 长度非常接近(比如都截断到了 512),那么这两者的分母和缩放比例几乎是等价的,计算出来的 Loss 确实差别很小。
但在 RLHF 阶段,模型生成的回复长度波动很大(有的思维链 CoT 回复 2000 词,有的拒绝回答只有 10 词):
-
用
token-mean,模型会拼命学习如何优化那个 2000 词的长回复。 -
用
seq-mean-token-sum,模型会同等对待这长、短两个回答。
建议:
如果你在做 DAPO,且数据集中回复长度分布不均,建议坚持用 token-mean。因为它在分布式环境下(不同卡上的 token 数量不同时)计算出的梯度方向最稳健。
你想看下这两种模式在多卡并行(FSDP/Megatron)时是如何处理不同卡之间数据不平衡的吗?
Dr.GRPO里面用的什么?
在 verl 框架中,Dr.GRPO (即 "GRPO Done Right") 默认使用的是:
seq-mean-token-sum-norm (带固定缩放因子的序列求和)
这是 Dr.GRPO 论文和 verl 框架针对该算法专门推荐的模式。
为什么 Dr.GRPO 要用这种模式?
Dr.GRPO 相比于原始的 GRPO,核心改进之一就是消除"长度偏置(Length Bias)"。我们可以对比一下:
-
原始 GRPO (
seq-mean-token-mean):-
做法:先对每个序列内所有 token 的 loss 取平均,再在序列间取平均。
-
问题:这会导致"短答案"的梯度被异常放大(因为分母小),模型为了降低 loss,会倾向于生成更长的回复来稀释单个 token 的 loss。
-
-
DAPO (
token-mean):-
做法:所有 token 平等,除以当前 batch 的总有效 token 数。
-
问题:虽然缓解了长度偏置,但分母是动态的(取决于当前采样出了多少 token),这在分布式训练中会导致梯度不稳定性。
-
-
Dr.GRPO (
seq-mean-token-sum-norm):-
做法 :将所有 token 的 loss 求和,然后除以一个固定的常数 (通常是配置中的
max_prompt_length + max_response_length或者一个预设的loss_scale_factor)。 -
优势:
-
无偏性:它不再除以动态的 token 数量,从而保证了梯度的期望值不会因为生成长度的变化而产生偏差。
-
惩罚一致性 :对于错误的 token,无论它出现在长回复还是短回复中,其梯度惩罚的力度是绝对一致的。
-
-
配置建议
如果你想在 verl 中完全复现 Dr.GRPO 的效果,除了设置 loss_agg_mode,通常还需要配合以下参数:
| 配置项 | Dr.GRPO 推荐值 | 作用 |
|---|---|---|
loss_agg_mode |
"seq-mean-token-sum-norm" |
使用固定因子归一化,消除长度偏置 |
use_kl_loss |
False |
Dr.GRPO 通常不直接把 KL 加在 Loss 里,而是作为 Reward 惩罚 |
norm_adv_by_std |
False |
甚至可能关闭优势函数的标准差归一化(视具体实现而定) |
总结
-
如果你追求训练最稳定、逻辑最严密 的 Dr.GRPO 复现,请使用
seq-mean-token-sum-norm。 -
如果你只是普通的 RLHF 或 SFT,
token-mean通常是效果和性能最平衡的选择。
你是准备在 DeepSeek R1 这种推理模型上尝试 Dr.GRPO 吗?如果是的话,这种 Loss 聚合方式对防止模型变得"越来越啰嗦(Verbosity Bias)"非常有帮助。
在 verl 框架以及 DeepSeek 相关的技术演进中,GSPO (Group Relative Policy Optimization) 本质上是 GRPO 的一个变体(有时在代码实现中,GSPO 指代加入了更严格组内优势规范化的版本)。
对于 GSPO,它通常遵循与 Dr.GRPO 相同的哲学,即使用:
seq-mean-token-sum-norm (或者在某些实现中为 token-mean)
但最关键的点不在于 token-sum 还是 token-mean,而在于它如何处理**组内(Group)**的归一化。
GSPO 的核心逻辑区别
GSPO 和 GRPO 的核心在于**"组内相对比较"**。假设一个 Prompt 生成了 G 个回答(一个 Group):
-
优势计算(Advantage):
GSPO 会计算这 G 个回答的奖励(Reward),然后减去这组的平均值,再除以标准差。
-
损失聚合(Loss Aggregation):
由于 Advantages 已经在组内进行了归一化,梯度的幅度已经通过 R 的相对差异控制住了。此时,为了防止长度偏置(Length Bias),GSPO 倾向于使用 seq-mean-token-sum-norm。
为什么 GSPO 特别依赖这种 Loss 聚合?
如果你在 GSPO 中使用标准的 token-mean,会发生以下情况:
-
长答案陷阱 :如果一组回答中,一个正确的答案非常长,而一个错误的答案非常短。在
token-mean下,长答案的每一个 token 贡献的梯度虽然被总数平均了,但因为它 token 多,总的梯度更新量依然可能主导模型,导致模型学会"通过变长来骗取更高分"。 -
Dr.GRPO 的修正 :Dr.GRPO(以及改进后的 GSPO)通过固定分母的
seq-mean-token-sum-norm,确保无论模型生成多长,梯度在 token 维度的"力度"是恒定的。
如何选择?
在 verl 中运行 GSPO 时,你可以根据你的目标选择:
| 你的目标 | 推荐模式 | 原因 |
|---|---|---|
| 严谨复现 DeepSeek R1 风格 | seq-mean-token-sum-norm |
彻底消除长度对梯度的不公平贡献 |
| 常规强化学习训练 | token-mean |
实现最简单,在大多数任务中收敛最快 |
| 模型回复长度非常固定 | seq-mean-token-sum |
逻辑简单,各样本权重绝对平等 |
总结
GSPO 在最新实践中首选 seq-mean-token-sum-norm。
它的核心目的是:通过组内比较解决"好坏"问题,通过固定因子归一化解决"长短"偏置问题。
你是发现使用默认设置训练时,模型的回复变得越来越长(Verbosity Explosion)了吗?如果是这样,切换到 seq-mean-token-sum-norm 通常能立即缓解这个问题。