Prhub

#6848 [fsdp, megatron, trainer] fix: enhance mem footprint for forward_kl_topk OPD

原始 PR 作者 dimjava 合并时间 2026-07-01 05:51 文件变更 7 提交数 2 评论 9 代码增减 +272 / -76

执行摘要

OPD top-k 蒸馏跳过 log_probs 和 PPO 损失,降低显存占用

OPD(Online Policy Distillation)训练中,当仅使用 top-k 蒸馏损失(forward_kl_topk)且不启用 policy gradient 和 task rewards 时,原本仍然会计算全词表 log_probs 和 PPO 损失,造成不必要的显存和计算开销,在显存紧张的场景下容易 OOM。PR 作者希望通过引入 distillation_only 标志来跳过这些冗余操作。

本 PR 设计精巧,通过一个标志串联 trainer→engine→loss 的多个模块,是典型的“配置驱动优化”案例。值得精读,尤其关注 FSDP engine 中操作顺序的修复和 Megatron 中 _distillation_use_topk_active 的传递方式。建议在后续工作中增加 end-to-end GPU 测试。

讨论亮点

Gemini Code Assist 在 review 中指出 FSDP engine 中存在潜在的顺序问题:当 calculate_entropy=False 时,logprobs_from_logits 会 inplace 修改 logits_rmpad,而后续 logits_processor_func 可能使用被污染后的 logits 计算蒸馏损失。作者回复“fixed”,并在实际代码中将 logits_processor_func 的调用提前到 log_probs 计算之前,确保蒸馏损失使用原始 logits。讨论结论为问题已解决,无未解决疑虑。

实现拆解

  1. Trainer 侧设置 distillation_only 标志:在 verl/trainer/ppo/ray_trainer.pyverl/trainer/ppo/v1/trainer_base.py_update_actor 中,当 distillation_use_topk=Trueuse_task_rewards=Falseuse_policy_gradient=False 时,将 distillation_only 设为 True,并通过 batch_tdextra_info 传递给 engine。
  2. Loss 函数跳过 PPO 计算verl/trainer/distillation/losses.pydistillation_ppo_loss 函数增加判断:若 use_task_rewards=Falseuse_policy_gradient=False,则直接跳过 ppo_loss 调用,避免不必要的 loss 计算。
  3. FSDP engine 条件跳过 log_probsverl/workers/engine/fsdp/transformer_impl.pyprepare_model_outputs 从 batch 中读取 distillation_only 标志,在 fused kernel 和 eager 分支中均跳过 log_probs 的计算,同时修复了在 eager 分支中先调用 logits_processor_func 再计算 log_probs 的顺序问题,防止 log_probs 的 inplace 操作污染 logits。
  4. Megatron engine 实现同样跳过逻辑verl/workers/engine/megatron/transformer_impl.py_lm_head_logits_processor 方法新增 distillation_only 参数,在蒸馏模式下直接跳过 vocab_parallel_log_probs_from_logits 的调用,仅保留 top-k 蒸馏所需的输出。此外,在 forward_backward_batch 中记录 _distillation_use_topk_active,并在 optimizer_step 前根据该标志主动调用 empty_cache(),缓解显存紧张。
  5. 配套测试:新增 tests/workers/test_megatron_distillation_only_on_cpu.py,通过 patch 的方式验证 Megatron engine 的 _lm_head_logits_processor 在蒸馏模式下不调用 vocab_parallel_log_probs_from_logits,同时蒸馏 key 被正确传播。修改 tests/workers/test_distillation_topk_symmetry_on_cpu.py 以覆盖两种 padding 模式。
文件 模块 状态 重要度
verl/workers/engine/megatron/transformer_impl.py Megatron 引擎 modified 8.23
tests/workers/test_megatron_distillation_only_on_cpu.py 测试 added 7.8
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 7.19
verl/trainer/distillation/losses.py 损失函数 modified 5.8
verl/trainer/ppo/ray_trainer.py Trainer modified 5.8
verl/trainer/ppo/v1/trainer_base.py Trainer modified 5.8
tests/workers/test_distillation_topk_symmetry_on_cpu.py 测试 modified 5.49

关键符号

_lm_head_logits_processor prepare_model_outputs distillation_ppo_loss _update_actor

关键源码片段

verl/workers/engine/megatron/transformer_impl.py core-logic

核心引擎修改:新增 _lm_head_logits_processor 方法(将原来内联逻辑提取为独立方法),并添加 distillation_only 参数以跳过 log_probs 计算;optimizer_step 前根据蒸馏标志主动清空缓存。

def _lm_head_logits_processor(
    self,
    logits, label, temperature, *,
    calculate_sum_pi_squared: bool,
    calculate_entropy: bool,
    distillation_use_topk: bool,
    distillation_only: bool, # 新增:蒸馏模式下跳过 log_probs
    logits_processor_func: Callable,
    batch: TensorDict,
    data_format: str,
):
    # 温度处理
    logits.div_(temperature.unsqueeze(dim=-1).to(logits.dtype))
    ret = {}
    if calculate_sum_pi_squared:
        ret["sum_pi_squared"] = vocab_parallel_sum_pi_squared(logits)
    if calculate_entropy:
        # 需要 clone 以保留 logits 给后续操作
        logits_bak = logits.clone()
        if self.engine_config.entropy_from_logits_with_chunking:
            entropy = vocab_parallel_entropy_with_chunking(logits, chunk_size=...)
        else:
            entropy = vocab_parallel_entropy(logits)
        ret["entropy"] = entropy
    # 核心选择:蒸馏模式跳过 log_probs
    if not distillation_only:
        ret["log_probs"] = vocab_parallel_log_probs_from_logits(logits, label)
    # 始终调用蒸馏 logits processor
    if distillation_use_topk and logits_processor_func is not None:
        outputs = logits_processor_func(
            student_logits=logits,
            data=data,
            data_format=data_format
        )
        ret.update(outputs)
    return ret
verl/workers/engine/fsdp/transformer_impl.py core-logic

FSDP 引擎修改:从 batch 读取 distillation_only 标志,在 fused kernel 和 eager 分支跳过 log_probs 计算,并修复了 logits_processor_func 与 log_probs 计算的顺序。

def prepare_model_outputs(self, output, output_args, micro_batch: TensorDict, logits_processor_func):
    # ... 读取标志
    distillation_only = tu.get_non_tensor_data(data=micro_batch, key="distillation_only", default=False)
    # ...
    if use_fused_kernels:
        log_probs = None
        if not distillation_only:
            log_probs = output.log_probs.squeeze(0) # (total_nnz,)
        entropy_rmpad = output.entropy.squeeze(0) if calculate_entropy else None
    else:
        logits_rmpad = output.logits.squeeze(0)
        logits_rmpad.div_(temperature_rmpad.clamp(min=1e-8).unsqueeze(-1).to(logits_rmpad.dtype))
        # 先调用蒸馏 processor,防止 log_probs 的 inplace 操作污染 logits
        if distillation_use_topk:
            outputs = logits_processor_func(student_logits=logits_rmpad.unsqueeze(0), data=micro_batch)
            # ... 存储蒸馏输出
        log_probs = None
        if not distillation_only:
            # 再计算 log_probs(可能 inplace 修改 logits_rmpad,但蒸馏已提取完毕)
            log_probs = logprobs_from_logits(logits_rmpad, labels=input_ids_rmpad_rolled, inplace_backward=True)
        # ...
verl/trainer/distillation/losses.py core-logic

Loss 函数调整:当 use_task_rewards=False 且 use_policy_gradient=False 时跳过 PPO 损失计算,避免不必要的计算。

def distillation_ppo_loss(config, distillation_config, model_output, data, dp_group):
    # ...
    distillation_loss_config = distillation_config.distillation_loss
    distill_loss, distill_metrics = distillation_loss(...)
​
    # 只有需要时才计算 policy loss
    if not distillation_loss_config.use_task_rewards and not distillation_loss_config.use_policy_gradient:
        policy_loss = 0.0
        policy_metrics = {}
    else:
        policy_loss, policy_metrics = ppo_loss(config, model_output, data, dp_group)
        if not distillation_loss_config.use_task_rewards:
            policy_loss = 0.0
    # 合并损失
    policy_loss += distill_loss * distillation_loss_coef
    return policy_loss, policy_metrics

评论区精华

FSDP engine 中 logits_processor_func 与 log_probs 的顺序问题 正确性

Gemini Code Assist 指出在 FSDP engine 的 eager 分支中,若 calculate_entropy=False,logprobs_from_logits 会 inplace 修改 logits_rmpad,而 logits_processor_func 在之后调用,导致蒸馏损失计算使用了被修改的 logits。

结论:作者修复:将 logits_processor_func 的调用提前到 log_probs 计算之前,确保蒸馏损失使用原始 logits。 · 已解决

风险与影响

  1. FSDP engine 顺序修复:虽然已修复,但如果未来有新的 inplace 操作加入,可能再次引入类似问题。
  2. distillation_only 默认 False,现有行为完全兼容,但若用户错误组合配置(如同时设置 use_policy_gradient=False 但通过其他路径依赖 log_probs)可能产生微妙错误。
  3. Megatron 中的 empty_cache 调用增加了运行时开销,但在蒸馏场景下利大于弊。
  4. 单元测试仅覆盖 CPU 和 mock,缺少真实 GPU 的端到端蒸馏测试,可能遗漏分布式下的通信问题。

影响范围:此变更主要影响使用 OPD 蒸馏且 use_policy_gradient=Falseuse_task_rewards=False 的用户,他们能观察到显存下降和训练吞吐提升。其他用户完全不受影响(distillation_only 默认 False)。系统层面:FSDP 和 Megatron 引擎均有修改,覆盖两大主流后端。团队:降低蒸馏训练的资源门槛,便于在更小的 GPU 集群上开展蒸馏实验。

核心路径变更 缺少端到端 GPU 测试 distillation_only 标志依赖配置正确性

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论