Prhub

#2144 perf: fuse PPO logprob entropy computation

原始 PR 作者 zhuzilin 合并时间 2026-06-29 16:08 文件变更 6 提交数 1 评论 0 代码增减 +1339 / -84

执行摘要

融合 PPO logprob/entropy 计算,减少 allreduce 一半

PR body 指出原本 logprob 和 entropy 分离计算存在冗余,通过 fused autograd helper 减少开销。移除不再使用的旧生产辅助函数。

建议团队成员精读 _VocabParallelLogProbEntropy 的 autograd 实现,作为融合通信的工程模式;同时也值得检查 CI 中新增的两个测试是否运行正常,确保数值稳定性。对于需要优化 PPO 训练性能的场景,此 PR 提供了解耦可复用的参考。

讨论亮点

无 reviewer 评论,作者自行合并。可能由于 PR 明确且已被充分验证。

实现拆解

  1. slime/utils/ppo_utils.py 中新增辅助函数 _maybe_all_reduce_get_vocab_parallel_rank_size,用于在任意进程组上进行 allreduce 和获取 rank/size。
  2. 定义 fused autograd 类 _VocabParallelLogProbEntropy,其 forward 方法中先通过 vocab_parallel_softmax 一次计算 softmax 及 sum_exp_logits,然后根据 with_entropylog_prob_keep_mask 条件分别计算 logprob 和 entropy,共享 softmax 结果;backward 方法中根据链式法则为 logprob 和 entropy 的梯度分别计算并相加,传回 grad_input。
  3. slime/backends/megatron_utils/loss.pyget_log_probs_and_entropy 函数中调用新的 fused 函数 calculate_log_probs_and_entropy(实际为 _VocabParallelLogProbEntropy.apply),替代原来的分离调用,调整接口适配。
  4. 移除 ppo_utils.py 中旧的 compute_log_probs 函数和 _VocabParallelEntropy 类,因为它们已被新实现取代。
  5. 新增两个测试文件:tests/test_ppo_logprob_entropy.py 提供 CPU 环境下的数值正确性验证,使用 Gloo 分布式模拟并对比 unfused 参考实现;tests/test_ppo_logprob_entropy_gpu.py 提供 GPU 环境下的 Megatron 多卡校验,覆盖各种 mask 和 entropy 组合,确保 forward 和 backward 与旧版一致。
  6. 在 CI 配置 .github/workflows/pr-test.yml.j2 和生成的 pr-test.yml 中将这两个新测试注册到对应 job 中(CPU 测试在无 GPU 的 job,GPU 测试在 2 GPU 的 job)。
文件 模块 状态 重要度
slime/utils/ppo_utils.py PPO 计算 modified 8.83
tests/test_ppo_logprob_entropy.py 测试·CPU added 8.05
tests/test_ppo_logprob_entropy_gpu.py 测试·GPU added 8.05
slime/backends/megatron_utils/loss.py 损失函数 modified 4.9
.github/workflows/pr-test.yml CI 配置 modified 4.49
.github/workflows/pr-test.yml.j2 CI 模板 modified 3.19

关键符号

_VocabParallelLogProbEntropy.forward _VocabParallelLogProbEntropy.backward _maybe_all_reduce _get_vocab_parallel_rank_size

关键源码片段

slime/utils/ppo_utils.py core-logic

核心修改:新增 fused autograd 类 `_VocabParallelLogProbEntropy`,替换旧分离实现,并添加辅助函数。

# slime/utils/ppo_utils.py ( 关键片段 )
# ( 前略 )def _maybe_all_reduce(tensor, op, process_group):
    '''Conditional all-reduce to handle both distributed and non-distributed cases.'''
    if dist.is_available() and dist.is_initialized():
        dist.all_reduce(tensor, op=op, group=process_group)class _VocabParallelLogProbEntropy(torch.autograd.Function):
    '''
    Fused autograd function that computes log-probability and optional entropy
    in one forward pass, sharing the softmax computation and its all-reduce
    cross communication. The backward similarly merges gradients from both losses.
    '''
    @staticmethod
    def forward(
        ctx,
        vocab_parallel_logits: torch.Tensor, # [seq_len, local_vocab]
        target: torch.Tensor, # [seq_len]
        log_prob_keep_mask: torch.Tensor | None,
        process_group,
        with_entropy: bool,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        vocab_parallel_logits = vocab_parallel_logits.float()
        seq_len, local_vocab = vocab_parallel_logits.shape
        rank, _ = _get_vocab_parallel_rank_size(process_group)
        vocab_start = rank * local_vocab
​
        # ---- Shared softmax primitive ----
        def vocab_parallel_softmax(logits):
            # stable softmax with cross-shard max/sum reduces
            max_vals = logits.max(dim=-1, keepdim=True).values
            _maybe_all_reduce(max_vals, dist.ReduceOp.MAX, process_group)
            normalized = logits - max_vals
            exp = normalized.exp()
            sum_exp = exp.sum(dim=-1, keepdim=True)
            _maybe_all_reduce(sum_exp, dist.ReduceOp.SUM, process_group)
            softmax = exp / sum_exp
            return normalized, sum_exp, softmax, max_vals
​
        # ---- Log-probability branch (always computed) ----
        if log_prob_keep_mask is None:
            norm, sum_exp, softmax, max_v = vocab_parallel_softmax(vocab_parallel_logits)
            log_probs = ... # gather and subtract log(sum_exp); handle partition boundaries
        else:
            masked_logits = vocab_parallel_logits.masked_fill(~log_prob_keep_mask, float('-inf'))
            norm, sum_exp, softmax, max_v = vocab_parallel_softmax(masked_logits)
            log_probs = ... # similar calculation
​
        # ---- Entropy branch (conditional) ----
        entropy = torch.zeros(seq_len, ...)
        if with_entropy:
            if log_prob_keep_mask is None:
                # reuse softmax from logprob path
                sum_s_t = (softmax * vocab_parallel_logits).sum(dim=-1, keepdim=True)
                _maybe_all_reduce(sum_s_t, dist.ReduceOp.SUM, process_group)
                entropy = (max_v + sum_exp.log() - sum_s_t).squeeze(-1)
            else:
                # separate softmax on unmasked logits for entropy
                _, entropy_sum_exp, entropy_softmax, entropy_max = vocab_parallel_softmax(vocab_parallel_logits)
                sum_s_t = (entropy_softmax * vocab_parallel_logits).sum(dim=-1, keepdim=True)
                _maybe_all_reduce(sum_s_t, dist.ReduceOp.SUM, process_group)
                entropy = (entropy_max + entropy_sum_exp.log() - sum_s_t).squeeze(-1)
​
        return log_probs, entropy

评论区精华

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

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

风险与影响

主要风险来自数值精度:融合计算改变了 softmax 和梯度累加的顺序,可能导致浮点误差。但测试使用 atol=1e-7/1e-8 严格验证了前向和反向的数值一致性。第二个风险是移除了旧函数,可能被外部模块引用;但重构中已确认所有调用点已更新(只有 loss.py 一处)。第三个风险是 fused 实现使用了 _maybe_all_reduce,在非分布式环境下可能未初始化的 process group,但辅助函数已处理回退。总体风险可控。

用户体验:PPO 训练性能提升,显存和通信带宽使用减少,梯度计算时间下降(具体幅度取决于模型规模)。系统层面:allreduce 调用次数减少一半,有助于减轻多节点通信压力。团队维护:代码结构简化,移除遗留函数,测试覆盖增强确保重构安全。影响范围限定在 PPO 相关训练的 Megatron backend,不影响其他算法(如 GRPO、Reinforce++)或后端(如 SGLang)。

数值精度变化风险 旧函数移除破坏外部引用 多 GPU 通信顺序改变

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论