Prhub

#52301 [Perf][Nemotron] Skip redundant latent-MoE all-reduce at TP>1 (~13% decode win)

原始 PR 作者 ECMGit 合并时间 2026-09-15 22:33 文件变更 2 提交数 4 评论 8 代码增减 +21 / -0

执行摘要

Nemotron 潜在 MoE 省一次 all-reduce,修复 TP>1 解码回归

PR body 直接给出动机:“Nemotron-3 decode regressed ~14% between v0.26.0 and v0.27.x at TP>1. This PR restores it.”。根因是 v0.27.0rc1 新增的 _maybe_reduce_routed_output_before_transform() 会在 transform 之前先把 routed 输出在 latent 空间 all-reduce,而合并结果随后又在 late 路径上被 all-reduce 一次,于是“every latent-MoE layer at TP>1 or EP>1 issues two tensor-parallel all-reduces per token instead of one”。NemotronH 之所以命中,是因为它把 fc2_latent_proj 作为 routed_output_transform 传入,而该函数在 v0.26.0 并不存在。作者说明机制是通用的但只有 NemotronH 选择启用,因此“it is the only one whose behaviour changes”,并且没有找到对应的已有 issue,属于定向的回归修复。

值得精读,且成本极低(21 行):这是一个“用 21 行换 13% 解码性能”的高杠杆修复,同时是 latent-MoE 通信契约的一个范本。建议关注三处设计决策:其一,默认 False 的 opt-in 语义如何把风险限制在单个模型内;其二,量化判定为什么必须看构造好的层(isinstance(self.fc2_latent_proj.quant_method, UnquantizedLinearMethod))而不是模型级 quant_config,这一条对任何做“模型级配置 = 层实际实现”假设的代码都有借鉴意义;其三,作者对“大 batch 下收益衰减”的诚实量化,说明通信延迟与带宽瓶颈的分野。若准备跟进后续工作,首要建议是补一个 mock tensor_model_parallel_all_reduce 的调用计数单测,覆盖 reduce_commutative 开/关两种情形;次要建议是把该契约写进 latent MoE 相关的设计文档,避免后来者在非线性 transform 上误设标记。

讨论亮点

本 PR 的 review 极其轻量:claude[bot] 因来自 fork 而拒绝自动评审,实际只有 amirkl94 的 “LGTM” 和 mgoin 的空 body approve,没有任何行内 review 评论。真正的技术交锋写在 commit message 与 PR body 里:

  • 量化判定口径:首个 commit 只用 not config.mlp_bias;第二个 commit(1222cb8)改为层级判定,理由是“Dynamic input quantization breaks commutativity, because each rank would quantize a different partial sum with a different scale”,而 quant_config is None 又“would be wrong in the other direction: quant_config is model-wide, so it is non-None for every quantized checkpoint even when the projection itself is left in high precision”。
  • 是否复用现成实现:作者在 body 中给出备选方案——“[LatentMoERunner] already fuses to one collective, but it now lives under vllm/models/kimi_k3/ (moved by #50678) and reads transform.norm/up_proj, which NemotronH does not have”,并明确“Happy to go that way if preferred”,最终未被采纳。
  • 安全性自证:body 附录说明 skip_final_all_reduce 断言的是 combine kernel 属性 _fused_output_is_reduced,本 PR 未触碰;NemotronH 上 reduce_results=True(附近的 reduce_results=False 属于 shared_experts),因此 late all-reduce 一定发生。
  • 流程层面:mgoin 需要两次执行 /ci run——第一次被机器人拒绝(“This PR is 215 commits behind upstream main”),合并 main 后在 9f34ba8 上触发 Buildkite CI #88933,mgoin 补充说明“CI retriggered but was green last round”。
  • 未解决的疑虑:整个线程没有任何人质疑缺失测试;PR 也没有新增覆盖 all-reduce 次数的单测。

实现拆解

实现只有两步源码改动 + 一次判定口径修正,未触及配置、schema、部署或测试文件:

  1. 定义可交换契约(vllm/model_executor/layers/fused_moe/runner/moe_runner.py):在 _maybe_reduce_routed_output_before_transform() 的早规约条件中插入 not getattr(self.routed_output_transform, 'reduce_commutative', False),并在 docstring 里写明数学前提 sum_r T(x_r) == T(sum_r x_r)——满足该等式的变换可以把早规约推迟到 late 路径 _maybe_reduce_final_output(),从而每 token 省一次 collective。默认 False 是关键设计:Kimi K3 的 KimiRoutedOutputTransform(内含 RMSNorm,非线性)不声明该属性,继续走两次规约。
  2. 在唯一受影响模型上 opt-in(vllm/model_executor/models/nemotron_h.py):新增 UnquantizedLinearMethod 导入,在构造 fc2_latent_proj 之后设置 reduce_commutative = not config.mlp_bias and isinstance(self.fc2_latent_proj.quant_method, UnquantizedLinearMethod)。判定覆盖两个破坏交换性的因素:bias 会引入与 rank 绑定的加项;动态输入量化会让每个 rank 用不同 scale 量化各自的部分和。
  3. 量化判定从模型级下沉到层(commit 1222cb8):初版只判断 not config.mlp_bias,会把真正量化了该投影的 checkpoint 也打开优化;改用 quant_config is None 则偏保守——quant_config 是模型级字段,ModelOpt 把 latent 投影排除在量化之外,量化 checkpoint 的 quant_config != None,但这一层实际仍是 UnquantizedLinearMethod。作者在 Ultra NVFP4 上运行时验证了这一组合,最终采用层级 isinstance 判定,否则优化会在回归最严重的量化 checkpoint 上被误关。
  4. 交互与测试配套(本 PR 缺失):作者核对了 skip_final_all_reduce 与 _fused_output_is_reduced 的既有断言不受影响,NemotronH 的 reduce_results 默认为 True 因而 late all-reduce 必然触发;测试方面没有任何新增文件,正确性仅由 gsm8k 5-shot(四个 delta 均在一个标准误内)和 ITL 压测支撑,作者认为无公开 API 变化故不需要文档更新。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/runner/moe_runner.py 专家执行器 modified 6.21
vllm/model_executor/models/nemotron_h.py 模型定义 modified 6.01

关键符号

_maybe_reduce_routed_output_before_transform(新增 reduce_commutative 短路条件) _maybe_reduce_final_output(承接被延迟的规约,必须始终在 NemotronH 路径上触发) NemotronH 的 MoE 模块 __init__(设置 fc2_latent_proj.reduce_commutative)

关键源码片段

vllm/model_executor/layers/fused_moe/runner/moe_runner.py core-logic

FusedMoE 共享 runner 的核心控制流:新增 reduce_commutative 短路条件,决定 latent MoE 每 token 发一次还是两次 TP all-reduce;该文件是所有模型共用的路径,契约定义在这里。

def _maybe_reduce_routed_output_before_transform(
    self,
    fused_output: torch.Tensor,
    fused_output_is_reduced: bool,
) -> tuple[torch.Tensor, bool]:
    '''All-reduce latent routed output before its output transform.
​
    A transform that commutes with the TP sum is exempt: if
    sum_r T(x_r) == T(sum_r x_r), applying the transform to the local
    partial output and letting the existing late all-reduce sum the
    combined result is equivalent, and costs one collective instead of two.
    Such a transform opts out by setting reduce_commutative = True.
    The default is False, so transforms that do not declare themselves
    keep being reduced early.
    '''
    if (
        self.routed_output_transform is not None
        # 新增短路条件:变换若声明自身与 TP 求和可交换,就跳过这次 early reduce。
        # getattr 默认 False,保证 Kimi K3 的 RMSNorm 变换等未声明者行为完全不变。
        and not getattr(self.routed_output_transform, 'reduce_commutative', False)
        and not self.moe_config.is_sequence_parallel
        and (self.moe_config.tp_size > 1 or self.moe_config.ep_size > 1)
        and not fused_output_is_reduced
    ):
        # 含非线性(如 RMSNorm)的变换必须先在 latent 空间求和,
        # 否则 sum_r T(x_r) != T(sum_r x_r),本分支即为此保留。
        # 可交换的线性变换则把这次 collective 让给 late 路径的
        # _maybe_reduce_final_output(),每 token 由两次 all-reduce 降为一次。
        fused_output = tensor_model_parallel_all_reduce(fused_output)
        fused_output_is_reduced = True
    return fused_output, fused_output_is_reduced
vllm/model_executor/models/nemotron_h.py data-contract

唯一的 opt-in 站点:在构造 fc2_latent_proj 后按“无 bias + 层级未量化”设置 reduce_commutative,并把它作为 routed_output_transform 交给 FusedMoEFactory,是判定口径(层而非模型级 quant_config)的落地点。

if self.use_latent_moe:
    self.fc1_latent_proj = ReplicatedLinear(
        input_size=config.hidden_size,
        output_size=self.moe_hidden_size,
        bias=config.mlp_bias,
        quant_config=quant_config,
        disable_tp=self.is_sequence_parallel,
        prefix=f'{prefix}.fc1_latent_proj',
    )
    self.fc2_latent_proj = ReplicatedLinear(
        input_size=self.moe_hidden_size,
        output_size=config.hidden_size,
        bias=config.mlp_bias,
        quant_config=quant_config,
        disable_tp=self.is_sequence_parallel,
        prefix=f'{prefix}.fc2_latent_proj',
    )
    # 无 bias 且未量化的线性层与 TP 求和可交换(sum_r W x_r == W sum_r x_r),
    # 因此只需在 transform 之后规约一次;这是本 PR 唯一启用该优化的模型。
    # 关键取舍:判断“构造好的层”而不是模型级 quant_config —— ModelOpt
    # 把 latent 投影排除在量化之外,量化 checkpoint 的 quant_config 非空,
    # 但这一层实际仍是 UnquantizedLinearMethod;
    # 若用模型级配置判断,会在 NVFP4 等回归最严重的 checkpoint 上误关优化。
    self.fc2_latent_proj.reduce_commutative = (
        not config.mlp_bias
        and isinstance(
            self.fc2_latent_proj.quant_method, UnquantizedLinearMethod
        )
    )
else:
    self.fc1_latent_proj = None
    self.fc2_latent_proj = None
​
self.experts = FusedMoEFactory(
    # ... 其余专家参数保持不变 ...
    routed_input_transform=self.fc1_latent_proj,
    # fc2_latent_proj 上的 reduce_commutative 会被 MoE runner 通过 getattr 读取,
    # 从而跳过 _maybe_reduce_routed_output_before_transform 中的 early all-reduce。
    routed_output_transform=self.fc2_latent_proj,
    routed_scaling_factor=self.routed_scaling_factor,
    apply_routed_scale_to_output=True,
    router_logits_dtype=self.gate.out_dtype,
)

评论区精华

量化判定应基于构造好的层,而非模型级 quant_config 设计

首个 commit 只用 `not config.mlp_bias` 判定可交换性,隐含地把真正量化该投影的 checkpoint 也打开了优化;改为 `quant_config is None` 又过于保守——commit 1222cb8 的说明是“Dynamic input quantization breaks commutativity, because each rank would quantize a different partial sum with a different scale”,而模型级字段在量化 checkpoint 上恒为非空。作者在 Ultra NVFP4 上实测确认 `quant_config=ModelOptMixedPrecisionConfig`(非空)但 `fc2_latent_proj.quant_method=UnquantizedLinearMethod`。

结论:采用层级 `isinstance(self.fc2_latent_proj.quant_method, UnquantizedLinearMethod)` 判定,既避开动态量化的 scale 不一致,又能在 ModelOpt 量化 checkpoint 上正确启用优化。 · 已解决

是否复用 Kimi K3 的 LatentMoERunner,而非新增属性契约 设计

作者在 PR body 的折叠小节里提出备选方案:LatentMoERunner“already fuses to one collective, but it now lives under `vllm/models/kimi_k3/` (moved by #50678) and reads `transform.norm`/`up_proj`, which `NemotronH` does not have”,并主动表示“Happy to go that way if preferred”。

结论:不采用该方案:跨模型复用需要把 runner 从 kimi_k3 目录提升并抽象出 NemotronH 没有的字段,成本高于新增一个布尔契约;改动停留在通用 runner + 模型侧 opt-in。 · 已定论(未采纳备选方案)

新属性默认 False,保证未声明模型字节级不变 正确性

docstring 明确“The default is False, so transforms that do not declare themselves keep being reduced early”,body 也点名 Kimi K3 的 KimiRoutedOutputTransform 内含 RMSNorm 因而“never sets the flag, and keeps both collectives”。配套核对还包括 `skip_final_all_reduce` 的断言针对的是 combine kernel 属性 `_fused_output_is_reduced`,本 PR 未触碰。

结论:通过 `getattr(..., False)` 兜底实现严格 opt-in,非 NemotronH 模型行为不变;等价性依赖 NemotronH 的 FusedMoE `reduce_results=True` 使 late all-reduce 必然触发。 · 已解决

没有新增测试守护 all-reduce 次数与等价性 测试

changed_files_count = 2,全部为源码;review 只有两票 approve(amirkl94 “LGTM”、mgoin 空 body),没有任何人要求补测。作者提供的证据是 ITL 四组采样与 gsm8k 5-shot 评测(四个 delta 均在一个标准误内),并自述“Bitwise identity is not expected”。

结论:未解决:核心共享 runner 的新契约没有单测覆盖,未来重构可能静默破坏等价性,只能靠端到端性能与精度评测发现。 · 未解决

CI 触发与分支落后 main 215 个提交的流程摩擦 other

ECMGit 执行 `/ci run` 被机器人拒绝(需要 write 权限评审者或 ready 标签);mgoin 接手后仍被拒:“This PR is 215 commits behind upstream `main`”;合并 main(commit 9f34ba8)后 CI 才在 Buildkite #88933 上跑起来,mgoin 说明“CI retriggered but was green last round”。

结论:已解决:分支同步 main 后 CI 通过,两位评审者 approve,PR 以 merge commit 0c7fe3f 落地。 · 已解决

风险与影响

  1. 数值顺序变化(可控但需知晓):把 all-reduce 移到线性变换之后会改变浮点求和顺序,作者也坦言“Bitwise identity is not expected”。Super BF16 / Ultra NVFP4 上 gsm8k 5-shot 四个 delta 都在一个标准误内,但没有逐 token 的等价性测试。
  2. 缺少测试覆盖:moe_runner.py 是 FusedMoE 的核心共享路径,却没有任何单测断言“开启 reduce_commutative 时只发一次 all-reduce、关闭时行为不变”。未来重构(例如重排 _maybe_reduce_final_output 的条件)可能静默破坏该等价性,CI 不会报警。
  3. 动态属性契约的时效性:reduce_commutative 是构造期在模块实例上设置的普通属性,getattr(..., False) 只在每次 forward 时读取;如果后续出现权重重载(weight refit,参考 #56706 修过的 HPC QK-norm 陈旧权重)或运行时把 quant_method 换成量化实现,该标记不会重新评估,会保留一次错误的跳过。
  4. 性能收益随 batch 衰减:作者自己的 sweep 显示大 batch 下收益收窄甚至接近持平——“at large batch they are bandwidth-bound and the surviving reduce (4096-wide) exceeds the removed one (1024-wide)”。测得的所有点都非负,但这是需要长期监控的权衡。
  5. 依赖 late 路径必然触发:等价性前提是 NemotronH 的 FusedMoE reduce_results=True。若未来有人对 NemotronH 的 latent MoE 打开 skip_final_all_reduce 或改默认值,会出现“少规约一次”的静默错误,目前没有任何 assert 守住这一点。
  6. 跨模型回归风险低:默认 False + getattr 兜底,Kimi K3 等未声明模型保持两次 collective;这部分是本 PR 最稳的设计。

对 NemotronH 用户:TP>1/EP>1 解码路径恢复 v0.26.0 水平,Super BF16 c=1 ITL 改善约 12%,并发 8/32/64 各点均回到回归前基线;Ultra 550B-A55B NVFP4 同节点 −5.8% ITL / +6.1% tok/s,量化 checkpoint 同样受益。对其他模型:零影响(opt-in 默认关闭),Kimi K3 行为不变。对系统:每个 latent-MoE 层每 token 少一次 NCCL all-reduce,在小 batch 延迟受限场景收益最大,同时降低通信 rank 上的排队压力。对团队与代码库:引入了一个新的 latent-MoE 扩展点(reduce_commutative),后续任何新的 latent 输出变换都需要判断自身是否与 TP 求和可交换;该契约目前只存在于 docstring 中,没有测试守护,属于需要长期维护的隐性接口。

核心路径变更 缺少测试覆盖 数值顺序变化 动态属性契约时效性 依赖 late 路径必然触发

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论