Prhub

#6348 [algo] fix: vectorized grpo low-variance scaling

原始 PR 作者 haoyang9804 合并时间 2026-05-14 21:47 文件变更 4 提交数 1 评论 0 代码增减 +35 / -4

执行摘要

修复向量化 GRPO 低方差组的缩放异常

PR body 指出 compute_grpo_vectorized_outcome_advantage() 旨在避免 Python 分组开销的同时匹配标准 GRPO 优势实现。但对于低方差奖励组,向量化路径会悄无声息地将归一化优势缩小数个数量级(示例中 canonical GRPO 给出 (-0.6196, 0.6196),而向量化版本仅 (-0.0050, 0.0050))。这个 bug 不会导致崩溃或报错,但会削弱低方差组的策略梯度信号,影响训练效果。

该 PR 值得精读,尤其是 groupwise.py 中方差计算的数值稳定性改进,以及 core_algos.pyeps 传递的设计权衡。建议熟悉 GRPO 实现细节的工程师关注。变更小而精确,测试覆盖充分,可以快速合并。

讨论亮点

无人工 review 评论。自动代码审查 bot (gemini-code-assist[bot]) 仅简要复述了变更内容,未提出问题。wuxibin89 直接批准。

实现拆解

  1. 重构 verl/utils/groupwise.py 中的 group_mean_std 函数:将方差计算从 s2 - (s1*s1)/count 的朴素公式改为基于中心化分数的 centered = scores - mean[gidx]index_add_ 累加。新公式具有更好的数值稳定性,尤其适用于组内奖励值相近的低方差场景。

  2. 修改 verl/trainer/ppo/core_algos.pycompute_grpo_vectorized_outcome_advantage 的调用:将传给 group_mean_stdeps 参数从原先的 epsilon 函数参数(默认 1e-6)改为硬编码 0.0。这样做的目的是去除内置的方差下限,让调用侧已有的 std_g[g] + epsilon 成为唯一的数值稳定措施,避免双重稳定导致缩放异常。

  3. 新增低方差回归测试:在 tests/trainer/ppo/test_core_algos_on_cpu.py 中添加 test_grpo_vectorized_matches_original_for_low_variance_rewards,使用 PR body 中的极低方差样例(四个奖励值 1.0, 1.00001, 2.0, 2.00001 分为两组)验证向量化 GRPO 和原始 GRPO 输出一致。在 tests/utils/test_groupwise.py 中添加 test_group_mean_std_low_variance_matches_torch_std,直接验证 group_mean_std 的输出与 PyTorch 标准 torch.std 一致。

文件 模块 状态 重要度
verl/utils/groupwise.py 工具函数 modified 6.11
verl/trainer/ppo/core_algos.py 训练器 modified 5.62
tests/trainer/ppo/test_core_algos_on_cpu.py 核心算法 modified 5.9
tests/utils/test_groupwise.py 分组工具 modified 5.53

关键符号

group_mean_std compute_grpo_vectorized_outcome_advantage

关键源码片段

verl/utils/groupwise.py core-logic

核心数值逻辑修复:改用中心化求和公式计算方差,提升低方差场景的稳定性。

# verl/utils/groupwise.py (group_mean_std 函数,关键变更部分 )ones = torch.ones_like(scores, dtype=torch.float32)count = torch.zeros(G, device=target, dtype=torch.float32).index_add_(0, gidx, ones)
s1 = torch.zeros(G, device=target, dtype=torch.float32).index_add_(0, gidx, scores)
# 原实现 : s2 = ... index_add_(0, gidx, scores * scores)
# var_num = s2 - (s1 * s1) / count ( 朴素求和,低方差时精度损失严重 )mean = s1 / count.clamp_min(1.0)
# 新实现 : 先计算中心化分数,再用 index_add 累加平方和,数值稳定性更好
centered = scores - mean[gidx]
var_num = torch.zeros(G, device=target, dtype=torch.float32).index_add_(
    0, gidx, centered * centered
)denom = (count - 1.0).clamp_min(1.0)
var = var_num / denom
std = torch.sqrt(torch.clamp(var, min=eps))# 单例组回退:mean=0, std=1
single = count <= 1.0
if torch.any(single):
    mean = mean.clone()
    std = std.clone()
    mean[single] = 0.0
    std[single] = 1.0return mean, std, count
verl/trainer/ppo/core_algos.py core-logic

修复向量化 GRPO 函数,将 eps 改为 0.0,消除双重稳定化。

# verl/trainer/ppo/core_algos.py (compute_grpo_vectorized_outcome_advantage)with torch.no_grad():
    scores = token_level_rewards.sum(dim=-1)
    g = as_torch_index(index, device=scores.device)
    # 关键修复:eps=0.0 避免内置方差底板,使调用侧 (std_g[g] + epsilon) 成为唯一稳定化措施
    mean_g, std_g, _ = group_mean_std(scores, g, eps=0.0, device=scores.device)
    if norm_adv_by_std_in_grpo:
        scalars = (scores - mean_g[g]) / (std_g[g] + epsilon)
    else:
        scalars = scores - mean_g[g]
    advantages = scalars.unsqueeze(-1) * response_mask
    return advantages, advantages
tests/trainer/ppo/test_core_algos_on_cpu.py test-coverage

新增低方差回归测试,验证向量化 GRPO 与原始 GRPO 输出一致。

# tests/trainer/ppo/test_core_algos_on_cpu.py ( 新增测试 )def test_grpo_vectorized_matches_original_for_low_variance_rewards():
    # 两个 prompt,每个生成 2 个 response,组内奖励值极其接近
    token_level_rewards = torch.tensor(
        [[1.0], [1.00001], [2.0], [2.00001]], dtype=torch.float32
    )
    response_mask = torch.ones_like(token_level_rewards)
    index = np.array(["prompt-a", "prompt-a", "prompt-b", "prompt-b"], dtype=object)
​
    # canonical GRPO(逐组循环)
    adv1, ret1 = compute_grpo_outcome_advantage(
        token_level_rewards=token_level_rewards,
        response_mask=response_mask,
        index=index,
    )
    # vectorized GRPO
    adv2, ret2 = compute_grpo_vectorized_outcome_advantage(
        token_level_rewards=token_level_rewards,
        response_mask=response_mask,
        index=index,
    )
​
    # 修复后两者应一致;修复前低方差组偏差数个数量级
    assert torch.allclose(adv1, adv2, rtol=1e-5, atol=1e-6)
    assert torch.allclose(ret1, ret2, rtol=1e-5, atol=1e-6)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险较低。变更集中在数值计算路径,且通过新增的回归测试覆盖了关键的低方差场景。但需注意:

  • group_mean_std 改用中心化求和可能影响其他调用方(如 RLOO vectorized),但 RLOO 路径通过已有测试 test_rloo_and_vectorized_equivalence 覆盖,且该测试未修改,说明新公式兼容已有场景。
  • 硬编码 eps=0.0 意味着任何潜在的被零除风险完全由调用侧的 + epsilon 承担,若以后某处调用 group_mean_std 时未确保加 epsilon,可能引入除零错误。当前在 GRPO vectorized 中已正确处理,但依赖约定。

只影响使用 algorithm.adv_estimator=grpo_vectorized 的训练实验。修复后,向量化 GRPO 在低方差奖励组中的优势值将与原始 GRPO 完全一致,策略梯度信号恢复正常。不受影响的其他路径包括原始 GRPO、RLOO 及其向量化版本。影响范围小,但修复本身对正确的 GRPO 实现至关重要。

数值稳定性修复 低方差路径回归

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论