[MoE] Add FlashInfer SM90 MXFP4 W4A8 CUTLASS MoE (#34967)

Co-authored-by: luoyuan.luo <luoyuan.luo@antgroup.com>
This commit is contained in:
Yuan Luo
2026-08-31 20:04:41 -07:00
committed by GitHub
co-authored by luoyuan.luo
parent 22337e9c56
commit 5b04408784
13 changed files with 662 additions and 81 deletions
@@ -401,6 +401,11 @@ class FusedMoE(torch.nn.Module):
self.quant_config = quant_config
self.use_flashinfer_mxfp4_moe = get_moe_runner_backend().is_flashinfer_mxfp4()
# TODO maybe we should remove this `if`, since `Mxfp4MoEMethod` does another round-up logic
# Keep the pre-round value: the mxfp4 SM90 CUTLASS post-load processor
# needs to know which trailing K columns are padding, because the
# loader never writes them and their scale entries keep the buffer's
# _UE8M0_ONE fill.
self.hidden_size_unpadded = hidden_size
if (
self.quant_config is not None
and self.quant_config.get_name() == "mxfp4"
@@ -65,8 +65,9 @@ class FlashInferCutlassMoeQuantInfo(MoeQuantInfo):
class FlashInferCutlassMxfp4MoeQuantInfo(MoeQuantInfo):
"""Quantization payload for CUTLASS MXFP4 MoE.
SM90 consumes W4A16-interleaved weights and scales. SM120 consumes packed
MXFP4 weights and block-interleaved scales with MXFP8 activations.
SM90 consumes either W4A16-interleaved weights/scales or the Humming-style
W4A8 layouts. SM120 consumes packed MXFP4 weights and block-interleaved
scales with MXFP8 activations.
"""
# SM90 weights are interleaved; SM120 weights remain checkpoint-packed.
@@ -80,6 +81,13 @@ class FlashInferCutlassMxfp4MoeQuantInfo(MoeQuantInfo):
# A non-None global scale selects the SM120 MXFP8 activation path.
mxfp4_weight_global_scale: Optional[torch.Tensor] = None
# A complete non-None triplet selects the SM90 Humming W4A8 path. The
# residuals are FP32 [num_local_experts] and already include the fixed 2^6
# compensation required by FlashInfer's epilogue.
w13_humming_residual_scale: Optional[torch.Tensor] = None
w2_humming_residual_scale: Optional[torch.Tensor] = None
humming_fc2_act_scale: Optional[torch.Tensor] = None
# Per-expert bias. GPT-OSS has both; DSv4 leaves both None.
w13_bias: Optional[torch.Tensor] = None # bf16 [E, 2*N]
w2_bias: Optional[torch.Tensor] = None # bf16 [E, K]
@@ -340,6 +348,22 @@ def fused_experts_none_to_flashinfer_mxfp4(
weight_global_scale = quant_info.mxfp4_weight_global_scale
use_mxfp8_act_scaling = weight_global_scale is not None
w13_humming_residual_scale = quant_info.w13_humming_residual_scale
w2_humming_residual_scale = quant_info.w2_humming_residual_scale
humming_fc2_act_scale = quant_info.humming_fc2_act_scale
humming_scales = (
w13_humming_residual_scale,
w2_humming_residual_scale,
humming_fc2_act_scale,
)
use_wfp4afp8_humming = any(scale is not None for scale in humming_scales)
if use_wfp4afp8_humming and not all(scale is not None for scale in humming_scales):
raise ValueError(
"SM90 Humming MXFP4 MoE requires both expert residual scales "
"and the FC2 activation scale."
)
if use_wfp4afp8_humming and use_mxfp8_act_scaling:
raise ValueError("SM90 Humming and SM120 MXFP8 scaling are mutually exclusive.")
input_sf = None
fc1_expert_weights = quant_info.w13_weight
fc2_expert_weights = quant_info.w2_weight
@@ -359,6 +383,17 @@ def fused_experts_none_to_flashinfer_mxfp4(
quant_info.w2_weight_scale.view(torch.int32),
weight_global_scale,
]
elif use_wfp4afp8_humming:
assert w13_humming_residual_scale is not None
assert w2_humming_residual_scale is not None
assert humming_fc2_act_scale is not None
quant_scales = [
quant_info.w13_weight_scale.view(torch.int32),
w13_humming_residual_scale,
humming_fc2_act_scale,
quant_info.w2_weight_scale.view(torch.int32),
w2_humming_residual_scale,
]
else:
quant_scales = [
quant_info.w13_weight_scale.view(torch.int32),
@@ -367,6 +402,10 @@ def fused_experts_none_to_flashinfer_mxfp4(
out_hidden = padded_hidden if do_pad else origin_hidden
output_dtype = torch.bfloat16
# FlashInfer 0.6.17 intentionally reverted the Humming API. Do not pass the
# new keyword at all on the existing W4A16/MXFP8 paths, so those paths keep
# working with SGLang's currently pinned release.
humming_kwargs = {"use_wfp4afp8_humming": True} if use_wfp4afp8_humming else {}
with use_symmetric_memory(get_tp_group(), disabled=not is_allocation_symmetric()):
out = torch.empty(x.shape[0], out_hidden, dtype=output_dtype, device=x.device)
@@ -398,6 +437,7 @@ def fused_experts_none_to_flashinfer_mxfp4(
tune_max_num_tokens=next_power_of_2(x.shape[0]),
output=out,
use_fused_finalize=envs.SGLANG_FLASHINFER_MOE_FUSED_FINALIZE.get(),
**humming_kwargs,
)
if do_pad:
+109 -29
View File
@@ -405,14 +405,19 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
self.flashinfer_mxfp4_moe_precision = (
get_exec().moe.flashinfer_mxfp4_moe_precision
)
self._use_sm90_humming = False
# When `flashinfer_mxfp4` is enabled, dispatch to one of three FlashInfer
# entry points depending on the GPU:
# - SM100 (Blackwell) -> trtllm_fp4_block_scale_moe (existing)
# - SM120 (Blackwell) -> cutlass_fused_moe(MXFP8 x MXFP4)
# - SM90 (Hopper) -> cutlass_fused_moe(use_w4_group_scaling=True)
# (FlashInfer PR #3084, post-0.6.10)
# - SM90 (Hopper) -> cutlass_fused_moe(use_w4_group_scaling=True),
# W4A16 by default (PR #3084) or opt-in
# Humming W4A8 (PR #3738/#4431)
self._fi_kernel: Optional[str] = None
if self.use_flashinfer:
# precision=fp8 is an SM90 knob (Humming W4A8). The Blackwell
# paths already run MXFP8 activations, so the flag is inert there
# rather than an error -- one config can move across hardware.
if get_platform().is_sm100:
self._fi_kernel = "trtllm_sm100"
elif get_platform().is_sm120:
@@ -425,6 +430,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
"from FlashInfer PR #3084 (>= 0.6.11). Upgrade flashinfer-python "
"or pick a different backend (e.g. marlin / triton_kernel)."
)
self._use_sm90_humming = self.flashinfer_mxfp4_moe_precision == "fp8"
self._fi_kernel = "cutlass_sm90"
else:
raise NotImplementedError(
@@ -497,6 +503,11 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
# CUTLASS post-load processor after the load completes.
self._padded_intermediate = round_up(intermediate_size_per_partition, 128)
self._padded_hidden = round_up(hidden_size, 128)
# `hidden_size` here may ALREADY be FusedMoE's rounded value (GPT-OSS
# 2880 -> 3072). Remember the checkpoint's K so the post-load
# processor can exclude the never-written tail from Humming's
# per-expert scale range.
self._unpadded_hidden = getattr(layer, "hidden_size_unpadded", hidden_size)
# create_weights below uses the *unpadded* sizes so the loader's
# naive-copy fast path is correct.
intermediate_size_per_partition_after_pad = intermediate_size_per_partition
@@ -1113,7 +1124,22 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
_interleaved = getattr(layer.moe_runner_config, "gate_up_interleaved", True)
def _stack_up_gate_w13(unpadded_w13, last_pad, last_un):
# FusedMoE may have rounded hidden up (GPT-OSS 2880 -> 3072) BEFORE
# create_weights, so K_un above is that rounded value, not the
# checkpoint's K. The loader never writes the trailing columns, so they
# keep the scale buffer's _UE8M0_ONE (2^0) fill -- far above a real
# per-expert max. Humming derives its residual from each expert's
# min/max E8M0 exponent, so letting those columns through would shift
# the residual and perturb the REAL weights. Copy only the checkpoint's
# columns and let the preserve_expert_range fill cover the rest; the
# packed weights there are zero, so the scale is numerically inert.
K_real = min(getattr(self, "_unpadded_hidden", None) or K_un, K_un)
# ceil: a partial trailing group is still real and must be kept.
w13_scale_real = -(-K_real // sf_block_size)
def _stack_up_gate_w13(
unpadded_w13, last_pad, last_un, preserve_expert_range=False, last_real=None
):
# unpadded_w13: [E, 2*N_un, last_un]
# Returns: [E, 2*N_pad, last_pad] in [up_padded; gate_padded] order.
if _interleaved:
@@ -1126,10 +1152,22 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
out = torch.zeros(
E, 2 * N_pad, last_pad, dtype=unpadded_w13.dtype, device=device
)
if preserve_expert_range:
# Humming derives one residual from each expert's min/max E8M0
# exponents. Fill padding with an existing expert value so
# padding cannot change that range.
out.copy_(unpadded_w13[:, :1, :1])
# When protecting the expert range, stop at the checkpoint's K so
# the stale tail is covered by the fill instead of copied through.
copy_un = (
min(last_real, last_un)
if (preserve_expert_range and last_real is not None)
else last_un
)
# First half: up (with row + col padding zeros).
out[:, :N_un, :last_un] = up_rows
out[:, :N_un, :copy_un] = up_rows[:, :, :copy_un]
# Second half: gate.
out[:, N_pad : N_pad + N_un, :last_un] = gate_rows
out[:, N_pad : N_pad + N_un, :copy_un] = gate_rows[:, :, :copy_un]
return out
w13_padded = _stack_up_gate_w13(
@@ -1139,6 +1177,8 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
layer.w13_weight_scale.data,
K_pad // sf_block_size,
K_un // sf_block_size,
preserve_expert_range=self._use_sm90_humming,
last_real=w13_scale_real,
)
# Bias: same de-interleave on dim=-1.
if _interleaved:
@@ -1151,9 +1191,19 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
w13_bias_padded[:, :N_un] = w13_bias_up
w13_bias_padded[:, N_pad : N_pad + N_un] = w13_bias_gate
def _pad_w2_3d(unpadded, last_pad, last_un):
def _pad_w2_3d(
unpadded, last_pad, last_un, preserve_expert_range=False, k_real=None
):
out = torch.zeros(E, K_pad, last_pad, dtype=unpadded.dtype, device=device)
out[:, :K_un, :last_un] = unpadded[:, :K_un, :]
if preserve_expert_range:
out.copy_(unpadded[:, :1, :1])
# Same stale-tail exclusion as _stack_up_gate_w13, on w2's K rows.
k_copy = (
min(k_real, K_un)
if (preserve_expert_range and k_real is not None)
else K_un
)
out[:, :k_copy, :last_un] = unpadded[:, :k_copy, :]
return out
# ---- w2 (no halving, just pad to [E, K_pad, N_pad/2]) ----------------
@@ -1164,6 +1214,8 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
layer.w2_weight_scale.data,
N_pad // sf_block_size,
N_un // sf_block_size,
preserve_expert_range=self._use_sm90_humming,
k_real=K_real,
)
w2_bias_padded = torch.zeros(E, K_pad, dtype=bias_dtype, device=device)
w2_bias_padded[:, :K_un] = layer.w2_weight_bias.data
@@ -1187,26 +1239,47 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
# ---- FlashInfer SM90 byte / scale interleave -----------------------
# The padded buffers above are contiguous by construction (allocated
# via torch.zeros + slice assignment), so we feed them straight in.
layer.w13_weight = Parameter(
interleave_moe_weights_for_sm90_mixed_gemm(w13_padded, "fp4"),
requires_grad=False,
)
layer.w2_weight = Parameter(
interleave_moe_weights_for_sm90_mixed_gemm(w2_padded, "fp4"),
requires_grad=False,
)
layer.w13_weight_scale = Parameter(
interleave_moe_scales_for_sm90_mixed_gemm(
if self._use_sm90_humming:
from flashinfer.fused_moe import (
preprocess_moe_weights_for_sm90_mixed_gemm_humming,
)
w13_il, w13_scale_il, w13_residual = (
preprocess_moe_weights_for_sm90_mixed_gemm_humming(
w13_padded, w13_scale_padded
)
)
w2_il, w2_scale_il, w2_residual = (
preprocess_moe_weights_for_sm90_mixed_gemm_humming(
w2_padded, w2_scale_padded
)
)
# Humming keeps the FP4->FP8 exponent-bias compensation in the
# epilogue. FlashInfer #4431 consumes these in local expert order.
layer.w13_humming_residual_scale = Parameter(
(w13_residual * 64.0).contiguous(), requires_grad=False
)
layer.w2_humming_residual_scale = Parameter(
(w2_residual * 64.0).contiguous(), requires_grad=False
)
layer.humming_fc2_act_scale = Parameter(
torch.ones((), dtype=torch.float32, device=device),
requires_grad=False,
)
else:
w13_il = interleave_moe_weights_for_sm90_mixed_gemm(w13_padded, "fp4")
w2_il = interleave_moe_weights_for_sm90_mixed_gemm(w2_padded, "fp4")
w13_scale_il = interleave_moe_scales_for_sm90_mixed_gemm(
w13_scale_padded, group_size=sf_block_size
),
requires_grad=False,
)
layer.w2_weight_scale = Parameter(
interleave_moe_scales_for_sm90_mixed_gemm(
)
w2_scale_il = interleave_moe_scales_for_sm90_mixed_gemm(
w2_scale_padded, group_size=sf_block_size
),
requires_grad=False,
)
)
layer.w13_weight = Parameter(w13_il, requires_grad=False)
layer.w2_weight = Parameter(w2_il, requires_grad=False)
layer.w13_weight_scale = Parameter(w13_scale_il, requires_grad=False)
layer.w2_weight_scale = Parameter(w2_scale_il, requires_grad=False)
layer.w13_weight_bias = Parameter(w13_bias_padded, requires_grad=False)
layer.w2_weight_bias = Parameter(w2_bias_padded, requires_grad=False)
@@ -1349,10 +1422,12 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
pass
def _apply_sm90_cutlass(self, layer, dispatch_output):
"""SM90 (Hopper) MXFP4 x BF16 MoE via FlashInfer's cutlass mixed-input
path (PR #3084). Routed through the unified ``MoeRunner`` -- this
helper only builds the quant_info; the actual kernel call lives in
:mod:`sglang.srt.layers.moe.moe_runner.flashinfer_cutlass`."""
"""SM90 MXFP4 x BF16/FP8 MoE via FlashInfer's mixed-input kernels.
Routed through the unified ``MoeRunner``; this helper only builds the
quant_info. The actual kernel call lives in
:mod:`sglang.srt.layers.moe.moe_runner.flashinfer_cutlass`.
"""
from sglang.srt.layers.moe.moe_runner.flashinfer_cutlass import (
FlashInferCutlassMxfp4MoeQuantInfo,
)
@@ -1362,6 +1437,11 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
w2_weight=layer.w2_weight,
w13_weight_scale=layer.w13_weight_scale,
w2_weight_scale=layer.w2_weight_scale,
w13_humming_residual_scale=getattr(
layer, "w13_humming_residual_scale", None
),
w2_humming_residual_scale=getattr(layer, "w2_humming_residual_scale", None),
humming_fc2_act_scale=getattr(layer, "humming_fc2_act_scale", None),
w13_bias=layer.w13_weight_bias,
w2_bias=layer.w2_weight_bias,
swiglu_alpha=layer.swiglu_alpha,
@@ -14,7 +14,7 @@ import torch
from torch.nn import Module
from torch.nn.parameter import Parameter
from sglang.srt.runtime_context import get_platform
from sglang.srt.runtime_context import get_exec, get_platform
from sglang.srt.utils import is_flashinfer_available, log_info_on_rank0
# Suppress TRT-LLM CUTLASS trace logs without overriding user configuration.
@@ -30,7 +30,7 @@ _GROUP_SIZE = 32
class Mxfp4FlashinferCutlassMoEMethod:
"""FlashInfer MXFP4 MoE: W4A16 on SM90 and W4A8 on SM120."""
"""FlashInfer MXFP4 MoE: W4A16/W4A8 on SM90 and W4A8 on SM120."""
fuse_routed_scaling_factor_in_topk = True
@@ -38,6 +38,11 @@ class Mxfp4FlashinferCutlassMoEMethod:
if not is_flashinfer_available():
raise RuntimeError("Mxfp4FlashinferCutlassMoEMethod requires FlashInfer.")
self._use_mxfp8_act_scaling = get_platform().is_sm120
precision = get_exec().moe.flashinfer_mxfp4_moe_precision
# precision=fp8 is an SM90 knob (Humming W4A8); on SM120 the MXFP8
# activation path already computes in FP8, so the flag is simply inert
# there rather than an error -- one config can move across hardware.
self._use_sm90_humming = not self._use_mxfp8_act_scaling and precision == "fp8"
self._fp8 = fp8_method
self.prefix = prefix
self._swiglu_limit_tensor: torch.Tensor | None = None
@@ -124,9 +129,12 @@ class Mxfp4FlashinferCutlassMoEMethod:
return
arch = "SM120" if self._use_mxfp8_act_scaling else "SM90"
precision = (
"W4A8" if self._use_sm90_humming or self._use_mxfp8_act_scaling else "W4A16"
)
log_info_on_rank0(
logger,
f"Preparing DSv4 MXFP4 experts for FlashInfer {arch} CUTLASS "
f"Preparing DSv4 MXFP4 experts for FlashInfer {arch} CUTLASS {precision} "
f"(layer: {self.prefix})...",
)
@@ -152,23 +160,51 @@ class Mxfp4FlashinferCutlassMoEMethod:
for scale_u8 in (w13_scale_u8, w2_scale_u8):
scale_u8.copy_(block_scale_interleave(scale_u8).reshape_as(scale_u8))
else:
from flashinfer.fused_moe import (
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
if self._use_sm90_humming:
from flashinfer.fused_moe import (
preprocess_moe_weights_for_sm90_mixed_gemm_humming,
)
w13_il = interleave_moe_weights_for_sm90_mixed_gemm(
layer.w13_weight.data.view(torch.uint8).contiguous(), "fp4"
)
w2_il = interleave_moe_weights_for_sm90_mixed_gemm(
layer.w2_weight.data.view(torch.uint8).contiguous(), "fp4"
)
w13_s_il = interleave_moe_scales_for_sm90_mixed_gemm(
w13_scale_u8, group_size=_GROUP_SIZE
)
w2_s_il = interleave_moe_scales_for_sm90_mixed_gemm(
w2_scale_u8, group_size=_GROUP_SIZE
)
w13_il, w13_s_il, w13_residual = (
preprocess_moe_weights_for_sm90_mixed_gemm_humming(
layer.w13_weight.data.view(torch.uint8).contiguous(),
w13_scale_u8,
)
)
w2_il, w2_s_il, w2_residual = (
preprocess_moe_weights_for_sm90_mixed_gemm_humming(
layer.w2_weight.data.view(torch.uint8).contiguous(),
w2_scale_u8,
)
)
layer.w13_humming_residual_scale = Parameter(
(w13_residual * 64.0).contiguous(), requires_grad=False
)
layer.w2_humming_residual_scale = Parameter(
(w2_residual * 64.0).contiguous(), requires_grad=False
)
layer.humming_fc2_act_scale = Parameter(
torch.ones((), dtype=torch.float32, device=w13_scale_u8.device),
requires_grad=False,
)
else:
from flashinfer.fused_moe import (
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
w13_il = interleave_moe_weights_for_sm90_mixed_gemm(
layer.w13_weight.data.view(torch.uint8).contiguous(), "fp4"
)
w2_il = interleave_moe_weights_for_sm90_mixed_gemm(
layer.w2_weight.data.view(torch.uint8).contiguous(), "fp4"
)
w13_s_il = interleave_moe_scales_for_sm90_mixed_gemm(
w13_scale_u8, group_size=_GROUP_SIZE
)
w2_s_il = interleave_moe_scales_for_sm90_mixed_gemm(
w2_scale_u8, group_size=_GROUP_SIZE
)
layer.w13_weight = Parameter(w13_il, requires_grad=False)
layer.w2_weight = Parameter(w2_il, requires_grad=False)
layer.w13_weight_scale_inv = Parameter(w13_s_il, requires_grad=False)
@@ -177,7 +213,11 @@ class Mxfp4FlashinferCutlassMoEMethod:
layer._dsv4_mxfp4_backend = (
"flashinfer_cutlass_sm120"
if self._use_mxfp8_act_scaling
else "flashinfer_cutlass_sm90"
else (
"flashinfer_cutlass_sm90_fp8"
if self._use_sm90_humming
else "flashinfer_cutlass_sm90"
)
)
# SM90 creates full-size interleaved copies; release old layouts per layer.
if not self._use_mxfp8_act_scaling:
@@ -198,6 +238,11 @@ class Mxfp4FlashinferCutlassMoEMethod:
w13_weight_scale=layer.w13_weight_scale_inv,
w2_weight_scale=layer.w2_weight_scale_inv,
mxfp4_weight_global_scale=self._mxfp4_weight_global_scale_tensor,
w13_humming_residual_scale=getattr(
layer, "w13_humming_residual_scale", None
),
w2_humming_residual_scale=getattr(layer, "w2_humming_residual_scale", None),
humming_fc2_act_scale=getattr(layer, "humming_fc2_act_scale", None),
w13_bias=None,
w2_bias=None,
swiglu_alpha=None,
@@ -54,6 +54,9 @@ class Mxfp4FlashinferTrtllmMoEMethod:
def __init__(self, fp8_method, prefix: str):
self._fp8 = fp8_method
self.prefix = prefix
# precision=fp8 is an SM90 knob (Humming W4A8); this SM100 trtllm path
# already runs MXFP8 activations, so the flag is inert here rather than
# an error -- one config can move across hardware.
self.flashinfer_mxfp4_moe_precision = (
get_exec().moe.flashinfer_mxfp4_moe_precision
)
+4 -2
View File
@@ -2422,8 +2422,10 @@ class ServerArgs:
NS("exec.moe"),
] = "auto"
flashinfer_mxfp4_moe_precision: A[
Literal["default", "bf16"],
"Choose the computation precision of flashinfer mxfp4 moe",
Literal["default", "bf16", "fp8"],
"Choose the computation precision of flashinfer mxfp4 moe. "
"On SM90, `fp8` selects the Humming-style MXFP4-weight x FP8-activation "
"path introduced by FlashInfer #3738 and requires FlashInfer >= 0.6.18.",
NS("exec.moe"),
] = "default"
deepep_mode: A[