Prhub

#54787 [ROCm][Perf][M3] Fused allreduce+GemmaRMSNorm fast path

原始 PR 作者 benenzhu 合并时间 2026-09-08 15:21 文件变更 3 提交数 7 评论 24 代码增减 +77 / -73

执行摘要

ROCm 接入 AITER 融合 allreduce+RMSNorm 快路径

PR body 明确指出问题动机:“fused_allreduce_gemma_rms_norm only has a flashinfer fast path; on ROCm it always falls back to all_reduce + GemmaRMSNorm — two kernel launches per layer”。首个 commit 进一步解释了为什么 helper 是唯一的落点:eager 模型(如 MiniMax-M3)不会走 torch.compile 的 RocmAiterAllReduceFusionPass,只能在 eager 路径上手工做融合。目标是复用 aiter 已有的 rocm_aiter_fused_allreduce_rmsnorm custom op,镜像 flashinfer 分支的形态,让 post-attention 的 AR + norm 从每层约 12.5us 降到约 7.5us,同时保持 fallback 路径数值不变。

值得精读,尤其是对 ROCm 后端或通信/融合算子感兴趣的读者。看点有三:一是“性能快路径必须携带反面门控”的工程范式——只在一阶段窗口启用,宁可回退也不用更慢的 two-stage 融合,这一结论直接来自 reviewer 的 A/B 数据而非直觉;二是谓词收敛策略——把散落在三个 impl 里的判定上提到 AiterCustomAllreduce.use_1stage_fused_ar_rms,既去重又统一了 eager/编译路径的语义;三是评论区展示的定位方法(按 M 维度扫 rank-max kernel 时间、push vs pull 通信方向对比、stage-2 norm kernel 常数开销拆分)。建议后续跟进两件事:补上 supports_dynamic_hidden_dim 守卫(旧 aiter 的静默 no-op 风险),以及为 use_1stage_fused_ar_rms 的边界条件补单元测试。

讨论亮点

1) 平台守卫:shen-shanshan 在 vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py 上指出 “We should check current_platform is rocm to make this helper method rocm-only, otherwise the imported rocm_aiter_ops could be None on other hardwares”,作者在 f338185 提交中补上 current_platform.is_rocm() 早退。
2) 去重:shen-shanshan 对 aiter_ops.py 连留两条 “These logics are totally the same as use_1stage = aiter_ar.use_1stage_fused_ar_rms(input), please also replace” 与 “Ditto.”,促成三处内联判定收敛到共享谓词。
3) 性能 crossover 之争:Fangzhou-Ai 给出 TP4 rank-max A/B(M=1 时融合快 14.66%,M=32 起反而慢 4.61%,M=256 慢 8.15%),并提醒 “There is a reproducible crossover exactly at AITER's one-stage/two-stage boundary”;benenzhu 用 H=6144 复现后承认 “beyond the one-stage boundary the fused path is slower … we should gate it under the size of 1stage”,并补做了 conc=32/64。
4) 根因追踪:benenzhu 在 benenzhu/aiter#1 中对比 two-stage 实现——融合内核写各 rank 的 tmp slice,而 plain custom_all_reduce 保持 slice 本地并从其它 rank 读,改成 pull 后 M=32 从 8.15us 回到 8.07us,M=256 的差距从 3.8us 缩到 1.2us;剩余约 1.1us 恒定差来自 aiter 的 local_device_load_rmsnorm 比 triton kernel 慢。
5) 未闭环项:CodeRabbit 标记 major——_can_use_aiter_fused_ar_rms 未在 supports_dynamic_hidden_dim 为 False 时拒绝 H 不在 {512,1024,2048,4096} 的输入,旧版 aiter 会静默 no-op;head 版本代码中未见对应守卫,讨论中也没有解决记录。

实现拆解

  1. 抽取共享的 one-stage 谓词(vllm/distributed/device_communicators/aiter_custom_all_reduce.py)。新增 AiterCustomAllreduce.use_1stage_fused_ar_rms(inp),把 AITER launcher(csrc/include/custom_all_reduce.cuh)的判据镜像成一处实现:token 数不超过 80、dtype 为 bf16/fp16、每行 16 字节 pack 且不超过 1024 个 pack、world_size==2 直接放行、非 fully_connected 拒绝、world_size<=4 要求 total_bytes < 256 KiB、world_size<=8 要求 total_bytes < 128 KiB。谓词被声明为 capture-static(只依赖 shape/dtype/TP size/拓扑),因此可以安全用于 CUDA graph 捕获期判定。

  2. 收敛重复判定(vllm/aiter_ops.py)。_rocm_aiter_fused_allreduce_rmsnorm_impl、_rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_impl 和 _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_impl 原本各自内联了一整段 hidden/token/字节判定(本文件净删 73 行),现在统一改为 use_1stage = aiter_ar.use_1stage_fused_ar_rms(input)。顺带修掉一处不一致:bf16-norm 变体用 input_.shape[0] 当 token 数,共享谓词改用 numel // hidden_dim,与 AITER launcher 及其它调用方一致(对 2-D 输入二者等价)。同时给 _rocm_aiter_fused_allreduce_rmsnorm_impl 与其 fake 增加 gemma_norm: bool = False 参数并透传给 ca.custom_fused_ar_rms。

  3. 新增 eager helper 门控与分支(vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py)。新增 _can_use_aiter_fused_ar_rms(hidden_states):先做 current_platform.is_rocm() 早退,再检查 rocm_aiter_ops.is_custom_all_reduce_enabled()、2-D/连续/bf16|fp16、aiter_ar 非 None 且未 disabled、total_bytes 不超过 effective_max_size()、should_custom_ar() 通过,最后要求 use_1stage_fused_ar_rms() 为真。命中后在 fused_allreduce_gemma_rms_norm 中于 flashinfer 分支之后插入 rocm_aiter_ops.get_fused_allreduce_rmsnorm_op()(...) 调用,透传 gemma_norm=True;不命中仍走原有 tensor_model_parallel_all_reduce + norm 兜底。

  4. 按 review 收紧门控范围。第二个 commit(d6b6a251)把“只在 one-stage 窗口启用”从讨论结论落成代码:A/B 显示 two-stage 变体(跨设备 reduce-scatter + 本地 norm)比显式 all_reduce + norm 慢 1-3us,所以 helper 宁可回退也不启用融合。这一改动同时反向定义了谓词的语义——它不只是“能否调用”,而是“调用是否划算”。

  5. 测试与验证配套。本次没有新增或修改任何测试文件,验证依赖 PR body 中的手工证据:TP4 tensor parity(token 数 1 到 32768,decode 规模 bit-exact,16/128/1024 时 relerr <= 2.7e-3,8192+ 因超过字节上限正确回退)、gfx950 上既有 kernel 测试(tests/kernels/test_minimax_m3_amd_ops.py、tests/kernels/attention/test_minimax_m3.py、tests/kernels/core/test_minimax_reduce_rms.py)、gsm8k 精度对照与 benchmark_serving A/B。这意味着新增谓词的边界行为(M=21/22、world_size=2/4/8、非连续输入等)没有单元测试兜底。

文件 模块 状态 重要度
vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py 融合算子 modified 7.51
vllm/distributed/device_communicators/aiter_custom_all_reduce.py 集合通信 modified 7.0
vllm/_aiter_ops.py 算子接入 modified 6.61

关键符号

_can_use_aiter_fused_ar_rms fused_allreduce_gemma_rms_norm use_1stage_fused_ar_rms _rocm_aiter_fused_allreduce_rmsnorm_impl _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_impl _rocm_aiter_fused_allreduce_rmsnorm_quant_per_group_with_bf16_norm_impl

关键源码片段

vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py core-logic

eager 模型的融合入口:新增 _can_use_aiter_fused_ar_rms 门控与 AITER 调用分支(透传 gemma_norm=True),是本次性能收益的直接来源,也是 reviewer 要求补平台守卫的位置。

# vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py
​
def _can_use_aiter_fused_ar_rms(hidden_states: torch.Tensor) -> bool:
    # 先做平台判定:rocm_aiter_ops 在非 ROCm 平台可能为 None,
    # 所以不能先 import 再判断(review 中 shen-shanshan 指出的问题)
    if not current_platform.is_rocm():
        return False
    from vllm._aiter_ops import rocm_aiter_ops
​
    if not rocm_aiter_ops.is_custom_all_reduce_enabled():
        return False
    # 与 flashinfer 分支共用同一套 2-D / 连续 / bf16|fp16 约束
    if (
        hidden_states.dim() != 2
        or not hidden_states.is_contiguous()
        or hidden_states.dtype not in _FI_SUPPORTED_DTYPES
    ):
        return False
    aiter_ar = rocm_aiter_ops.get_aiter_allreduce()
    if aiter_ar is None or aiter_ar.disabled:
        return False
    # 复用 AITER custom all-reduce 自身的字节预算
    total_bytes = hidden_states.numel() * hidden_states.element_size()
    if total_bytes > aiter_ar.effective_max_size():
        return False
    if not aiter_ar.should_custom_ar(hidden_states):
        return False
    # 最关键的一条:只有 aiter 会走 one-stage 内核时才启用融合。
    # two-stage(跨设备 reduce-scatter + 本地 norm)比显式
    # all_reduce + norm 更慢,此时宁可回退
    return aiter_ar.use_1stage_fused_ar_rms(hidden_states)
​
​
def fused_allreduce_gemma_rms_norm(
    hidden_states: torch.Tensor,
    residual: torch.Tensor,
    norm: GemmaRMSNorm,
) -> tuple[torch.Tensor, torch.Tensor]:
    tp_size = get_tensor_model_parallel_world_size()
    if tp_size == 1:
        # 无需 all-reduce,等价于未融合路径
        return norm(hidden_states, residual)
​
    ok, max_token_num = _can_use_flashinfer(hidden_states, tp_size)
    if ok:
        # flashinfer(NVIDIA)快路径,本 PR 未改动,此处略去实现细节
        ...
        return norm_out, hidden_states
​
    # 本次新增:ROCm/AITER 分支,一次 launch 完成
    # all-reduce + 残差加 + GemmaRMSNorm
    if _can_use_aiter_fused_ar_rms(hidden_states):
        from vllm._aiter_ops import rocm_aiter_ops
​
        return rocm_aiter_ops.get_fused_allreduce_rmsnorm_op()(
            input_=hidden_states,
            residual=residual,
            weight=norm.weight,
            epsilon=norm.variance_epsilon,
            gemma_norm=True, # Gemma 风格:weight_bias = 1.0
        )
​
    # 兜底:显式 all-reduce + GemmaRMSNorm,与未融合模型数值一致
    reduced = tensor_model_parallel_all_reduce(hidden_states)
    return norm(reduced, residual)
vllm/distributed/device_communicators/aiter_custom_all_reduce.py core-logic

新增共享谓词 AiterCustomAllreduce.use_1stage_fused_ar_rms,把 AITER launcher 的 one-stage 契约(pack / token / 字节上限)收敛为单一实现,供 eager helper 与 _aiter_ops 的三个融合 op 复用,是本次去重与门控的共同基石。

# vllm/distributed/device_communicators/aiter_custom_all_reduce.py
​
    def use_1stage_fused_ar_rms(self, inp: torch.Tensor) -> bool:
        """AITER 的融合 allreduce+RMSNorm 是否会走 one-stage 内核。
​
        判据镜像 aiter 的 fused_allreduce_rmsnorm launcher:
        每行 16 字节 pack、每行最多 1024 个 pack、最多 80 个 token,
        以及当前 TP size/拓扑下 one-stage custom all-reduce 的字节上限。
        超出窗口时融合 op 会走 two-stage(跨设备 reduce-scatter + 本地 norm),
        它比显式 all_reduce + norm 更慢,因此能回退的调用方必须要求本判据。
        capture-static:只依赖 shape、dtype、TP size 与拓扑。
        """
        hidden_dim = inp.shape[-1]
        # token 上限先行:prefill 规模输入一次比较就退出
        if inp.numel() // hidden_dim > 80:
            return False
        if inp.dtype not in (torch.bfloat16, torch.float16):
            return False
        # aiter 内核按 16 字节 pack 处理每一行
        pack_size = 16 // inp.element_size()
        if hidden_dim % pack_size != 0 or hidden_dim // pack_size > 1024:
            return False
        ca = self._impl
        world_size = ca.world_size
        if world_size == 2:
            return True
        # 非全互联拓扑不具备 one-stage 的通信前提
        if not ca.fully_connected:
            return False
        total_bytes = inp.numel() * inp.element_size()
        if world_size <= 4:
            return total_bytes < 256 * 1024
        if world_size <= 8:
            return total_bytes < 128 * 1024
        return False
vllm/_aiter_ops.py core-logic

三处内联的 one-stage 判定被替换为共享谓词(净删 73 行),并给 _rocm_aiter_fused_allreduce_rmsnorm_impl 及其 fake 增加 gemma_norm 参数以支撑 Gemma 语义;同时修正 bf16-norm 变体用 shape[0] 计 token 的写法。

# vllm/_aiter_ops.py
​
def _rocm_aiter_fused_allreduce_rmsnorm_impl(
    input_: torch.Tensor,
    residual: torch.Tensor,
    weight: torch.Tensor,
    epsilon: float,
    gemma_norm: bool = False, # 新增:走 GemmaRMSNorm 语义(weight_bias = 1.0)
) -> tuple[torch.Tensor, torch.Tensor]:
    aiter_ar = rocm_aiter_ops.get_aiter_allreduce()
    assert aiter_ar is not None, "aiter allreduce must be initialized"
    ca = aiter_ar.aiter_ca
    # 原本内联的 hidden_dim / token_num / total_bytes 三段判定已删除,
    # 统一改为调用上提到 AiterCustomAllreduce 的共享谓词,
    # 保证与 per-group quant 变体和 eager helper 的判定完全一致
    use_1stage = aiter_ar.use_1stage_fused_ar_rms(input_)
​
    result = ca.custom_fused_ar_rms(
        input_,
        residual,
        weight,
        epsilon,
        use_1stage=use_1stage,
        gemma_norm=gemma_norm,
    )
    assert result is not None
    return result[0], result[1]
​
​
def _rocm_aiter_fused_allreduce_rmsnorm_fake(
    input_: torch.Tensor,
    residual: torch.Tensor,
    weight: torch.Tensor,
    epsilon: float,
    gemma_norm: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
    # fake 实现只需保持签名与真实实现一致,便于 custom op 注册与 tracing
    return torch.empty_like(input_), torch.empty_like(residual)

评论区精华

helper 必须先做 ROCm 平台守卫 正确性

shen-shanshan 在 vllm/model_executor/layers/fused_allreduce_gemma_rms_norm.py 的 _can_use_aiter_fused_ar_rms 上评论:“We should check current_platform is rocm to make this helper method rocm-only, otherwise the imported rocm_aiter_ops could be None on other hardwares.”

结论:作者在 f338185 提交中补上 current_platform.is_rocm() 早退,并把它放在 import rocm_aiter_ops 之前。 · 已解决

_aiter_ops 三处重复的 1-stage 判定需要去重 设计

shen-shanshan 对 _aiter_ops.py 连发两条评论:“These logics are totally the same as use_1stage = aiter_ar.use_1stage_fused_ar_rms(input_), please also replace.” 与 “Ditto.”,指向 per-group quant 与 bf16-norm 变体。

结论:三处内联判定全部替换为共享谓词,净删 73 行;同时把 bf16-norm 变体用 shape[0] 计 token 的差异统一为 numel // hidden_dim。 · 已解决

融合路径在一阶段 / 二阶段边界处存在性能 crossover 性能

Fangzhou-Ai 给出 TP4 rank-max A/B:M=1 时融合快 14.66%、M=8 快 15.37%,但 M=32 起融合反而慢 4.61%,M=256 慢 8.15%,并称 “There is a reproducible crossover exactly at AITER's one-stage/two-stage boundary”。benenzhu 用 H=6144 复现后承认 “beyond the one-stage boundary the fused path is slower … we should gate it under the size of 1stage”,Fangzhou-Ai 还要求补 conc=64 的测试。

结论:helper 增加 use_1stage_fused_ar_rms 门控:只有在 AITER 会走 one-stage 内核的 shape 上才启用融合,超出窗口一律回退显式 all_reduce + GemmaRMSNorm。 · 已解决

two-stage 融合内核为何慢于 plain custom_all_reduce 性能

benenzhu 在上游 benenzhu/aiter#1 中定位:two-stage 融合内核写各 rank 的 tmp slice,而 plain custom_all_reduce 保持 slice 本地并从其它 rank 读;改成 pull 后 M=32 由 8.15us 回到 8.07us、M=64 由 11.9us 回到 12.1us,M=256 的差距从 3.8us 缩到 1.2us;剩余约 1.1us 恒定差来自 aiter 的 local_device_load_rmsnorm 比 triton kernel 慢。

结论:根因部分定位并反馈到 aiter 上游,vLLM 侧先用门控规避,不阻塞本 PR 合入。 · 待处理

旧版 aiter 的 hidden_dim 兼容守卫缺失 正确性

CodeRabbit 标记 major:“Update _can_use_aiter_fused_ar_rms to reject hidden dimensions other than 512, 1024, 2048, and 4096 when supports_dynamic_hidden_dim is false”,即 aiter v0.1.12 之前的 launcher 对非特化尺寸会静默 no-op。

结论:head 版本的 _can_use_aiter_fused_ar_rms 中仍未看到 supports_dynamic_hidden_dim 检查,讨论里也没有对应的修复记录,属于遗留风险。 · 待处理

风险与影响

1) 旧版 aiter 兼容性(最需要关注):vllm/distributed/device_communicators/aiter_custom_all_reduce.py 的 supports_dynamic_hidden_dim 文档已说明 aiter v0.1.12 之前 launcher 对 H 不在 {512,1024,2048,4096} 的尺寸会“silently no-op”;新增的 _can_use_aiter_fused_ar_rms 没有引用该属性,CodeRabbit 的 major 评论在 head 代码中看不到对应修复。若在旧 aiter 上跑到非常规 hidden_dim,融合 op 可能返回未定义/未归约的结果而不是回退。
2) 门控与 aiter 内部契约耦合:use_1stage_fused_ar_rms 是对 aiter launcher 判据的“镜像”而非调用其 API,80 token / 1024 pack / 256 KiB / 128 KiB 这些常量一旦在 aiter 侧调整,vLLM 侧会静默失配——乐观方向是性能退化(回退或误用 two-stage),悲观方向是调用到内核拒绝的尺寸。
3) 测试覆盖缺口:本 PR 只有源码改动,没有 tests/ 下的新增用例,谓词边界(M=21/22、world_size=4/8 的字节阈值、非连续或非 2-D 输入)完全依赖手工 A/B 与既有 kernel 测试。
4) 数值与精度:bf16 reassociation 使 16/128/1024 token 下 relerr 到 2.7e-3;gsm8k 从 0.9704 到 0.9644(stderr 约 0.005),落在统计噪声边缘,但属于质量指标方向性变化,值得在后续版本继续跟踪。
5) 执行模式分叉:编译期的 RocmAiterAllReduceFusionPass 与本次 eager helper 各自维护一份启用条件,若二者对同一 shape 给出不同判定,同一模型在 eager 与 compile 下会出现不同的算子组合(性能差异与潜在的数值差异)。

用户侧:仅影响 ROCm/AMD 平台且启用 AITER custom all-reduce 的部署(示例为 MI355X x4 TP4 + MiniMax-M3-MXFP4 + EAGLE3)。decode 阶段 TPOT 3.32ms→3.26ms(conc=1)、12.68ms→12.31ms(conc=16),每层 AR+norm 从约 12.5us 降到约 7.5us;prefill 规模输入因字节上限正确回退,不受影响。系统侧:NVIDIA/flashinfer 路径、TP==1 路径行为完全不变,回退路径数值与未融合模型一致,属于受控的局部优化。团队侧:_aiter_ops.py 三处重复判定收敛到 AiterCustomAllreduce 上的单一谓词,后续 per-group quant(含 emit_bf16 变体)与 eager helper 共享同一份契约,维护成本下降,但也让该谓词成为多个调用方的公共依赖,改动风险集中度提高。

旧版 aiter 兼容守卫缺失 缺少新增单元测试 门控阈值与 aiter 内部契约镜像耦合 eager 与 compile 路径判定可能分叉 精度指标方向性波动

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论