Prhub

#39613 Accept MXFP8 dispatch in FlashInfer A2A TRT-LLM MoE

原始 PR 作者 mmangkad 合并时间 2026-09-16 10:23 文件变更 1 提交数 2 评论 3 代码增减 +21 / -10

执行摘要

放宽 MoE 入口守卫,MXFP8 dispatch 不再启动崩溃

PR body 给出的动机非常具体:以 FlashInfer A2A 与 --flashinfer-a2a-dispatch-type mxfp8 服务 MXFP8 模型时,初始化阶段直接崩在 TypeError: FlashInfer A2A + TRT-LLM Gen FP8 MoE requires a BF16 dispatch payload, got torch.float8_e4m3fn.。作者指出 #31470 已经让 dispatcher 发送「已量化的 float8_e4m3fn payload + activation scales」并让 runner 具备消费能力,但入口处这条 BF16-only 守卫没有同步放宽,形成前后端契约错配。作者在 4x GB300 上验证:改动前「dies during init」,改动后服务正常启动且 GSM8K(1319 线程单发)达到 96.06%、error_rate 0.00%。

值得精读,但属于「小改动、大信息量」的类型:31 行内完整体现了 dispatcher 侧量化契约演进后,入口守卫如何跟上。

重点关注:

  1. 为什么用 quant_info.use_mxfp8 and dtype == float8_e4m3fn 的联合判定,而不是单纯 quant_info.use_mxfp8——理解这条「双条件」是为了避免配置与载荷不一致时静默走错路径,这个模式可以复用到其他量化入口的守卫。
  2. else 分支完整保留原有两条错误信息,意味着作者刻意让既有 FP8 失败语义零漂移,这是低风险修补的示范做法。
  3. 输出侧仍强制 BF16 的不对称设计,是阅读时最值得追问的一点:它到底是 TRT-LLM 的硬约束,还是当前实现尚未覆盖的遗留假设。
  4. 建议后续补一个覆盖 mxfp8 dispatch 的三分支单元测试,并考虑在 else 分支的错误信息中提示「use_mxfp8 与实际 payload dtype 不一致」这一可能原因。
讨论亮点

本 PR 没有任何行内 review 评论,讨论几乎全部发生在 CI 流程侧:

  • 作者主动针对最相关的后端测试发起重跑:/rerun-test test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py,结果显示 4-gpu-b200(1 test)通过。这是本次改动唯一可观测的自动化证据。
  • CI 机器人拦截过一次重跑:「Your PR is diverged relative to required base commit ddd4600197a4. Re-run was not dispatched.」,要求先 rebase 到最新 main(关联 issue #21065)。作者随后提交 Merge branch 'main' into mmangkad/flashinfer-a2a-mxfp8-trtllm-fp8-moe,与本 PR 共 2 个 commit 的记录一致。
  • 维护者 Fridge003 以 APPROVED 状态合入,未留下评论正文,说明改动范围足够小、入口守卫的放宽方式没有引发设计争议。
  • 需要注意:PR body 内嵌的 CI 状态块显示 pr-test、pr-test-extra、amd-rocm720 三条流水线在标记更新时刻均为失败标记,作者只对单一测试做了定向重跑。材料中无法判断这些失败是否与本改动相关。

实现拆解

变更入口是 python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py 中的 fused_experts_flashinfer_to_flashinfer_trtllm——它同时注册为 flashinfer + flashinfer_trtllm 与 flashinfer + flashinfer_trtllm_routed 两组 fused func,是 FlashInfer A2A 与 TRT-LLM Gen MoE 的唯一汇合点。

  1. 定位失效守卫:FlashInferTrtllmFp8MoeQuantInfo 分支开头原本无条件执行两条校验——payload 必须是 torch.bfloat16,且不得携带 hidden_states_scale。这两条共同表达了「TRT-LLM 本地量化」这一旧契约,但它与 #31470 之后 dispatcher 已量化的 MXFP8 载荷直接冲突。
  2. 引入联合判定条件:新增局部判定 mxfp8_dispatch = quant_info.use_mxfp8 and dispatch_output.hidden_states.dtype == torch.float8_e4m3fn。把「量化配置声明的量化方案」和「上游实际送来的 payload 形态」同时纳入判断,避免仅凭 dtype 或仅凭 flag 单方面放行。
  3. 拆分出两条互斥校验路径:MXFP8 分支只要求 dispatch_output.hidden_states_scale is not None,缺失则抛 ValueError;非 MXFP8 分支完整保留原有的 BF16 与「无 scale」双约束,错误信息与抛出类型均未改动,因此既有 FP8 用户的失败语义和可观测性完全不变。
  4. 两条路径汇入同一 kernel:分支结束后统一调用 fused_experts_none_to_flashinfer_trtllm_fp8(..., use_routed_topk=True),MXFP8 只改变输入 payload 契约,不引入新的 kernel 或 runner 分支;FlashInferTrtllmFp4MoeQuantInfo / FlashInferTrtllmBf16MoeQuantInfo 分支与 StandardDispatchOutput / FlashinferCombineInput 的包装选择逻辑均未触碰。
  5. 输出侧保持保守:函数尾部的 result.hidden_states.dtype != torch.bfloat16 校验仍对所有 FP8 量化信息生效,MXFP8 只放宽输入,combine 结果依旧必须回到 BF16。
  6. 测试与验证配套:本 PR 未新增或修改任何测试文件,验证依赖两条外部证据——PR body 中 4x GB300 的手工端到端结果(GSM8K 96.06%),以及作者对既有 test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py 发起的 /rerun-test,该次重跑在 4-gpu-b200 上通过。
文件 模块 状态 重要度
python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py MoE 执行器 modified 6.74

关键符号

fused_experts_flashinfer_to_flashinfer_trtllm fused_experts_none_to_flashinfer_trtllm_fp8

关键源码片段

python/sglang/srt/layers/moe/moe_runner/flashinfer_trtllm.py core-logic

唯一变更文件,也是 FlashInfer A2A 与 TRT-LLM Gen MoE 的融合入口;`fused_experts_flashinfer_to_flashinfer_trtllm` 中的 FP8 分支守卫由 BF16-only 改为按量化方案联合判定,是本次修复的全部内容。

@register_fused_func("flashinfer", "flashinfer_trtllm")
@register_fused_func("flashinfer", "flashinfer_trtllm_routed")
def fused_experts_flashinfer_to_flashinfer_trtllm(
    dispatch_output: FlashinferDispatchOutput | StandardDispatchOutput,
    quant_info: MoeQuantInfo,
    runner_config: MoeRunnerConfig,
) -> FlashinferCombineInput | StandardCombineInput:
    """FlashInfer A2A + TRT-LLM Gen MoE 的融合函数。
​
    一侧发送式 decode 与 AG+RS prefill 都会物化 routing IDs 与权重,
    因此普通后端名和显式 routed 后端名都会进入 TRT-LLM 的 routed kernel;
    两种 dispatch 格式共享实现所消费的字段,只有 combine 包装不同。
    """
    if isinstance(quant_info, FlashInferTrtllmFp4MoeQuantInfo):
        result = fused_experts_none_to_flashinfer_trtllm_fp4(
            dispatch_output, quant_info, runner_config, use_routed_topk=True
        )
    elif isinstance(quant_info, FlashInferTrtllmFp8MoeQuantInfo):
        # 本 PR 的核心:守卫不再只看 dtype,而是「量化配置 + payload 形态」联合判定。
        # 只有 use_mxfp8 为真、且上游确实送来 FP8 payload 时,才走 MXFP8 契约。
        mxfp8_dispatch = (
            quant_info.use_mxfp8
            and dispatch_output.hidden_states.dtype == torch.float8_e4m3fn
        )
        if mxfp8_dispatch:
            # MXFP8 路径:dispatcher 已完成量化(见 #31470),
            # 因此 activation scales 是反量化必需项,缺失就以 ValueError 快速失败。
            if dispatch_output.hidden_states_scale is None:
                raise ValueError(
                    "FlashInfer A2A + TRT-LLM Gen MXFP8 MoE requires activation "
                    "scales alongside the FP8 dispatch payload."
                )
        else:
            # 传统 FP8 路径:由 TRT-LLM 侧本地量化,故 payload 必须是 BF16,
            # 且不得携带 activation scales —— 两条约束与改动前完全一致。
            if dispatch_output.hidden_states.dtype != torch.bfloat16:
                raise TypeError(
                    "FlashInfer A2A + TRT-LLM Gen FP8 MoE requires a BF16 "
                    f"dispatch payload, got {dispatch_output.hidden_states.dtype}."
                )
            if dispatch_output.hidden_states_scale is not None:
                raise ValueError(
                    "FlashInfer A2A + TRT-LLM Gen FP8 MoE quantizes locally; "
                    "the BF16 dispatch payload must not carry activation scales."
                )
        # 两条契约最终汇入同一个 TRT-LLM FP8 kernel,差异只体现在输入 payload。
        result = fused_experts_none_to_flashinfer_trtllm_fp8(
            dispatch_output,
            quant_info,
            runner_config,
            use_routed_topk=True,
        )
    elif isinstance(quant_info, FlashInferTrtllmBf16MoeQuantInfo):
        result = fused_experts_none_to_flashinfer_trtllm_bf16(
            dispatch_output, quant_info, runner_config, use_routed_topk=True
        )
    else:
        raise TypeError(
            f"Unexpected quant_info type for flashinfer a2a + flashinfer_trtllm: {type(quant_info)}"
        )
​
    # 输出侧契约未变:MXFP8 只放宽输入 payload,combine 结果仍必须回到 BF16。
    if (
        isinstance(quant_info, FlashInferTrtllmFp8MoeQuantInfo)
        and result.hidden_states.dtype != torch.bfloat16
    ):
        raise TypeError(
            "FlashInfer A2A + TRT-LLM Gen FP8 MoE must return a BF16 combine "
            f"payload, got {result.hidden_states.dtype}."
        )
    if isinstance(dispatch_output, StandardDispatchOutput):
        return result
    return FlashinferCombineInput(hidden_states=result.hidden_states)

评论区精华

针对 TRT-LLM MoE 后端测试的定向重跑 测试

作者在改动后发起 `/rerun-test test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py`,机器人回执显示 `4-gpu-b200`(1 test)通过,执行命令为 `cd test/ && python3 registered/backends/test_flashinfer_trtllm_gen_moe_backend.py`。

结论:既有后端注册测试在 4-gpu-b200 上通过;但未新增针对 mxfp8 dispatch 的新用例,覆盖边界仍依赖既有测试。 · 已解决

main 分支要求 rebase 才能重跑 CI other

CI 机器人提示:「Your PR is `diverged` relative to required base commit `ddd4600197a4`. Re-run was not dispatched.」,要求先 rebase 到最新 `main`(关联 issue #21065)。

结论:作者将 `main` 合并回特性分支(commit `a190e40`),随后重跑成功,本次重跑拦截未影响最终合入。 · 已解决

入口守卫放宽方案获维护者无异议批准 设计

维护者 Fridge003 给出 APPROVED,review body 为空,行内 review 评论为 0,说明「按量化方案 + payload 形态联合判定」这一放宽方式未引发设计分歧。

结论:改动以零评论批准合入,评审重点落在改动范围可控与既有路径语义不变上。 · 已解决

风险与影响

  1. 判定条件耦合带来的误判面:mxfp8_dispatch 依赖 quant_info.use_mxfp8 与 payload dtype 同时成立。若 dispatcher 已按 MXFP8 发出 FP8 payload 但 use_mxfp8 未置位(或反之),代码会落入 else 分支并抛出「requires a BF16 dispatch payload, got torch.float8_e4m3fn」,错误信息会把读者引向 dtype 问题而非配置不一致,排查成本升高。
  2. scale 校验偏薄:MXFP8 分支只检查 hidden_states_scale is not None,未校验 scale 的 dtype、形状、block 尺寸与 payload 是否匹配。格式错误会穿透 Python 层校验,推迟到 TRT-LLM kernel 内才暴露,错误栈更难定位。
  3. 缺少针对性测试覆盖:本次没有新增单测来固化「use_mxfp8=True + FP8 payload + 有 scale 放行」「use_mxfp8=True + FP8 payload + 无 scale 报 ValueError」「非 MXFP8 且 dtype 非 BF16 仍报 TypeError」这三条边界;回归保护依赖既有的 test_flashinfer_trtllm_gen_moe_backend.py,而该测试是否显式覆盖 mxfp8 dispatch type 从材料中无法确认。
  4. 输入放宽与输出保守的不对称:MXFP8 允许输入是 FP8,输出仍强制 BF16。若某些 TRT-LLM 版本在 MXFP8 路径返回非 BF16 combine payload,会在此处被拦下并报错;这是有意的保守设计,但属于需要后续验证的假设。
  5. 环境相关性:手工验证仅在 4x GB300(Blackwell)上完成,其他平台(如 AMD ROCm、Hopper)上 MXFP8 dispatch 的实际行为未在材料中体现。

用户影响:修复了一类「模型能拉起来但服务初始化即失败」的硬阻断。使用 MXFP8 量化模型(如 Qwen3-30B-A3B-Instruct-2507-MXFP8)并开启 --moe-a2a-backend flashinfer --flashinfer-a2a-dispatch-type mxfp8 的用户,从「dies during init」变为可正常服务,PR body 给出的 GSM8K 96.06% 表明精度无损。

系统影响:改动收敛在 MoE 融合函数的单个量化分支内,默认 FP8(BF16 payload 本地量化)、BF16、FP4 三条路径的校验逻辑与错误类型零变化,StandardDispatchOutput 与 FlashinferCombineInput 包装逻辑未变,不存在调度器、KV cache 或通信层的外溢影响。

团队影响:这是一次「契约对齐」式修复,把入口守卫的语义从「看 dtype」升级为「看量化方案 + 载荷形态」,为后续更多已量化 dispatch 方案(不同 scale 粒度、不同 FP8 变体)提供了可扩展的判定范式,但同时也提高了新增量化路径时必须同步维护这段守卫的要求。

缺少针对性测试覆盖 判定条件依赖 flag 与 dtype 双条件 scale 形状与类型未校验 平台验证仅限 GB300 CI 部分流水线状态不佳

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论