Prhub

#6511 [veomni, fsdp] feat: enable fused top-K distillation kernel for OPD

原始 PR 作者 Luosuu 合并时间 2026-05-29 13:52 文件变更 9 提交数 7 评论 3 代码增减 +337 / -5

执行摘要

为 VeOmni 引擎集成 fused top-K 蒸馏核,避免材料化完整 logits

原先 fused 路径只产生 per-token log_probs/entropy,而 top-K 蒸馏需要完整 logits 计算加权交叉熵,因此 OPD 批次必须设置 use_fused_kernels=False,无法享受 fused kernel 的显存节省。VeOmni 0.1.11 的 chunk_topk_distill_function 在分块 fused-linear 前向中内联计算蒸馏损失,避免了材料化完整 [B, L, V] logits。本 PR 将其接入 verl 的 actor 流水线,使得 fused 路径也能支持 top-K 蒸馏。

值得精读,尤其是学习如何在引擎层面注入额外张量以支持 fused kernel 的蒸馏功能。输入注入和输出提取的对称设计(eager vs fused 路径输出格式一致)是一个好实践。但需注意该功能目前仅支持 VeOmni backend,且依赖于 veomni 的 bug 修复。

讨论亮点
  • gemini-code-assist[bot] 发现 SP 切片缺失:在 Ulysses 序列并行启用时,teacher 的 top-K 张量未进行 padding 和切片,会导致与 student 序列维度不匹配而崩溃。作者在后续提交中修复,先尝试 ulysses_pad_and_slice_inputs,后改用更通用的 slice_input_tensor 处理 3D 张量。
  • wuxibin89 询问 FSDP backend 支持:询问 chunk_topk_distill_function 是否也适用于 FSDP backend。Luosuu 明确回答“Not yet”,表明该功能当前仅 VeOmni backend 支持。

实现拆解

  1. 依赖升级:将 CI 中的 veomni 从 0.1.10 升级到 0.1.11(涉及 5 个 workflow 文件),获取所需的 chunk_topk_distill_function 和 FusedLinearAuxOutput 中的蒸馏字段。
  2. VeOmni 引擎输入注入:在 VeOmniEngineWithLMHead.prepare_model_inputs 中,当 use_fused_kernels=True、distillation_use_topk=True 且 micro_batch 包含 teacher_ids 时,从 micro_batch 读取 teacher_ids 和 teacher_logprobs(均从嵌套张量中提取 values() 并 unsqueeze(0) 得到 (1, total_nnz, K) 形状),若启用 Ulysses SP 则通过 slice_input_tensor 沿 seqlen 维度切片,然后分别以 teacher_topk_ids 和 teacher_topk_log_probs 键放入 model_inputs,使其经模型前向传递到 ForCausalLMLoss,进而路由到 chunk_topk_distill_function。同时增加了 paired validation,防止缺少 teacher_logprobs 时静默失败。
  3. FSDP 引擎输出提取:在 FSDPEngineWithLMHead.prepare_model_outputs 的 fused 分支中,当 distillation_use_topk=True 且 output.fused_linear_aux 存在时,从 aux_outputs 中逐个提取 distillation_losses、student_mass、teacher_mass,进行 squeeze(0) 和必要的 Ulysses SP 去填充(通过 gather_outputs_and_unpad),最后封装为嵌套张量(nested_tensor_from_jagged),存入 model_output,与 eager 路径的格式一致,使得下游损失函数无需区分路径。
  4. 配套示例脚本:新增两个 OPD 示例脚本(run_qwen3_8b_mopd_veomni.sh 多教师 VL 和 run_qwen3_0.6b_opd_veomni.sh 单教师文本),展示如何通过设置 use_fused_kernels=True、distillation_use_topk=True 等参数启用 fused 蒸馏路径,并给出默认超参数配置。
文件 模块 状态 重要度
verl/workers/engine/veomni/transformer_impl.py VeOmni 引擎 modified 6.88
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 6.25
examples/on_policy_distillation_trainer/run_qwen3_8b_mopd_veomni.sh 示例脚本 added 5.13
examples/on_policy_distillation_trainer/run_qwen3_0.6b_opd_veomni.sh 示例脚本 added 5.08
.github/workflows/e2e_ppo_trainer_veomni_vllm.yml CI 配置 modified 2.38
.github/workflows/e2e_ppo_trainer_veomni_vllm_ascend.yml CI 配置 modified 2.38
.github/workflows/e2e_sft_llm.yml CI 配置 modified 2.38
.github/workflows/e2e_sft_llm_ascend.yml CI 配置 modified 2.38
.github/workflows/e2e_sft_vlm.yml CI 配置 modified 2.38

关键符号

VeOmniEngineWithLMHead.prepare_model_inputs FSDPEngineWithLMHead.prepare_model_outputs

关键源码片段

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

核心改动:在 prepare_model_inputs 中注入 teacher top-K 张量并支持 SP 切片,是 fused 蒸馏路径的入口。

        # ... 前面已设置 shift_labels 和 return_log_probs ...
        # Pass teacher top-K tensors so ForCausalLMLoss routes to
        # chunk_topk_distill_function for fused distillation.
        # 从 micro_batch 中获取配置标记
        distillation_use_topk = tu.get_non_tensor_data(
            data=micro_batch, key="distillation_use_topk", default=False
        )
        # 仅当 fused 蒸馏启用且 teacher_ids 存在时才注入
        if distillation_use_topk and "teacher_ids" in micro_batch.keys():
            # 配对校验:如果 teacher_ids 存在但 teacher_logprobs 缺失则报错
            if "teacher_logprobs" not in micro_batch.keys():
                raise ValueError(
                    "teacher_ids present without teacher_logprobs; "
                    "both must be provided together for fused top-K distillation."
                )
            # 从嵌套张量中提取 values 并添加 batch 维度 → (1, total_nnz, K)
            teacher_topk_ids = micro_batch["teacher_ids"].values().unsqueeze(0)
            teacher_topk_log_probs = micro_batch["teacher_logprobs"].values().unsqueeze(0)
            # 如果启用了 Ulysses 序列并行,沿 seqlen 维度 (dim=1) 进行切片
            if self.use_ulysses_sp:
                from verl.utils.ulysses import slice_input_tensor
                # teacher 张量为 3D,与 input_ids 的 2D 不同,需直接调用 slice_input_tensor
                teacher_topk_ids = slice_input_tensor(teacher_topk_ids, dim=1, padding=True)
                teacher_topk_log_probs = slice_input_tensor(teacher_topk_log_probs, dim=1, padding=True)
            # 将处理后的张量放入 model_inputs,供模型前向使用
            model_inputs["teacher_topk_ids"] = teacher_topk_ids
            model_inputs["teacher_topk_log_probs"] = teacher_topk_log_probs
verl/workers/engine/fsdp/transformer_impl.py core-logic

在 prepare_model_outputs 的 fused 分支中提取蒸馏输出,转换为嵌套张量,与 eager 路径格式一致。

                # After extracting log_probs and entropy from fused kernel...
                # 如果启用了 fused top-K 蒸馏,从 aux_outputs 中提取逐 token 蒸馏结果
                if distillation_use_topk:
                    # fused_linear_aux 由 chunk_topk_distill_function 填充
                    aux_outputs = getattr(output, "fused_linear_aux", None)
                    if aux_outputs is not None and aux_outputs.distillation_losses is not None:
                        cu_seqlens = input_ids.offsets() # 原始嵌套张量的 cu_seqlens
                        # 需要提取的三个字段
                        for field_name in ("distillation_losses", "student_mass", "teacher_mass"):
                            # 去掉 batch 维度 → (total_nnz,) 或 (total_nnz, K)
                            v = getattr(aux_outputs, field_name).squeeze(0)
                            # 如果使用了 Ulysses SP,需要先 gather 并去填充
                            if self.use_ulysses_sp:
                                pad_size = output_args["pad_size"]
                                v = gather_outputs_and_unpad(
                                    v, gather_dim=0, unpad_dim=0, padding_size=pad_size
                                )
                            # 重新包装为嵌套张量,与 eager 路径的输出格式一致
                            model_output[field_name] = torch.nested.nested_tensor_from_jagged(v, cu_seqlens)

评论区精华

SP 切片缺失导致形状不匹配 正确性

gemini-code-assist[bot] 指出当启用 Ulysses SP 时,teacher top-K 张量未进行 padding 和切片,会与 student 序列维度不一致导致崩溃。

结论:作者在后续提交中通过 slice_input_tensor 对 teacher 张量沿 seqlen 维度进行了 SP 切片。 · 已解决

FSDP backend 是否支持 fused 蒸馏 question

wuxibin89 询问 chunk_topk_distill_function 是否也适用于 FSDP backend。

结论:Luosuu 回答 'Not yet',表明当前仅 VeOmni backend 支持。 · 已解决

风险与影响

  1. 兼容性风险:依赖 veomni >=0.1.11,且存在已知 bug (#804) 阻碍端到端运行,需要 veomni 修复后才能正常使用。单元测试验证了路径正确,但生产使用需等待修复。
  2. 正确性风险:teacher 张量 SP 切片逻辑已修复,但如果未来修改 SP 切片规则,需要同步更新此处的切片调用。耦合在 prepare_model_inputs 中的切片逻辑可能成为维护点。
  3. 性能风险:无负面性能影响,但 fused 路径的蒸馏内核行为与 eager 不同,需确保 veomni 的 kernel 实现正确,目前单元测试验证了有限性和非负性。
  4. 配置风险:新增 distillation_use_topk 等配置项,若在其他引擎中误用可能静默忽略,目前未做严格校验。
  • 用户影响:为使用 VeOmni 引擎进行 OPD 训练的用户提供显存更友好的选项,尤其适合大词表模型。现有 eager 路径不受影响。
  • 系统影响:仅影响 VeOmni 引擎和 FSDP 引擎的 fused 分支(+25/+15 行),其他引擎无改动。示例脚本提供新参考,但端到端运行受阻于 veomni #804。
  • 团队影响:需要维护与 veomni 版本的兼容性;建议在 veomni #804 修复后重新验证端到端示例,并将其纳入 CI。
依赖 veomni bug #804 SP 切片对齐需维护 仅 VeOmni backend 支持

关联 Issue

#804 [ops] chunk_logprobs backward hits "setStorage ... storage of size 0" with FSDP2 + use_fused_kernels=True

完整报告

参与讨论