Prhub

#33962 enable TRT-LLM for MiniMax M3 by preserving SwiGLU params

原始 PR 作者 zcnrex 合并时间 2026-08-10 14:30 文件变更 8 提交数 5 评论 7 代码增减 +68 / -2

执行摘要

MiniMax M3 启用 TRT-LLM MoE 后端,保留 SwiGLU 参数

PR body 的目标是让 --moe-runner-backend flashinfer_trtllm_routed 能在 MiniMax M3 上运行。M3 的 gated MoE 使用带 swiglu_alphaswiglu_limit 等参数的 SwiGLU 激活,而 FlashInfer TRT-LLM FP8 kernel 需要按专家传入这些参数才能正确复现激活行为;此前链路中这些参数没有保留,导致 TRT-LLM 后端对 M3 不可用。作者还提到之前为规避 FlashInfer mxfp8_gemm autotuning IMA 而加的跳过逻辑(来自 #29462)已不再需要,因此一并清理并重新启用 autotune。

值得精读。该 PR 是一个典型的"模型新后端使能"案例:核心设计决策是把模型级标量配置在权重加载阶段物化为 per-expert 张量,再通过 MoeQuantInfo 数据契约透传到 custom op wrapper,这种"整数/浮点配置到张量参数"的物化模式在多个 MoE 后端间可复用。同时应关注两个后续风险点:fp8_utils 的 cute-dsl 路由需要补充 SM120 防护,以及移除 autotune workaround 后的长尾硬件验证。建议合入后跟进毫米波的后续 --fp8-gemm-backend=flashinfer_cutedsl PR,把这次的小 M 路由逻辑统一收口。

讨论亮点

review 中主要有三处交锋:

  1. mmangkad 建议 minimax_m3.pygemm1_beta=1.0 改为 getattr(config, "swiglu_beta", 1.0),以兼容未来 checkpoint 显式携带 swiglu_beta 的情况;最终合入版本仍为硬编码 1.0,因为 M3 当前 config 无此字段。
  2. mmangkad 建议 fp8.pyapply 阶段用 getattr(layer, "_flashinfer_trtllm_gemm1_alpha", None) 做兜底,避免属性缺失时 AttributeError;最终合入版本仍为直接属性访问,依赖 _prepare_flashinfer_trtllm_activation_params 在权重加载流程中保证属性存在。
  3. mmangkad 对 fp8_utils.pycute-dsl 小 M 路由要求 benchmark 数据,zcnrex 给出 GB300 TP4 上 158.4 vs 126.0 tok/s(+25.7%);mmangkad 随后指出他自己也正在做 --fp8-gemm-backend=flashinfer_cutedsl 相关工作,且 cute-dsl 通常到 M <= 256 都优于 cutlass,但该路由改动会破坏不支持 cute-dsl 的 SM120,属于遗留风险。

实现拆解

该变更从配置到内核调用打通了一条完整参数链路,按以下步骤实现:

  1. 补齐配置字段:在 moe_runner/base.pyMoeRunnerConfig 中新增 gemm1_beta 字段;在 fused_moe_triton/layer.pyFusedMoE.__init__ 中增加同名参数并透传;在 minimax_m3.pyMiniMaxM3MoE.__init__ 中传入 gemm1_beta=1.0(M3 checkpoint 没有 swiglu_beta 配置,默认 beta 为 1.0)。这样 gemm1_alphagemm1_betagemm1_clamp_limit 三个标量就能随 model config 进入 MoE runner 配置。

  2. 激活参数物化:在 layers/quantization/fp8.pyprocess_weights_after_loading 中,当 MoE runner 为 flashinfer_trtllmflashinfer_trtllm_routed 时调用新增的 _prepare_flashinfer_trtllm_activation_params,把三个标量(或 None)以 torch.full 展开成 [num_local_experts] 形状的 float32 张量,挂到 layer 上(属性名为 _flashinfer_trtllm_gemm1_alpha 等),供后续 apply 阶段读取。

  3. kernel 参数透传:在 layers/moe/moe_runner/flashinfer_trtllm.pyFlashInferTrtllmFp8MoeQuantInfo 中新增 gemm1_alphagemm1_betagemm1_clamp_limit 三个可选张量字段,并在 fused_experts_none_to_flashinfer_trtllm_fp8 的两条调用路径(routed 与非 routed)中把 quant_info 中的参数传给 wrapper;同时 layers/moe/flashinfer_trtllm_moe.py 中两个 custom op wrapper(trtllm_fp8_block_scale_moe_out_wrappertrtllm_fp8_block_scale_routed_moe_out_wrapper)新增同名参数并放入 kwargs 转发给 FlashInfer kernel。

  4. autotune 与性能配套model_executor/runner/flashinfer_autotune.pyFLASHINFER_AUTOTUNE_WORKAROUND_SKIPS{"mxfp8_gemm"} 清空为 frozenset(),恢复 mxfp8_gemm 的 autotune;layers/quantization/fp8_utils.pyflashinfer_mxfp8_blockscaled_linearbackend == "cutlass"q_input.shape[0] <= 64 时自动切换到 cute-dsl,利用其在 decode 小 batch 下 2-5 倍的 kernel 优势。

  5. 测试与验证配套:本 PR 没有新增单元测试文件;验证依赖 PR body 中的 GSM8K(97%)与 GB300 TP4 上的速度数据(bs1 解码从 126.0 提升到 158.4 tok/s,加 dense sparse decode 后 167.7 tok/s)。

文件 模块 状态 重要度
python/sglang/srt/layers/quantization/fp8.py 量化层 modified 7.1
python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py MoE 运行时 modified 5.51
python/sglang/srt/layers/moe/flashinfer_trtllm_moe.py 内核封装 modified 5.93
python/sglang/srt/layers/quantization/fp8_utils.py 量化工具 modified 5.75
python/sglang/srt/model_executor/runner/flashinfer_autotune.py 自动调优 modified 5.2
python/sglang/srt/models/minimax_m3.py 模型定义 modified 4.96
python/sglang/srt/layers/moe/fused_moe_triton/layer.py Triton 层 modified 4.69
python/sglang/srt/layers/moe/moe_runner/base.py 运行配置 modified 4.55

关键符号

_prepare_flashinfer_trtllm_activation_params trtllm_fp8_block_scale_moe_out_wrapper trtllm_fp8_block_scale_routed_moe_out_wrapper flashinfer_mxfp8_blockscaled_linear fused_experts_none_to_flashinfer_trtllm_fp8 get_flashinfer_autotune_skip_ops

关键源码片段

python/sglang/srt/layers/quantization/fp8.py core-logic

核心改动文件:新增 `_prepare_flashinfer_trtllm_activation_params` 将标量 SwiGLU 参数物化为 per-expert 张量,并在 `apply` 阶段写入 `FlashInferTrtllmFp8MoeQuantInfo`,是整个参数链路的源头。

# 权重加载完成后,把 TRT-LLM SwiGLU 的标量参数物化为 per-expert tensor。
# 背景:MiniMax M3 的 gated MoE 携带 gemm1_alpha / gemm1_beta /
# gemm1_clamp_limit 等激活参数,FlashInfer TRT-LLM FP8 kernel 需要以
# [num_experts] 形状的张量接收,而不是 Python 标量。
def _prepare_flashinfer_trtllm_activation_params(self, layer: Module) -> None:
    """Materialize optional TRT-LLM SwiGLU parameters once per expert."""
    num_experts = int(layer.num_local_experts)
    device = layer.w13_weight.device
    # 每个参数要么是 None(kernel 用默认值),要么展开成与专家数量对齐的
    # float32 张量,挂到 layer 上供 apply 阶段读取。
    for name, value in (
        ("gemm1_alpha", self.moe_runner_config.gemm1_alpha),
        ("gemm1_beta", self.moe_runner_config.gemm1_beta),
        ("gemm1_clamp_limit", self.moe_runner_config.gemm1_clamp_limit),
    ):
        tensor = (
            None
            if value is None
            else torch.full(
                (num_experts,),
                float(value),
                dtype=torch.float32,
                device=device,
            )
        )
        setattr(layer, f"_flashinfer_trtllm_{name}", tensor)# 调用点在 process_weights_after_loading 中,与权重 layout 对齐(
# align_fp8_moe_weights_for_flashinfer_trtllm)同属 flashinfer_trtllm
# 后端的加载流程,保证 apply 阶段可以安全直接访问上面的属性。
if (
    get_moe_runner_backend().is_flashinfer_trtllm()
    or get_moe_runner_backend().is_flashinfer_trtllm_routed()
):
    self._prepare_flashinfer_trtllm_activation_params(layer)
python/sglang/srt/layers/moe/flashinfer_trtllm_moe.py core-logic

两个 custom op wrapper 新增 gemm1_alpha/beta/clamp_limit 参数并转发给 FlashInfer kernel,是 torch.compile 兼容包装层的必要改动。

# TRT-LLM routed MoE 的 custom op 封装:把 sglang 侧的 per-expert
# SwiGLU 参数(gemm1_alpha / gemm1_beta / gemm1_clamp_limit)透传给
# FlashInfer kernel。此前这些参数缺失,导致 MiniMax M3 无法走该后端。
@register_custom_op(
    fake_impl=_fake_fp8_block_scale_routed_moe_out,
    mutates_args=["output"],
)
def trtllm_fp8_block_scale_routed_moe_out_wrapper(
    topk_ids: torch.Tensor,
    routing_bias: Optional[torch.Tensor],
    hidden_states: torch.Tensor,
    hidden_states_scale: torch.Tensor,
    gemm1_weights: torch.Tensor,
    gemm1_weights_scale: torch.Tensor,
    gemm1_alpha: Optional[torch.Tensor],
    gemm1_beta: Optional[torch.Tensor],
    gemm1_clamp_limit: Optional[torch.Tensor],
    gemm2_weights: torch.Tensor,
    gemm2_weights_scale: torch.Tensor,
    num_experts: int,
    top_k: int,
    n_group: Optional[int],
    topk_group: Optional[int],
    intermediate_size: int,
    local_expert_offset: int,
    local_num_experts: int,
    routed_scaling_factor: Optional[float],
    output: torch.Tensor,
    routing_method_type: int = 0,
    use_shuffled_weight: bool = False,
    weight_layout: int = 0,
    enable_pdl: Optional[bool] = None,
    tune_max_num_tokens: int = 8192,
    fp8_quantization_type: Optional[int] = None,
    activation_type: Optional[int] = None,
) -> None:
    try:
        from flashinfer.fused_moe import trtllm_fp8_block_scale_routed_moe
    except ImportError as e:
        raise ImportError(
            "Can't import trtllm_fp8_block_scale_routed_moe from flashinfer. "
            "Please check flashinfer version."
        ) from e
​
    kwargs = {
        "topk_ids": topk_ids,
        "routing_bias": routing_bias,
        "hidden_states": hidden_states,
        "hidden_states_scale": hidden_states_scale,
        "gemm1_weights": gemm1_weights,
        "gemm1_weights_scale": gemm1_weights_scale,
        # 新增参数:即使为 None 也要显式传递,FlashInfer kernel 内部
        # 会退回默认 SwiGLU 行为(alpha=1 / beta=0 / 不 clamp)。
        "gemm1_alpha": gemm1_alpha,
        "gemm1_beta": gemm1_beta,
        "gemm1_clamp_limit": gemm1_clamp_limit,
        "gemm2_weights": gemm2_weights,
        "gemm2_weights_scale": gemm2_weights_scale,
        "output": output,
        "num_experts": num_experts,
        "top_k": top_k,
        "n_group": n_group,
        "topk_group": topk_group,
        "intermediate_size": intermediate_size,
        "local_expert_offset": local_expert_offset,
        "local_num_experts": local_num_experts,
        "routed_scaling_factor": routed_scaling_factor,
        "routing_method_type": routing_method_type,
        "use_shuffled_weight": use_shuffled_weight,
        "weight_layout": weight_layout,
        "enable_pdl": enable_pdl,
        "tune_max_num_tokens": tune_max_num_tokens,
    }
    if fp8_quantization_type is not None:
        from flashinfer.fused_moe import Fp8QuantizationType
​
        kwargs["fp8_quantization_type"] = Fp8QuantizationType(fp8_quantization_type)
​
    if activation_type is not None:
        from flashinfer.fused_moe.core import ActivationType
​
        kwargs["activation_type"] = ActivationType(activation_type)
​
    trtllm_fp8_block_scale_routed_moe(**kwargs)
python/sglang/srt/layers/quantization/fp8_utils.py core-logic

MXFP8 线性层在 M <= 64 时自动从 cutlass 切换到 cute-dsl,带来约 25% decode 吞吐提升,但引入 SM120 不支持的兼容性风险,是讨论焦点。

# 小 batch 场景(M <= 64)下,CUTLASS 持久化 kernel 比 CuTe-DSL
# swap-AB / split-K kernel 慢 2-5 倍,且两者消费相同的 swizzled 1D scale,
# 因此在 decode 阶段自动切到 cute-dsl,换取约 25% 吞吐提升。
#
# 注意:这是一个有硬件边界的优化,SM120 不支持 cute-dsl,后续需要
# 在 backend 路由逻辑中补充硬件过滤(见 review 讨论)。# 前置:输入被量化为 q_input 与其缩放 x_scale_u8,输出 dtype 已确定。
# 在保留 swizzled scale layout 的前提下切换 kernel 后端是安全的。
if backend == "cutlass" and q_input.shape[0] <= 64:
    backend = "cute-dsl"# 两种后端都消费同一个 swizzled 1D scale;TRT-LLM 后端保持原有
# scale.view(-1) 路径不变。
if backend == "trtllm":
    weight_scale_t = weight_scale.view(-1)
else:
    weight_scale_t = weight_scale.t() if weight_scale.ndim == 2 else weight_scaleoutput = flashinfer_mm_mxfp8(
    q_input,
    weight.t(),
    x_scale_u8,
    weight_scale_t,
    out_dtype=output_dtype,
    use_8x4_sf_layout=False,
    backend=backend,
)

评论区精华

gemm1_beta 取值的健壮性 设计

mmangkad 建议 `minimax_m3.py` 中使用 `gemm1_beta=getattr(config, "swiglu_beta", 1.0)`,以兼容未来 checkpoint 显式配置 `swiglu_beta` 的情况;作者最终保留硬编码 `1.0`。

结论:维持 `gemm1_beta=1.0`:当前 M3 checkpoint 没有 `swiglu_beta` 字段,且 `swiglu_beta` 语义上等同于 beta 缩放系数,默认 1.0 是正确的。 · 已解决

layer 属性访问是否需要 getattr 兜底 style

mmangkad 建议 `fp8.py` 的 `apply` 阶段用 `getattr(layer, "_flashinfer_trtllm_gemm1_alpha", None)` 保护,避免属性缺失时崩溃;作者最终采用直接属性访问。

结论:直接访问属性:`_prepare_flashinfer_trtllm_activation_params` 在权重加载流程中保证属性一定存在,getattr 兜底会掩盖真实 bug。 · 已解决

cute-dsl 小 M 路由的 benchmark 依据 性能

mmangkad 询问 `fp8_utils.py` 中 M <= 64 切 cute-dsl 是否有 benchmark 数据;zcnrex 给出 GB300 TP4 MiniMax-M3-MXFP8 上 bs=1(isl 8192 / osl 1024)的数据:cute-dsl 158.4 tok/s vs cutlass 126.0 tok/s,+25.7%。

结论:数据充分,路由改动被接受;mmangkad 补充说明他也在做 `--fp8-gemm-backend=flashinfer_cutedsl`,发现 cute-dsl 通常到 M <= 256 都优于 cutlass。 · 已解决

cute-dsl 路由对 SM120 的兼容性影响 正确性

mmangkad 指出该路由改动会破坏 SM120,因为 SM120 不支持 `cute-dsl`;建议在后续统一的 `flashinfer_cutedsl` 后端工作中调整。

结论:未在本 PR 中修复,作为已知风险遗留,留待后续 fp8-gemm-backend 统一工作收口。 · unresolved

移除 mxfp8_gemm autotune 跳过是否安全 question

zcnrex 加了启用 flashinfer autotune for mxfp8_gemm 的 commit,并称未遇到 IMA;mmangkad 表示自己也没遇到过,且没有看到相关 issue 讨论,原始问题来自 #29462 并被一直携带。

结论:双方均未复现 IMA,同意清空 `FLASHINFER_AUTOTUNE_WORKAROUND_SKIPS`,恢复 autotune。 · 已解决

风险与影响

  1. SM120 兼容性回归(fp8_utils.py)flashinfer_mxfp8_blockscaled_linearM <= 64 且 backend 为 cutlass 时无条件切到 cute-dsl,而 SM120 设备不支持 cute-dsl,可能导致该设备上 MXFP8 小块 GEMM 直接失败或回退,reviewer mmangkad 明确指出了这一点。
  2. 隐式属性契约(fp8.py)apply 阶段直接读取 layer._flashinfer_trtllm_gemm1_alpha 等属性,依赖 _prepare_flashinfer_trtllm_activation_params 一定在 process_weights_after_loading 中执行;若未来有其他代码路径绕过该函数(如直接构造 layer 后 apply),会因缺少属性而报错。reviewer 的 getattr 兜底建议未被采纳。
  3. autotune workaround 移除(flashinfer_autotune.py):清空 mxfp8_gemm 的跳过集合后,可能重新引入 #29462 描述的 autotune IMA;作者与 reviewer 均表示未复现,但覆盖硬件和 FlashInfer 版本范围有限,不能完全排除。
  4. 缺少测试覆盖:8 个文件改动没有任何对应单元测试,参数链路(config -> layer -> quant_info -> wrapper -> kernel)的回退行为完全依赖手工 benchmark 与 GSM8K 验证。
  5. gemm1_beta 硬编码:若未来 M3 checkpoint 引入非 1.0 的 swiglu_beta,硬编码会导致静默数值偏差。

用户侧:MiniMax M3(MXFP8)用户现在可以在 flashinfer_trtllm_routed 后端下推理,配合 SGLANG_OPT_USE_MINIMAX_DENSE_SPARSE_DECODE=1 解码吞吐可达 167.7 tok/s,GSM8K 精度 97%;系统侧:fp8_utils.py 的小 M 路由影响所有使用 MXFP8 且走 cutlass 后端的模型,decode 小 batch 场景普遍受益(约 25% 提升),但同时给 SM120 用户带来兼容性风险;团队侧:该 PR 清理了遗留的 autotune workaround,为后续 --fp8-gemm-backend=flashinfer_cutedsl 的统一后端选择工作提供了实践依据,但本 PR 未附带测试,需要后续补齐。

SM120 兼容性回归 缺少测试覆盖 autotune workaround 移除风险 隐式加载顺序契约

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论