Prhub

#25663 [MoE Refactor] [NPU] Refactor Ascend MoE implementation to reduce code duplication and align with community design

原始 PR 作者 OrangeRedeng 合并时间 2026-07-15 19:59 文件变更 54 提交数 29 评论 59 代码增减 +2857 / -2381

执行摘要

重构 NPU MoE 为 5 组件解耦,对齐社区 All-to-All 架构

当前 Ascend NPU 上的 MoE 实现存在两大问题:

1) 高代码重复 – MoE 前向逻辑在 10 多个量化文件中重复出现,每次修复或升级 kernel 都需要同步修改所有副本,维护开销极高;
2) 与社区设计偏离 – dispatch 和 grouped-GEMM 被硬编码在单片 apply 流程中,无法融入标准 All-to-All 后端和 MoE Runner 模式(见 Issue #8715 MoE Refactor 路线图)。

建议所有 NPU 后端开发者深度阅读此 PR,尤其是 5 组件的接口设计(base.py 中的 MoeRunnerCore 及其子类)和 AscendTPDispatcherdispatch 流程。review 中关于循环导入的教训也值得其他模块借鉴。PR 本身合并后需密切监控 NPU MoE 各项 CI job 的稳定性。

讨论亮点
  • ch-wan 指出 P0 问题:顶层 import Ascend runner 导致循环导入,import sglang.srt.layers.moe 直接失败。作者随后将 import 移至函数内部或延迟导入,解决了核心模块加载问题。
  • ch-wan 的回归分析:将新旧 W4A4 打包路径、AWQ 权重处理等差异逐一对比,确认新增的 SGLANG_NPU_W4A4_NEW_PACKING 门控避免对现有方案的影响,AWQ 恢复了 #10158 的正确实现。
  • ping1jing2 的命名 & 风格建议:要求将 torch_npu 统一改为 ascend_tp,避免引入新词汇;为 awq_kernels.py 中的 magic number 添加详细注释;建议用 __call__ 替代 forward 等。作者均配合修改。
  • gemini-code-assist 类型提示修复:指出 init_routing.py 中抽象方法返回值标注为 3 元组但实现返回 4 元组,建议统一为 Tuple[..., Optional[torch.Tensor]]

实现拆解

  1. 删除遗留单片代码:移除 fused_moe_method_npu.py(1217 行),该文件包含了多个重复的 npu_fused_experts_* 函数。
  2. 定义 5 个抽象组件及 NPU 实现:在 hardware_backend/npu/moe/ 下创建 init_routing.pyhidden_states_quant.pymatmul.pyactivation.pyfinalize_routing.py,每个组件提供 Base* 抽象类和一至多个 NPU 具体实现(如 NPUMoEInitRouting_v1/v2HiddenStatesDynamicQuantNPUSwigluQuant)。
  3. 新建 Ascend MoE Runner:在 layers/moe/moe_runner/ascend.py 中创建 AscendRunnerInput/AscendRunnerOutput 数据和 AscendRunnerCore 类,负责在运行时根据模型配置和量化类型选择合适的组件组合,执行统一的 run 方法。
  4. 新建 Ascend TP Dispatcher:在 layers/moe/token_dispatcher/ascend_tp.py 中创建 AscendTPDispatcher,封装初始路由和最终路由逻辑,对齐社区 BaseDispatcher 接口,支持 GGUF 下的 TP all-gather 包装。
  5. 改造量化 Scheme:逐一修改 AWQ、ModelSlim(W4A4 / W4A8 / W8A8)等量化模块,使其通过 create_moe_runnerapply_weights/apply_without_routing_weights 接口与新 Runner 集成,权重处理逻辑被抽取为 _NPUMoEMethodBase 的工具方法。
  6. 补充激活函数组件:在 activation.py 中实现 NPUSwigluNPUSwigluQuantNPUSwigluDeepEPKernelNPUGeluAndMul 等变体,均继承自 BaseActivation
  7. 测试与文档:更新 NPU 相关测试用例(如 test_npu_minimax_m2_5_w8a8_*),新增 TensorBoard/Waifu 等文档说明新架构。
文件 模块 状态 重要度
python/sglang/srt/hardware_backend/npu/quantization/fused_moe_method_npu.py 旧 MoE 单片 removed 9.26
python/sglang/srt/hardware_backend/npu/quantization/moe_methods.py MoE 量化统一入口 added 9.26
python/sglang/srt/layers/moe/moe_runner/ascend.py Ascend Runner added 9.24
python/sglang/srt/hardware_backend/npu/moe/activation.py 激活组件 added 9.09
python/sglang/srt/layers/moe/token_dispatcher/ascend_tp.py Ascend 调度器 added 8.98
python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_w4a4_int4_moe.py ModelSlim W4A4 modified 9.0

关键符号

npu_fused_experts_w4a4 npu_fused_experts npu_fused_experts_w8a8_decode fused_moe_npu _NPUMoEMethodBase.__init__ AscendRunnerCore.__init__ AscendRunnerCore.run BaseActivation._apply_activation NPUSwiglu._apply_activation NPUSwigluQuant._apply_activation NPUSwigluDeepEPKernel._apply_activation AscendTPDispatcher.dispatch ModelSlimW4A4Int4MoE.create_weights ModelSlimW4A4Int4MoE.apply_weights

关键源码片段

python/sglang/srt/hardware_backend/npu/quantization/moe_methods.py dependency-wiring

新增核心文件,包含 `_NPUMoEMethodBase` 基类和所有 NPU 量化方法的统一入口(`apply`/`apply_without_routing_weights`),以及废弃的 `fused_moe_npu` 兼容函数。

# DEPRECATED METHOD – 保留用于向后兼容
# TODO: Remove in future releases
def fused_moe_npu(
    x,
    w1,
    w2,
    topk_output,
    moe_runner_config,
):
    logger.warning_once(
        "The fused_moe_npu method deprecated and will be removed in future releases"
    )
    topk_weights, topk_ids, _ = topk_output
    original_dtype = x.dtype
    num_tokens = x.shape[0]
    topk_weights = topk_weights.to(x.dtype)
    topk_ids = topk_ids.to(torch.int32)
    num_experts = w1.shape[0]
    top_k = topk_weights.shape[-1]
    row_idx_len = num_tokens * top_k
    row_idx = (
        torch.arange(0, row_idx_len, dtype=torch.int32, device=topk_weights.device)
        .view(top_k, -1)
        .permute(1, 0)
        .contiguous()
    )
​
    # 使用 NPU 原生 init_routing v1
    hidden_states, expanded_row_idx, expanded_expert_idx = (
        torch.ops.npu.npu_moe_init_routing(
            x, row_idx=row_idx, expert_idx=topk_ids, active_num=num_tokens
        )
    )
    expert_tokens = torch.ops.npu.npu_moe_compute_expert_tokens(
        expanded_expert_idx, num_experts
    ).to(torch.int64)
​
    # gmm1: gate_up_proj,注意 weight 需 permute
    hidden_states = torch.ops.npu.npu_grouped_matmul(
        x=[hidden_states],
        weight=[w1.permute(0, 2, 1)],
        bias=None,
        split_item=2,
        group_list_type=0,
        group_type=0,
        group_list=expert_tokens,
        output_dtype=original_dtype,
    )[0]
​
    # 根据配置选择激活函数
    if moe_runner_config.activation == "silu":
        hidden_states = torch.ops.npu.npu_swiglu(hidden_states)
    else:
        from sglang.srt.layers.activation import GeluAndMul
        hidden_states = GeluAndMul()(hidden_states)
​
    # gmm2: down_proj
    hidden_states = torch.ops.npu.npu_grouped_matmul(
        x=[hidden_states],
        weight=[w2.permute(0, 2, 1)],
        bias=None,
        split_item=2,
        group_list_type=0,
        group_type=0,
        group_list=expert_tokens,
        output_dtype=original_dtype,
    )[0]
​
    final_hidden_states = torch.ops.npu.npu_moe_finalize_routing(
        hidden_states,
        skip1=None,
        skip2=None,
        bias=None,
        scales=topk_weights,
        expanded_src_to_dst_row=expanded_row_idx,
        export_for_source_row=topk_ids,
    )
    return final_hidden_states
python/sglang/srt/layers/moe/moe_runner/ascend.py core-logic

新增的 Ascend MoE Runner 核心实现,包含 `AscendRunnerInput`/`AscendRunnerOutput` 数据类、`AscendRunnerCore`(继承自 `MoeRunnerCore`)以及 `run` 方法,是整个组件化架构的执行中枢。

@dataclass
class AscendRunnerInput(RunnerInput):
    """NPU runner 的输入 bundle,包含已排好序的 hidden_states 和专家计数信息。"""
    hidden_states: torch.Tensor
    hidden_states_scale: Optional[torch.Tensor] # 非量化时为 None
    expert_tokens: torch.Tensor # 每个专家分配的 token 数
    group_list_type: int # 0 或 1,传给 NPU grouped matmul 操作
​
    @property
    def runner_backend(self) -> MoeRunnerBackend:
        return MoeRunnerBackend.ASCEND
​
​
class AscendRunnerCore(MoeRunnerCore):
    runner_backend = MoeRunnerBackend.ASCEND
​
    def __init__(self, config: MoeRunnerConfig):
        super().__init__(config)
        kernel = config.layer.w2_kernel
​
        if get_moe_a2a_backend().is_deepep():
            # DeepEP 路径:使用统一 kernel 决定是否需要量化
            is_quant_kernel = isinstance(
                kernel, (NPUW4A8Int8MoEMethod, NPUW8A8Int8MoEMethod)
            )
            self.activation = NPUSwigluDeepEPKernel(need_quant=is_quant_kernel)
        else:
            # 非 DeepEP(ascend_tp)路径
            if isinstance(kernel, (NPUW4A8Int8MoEMethod, NPUW8A8Int8MoEMethod)):
                inner = NPUSwigluQuant()
            else:
                # 根据配置选择具体的激活变体
                if config.activation == "npu_swiglu_oai":
                    inner = NPUSwigluOAI(moe_runner_config=config)
                elif config.activation == "silu":
                    if config.gemm1_clamp_limit is not None:
                        inner = NPUSwigluStepAndMul(clamp_limit=config.gemm1_clamp_limit)
                    else:
                        inner = NPUSwiglu()
                else:
                    inner = NPUGeluAndMul()
​
            if getattr(config, "use_tp_all_gather_activation", False):
                self.activation = AllGatherActivationWrapper(inner, dim=-1)
            else:
                self.activation = inner
​
    def run(self, runner_input: AscendRunnerInput, quant_info: AscendQuantInfo,
            running_state: dict, hooks=None) -> AscendRunnerOutput:
        """执行 MoE 层:w13 -> 激活 -> w2 -> 最终化。"""
        x = runner_input.hidden_states
        original_dtype = torch.float16 if x.dtype == torch.float16 else torch.bfloat16
        expert_tokens = runner_input.expert_tokens
        group_list_type = runner_input.group_list_type
​
        # w13 (gate & up) 投影
        hidden_states = self.config.layer.w13_kernel.apply(
            quant_info, x, expert_tokens,
            pertoken_scale=runner_input.hidden_states_scale,
            output_dtype=original_dtype,
            weight_prefix="w13"
        )
​
        # 激活函数(可能包含量化)
        hidden_states, pertoken_scale = self.activation._apply_activation(
            hidden_states, expert_tokens, group_list_type
        )
​
        # w2 (down) 投影
        hidden_states = self.config.layer.w2_kernel.apply(
            quant_info, hidden_states, expert_tokens,
            pertoken_scale=pertoken_scale,
            output_dtype=original_dtype,
            weight_prefix="w2"
        )
​
        # 最终化路由(合并结果还原 token 顺序)
        final_hidden_states = self.config.layer.finalize_kernel._finalize_routing(
            hidden_states, quant_info, runner_input.topk_weights,
            runner_input.expanded_row_idx, runner_input.topk_ids
        )
​
        return AscendRunnerOutput(hidden_states=final_hidden_states)

评论区精华

顶层 import 导致循环导入(P0) 正确性

ch-wan 在 review 中指出 `runner.py` 中顶层 import `ascend` 导致 `import sglang.srt.layers.moe` 在未完全初始化时失败,阻塞核心模块加载。

结论:作者将 import 移至函数内部或使用延迟导入,解决了循环依赖。 · 已解决

W4A4 新打包路径回归分析 设计

ch-wan 逐项对比新旧 W4A4 的 scale 打包、weight 布局差异,指出新代码可能改变行为。作者解释新路径通过 `SGLANG_NPU_W4A4_NEW_PACKING` 环境变量门控,旧路径保持兼容。

结论:确定为有意扩展,门控机制避免了回归风险。 · 已解决

命名风格统一(torch_npu → ascend_tp) style

ping1jing2 要求避免引入 `torch_npu` 新词汇,统一使用 `ascend_tp` 强调 TP 场景。作者配合重命名。

结论:已按建议修改。 · 已解决

Magic Number 注释与公式说明 documentation

ping1jing2 在 `awq_kernels.py` 中要求对整数位宽转换、scale 偏移等 magic number 添加注释和公式。

结论:作者补充了详细注释。 · 已解决

风险与影响

  1. 循环导入风险(已修复):顶层 import 曾导致部分环境无法导入 sglang.srt.layers.moe。修复后需确保无残留。
  2. W4A4 新打包路径兼容性:W4A4 的通用逐前缀 matmul 合约受环境变量 SGLANG_NPU_W4A4_NEW_PACKING 门控,旧路径仍受支持,但若门控逻辑有误可能导致静默精度回退。
  3. DeepEP / FuseEP 集成:新 Runner 的 DeepEP 路径使用了 NPUSwigluDeepEPKernel,但该路径的测试覆盖不足,可能存在运行时 shape 或 dtype 不匹配。
  4. 性能回归:组件化增加了间接层(虚函数调用、配置分支),在极端高频的 MoE 前向路径可能引入微小开销,需 benchmark 验证。
  5. 多量化方案回归:影响 AWQ、ModelSlim、GGUF 等全部 NPU 量化方案,每个方案都需在 NPU 硬件上重新验证精度与吞吐。

影响范围:所有使用 Ascend NPU 的 MoE 模型(DeepSeek、Qwen、MiniMax 等),涉及推理核心路径。
用户影响:对外 API 和 CLi 参数保持兼容,旧 fused_moe_npu 函数仍保留并标记废弃,用户无感知。
团队影响:新架构大幅降低后续添加量化方法或路由 kernel 的维护成本;代码结构清晰,组件可独立测试。

核心路径变更 循环导入修复 新打包路径门控 多量化兼容性

关联 Issue

#8715 [Roadmap] MoE Refactor

完整报告

参与讨论