Prhub

#3211 Fix MTP gradient detachment in AutoBridge mode

原始 PR 作者 Shi-Dong 合并时间 2026-09-12 04:26 文件变更 2 提交数 1 评论 1 代码增减 +88 / -0

执行摘要

修复桥接模式 MTP 梯度分离缺失,防止辅助损失污染共享权重

PR body 明确指出:bridge 模式下模型由 HF checkpoint 直接构建,跳过了 core_transformer_config_from_args,命令行参数从未同步到 provider,因此 Megatron 默认的 mtp_detach_heads=False 被原样保留,MTP 辅助损失会错误地更新共享策略权重。修复目标是 'Apply the same detachment rule as the regular model provider before finalization, so the auxiliary loss trains only MTP-specific parameters',即让桥接路径与常规路径的训练语义对齐。PR body 还声明了边界:Environment-variable parsing、auxiliary-loss scaling、label-mask alignment 属于后续独立变更。

值得精读,重点看两点:一是 getattr 容错与“训练 flag 集中放置”的模块约定,二是测试文件用 AST 提取生产函数的技巧——它让无法在 CPU 导入的 GPU 模块函数获得回归覆盖,可复用到同类场景。由于 MTP 训练正确性还依赖后续的辅助损失缩放、label-mask 对齐等变更,建议与这些后续 PR 联动阅读。

讨论亮点

本 PR 没有留下实质的行内 review 评论(review_comments_count=0)。唯一 Issue 评论是作者触发 @claude review;claude[bot] 两轮评论分别为订阅提示与 'Code review found no issues';人类 reviewer guapisolo 与 Zhichenzzz 均 APPROVED,无遗留争议。值得记录的是自动与人工审核结论一致,且 PR body 主动声明了测试边界(CPU probe 非完整 GPU 训练测试),没有未解决的疑虑。

实现拆解

  1. 生产修改:在 miles/backends/megatron_utils/model_provider.py 的 _apply_bridge_runtime_config 函数中,紧邻 calculate_per_token_loss / variable_seq_lengths 的 loss 处理区块之后插入 4 行逻辑:通过 getattr(args, "enable_mtp_training", False) 读取训练开关,为真时将 provider.mtp_detach_heads 强制置为 True,并加注释说明对齐非 bridge 路径。使用 getattr 而非直接属性访问,是因为 bridge 模式的调用者可能只注册了 Megatron 标准参数,自定义 flag 可能不存在;缺失时回退为 False 以保持原行为。该函数 docstring 明确约定“新增训练 flag 集中放在此处”,此改动遵循既有约定。
  2. 行为语义:启用 MTP 训练时,MTP 辅助损失的反向梯度不再流入共享 backbone / embedding / output 层,只训练 draft 参数;mtp_num_layers 等其它字段不受影响。当 flag 关闭或缺失时,保留 provider 原有的 mtp_detach_heads 值,不强行改写。
  3. 测试配套:新增 tests/fast/backends/megatron_utils/test_bridge_mtp_detachment.py,注册到 stage-a-cpu CPU 套件(register_cpu_ci)。为绕开 model_provider.py 对 GPU-only Megatron 模块的导入,fixture 用 ast.parse 定位并提取 _apply_bridge_runtime_config 函数源码,在仅注入 argparse 的命名空间中 exec 后返回可调用对象。参数化 6 组用例覆盖 enable_mtp_training 的 True / False / 缺失三种状态 × 初始 mtp_detach_heads 的 True / False,断言最终值符合预期且 mtp_num_layers 未被误改。
  4. 验证结果:PR body 报告 6 个回归用例全部通过,未打补丁的 main 在 enabled-MTP 用例上失败;Nemotron Lightning AutoBridge 构造 + finalization 的 CPU 探针确认 mtp_detach_heads=True;Native Megatron MTP 的 CPU probe 显示共享 backbone / embedding / output 辅助梯度归零、draft 梯度非零、主模型损失仍更新 backbone。该探针使用小型 CPU 层与 mock 流水线元数据,并非完整 GPU 训练测试。
文件 模块 状态 重要度
miles/backends/megatron_utils/model_provider.py 模型提供 modified 5.84
tests/fast/backends/megatron_utils/test_bridge_mtp_detachment.py 回归测试 added 6.48

关键符号

_apply_bridge_runtime_config test_bridge_mtp_detachment apply_bridge_runtime_config runtime_args

关键源码片段

miles/backends/megatron_utils/model_provider.py data-contract

生产修复的唯一源码文件,在 `_apply_bridge_runtime_config` 中补上 MTP 梯度分离规则,是本次变更的核心。

def _apply_bridge_runtime_config(provider, args: argparse.Namespace) -> None:
    """Copy the runtime config from args onto a bridge-built provider.
​
    Bridge 模式从 HF checkpoint 构建模型,跳过了
    core_transformer_config_from_args,命令行参数因此从未到达 provider。
    这里只复制真正来自 args 的训练、并行、内存与数值配置字段;
    模型形状与 dtype 字段 provider 已从 checkpoint 持有正确值,
    直接覆盖会静默破坏模型(bridge 只警告、不报错)。
    新增训练 flag 应集中放在本函数内,而不是散落在其他代码中。
    """
    # 并行 / 分片配置
    provider.tensor_model_parallel_size = args.tensor_model_parallel_size
    provider.pipeline_model_parallel_size = args.pipeline_model_parallel_size
    provider.expert_model_parallel_size = args.expert_model_parallel_size
    provider.expert_tensor_parallel_size = args.expert_tensor_parallel_size
    provider.sequence_parallel = args.sequence_parallel
    provider.context_parallel_size = args.context_parallel_size
​
    # loss / 序列处理
    provider.calculate_per_token_loss = args.calculate_per_token_loss # CP>1 变长模型会断言此值
    provider.variable_seq_lengths = args.variable_seq_lengths
​
    # 对齐非 bridge 路径:MTP 只能训练自身 draft 参数。
    # getattr 兜底缺失的 flag——bridge 调用者可能只注册了 Megatron 参数,
    # 自定义的 enable_mtp_training 未注册时按 False 处理,保持原行为。
    if getattr(args, "enable_mtp_training", False):
        provider.mtp_detach_heads = True
​
    # 数值精度(训练基础设施,非模型定义字段)
    provider.attention_softmax_in_fp32 = args.attention_softmax_in_fp32
    provider.gradient_accumulation_fusion = args.gradient_accumulation_fusion
    provider.fp32_residual_connection = args.fp32_residual_connection
    provider.deterministic_mode = args.deterministic_mode
​
    # 激活重计算(此前被静默丢弃 -> 无 checkpointing -> 长上下文 OOM)
    provider.recompute_granularity = args.recompute_granularity
    provider.recompute_method = args.recompute_method
    provider.recompute_num_layers = args.recompute_num_layers
    provider.recompute_modules = args.recompute_modules
​
    # 激活 / 内存 offload
    provider.cpu_offloading_num_layers = args.cpu_offloading_num_layers
    provider.distribute_saved_activations = args.distribute_saved_activations
    # cpu_offloading 是派生字段,仅当 cpu_offloading_num_layers>0 时设置,先确认字段存在
    if hasattr(args, "cpu_offloading"):
        provider.cpu_offloading = args.cpu_offloading
​
    # 通信重叠
    provider.tp_comm_overlap = args.tp_comm_overlap
​
    # FP8 量化配置
    provider.fp8 = args.fp8
    provider.fp8_recipe = args.fp8_recipe
​
    # 注意力核选择
    provider.attention_backend = args.attention_backend
​
    # MoE token dispatcher(同名且始终存在)
    provider.moe_token_dispatcher_type = args.moe_token_dispatcher_type
tests/fast/backends/megatron_utils/test_bridge_mtp_detachment.py test-coverage

新增 6 用例 CPU 回归测试,覆盖 flag 开启 / 关闭 / 缺失与初始 detach 值的全部组合,并验证未被误改的字段。

@pytest.fixture(scope="module")
def apply_bridge_runtime_config() -> Callable:
    # 通过 AST 解析生产源码,只提取目标函数并在隔离命名空间中执行,
    # 避免导入 model_provider.py 时拉起 GPU-only 的 Megatron 模块,
    # 从而让该函数能在纯 CPU CI 上获得回归覆盖。
    path = Path(__file__).resolve().parents[4] / "miles/backends/megatron_utils/model_provider.py"
    tree = ast.parse(path.read_text())
    function = next(
        node for node in tree.body
        if isinstance(node, ast.FunctionDef) and node.name == "_apply_bridge_runtime_config"
    )
    namespace = {"argparse": argparse}
    exec(compile(ast.Module(body=[function], type_ignores=[]), str(path), "exec"), namespace)
    return namespace["_apply_bridge_runtime_config"]
​
​
@pytest.mark.parametrize(
    ("enabled", "initial_detach", "expected_detach"),
    [
        (True, False, True), # 开启 MTP 训练:强制分离,修正旧默认行为
        (True, True, True), # 开启 MTP 训练:已是分离状态则保持不变
        (False, False, False), # 未开启:保留 provider 原有设置
        (False, True, True), # 未开启:保留已有的分离设置
        (None, False, False), # flag 缺失(只注册 Megatron 参数):保持默认
        (None, True, True), # flag 缺失:保留已有设置
    ],
)
def test_bridge_mtp_detachment(
    apply_bridge_runtime_config: Callable,
    runtime_args: argparse.Namespace,
    enabled: bool | None,
    initial_detach: bool,
    expected_detach: bool,
) -> None:
    # None 代表调用方没有注册 enable_mtp_training 参数,模拟缺省场景。
    if enabled is not None:
        runtime_args.enable_mtp_training = enabled
    provider = SimpleNamespace(mtp_num_layers=1, mtp_detach_heads=initial_detach)
​
    apply_bridge_runtime_config(provider, runtime_args)
​
    # 只允许调整 mtp_detach_heads,不允许误改 mtp_num_layers 等结构字段。
    assert provider.mtp_detach_heads is expected_detach
    assert provider.mtp_num_layers == 1

评论区精华

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

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

风险与影响

  1. 训练语义行为变更:启用 --enable-mtp-training 的 bridge 训练脚本中,mtp_detach_heads 会被强制为 True,即使用户显式传 False 也会被覆盖。这是与非 bridge 路径对齐的有意设计,但属于静默语义改变,历史训练产出的权重是在旧语义下计算的,可能需要重训才能获得预期效果。
  2. 测试技术债:AST 提取生产函数的测试方式依赖函数名 _apply_bridge_runtime_config、模块顶层位置以及该函数可独立编译;若未来函数改名或引入额外的模块级依赖,测试会以解析或执行失败的形式暴露,属于可发现但需维护的脆弱点。
  3. GPU 覆盖缺口:没有完整 GPU e2e 训练测试验证真实 backward 梯度流;PR body 明确承认 CPU probe 规模有限,梯度行为结论主要依赖小型 CPU 层与 mock 元数据。
  4. 影响范围收窄:变更只作用于 bridge 模式且 MTP 训练开启的组合,非 bridge 路径与普通训练完全不受影响,回归面小。

用户影响:使用 AutoBridge 模式训练 MTP 的用户会观察到辅助损失不再更新共享权重,训练语义被修正;已产出的旧 checkpoint 是在错误语义下训练的,建议重训。系统影响:运行时开销为零(一次布尔赋值),无性能影响;与 checkpoint 格式兼容性无关(属于配置项而非数据格式变更)。团队影响:新增了低成本、可复用的 CPU 回归测试范式,为后续在 _apply_bridge_runtime_config 中新增训练 flag 提供了样板;PR body 预告 env var 解析、辅助损失缩放、label-mask 对齐将作为独立变更跟进,说明该领域将持续演进。

训练语义静默变更 缺少 GPU e2e 测试 测试依赖 AST 提取 影响范围限定桥接模式

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论