Prhub

#6386 [fsdp] fix: emit distillation outputs in use_remove_padding=False path

原始 PR 作者 abinggo 合并时间 2026-05-18 18:17 文件变更 2 提交数 1 评论 1 代码增减 +174 / -0

执行摘要

重新应用 no-rmpad 蒸馏修复并修复 CPU 测试

6293 报告了 use_remove_padding=False 路径缺失 distillation top-k 处理,导致 distillation_losses 缺失并触发 KeyError。#6350 尝试修复但 #6360 因 CPU 测试失败(Triton CrossEntropyLoss 只能在 CUDA 上运行)而 revert。本 PR 重新应用生产修复并修正测试,确保两个 padding 分支的对称性。

值得精读,特别是测试中通过 patch 隔离外部依赖(Triton 内核)的做法,这为编写跨平台的单元测试提供了可借鉴的模式。生产修复虽小但正确,体现了对两个分支对称性的重视。

讨论亮点

没有 review 评论,但作者在 PR body 中解释了 #6350 被 revert 的原因(CPU 测试失败),并说明了本 PR 的测试修复方案(patch logprobs_from_logits)。原作者 wuxibin89 批准了该 PR。

实现拆解

  1. verl/workers/engine/fsdp/transformer_impl.pyprepare_model_outputs 方法中,在 no-rmpad 路径(else 分支末尾)增加了 distillation_use_topk 的处理逻辑,镜像 use_remove_padding=True 分支的做法:调用 logits_processor_func 得到输出,将每个 key 的 tensor 变换为嵌套 tensor 放入 model_output
  2. 新增测试文件 tests/workers/test_distillation_topk_symmetry_on_cpu.py,创建一个最小的 engine stub,用 object.__new__ 绕过初始化,并分别测试 use_remove_padding=TrueFalse。测试通过 patch 拦截 logprobs_from_logits,返回一个形状正确的零张量,避免调用 flash-attn 的 Triton 内核。
  3. 验证了蒸馏 keys 的传播正确性。
文件 模块 状态 重要度
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 6.54
tests/workers/test_distillation_topk_symmetry_on_cpu.py 蒸馏测试 added 7.32

关键符号

FSDPEngineWithLMHead.prepare_model_outputs test_distillation_outputs_emitted_in_both_padding_modes

关键源码片段

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

核心生产代码修复,添加 no-rmpad 路径下的 distillation top-k 输出传播

# 计算 log_probs 后
log_probs = logprobs_from_logits(logits=logits_rmpad, labels=input_ids_rmpad_rolled)# 镜像 use_remove_padding=True 分支的处理(参见 verl#6293)
# 这里没有 Ulysses SP gather,因为本分支是 no-SP 路径
# (log_probs 也没有被 gather),且 pad_size 只在
# prepare_model_inputs 的 use_remove_padding=True 路径中填充到 output_args
if distillation_use_topk:
    outputs = logits_processor_func(student_logits=logits_rmpad.unsqueeze(0), data=micro_batch)
    for k, v in outputs.items():
        v = v.squeeze(0)
        assert v.shape == log_probs.shape, (
            f'log_probs shape: {log_probs.shape}, {k} shape: {v.shape}'
        )
        model_output[k] = torch.nested.nested_tensor_from_jagged(v, cu_seqlens)# (bsz, j1), for each sample, length: [real_prompt_length + real_response_length]
log_probs = torch.nested.nested_tensor_from_jagged(log_probs, cu_seqlens)
tests/workers/test_distillation_topk_symmetry_on_cpu.py test-coverage

新增回归测试,确保两个 padding 分支蒸馏输出一致性,并修复了 CPU 兼容性问题

@pytest.mark.parametrize('use_remove_padding', [True, False])
def test_distillation_outputs_emitted_in_both_padding_modes(use_remove_padding):
    '''验证蒸馏输出在两个 padding 分支都能正确传播。参见 verl#6293。'''
    bsz = 2
    seq_lengths_list = [3, 2]
    seq_lengths = torch.tensor(seq_lengths_list, dtype=torch.int64)
    total_nnz = int(seq_lengths.sum())
    cu_seqlens = torch.cat([torch.tensor([0]), seq_lengths.cumsum(0)]).to(torch.int64)
    # ... 准备 input_ids, output 等 ...
​
    # 设置 micro_batch 参数
    micro_batch = TensorDict({'input_ids': input_ids_nested}, batch_size=[])
    tu.assign_non_tensor(micro_batch,
                         use_remove_padding=use_remove_padding,
                         pad_mode=DatasetPadMode.NO_PADDING,
                         use_fused_kernels=False,
                         calculate_entropy=False,
                         calculate_sum_pi_squared=False,
                         distillation_use_topk=True,
                         max_response_length=max(seq_lengths_list))
​
    eng = _make_engine_stub()
​
    # 使用 patch 替换 logprobs_from_logits,避免在 CPU 上调用 Triton 内核
    with patch('verl.workers.engine.fsdp.transformer_impl.logprobs_from_logits',
               return_value=torch.zeros(total_nnz)):
        model_output = eng.prepare_model_outputs(
            output, output_args, micro_batch,
            logits_processor_func=_make_logits_processor(_DISTILLATION_KEYS),
            logits_processor_func_requires_grad=False,
            loss_function=None)
​
        # 断言蒸馏 keys 在 model_output 中存在且形状匹配
        expected_keys = {'distillation_losses', 'student_mass'}
        assert expected_keys.issubset(model_output.keys()), \
            f'Missing distillation keys: {expected_keys - set(model_output.keys())}'
        for k in expected_keys:
            v = model_output[k]
            assert isinstance(v, torch.Tensor), f'{k} should be tensor, got {type(v)}'
            assert v.dim() == 2, f'{k} dim should be 2, got {v.dim()}'

评论区精华

CPU 测试环境下的 Triton 内核兼容性 测试

#6350 的测试在 CPU CI 上失败,因为 flash-attn 安装时 logprobs_from_logits 调用 Triton CrossEntropyLoss 只能在 CUDA 上运行。

结论:本 PR 通过 patch 在测试中替换 logprobs_from_logits 为返回零张量的存根,避免调用 Triton 内核,从而通过测试。 · 已解决

风险与影响

核心逻辑变更是增添了一个条件分支(distillation_use_topk),如果使用该功能但未提供正确的 logits_processor_func,可能引发错误。不过该函数应在调用前配置好,风险可控。测试通过存根隔离了外部依赖,不会影响其他测试。没有性能或安全性风险。

影响范围限于使用 FSDP 引擎且开启蒸馏训练的用户,特别是那些使用 use_remove_padding=False 配置的。这些用户之前会遇到 KeyError 错误,现在可以正常运行。影响程度较小,因为蒸馏功能还不是主线默认配置。

核心路径变更 测试依赖注入

关联 Issue

#6293 [bug] use_remove_padding=False 路径缺失 distillation top-k 处理,导致 distillation_losses 缺失并触发 KeyError
#6360 Revert "[fsdp] fix: emit distillation outputs in use_remove_padding=False path (#6293)"

完整报告

参与讨论