Prhub

#28666 [AMD] Fuse shared_expert_gate GEMV into the MoE append kernel (HIP/aiter)

原始 PR 作者 yichiche 合并时间 2026-08-14 12:32 文件变更 4 提交数 8 评论 26 代码增减 +670 / -38

执行摘要

AMD 路径融合共享专家 GEMV 进 append 内核,消除独立启动

PR body 指出,AITER shared-expert-fusion 路径上 self.shared_expert_gate(hidden_states) 是一个 [M, hidden] × [hidden, 1] 矩阵-向量运算(Cijk 内核 decode 下约 8.9µs),其唯一输出只喂给后续 append 内核;在 decode batch size 下这是纯 kernel-launch 开销(约 0 TFLOPs),因此是融合进 append 内核的典型候选。作者在评论中进一步说明 qwen3.5 mxfp4-moe 的后续优化依赖本 PR,预期带来 1~2% e2e 提升。

值得精读。一是 launch-bound vs compute-bound 的判断方法(GEMV 在 decode 下约 0 TFLOPs,调优不如融合);二是"默认路径 byte-for-byte 不变 + 新参数互斥断言"的向后兼容策略;三是用双 checkpoint 隔离变量验证融合路径的测试设计。建议后续跟进 nightly MI35x 结果,并推动 AMD CI 基础设施改进以消除 gate 超时。

讨论亮点
  1. HaiShaw 在 CHANGES_REQUESTED 中指出初版丢弃了 apply_sigmoid 逻辑("the logic of apply_sigmoid is dropped, please try to retain it."),作者随后恢复,并在 kernel 入口增加 fuse_gateapply_sigmoid 互斥断言与对应单测,最终 APPROVED。
  2. amd-bot 多轮 CI 状态反复强调"绿色 ≠ 已验证":PR 核心代码只在 AMD aiter 上执行,而多个 AMD stage 因 wait-for-stage-a-amd 超时被跳过;最终 kernel 单测在 CUDA 与 AMD 均通过,但 MXFP4 模型级 e2e 只在 nightly 套件中运行。
  3. yichiche 澄清 B200 失败为上游已知 flake(test_spec_ngram 连接拒绝,issue #17050),与 PR 无关,并说明该 PR 是 qwen3.5 mxfp4-moe 进一步优化的前置,预期 e2e 提升 1~2%。

实现拆解

  1. 内核扩展(fused_moe_triton_kernels.py)_fused_append_shared_experts_with_weights_kernel 新增 hidden_ptrwgate_ptrscale 运行时参数与 FUSE_GATEHIDDENBLOCK_H 编译期常量。FUSE_GATE=True 时,每个 program(一个 token)把 hidden[pid, :]W_gate[:] 在 fp32 中做点积得到 logit,再 tl.sigmoid(logit) * scale 广播到 BLOCK_S 个共享槽位,替换原先从 shared_weights_ptr 的 load。外层 fused_append_shared_experts_with_weights 增加 fuse_gate=False, hidden_states=None, gate_weight=None, scale=1.0 参数;默认 False 时路径与原先一致,fuse_gate=Trueapply_sigmoid=True 互斥(assert),且强制要求传入 hidden 与 gate 权重;fuse_gate 分支将 gate_weight reshape 为一维、hidden_dim 向上取 2 的幂作为 BLOCK_H,并改用 num_warps=8 以支撑归约。
  2. 模型接线(qwen2_moe.py):新增 _shared_expert_scale(),把 1/ep_size 预缩放逻辑从 _get_shared_expert_weights 中抽出(Allreduce-EP 下后置 all_reduce 会把共享输出累加 ep_size 次,需预除以 ep_size)。_append_shared_to_topk_output 改为按后端分流:_use_aiter 时不再调用 _get_shared_expert_weights,以 fuse_gate=True 直接传入 hidden_statesshared_expert_gate.weight;CUDA 走原 _get_shared_expert_weights 路径不变。
  3. 测试配套:新增 test/registered/moe/test_fused_append_shared_experts.py,用 _eager_append/_eager_gate 两个 eager 参考实现,验证 legacy(bitwise 相等)、apply_sigmoid、fuse_gate 三条路径;覆盖互斥断言、参数校验、s <= 0 no-op、routed 列保持;注册到 CUDA base-b 与 AMD stage-b。新增 test/registered/amd/accuracy/mi35x/test_qwen35_mxfp4_eval_mi35x.py(nightly):两个 Qwen3.5-397B MXFP4 checkpoint(共享专家是否量化)分别做 GSM8K 精度门限(> 0.91)与 fuse_gate run-through(> 0.0)+ perf 基准,补齐 e2e 覆盖盲区。
  4. 验收数据:作者在 PR body 给出 torch profiler 数据:独立 GEMV launch 从约 8.9µs 降为 0,append 内核从约 4.0µs 增至约 5.2µs,每 MoE 层约 12.9µs→约 5.2µs;GSM8K 精度在运行间方差内。单测全部 PASSED。
文件 模块 状态 重要度
python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py 内核层 modified 6.1
python/sglang/srt/models/qwen2_moe.py 模型层 modified 7.41
test/registered/moe/test_fused_append_shared_experts.py 单元测试 added 7.48
test/registered/amd/accuracy/mi35x/test_qwen35_mxfp4_eval_mi35x.py 夜测脚本 added 7.21

关键符号

_fused_append_shared_experts_with_weights_kernel fused_append_shared_experts_with_weights _shared_expert_scale _append_shared_to_topk_output _get_shared_expert_weights

关键源码片段

python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py core-logic

核心改动:append kernel 新增 FUSE_GATE 分支与 num_warps=8 的 GEMV 归约;外层 API 新增 fuse_gate/hidden_states/gate_weight 契约并保证默认路径不变。

# python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py
# 关键实现单元:fuse_gate 模式的外层启动函数(默认关闭,兼容 legacy 路径)
def fused_append_shared_experts_with_weights(
    topk_ids, topk_weights, shared_weights,
    num_fused_shared_experts, N=None,
    apply_sigmoid=False, fuse_gate=False,
    hidden_states=None, gate_weight=None, scale=1.0,
):
    # fuse_gate 路径已内置 sigmoid 与 scale,与 apply_sigmoid 互斥,避免双重激活
    assert not (fuse_gate and apply_sigmoid), (
        'fuse_gate already applies sigmoid in-kernel; do not also set apply_sigmoid'
    )
    assert N is not None, 'N (shared expert base id) must be provided'
    m, k = topk_ids.shape
    s = int(num_fused_shared_experts)
    if s <= 0:
        return topk_ids, topk_weights
​
    if fuse_gate:
        # GEMV 场景:要求 hidden_states 与 gate_weight,gate_weight 展平为一维
        assert hidden_states is not None and gate_weight is not None, (
            'fuse_gate=True requires hidden_states and gate_weight'
        )
        hidden_arg = hidden_states.contiguous()
        wgate_arg = gate_weight.reshape(-1).contiguous()
        hidden_dim = hidden_arg.shape[1] # 每 token 归约长度
        block_h = triton.next_power_of_2(hidden_dim)
        shared_arg = topk_weights # FUSE_GATE 下不再读 shared_weights
        num_warps = 8 # 点积归约需要更多线程
    else:
        # legacy/apply_sigmoid:保持原语义(dtype 转换、unsqueeze、expand)
        shared_weights_2d = (
            shared_weights if apply_sigmoid else shared_weights.to(topk_weights.dtype)
        )
        if shared_weights_2d.ndim == 1:
            shared_weights_2d = shared_weights_2d.unsqueeze(-1)
        if shared_weights_2d.shape[1] < s:
            shared_weights_2d = shared_weights_2d.expand(m, s)
        shared_arg = shared_weights_2d.contiguous()
        hidden_arg = topk_weights # 占位,避免 kernel 签名分支
        wgate_arg = topk_weights
        hidden_dim = 1
        block_h = 1
        num_warps = 1
​
    # ... 输出张量分配与 block_k/block_s 计算省略,与原有逻辑一致 ...
    _fused_append_shared_experts_with_weights_kernel[(m,)](
        topk_ids, topk_weights, shared_arg, out_ids, out_weights,
        hidden_arg, wgate_arg,
        N_BASE=N, scale=scale, K=k, S=s,
        BLOCK_K=block_k, BLOCK_S=block_s,
        APPLY_SIGMOID=apply_sigmoid,
        FUSE_GATE=fuse_gate, HIDDEN=hidden_dim, BLOCK_H=block_h,
        num_warps=num_warps,
    )
    return out_ids, out_weights# 内核内部:每个 program 处理一个 token
# FUSE_GATE 分支:核内完成 hidden[pid, :] · W_gate[:] 的 fp32 点积
if FUSE_GATE:
    offs_h = tl.arange(0, BLOCK_H)
    mask_h = offs_h < HIDDEN
    h = tl.load(hidden_ptr + pid * HIDDEN + offs_h, mask=mask_h, other=0.0).to(tl.float32)
    w = tl.load(wgate_ptr + offs_h, mask=mask_h, other=0.0).to(tl.float32)
    logit = tl.sum(h * w) # 标量 logit(fp32 归约)
    shared_val = tl.sigmoid(logit) * scale # sigmoid + 1/ep_size 预缩放
    shared_ws = tl.zeros((BLOCK_S,), dtype=tl.float32) + shared_val # 广播到全部共享槽
else:
    # 旧路径:加载预计算权重,必要时应用 apply_sigmoid
    shared_ws = tl.load(shared_weights_ptr + pid * S + offs_s, mask=mask_s)
    if APPLY_SIGMOID:
        shared_ws = tl.sigmoid(shared_ws.to(tl.float32)) * scale
python/sglang/srt/models/qwen2_moe.py core-logic

模型侧接线:抽取 _shared_expert_scale,_append_shared_to_topk_output 按 _use_aiter 分流,aiter 下不再调用 _get_shared_expert_weights 与独立 gate GEMV。

# python/sglang/srt/models/qwen2_moe.py
# aiter 路径:把 shared_expert_gate GEMV、sigmoid、1/ep_size 缩放全部交给 append 内核
def _shared_expert_scale(self) -> float:
    # 1/ep_size 预缩放:Allreduce-EP 下每个 rank 都算同一共享输出,
    # 后置 all_reduce 会累加 ep_size 次,这里预除以 ep_size 抵消
    # (与 _get_shared_expert_weights 中的逻辑保持一致)
    moe_ep_size = get_parallel().moe_ep_size
    if moe_ep_size > 1 and not is_deepep_class_backend():
        return 1.0 / float(moe_ep_size)
    return 1.0
​
​
def _append_shared_to_topk_output(self, topk_output, hidden_states):
    # 把共享专家 id 与权重追加到 topk 输出后再进入 fused MoE
    if not self.enable_shared_expert_fusion or self.shared_expert_gate is None:
        return topk_output
​
    from sglang.kernels.ops.moe.fused_moe_triton_kernels import (
        fused_append_shared_experts_with_weights,
    )
​
    if _use_aiter:
        # HIP/aiter:消除独立 gate GEMM launch,GEMV + sigmoid + scale 全部
        # 在 append 内核内完成;此路径不再调用 _get_shared_expert_weights
        fused_topk_ids, fused_topk_weights = fused_append_shared_experts_with_weights(
            topk_output.topk_ids,
            topk_output.topk_weights,
            None,
            self.num_fused_shared_experts,
            N=self.num_experts,
            fuse_gate=True,
            hidden_states=hidden_states,
            gate_weight=self.shared_expert_gate.weight,
            scale=self._shared_expert_scale(),
        )
    else:
        # CUDA:保持 legacy 行为,sigmoid + scale 由 eager 路径预先算好
        shared = self._get_shared_expert_weights(hidden_states)
        if shared is None:
            return topk_output
        shared_weights, _ = shared
        fused_topk_ids, fused_topk_weights = fused_append_shared_experts_with_weights(
            topk_output.topk_ids,
            topk_output.topk_weights,
            shared_weights,
            self.num_fused_shared_experts,
            N=self.num_experts,
        )
    return StandardTopKOutput(
        topk_weights=fused_topk_weights,
        topk_ids=fused_topk_ids,
        router_logits=topk_output.router_logits,
    )

评论区精华

apply_sigmoid 逻辑被初版丢弃,需保留 设计

HaiShaw 在 CHANGES_REQUESTED 中指出:"the logic of `apply_sigmoid` is dropped, please try to retain it." 初版 fuse_gate 直接替代了 apply_sigmoid 的核内 sigmoid 融合,导致该能力丢失。

结论:作者恢复 apply_sigmoid,并在 kernel 入口增加 fuse_gate 与 apply_sigmoid 互斥断言,同时补单测 test_fuse_gate_and_apply_sigmoid_mutually_exclusive;后续 CI 重新跑通,最终 APPROVED。 · 已解决

PR CI 无法覆盖 AMD aiter e2e 路径 测试

amd-bot 多轮指出:kernel 单测在 CUDA 与 AMD stage-b 均通过,但 qwen2_moe.py 的 `_use_aiter` fuse_gate 服务路径只由 nightly MI35x 测试执行;期间多次因 wait-for-stage-a-amd 超时导致 AMD stage 整体跳过,NVIDIA base-c 也多次 fast-fail-skip。

结论:作者新增 nightly run-through 测试(test_qwen35_mxfp4_eval_mi35x.py)并在本地跑通(参考 0.933、fused 0.720);PR CI 上内核级已验证,模型级覆盖仍依赖 nightly。 · partial

B200/NVIDIA CI 失败归因 question

yichiche 说明 base-b-test-1-gpu-large 的 test_spec_ngram(ConnectionRefusedError)为上游已知 flake(issue #17050),与 PR 无关;amd-bot 也确认未发现可归因于本 PR 的执行失败。

结论:确认失败为基础设施 / 上游 flake 后,作者申请合并,最终由 HaiShaw 合入。 · 已解决

双 checkpoint 隔离验证 fuse_gate 路径 设计

nightly 测试 docstring 明确两个 MXFP4 checkpoint 唯一差异是共享专家是否量化:非量化(BF16)时 fusion 关闭,作为精度参考(门限 0.91);量化时 fuse_gate 路径 e2e 运行,只做 run-through 断言(> 0.0)。

结论:该测试设计已实现,作者给出本地结果(参考 accuracy 0.933、fused run-through 0.720),用于夜间验证融合路径健康。 · 已解决

风险与影响

  1. 共享代码路径重构_append_shared_to_topk_output 改为前后端分流,CUDA 分支虽然逻辑不变,但该函数是多个 Qwen MoE 变体的公共出口,回归风险集中在非 aiter 环境;NVIDIA CI 多次 fast-fail-skip,实际覆盖有限。
  2. 数值一致性:Triton tl.sigmoidtorch.sigmoid 存在 ULP 级差异,fp32 归约顺序与 cuBLAS 不同,测试用宽松容差(fp32 rtol=1e-4);作者 GSM8K 结果在运行间方差内,但长尾场景需关注。
  3. 缩放逻辑双处维护_shared_expert_scale_get_shared_expert_weights 内联了相同的 1/ep_size 判断,未来若 EP 语义变化,两处可能漂移。
  4. 覆盖盲区:fuse_gate 的模型级 e2e 只在 nightly MI35x 测试中运行,PR CI 不覆盖;若 nightly 停摆,回归难被及时捕获。另外 num_warps=8 的新启动配置只在该路径生效,未在 CUDA 下验证编译行为。

用户侧:AMD(HIP/aiter)部署 Qwen 系 MoE 模型 decode 性能提升,每 MoE 层约 7.5µs(层耗时下降约 58%),作者估计 e2e 提升 1~2%;CUDA 用户行为不变。系统侧:无新依赖;新增两个测试文件与 CI 注册;kernel API 扩展为向后兼容的默认关闭参数。团队侧:为后续 AMD kernel 融合优化立下模式(gate GEMV 进 append 内核),也暴露了 AMD stage 超时与 nightly-only 覆盖的基础设施问题。

AMD-only 路径变更 e2e 覆盖依赖 nightly 内核数值非 bitwise 一致 EP 缩放逻辑双处维护

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论