Add FlashInfer SM90 cutlass MXFP4 MoE backend (W4A16) for GPT-OSS + DeepSeek-V4 (#24816)

Co-authored-by: luoyuan.luo <luoyuan.luo@antgroup.com>
This commit is contained in:
Yuan Luo
2026-05-13 14:53:18 -07:00
committed by GitHub
co-authored by luoyuan.luo
parent d6b28b4a69
commit 28758d37dd
8 changed files with 1542 additions and 3 deletions
+12
View File
@@ -243,6 +243,18 @@ class BypassedTopKOutput(NamedTuple):
def format(self) -> TopKOutputFormat:
return TopKOutputFormat.BYPASSED
def to_standard(self, layer_id: Optional[int] = None) -> "StandardTopKOutput":
"""Materialize routing tensors. Used by MoE kernels that need explicit
topk_ids / topk_weights rather than doing routing internally."""
return select_experts(
hidden_states=self.hidden_states,
router_logits=self.router_logits,
topk_config=self.topk_config,
layer_id=layer_id,
num_token_non_padded=self.num_token_non_padded,
expert_location_dispatch_info=self.expert_location_dispatch_info,
)
# -------------------------------- TopK ---------------------------------------
@@ -262,6 +262,15 @@ class Fp8Config(QuantizationConfig):
return Mxfp4MarlinMoEMethod(fp8_method, prefix=prefix)
if self.is_fp4_experts and get_moe_runner_backend().is_flashinfer_mxfp4():
# SM100 (Blackwell) -> trtllm-gen path.
# SM90 (Hopper) -> cutlass mixed-input path (FlashInfer #3084).
if is_sm90_supported() and not is_sm100_supported():
from sglang.srt.layers.quantization.mxfp4_flashinfer_cutlass_moe import (
Mxfp4FlashinferCutlassMoEMethod,
)
return Mxfp4FlashinferCutlassMoEMethod(fp8_method, prefix=prefix)
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
Mxfp4FlashinferTrtllmMoEMethod,
)
+269 -1
View File
@@ -16,12 +16,18 @@
from __future__ import annotations
import os
from dataclasses import replace
from typing import TYPE_CHECKING, List, Optional
import torch
from torch.nn.parameter import Parameter
# Silence the TRT-LLM cutlass autotune trace embedded inside FlashInfer's
# cutlass_fused_moe. Its C++ logger reads TLLM_LOG_LEVEL on first kernel launch;
# setdefault preserves any explicit user override.
os.environ.setdefault("TLLM_LOG_LEVEL", "INFO")
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
@@ -62,7 +68,27 @@ if is_flashinfer_available():
nvfp4_block_scale_interleave,
trtllm_fp4_block_scale_moe,
)
from flashinfer.fused_moe.core import get_w2_permute_indices_with_cache
from flashinfer.fused_moe import cutlass_fused_moe as flashinfer_cutlass_fused_moe
from flashinfer.fused_moe.core import (
ActivationType,
get_w2_permute_indices_with_cache,
)
# SM90 mixed-input helpers landed in FlashInfer #3084 (post-0.6.10). Older
# versions don't ship them; gate at import so unrelated code paths still load.
try:
from flashinfer.fused_moe import (
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
_FI_HAS_SM90_CUTLASS_MXFP4 = True
except ImportError:
interleave_moe_scales_for_sm90_mixed_gemm = None
interleave_moe_weights_for_sm90_mixed_gemm = None
_FI_HAS_SM90_CUTLASS_MXFP4 = False
else:
_FI_HAS_SM90_CUTLASS_MXFP4 = False
_flashinfer_mxfp4_permute_indices_cache: dict[torch.Size, torch.Tensor] = {}
_flashinfer_mxfp4_permute_indices_device_cache: dict[
@@ -318,6 +344,28 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
self.flashinfer_mxfp4_moe_precision = (
get_global_server_args().flashinfer_mxfp4_moe_precision
)
# When `flashinfer_mxfp4` is enabled, dispatch to one of two FlashInfer
# entry points depending on the GPU:
# - SM100 (Blackwell) -> trtllm_fp4_block_scale_moe (existing)
# - SM90 (Hopper) -> cutlass_fused_moe(use_w4_group_scaling=True)
# (FlashInfer PR #3084, post-0.6.10)
self._fi_kernel: Optional[str] = None
if self.use_flashinfer:
if is_sm100_supported():
self._fi_kernel = "trtllm_sm100"
elif is_sm90_supported():
if not _FI_HAS_SM90_CUTLASS_MXFP4:
raise RuntimeError(
"moe_runner_backend=flashinfer_mxfp4 on SM90 requires the "
"interleave_moe_{weights,scales}_for_sm90_mixed_gemm helpers "
"from FlashInfer PR #3084 (>= 0.6.11). Upgrade flashinfer-python "
"or pick a different backend (e.g. marlin / triton_kernel)."
)
self._fi_kernel = "cutlass_sm90"
else:
raise NotImplementedError(
"moe_runner_backend=flashinfer_mxfp4 requires SM90 or SM100."
)
def create_weights(
self,
@@ -349,6 +397,26 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
intermediate_size_per_partition_after_pad = round_up(
intermediate_size_per_partition, triton_kernels_padding_alignment
)
elif self._fi_kernel == "cutlass_sm90":
# cutlass mixed-input GEMM contraction dim K must be % 128 == 0
# (interleave factor for MXFP4 group_size=32 is 4). The kernel
# also expects ``fc1_expert_weights`` in halved ``[up; gate]``
# layout, which means the padding boundary must fall on the
# gate / up split.
#
# The mxfp4 weight loader (FusedMoE.weight_loader fast path) does
# a NAIVE copy of HF's ``[2*intermediate_size, hidden_packed]``
# tensor into the buffer's ``[:dim1, :dim2]`` slice. Padding the
# buffer here would push the gate/up boundary, so HF's "up"
# rows would land in the buffer's "gate" half and vice versa.
# Marlin sidesteps this by not padding; we do the same and
# rebuild a properly-padded buffer in
# ``_process_weights_for_sm90_cutlass`` after the load completes.
self._padded_intermediate = round_up(intermediate_size_per_partition, 128)
self._padded_hidden = round_up(hidden_size, 128)
# 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
elif _use_aiter:
intermediate_size_per_partition_after_pad = round_up(
@@ -438,6 +506,9 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
set_weight_attrs(w2_weight_bias, extra_weight_attrs)
def process_weights_after_loading(self, layer):
if self._fi_kernel == "cutlass_sm90":
self._process_weights_for_sm90_cutlass(layer)
return
if self.use_flashinfer:
# TODO: these values are hardcoded for now, we need to get them from the model
layer.gemm1_alpha = Parameter(
@@ -736,6 +807,133 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
layer.w2_weight = Parameter(w2_weight.data, requires_grad=False)
torch.cuda.empty_cache()
def _process_weights_for_sm90_cutlass(self, layer):
"""De-interleave + pad + halving-swap + byte-interleave MXFP4 weights
for FlashInfer's SM90 ``cutlass_fused_moe(use_w4_group_scaling=True)``
path (PR #3084).
The cutlass kernel needs (a) K (contraction dim) % 128 == 0, and (b)
``fc1_expert_weights`` in halved ``[up; gate]`` order -- the
``compute_with_experts`` reference in FlashInfer's
``test_trtllm_cutlass_fused_moe.py`` splits
``w3, w1 = chunk(W, 2, dim=0)`` and uses w3 as up, w1 as gate.
GPT-OSS's HF layout is *interleaved* ``[g_0, u_0, g_1, u_1, ..., g_{N-1}, u_{N-1}]``
(each pair occupies two adjacent rows). The mxfp4 weight loader does
a naive copy, so our unpadded buffer is interleaved post-load. We
de-interleave (even rows -> gate, odd rows -> up), pad each half from
N_un to N_pad, concatenate as halved ``[up; gate]``, and then run
FlashInfer's byte / scale interleave helpers.
"""
sf_block_size = 32 # MXFP4 group size
# Sizes from the unpadded loaded buffers.
N_un = layer.w13_weight.shape[1] // 2 # intermediate (unpadded)
K_un = (
layer.w13_weight.shape[2] * 2
) # hidden (unpadded, *2 because packed 4-bit)
N_pad = self._padded_intermediate
K_pad = self._padded_hidden
# Use the local expert count (matches the existing buffer allocation in
# create_weights) so the SM90 cutlass path remains correct under
# Expert Parallelism. `self.num_experts` is the *global* count.
E = layer.num_local_experts
device = layer.w13_weight.device
bias_dtype = layer.w13_weight_bias.dtype
# ---- De-interleave + pad w13 weight/scale/bias to halved [up; gate]
# Even rows of HF = gate, odd rows = up. After splitting we pad each
# half along its row dim (N) from N_un to N_pad with zeros, and along
# its last dim (K) from K_un (or K_un / sf_block_size) to K_pad.
def _stack_up_gate_w13(unpadded_w13, last_pad, last_un):
# unpadded_w13: [E, 2*N_un, last_un]
# Returns: [E, 2*N_pad, last_pad] in [up_padded; gate_padded] order.
gate_rows = unpadded_w13[:, 0::2, :] # [E, N_un, last_un]
up_rows = unpadded_w13[:, 1::2, :] # [E, N_un, last_un]
out = torch.zeros(
E, 2 * N_pad, last_pad, dtype=unpadded_w13.dtype, device=device
)
# First half: up (with row + col padding zeros).
out[:, :N_un, :last_un] = up_rows
# Second half: gate.
out[:, N_pad : N_pad + N_un, :last_un] = gate_rows
return out
w13_padded = _stack_up_gate_w13(
layer.w13_weight.data.view(torch.uint8), K_pad // 2, K_un // 2
)
w13_scale_padded = _stack_up_gate_w13(
layer.w13_weight_scale.data,
K_pad // sf_block_size,
K_un // sf_block_size,
)
# Bias: same de-interleave on dim=-1.
w13_bias_gate = layer.w13_weight_bias.data[:, 0::2] # [E, N_un]
w13_bias_up = layer.w13_weight_bias.data[:, 1::2] # [E, N_un]
w13_bias_padded = torch.zeros(E, 2 * N_pad, dtype=bias_dtype, device=device)
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):
out = torch.zeros(E, K_pad, last_pad, dtype=unpadded.dtype, device=device)
out[:, :K_un, :last_un] = unpadded[:, :K_un, :]
return out
# ---- w2 (no halving, just pad to [E, K_pad, N_pad/2]) ----------------
w2_padded = _pad_w2_3d(
layer.w2_weight.data.view(torch.uint8), N_pad // 2, N_un // 2
)
w2_scale_padded = _pad_w2_3d(
layer.w2_weight_scale.data,
N_pad // sf_block_size,
N_un // sf_block_size,
)
w2_bias_padded = torch.zeros(E, K_pad, dtype=bias_dtype, device=device)
w2_bias_padded[:, :K_un] = layer.w2_weight_bias.data
# ---- Per-expert SwiGLU scalars (GPT-OSS defaults) ------------------
layer.swiglu_alpha = Parameter(
torch.full((E,), 1.702, dtype=torch.float32, device=device),
requires_grad=False,
)
layer.swiglu_beta = Parameter(
torch.full((E,), 1.0, dtype=torch.float32, device=device),
requires_grad=False,
)
layer.swiglu_limit = Parameter(
torch.full((E,), 7.0, dtype=torch.float32, device=device),
requires_grad=False,
)
# ---- 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(
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_padded, group_size=sf_block_size
),
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)
torch.cuda.empty_cache()
def create_moe_runner(
self, layer: torch.nn.Module, moe_runner_config: MoeRunnerConfig
):
@@ -761,6 +959,74 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
# TODO(cwan): refactor other backends
pass
def _apply_sm90_cutlass(self, layer, x, topk_output):
"""SM90 (Hopper) MXFP4 x BF16 MoE via FlashInfer's cutlass mixed-input
path (PR #3084). The fused kernel does GEMM1 + SwiGLU + GEMM2 in one
call; weights/scales were pre-interleaved at load time."""
from sglang.srt.layers.moe.token_dispatcher import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
# Under ``--moe-runner-backend flashinfer_mxfp4`` the SGLang TopK layer
# emits BypassedTopKOutput by default (the SM100 trtllm-gen kernel does
# routing internally). The cutlass kernel needs explicit topk_ids /
# topk_weights, so materialize them here when bypassed.
if TopKOutputChecker.format_is_bypassed(topk_output):
topk_output = topk_output.to_standard()
topk_weights, topk_ids = topk_output.topk_weights, topk_output.topk_ids
# Pad input hidden dim to the (already-padded) loaded weight width.
origin_hidden = x.shape[-1]
padded_hidden = self._padded_hidden
if padded_hidden != origin_hidden:
x = torch.nn.functional.pad(
x,
(0, padded_hidden - origin_hidden),
mode="constant",
value=0.0,
)
output_dtype = torch.bfloat16
# Output is allocated at padded width (kernel writes padded_hidden
# columns), then trimmed back to origin_hidden before returning.
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
out_padded = torch.empty(
x.shape[0], padded_hidden, dtype=output_dtype, device=x.device
)
flashinfer_cutlass_fused_moe(
input=x,
token_selected_experts=topk_ids.to(torch.int),
token_final_scales=topk_weights,
fc1_expert_weights=layer.w13_weight, # uint8 [E, 2*N, K/2] interleaved
fc2_expert_weights=layer.w2_weight, # uint8 [E, K, N/2] interleaved
output_dtype=output_dtype,
quant_scales=[
layer.w13_weight_scale.view(torch.int32),
layer.w2_weight_scale.view(torch.int32),
],
fc1_expert_biases=layer.w13_weight_bias, # bf16 [E, 2*N]
fc2_expert_biases=layer.w2_weight_bias, # bf16 [E, K]
swiglu_alpha=layer.swiglu_alpha,
swiglu_beta=layer.swiglu_beta,
swiglu_limit=layer.swiglu_limit,
tp_size=layer.moe_tp_size,
tp_rank=layer.moe_tp_rank,
ep_size=layer.moe_ep_size,
ep_rank=layer.moe_ep_rank,
use_w4_group_scaling=True,
activation_type=ActivationType.Swiglu,
tune_max_num_tokens=next_power_of_2(x.shape[0]),
output=out_padded,
)
if padded_hidden != origin_hidden:
out = out_padded[:, :origin_hidden].contiguous()
else:
out = out_padded
return StandardCombineInput(hidden_states=out)
def apply(
self,
layer: torch.nn.Module,
@@ -773,6 +1039,8 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
x = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
if self._fi_kernel == "cutlass_sm90":
return self._apply_sm90_cutlass(layer, x, topk_output)
if self.use_flashinfer:
# When bf16 mode is enabled, we don't need to quantize the input,
# TRT-LLM automatically handles quantization in the kernel implementation and pipelines it with GEMM operations,
@@ -0,0 +1,263 @@
"""DeepSeek-V4 MXFP4 expert backend backed by FlashInfer's SM90 cutlass
mixed-input MoE GEMM (FlashInfer PR #3084).
Sibling of :class:`Mxfp4MarlinMoEMethod` and :class:`Mxfp4FlashinferTrtllmMoEMethod`.
Wired into :func:`Fp8MoEConfig.get_quant_method` when
``is_fp4_experts=True`` and ``--moe-runner-backend flashinfer_mxfp4`` is
selected on a Hopper (SM90) device. SM100 still routes to
:class:`Mxfp4FlashinferTrtllmMoEMethod` (trtllm-gen).
Performance trade-off vs Marlin (kernel-level on H100, GPT-OSS-like body):
- decode (M <= 64) : Marlin +12-15 %
- tie (M ~= 256)
- prefill (M >= 1024) : FlashInfer +24-36 %
PD-disaggregated prefill workers are the natural fit; decode workers should
keep the Marlin default.
"""
from __future__ import annotations
import logging
import os
from typing import TYPE_CHECKING
import torch
from torch.nn import Module
from torch.nn.parameter import Parameter
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.layers.dp_attention import is_allocation_symmetric
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.utils import is_flashinfer_available, log_info_on_rank0
from sglang.srt.utils.common import next_power_of_2
# Silence the TRT-LLM cutlass autotune trace embedded inside FlashInfer's
# cutlass_fused_moe. Its C++ logger reads TLLM_LOG_LEVEL on first kernel launch;
# setdefault preserves any explicit user override.
os.environ.setdefault("TLLM_LOG_LEVEL", "INFO")
if is_flashinfer_available():
from flashinfer.fused_moe import cutlass_fused_moe as flashinfer_cutlass_fused_moe
from flashinfer.fused_moe.core import ActivationType
try:
from flashinfer.fused_moe import (
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
_FI_HAS_SM90_CUTLASS_MXFP4 = True
except ImportError:
interleave_moe_scales_for_sm90_mixed_gemm = None
interleave_moe_weights_for_sm90_mixed_gemm = None
_FI_HAS_SM90_CUTLASS_MXFP4 = False
else:
_FI_HAS_SM90_CUTLASS_MXFP4 = False
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.layers.moe.token_dispatcher import CombineInput, DispatchOutput
# MXFP4 group/block size (E8M0 scale per 32 fp4 weights).
_GROUP_SIZE = 32
class Mxfp4FlashinferCutlassMoEMethod:
"""DeepSeek-V4 W4A16 MXFP4 MoE via FlashInfer's SM90 mixed-input cutlass
grouped GEMM. The fused kernel does GEMM1 + clamped SwiGLU + GEMM2 in one
call after a one-shot weight/scale interleave at load time."""
def __init__(self, fp8_method, prefix: str):
if not _FI_HAS_SM90_CUTLASS_MXFP4:
raise RuntimeError(
"Mxfp4FlashinferCutlassMoEMethod requires FlashInfer >= 0.6.11 "
"(PR #3084 SM90 mixed-input helpers). Older builds lack "
"interleave_moe_{weights,scales}_for_sm90_mixed_gemm; "
"either upgrade flashinfer-python or fall back to "
"--moe-runner-backend marlin."
)
self._fp8 = fp8_method
self.prefix = prefix
self._swiglu_alpha_tensor: torch.Tensor | None = None
self._swiglu_beta_tensor: torch.Tensor | None = None
self._swiglu_limit_tensor: torch.Tensor | None = None
# --- Lifecycle ---------------------------------------------------------
def create_weights(
self,
layer: Module,
num_experts: int,
hidden_size: int,
intermediate_size_per_partition: int,
params_dtype,
**extra_weight_attrs,
):
# SM90 mixed-input GEMM: contraction dim K must be a multiple of 128
# (interleave factor = 128 / group_size = 4). For DSv4 (hidden=7168,
# inter=2048) both are already multiples of 128; we assert rather than
# silently pad here, since padding the FP8-base buffers in-place would
# require deeper changes.
if hidden_size % 128 != 0 or intermediate_size_per_partition % 128 != 0:
raise ValueError(
"Mxfp4FlashinferCutlassMoEMethod requires hidden_size and "
"intermediate_size_per_partition to be multiples of 128 "
f"(got hidden={hidden_size}, "
f"intermediate={intermediate_size_per_partition})."
)
# Raw weight shapes match what the fp8 base method allocates for fp4
# experts (uint8 4-bit packed weights, fp32 E8M0 scales). Delegate.
self._fp8.create_weights(
layer,
num_experts,
hidden_size,
intermediate_size_per_partition,
params_dtype,
**extra_weight_attrs,
)
def create_moe_runner(self, layer: Module, moe_runner_config) -> None:
self.moe_runner_config = moe_runner_config
# DSv4 uses standard SwiGLU plus a config-driven activation clamp.
# We pass all three (alpha, beta, limit) as explicit per-expert tensors
# rather than mixing tensors with None: the cutlass SwiGLU kernel
# branches on whether each is None, and partial-None inputs land in
# less-tested code paths. ``alpha=1.0``, ``beta=0.0`` reproduce plain
# ``silu(gate) * up``; ``limit`` enforces the activation clamp the
# checkpoint was trained with.
swiglu_limit = getattr(moe_runner_config, "swiglu_limit", None)
if swiglu_limit is not None:
E = layer.num_local_experts
device = layer.w13_weight.device
self._swiglu_alpha_tensor = torch.ones(
E, dtype=torch.float32, device=device
)
self._swiglu_beta_tensor = torch.zeros(
E, dtype=torch.float32, device=device
)
self._swiglu_limit_tensor = torch.full(
(E,), float(swiglu_limit), dtype=torch.float32, device=device
)
else:
self._swiglu_alpha_tensor = None
self._swiglu_beta_tensor = None
self._swiglu_limit_tensor = None
def process_weights_after_loading(self, layer: Module) -> None:
from sglang.srt.layers.quantization.utils import reorder_w1w3_to_w3w1
# Run the fp8 base hook first (ROCm normalization, mxfp8 requant, ...).
self._fp8.process_weights_after_loading(layer)
if getattr(layer, "_mega_moe_weights_built", False):
return
# cutlass_fused_moe expects fc1 in [w3; w1] = [up; gate] order, just
# like the trtllm-gen path. The HF / FP8 loader emits [w1; w3].
w13, w13_s = reorder_w1w3_to_w3w1(
layer.w13_weight.data, layer.w13_weight_scale_inv.data
)
layer.w13_weight = Parameter(w13, requires_grad=False)
layer.w13_weight_scale_inv = Parameter(w13_s, requires_grad=False)
log_info_on_rank0(
logger,
f"Preparing DSv4 MXFP4 experts for FlashInfer SM90 cutlass "
f"(layer: {self.prefix})...",
)
# FP8 base stores scales as fp32 numerical values (= 2**e). The
# FlashInfer SM90 helper reads raw E8M0 bytes (uint8 with the
# exponent + 127 bias). Cast through float8_e8m0fnu to extract the
# raw byte without losing the exponent.
w13_scale_u8 = (
layer.w13_weight_scale_inv.data.to(torch.float8_e8m0fnu)
.view(torch.uint8)
.contiguous()
)
w2_scale_u8 = (
layer.w2_weight_scale_inv.data.to(torch.float8_e8m0fnu)
.view(torch.uint8)
.contiguous()
)
# C++ byte interleave on packed 4-bit weights.
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"
)
# Pure-PyTorch reshape+permute on E8M0 block scales.
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)
layer.w2_weight_scale_inv = Parameter(w2_s_il, requires_grad=False)
layer._dsv4_mxfp4_backend = "flashinfer_cutlass_sm90"
torch.cuda.empty_cache()
# --- Forward -----------------------------------------------------------
def apply(
self,
layer: Module,
dispatch_output: "DispatchOutput",
) -> "CombineInput":
topk_output = dispatch_output.topk_output
if not TopKOutputChecker.format_is_standard(topk_output):
raise ValueError(f"Unsupported topk output format: {topk_output.format}")
x = dispatch_output.hidden_states
topk_weights = topk_output.topk_weights
topk_ids = topk_output.topk_ids
output_dtype = torch.bfloat16
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
out = torch.empty(
x.shape[0], x.shape[-1], dtype=output_dtype, device=x.device
)
flashinfer_cutlass_fused_moe(
input=x,
token_selected_experts=topk_ids.to(torch.int),
token_final_scales=topk_weights,
fc1_expert_weights=layer.w13_weight,
fc2_expert_weights=layer.w2_weight,
output_dtype=output_dtype,
quant_scales=[
layer.w13_weight_scale_inv.view(torch.int32),
layer.w2_weight_scale_inv.view(torch.int32),
],
fc1_expert_biases=None, # DSv4 has no MoE expert bias.
fc2_expert_biases=None,
swiglu_alpha=self._swiglu_alpha_tensor, # ones: standard SiLU gate
swiglu_beta=self._swiglu_beta_tensor, # zeros: standard up
swiglu_limit=self._swiglu_limit_tensor,
tp_size=layer.moe_tp_size,
tp_rank=layer.moe_tp_rank,
ep_size=layer.moe_ep_size,
ep_rank=layer.moe_ep_rank,
use_w4_group_scaling=True,
activation_type=ActivationType.Swiglu,
tune_max_num_tokens=next_power_of_2(x.shape[0]),
output=out,
)
return StandardCombineInput(hidden_states=out)
@@ -445,12 +445,20 @@ def maybe_fuse_routed_scale_and_shared_add(
# alpha=scale)`. With no shared output, the missing scale is applied
# in-place. Otherwise `routed` is already scale-final and we just add
# `shared` (or pass through if there is none).
from sglang.srt.layers.quantization.mxfp4_flashinfer_cutlass_moe import (
Mxfp4FlashinferCutlassMoEMethod,
)
from sglang.srt.layers.quantization.mxfp4_marlin_moe import (
Mxfp4MarlinMoEMethod,
)
fused = isinstance(
experts.quant_method, (Mxfp4FlashinferTrtllmMoEMethod, Mxfp4MarlinMoEMethod)
experts.quant_method,
(
Mxfp4FlashinferTrtllmMoEMethod,
Mxfp4FlashinferCutlassMoEMethod,
Mxfp4MarlinMoEMethod,
),
)
if fused:
if shared is not None:
@@ -0,0 +1,366 @@
"""Benchmark MXFP4 MoE kernels on H100/H200: SGLang Marlin vs FlashInfer cutlass.
Compares per-call latency of:
* Marlin path : ``fused_marlin_moe(...)`` after Marlin weight repack
* FlashInfer : ``cutlass_fused_moe(use_w4_group_scaling=True, ...)``
(PR #3084's SM90 mixed-input path)
Both run on the same random MXFP4 weights/scales (semantics differ slightly --
Marlin uses a scalar swiglu clamp + no bias, FlashInfer fuses per-expert
SwiGLU with bias -- so the timing comparison reports kernel cost for
*equivalent compute volume*, not bit-exact numerics).
Run on H100/H200:
cd /sgl-workspace/sglang_dev3 && \\
PYTHONPATH=python:/sgl-workspace/flashinfer FLASHINFER_DISABLE_VERSION_CHECK=1 \\
python python/sglang/test/bench_mxfp4_sm90_kernels.py
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from typing import Callable, List, Tuple
import torch
from flashinfer.autotuner import autotune
# ---- FlashInfer ----
from flashinfer.fused_moe import (
cutlass_fused_moe,
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
from flashinfer.fused_moe.core import ActivationType
# ---- SGLang Marlin ----
from sglang.jit_kernel.gptq_marlin_repack import gptq_marlin_repack
from sglang.srt.layers.moe.fused_moe_triton.fused_marlin_moe import fused_marlin_moe
from sglang.srt.layers.quantization.marlin_utils import (
marlin_make_workspace,
marlin_permute_scales,
)
from sglang.srt.layers.quantization.marlin_utils_fp4 import mxfp4_marlin_process_scales
GROUP_SIZE = 32
@dataclass
class Shape:
tokens: int
hidden: int
inter: int
num_experts: int
top_k: int
def label(self) -> str:
return (
f"m={self.tokens:>4} h={self.hidden} i={self.inter} "
f"E={self.num_experts} k={self.top_k}"
)
# Sweep tokens at a fixed GPT-OSS-like body (hidden=4096, inter=2048, E=256,
# topk=6 -- matches PR #3084's headline shape so the small-batch numbers stay
# directly comparable). Token range covers decode (4-256) and prefill chunks
# (1024-8192).
_BODY = dict(hidden=4096, inter=2048, num_experts=256, top_k=6)
DEFAULT_SHAPES: List[Shape] = [
Shape(tokens=4, **_BODY),
Shape(tokens=16, **_BODY),
Shape(tokens=64, **_BODY),
Shape(tokens=256, **_BODY),
Shape(tokens=1024, **_BODY),
Shape(tokens=2048, **_BODY),
Shape(tokens=4096, **_BODY),
Shape(tokens=8192, **_BODY),
]
def _make_random_mxfp4(shape: Shape, seed: int = 0):
g = torch.Generator(device="cuda").manual_seed(seed)
e = shape.num_experts
n = shape.inter
k = shape.hidden
w13 = torch.randint(
0, 256, (e, 2 * n, k // 2), dtype=torch.uint8, device="cuda", generator=g
)
w2 = torch.randint(
0, 256, (e, k, n // 2), dtype=torch.uint8, device="cuda", generator=g
)
# Narrow E8M0 band so dequant magnitudes stay sane.
w13_s = torch.randint(
125,
130,
(e, 2 * n, k // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w2_s = torch.randint(
125,
130,
(e, k, n // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w13_b = (
torch.randn(e, 2 * n, dtype=torch.float32, device="cuda", generator=g).to(
torch.bfloat16
)
* 0.01
)
w2_b = (
torch.randn(e, k, dtype=torch.float32, device="cuda", generator=g).to(
torch.bfloat16
)
* 0.01
)
return w13, w2, w13_s, w2_s, w13_b, w2_b
def _make_topk(shape: Shape, seed: int = 1):
g = torch.Generator(device="cuda").manual_seed(seed)
logits = torch.randn(
shape.tokens,
shape.num_experts,
dtype=torch.float32,
device="cuda",
generator=g,
)
weights, ids = torch.topk(torch.softmax(logits, dim=-1), shape.top_k, dim=-1)
weights = weights / weights.sum(dim=-1, keepdim=True)
return logits, weights.to(torch.float32), ids.to(torch.int32)
# ---------------------------------------------------------------------------
# FlashInfer cutlass path
# ---------------------------------------------------------------------------
def build_flashinfer_inputs(shape: Shape, w13, w2, w13_s, w2_s, w13_b, w2_b):
w13_il = interleave_moe_weights_for_sm90_mixed_gemm(w13, "fp4")
w2_il = interleave_moe_weights_for_sm90_mixed_gemm(w2, "fp4")
w13_s_il = interleave_moe_scales_for_sm90_mixed_gemm(w13_s, group_size=GROUP_SIZE)
w2_s_il = interleave_moe_scales_for_sm90_mixed_gemm(w2_s, group_size=GROUP_SIZE)
e = shape.num_experts
swiglu_alpha = torch.full((e,), 1.702, dtype=torch.float32, device="cuda")
swiglu_beta = torch.full((e,), 1.0, dtype=torch.float32, device="cuda")
swiglu_limit = torch.full((e,), 7.0, dtype=torch.float32, device="cuda")
return {
"w13": w13_il,
"w2": w2_il,
"quant_scales": [w13_s_il.view(torch.int32), w2_s_il.view(torch.int32)],
"w13_b": w13_b,
"w2_b": w2_b,
"swiglu_alpha": swiglu_alpha,
"swiglu_beta": swiglu_beta,
"swiglu_limit": swiglu_limit,
}
def make_flashinfer_runner(
shape: Shape, prep, x, topk_w, topk_i, autotuned: bool, with_bias: bool = True
):
out = torch.empty(shape.tokens, shape.hidden, dtype=torch.bfloat16, device="cuda")
fc1_b = prep["w13_b"] if with_bias else None
fc2_b = prep["w2_b"] if with_bias else None
def _call():
cutlass_fused_moe(
input=x,
token_selected_experts=topk_i,
token_final_scales=topk_w,
fc1_expert_weights=prep["w13"],
fc2_expert_weights=prep["w2"],
output_dtype=torch.bfloat16,
quant_scales=prep["quant_scales"],
fc1_expert_biases=fc1_b,
fc2_expert_biases=fc2_b,
swiglu_alpha=prep["swiglu_alpha"],
swiglu_beta=prep["swiglu_beta"],
swiglu_limit=prep["swiglu_limit"],
use_w4_group_scaling=True,
activation_type=ActivationType.Swiglu,
output=out,
)
if autotuned:
# Populate FlashInfer's tactic cache once before timing.
with autotune(True):
_call()
return _call
# ---------------------------------------------------------------------------
# SGLang Marlin path
# ---------------------------------------------------------------------------
def build_marlin_inputs(shape: Shape, w13, w2, w13_s, w2_s):
"""Repack MXFP4 weights into Marlin layout. Mirrors
``prepare_moe_mxfp4_layer_for_marlin`` but does not require a layer object."""
e = shape.num_experts
n = shape.inter
k = shape.hidden
device = w13.device
perm = torch.empty(0, dtype=torch.int, device=device)
def _repack(weight, size_n, size_k):
out_list = []
for i in range(e):
qweight = weight[i].view(torch.int32).T.contiguous()
out_list.append(
gptq_marlin_repack(
b_q_weight=qweight,
perm=perm,
size_k=size_k,
size_n=size_n,
num_bits=4,
)
)
return torch.stack(out_list)
def _scales_for(scales, size_n, size_k):
out_list = []
# Reinterpret uint8 E8M0 byte as float8_e8m0fnu, then to bf16 numerical.
scales_bf16 = scales.view(torch.float8_e8m0fnu).to(torch.bfloat16)
for i in range(e):
s = scales_bf16[i].T.contiguous()
ms = marlin_permute_scales(
s=s, size_k=size_k, size_n=size_n, group_size=GROUP_SIZE
)
out_list.append(mxfp4_marlin_process_scales(ms, input_dtype=torch.bfloat16))
return torch.stack(out_list)
w13_marlin = _repack(w13, size_n=2 * n, size_k=k)
w2_marlin = _repack(w2, size_n=k, size_k=n)
w13_s_marlin = _scales_for(w13_s, size_n=2 * n, size_k=k)
w2_s_marlin = _scales_for(w2_s, size_n=k, size_k=n)
workspace = marlin_make_workspace(device, 4)
return {
"w13": w13_marlin,
"w2": w2_marlin,
"w13_s": w13_s_marlin,
"w2_s": w2_s_marlin,
"workspace": workspace,
}
def make_marlin_runner(shape: Shape, prep, x_bf16, router_logits, topk_w, topk_i):
def _call():
fused_marlin_moe(
hidden_states=x_bf16,
w1=prep["w13"],
w2=prep["w2"],
w1_scale=prep["w13_s"],
w2_scale=prep["w2_s"],
gating_output=router_logits,
topk_weights=topk_w,
topk_ids=topk_i,
workspace=prep["workspace"],
num_bits=4,
is_k_full=True,
inplace=False,
clamp_limit=7.0,
)
return _call
# ---------------------------------------------------------------------------
# Timing harness
# ---------------------------------------------------------------------------
def time_call(fn: Callable, warmup: int = 5, iters: int = 30) -> Tuple[float, float]:
"""Returns (median_ms, min_ms) across ``iters`` calls after ``warmup``."""
for _ in range(warmup):
fn()
torch.cuda.synchronize()
starts = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
ends = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
for s, e in zip(starts, ends):
s.record()
fn()
e.record()
torch.cuda.synchronize()
times = sorted(s.elapsed_time(e) for s, e in zip(starts, ends))
return times[len(times) // 2], times[0]
def run_one_shape(shape: Shape, run_marlin: bool):
print(f"\n=== {shape.label()} ===")
w13, w2, w13_s, w2_s, w13_b, w2_b = _make_random_mxfp4(shape, seed=0)
router_logits, topk_w, topk_i = _make_topk(shape, seed=1)
x = (
torch.randn(shape.tokens, shape.hidden, dtype=torch.bfloat16, device="cuda")
* 0.1
)
# FlashInfer cutlass (autotune ON, with bias).
fi_prep = build_flashinfer_inputs(shape, w13, w2, w13_s, w2_s, w13_b, w2_b)
fi_at_call = make_flashinfer_runner(
shape, fi_prep, x, topk_w, topk_i, autotuned=True, with_bias=True
)
fi_at_med, fi_at_min = time_call(fi_at_call)
print(
f" FlashInfer cutlass (autotune): median={fi_at_med:.3f} ms "
f"min={fi_at_min:.3f} ms"
)
# FlashInfer cutlass (autotune ON, no bias) -- isolate bias epilogue cost.
fi_at_nb_call = make_flashinfer_runner(
shape, fi_prep, x, topk_w, topk_i, autotuned=True, with_bias=False
)
fi_at_nb_med, fi_at_nb_min = time_call(fi_at_nb_call)
print(
f" FlashInfer cutlass (AT, no-bias): median={fi_at_nb_med:.3f} ms "
f"min={fi_at_nb_min:.3f} ms "
f"(bias overhead = {fi_at_med - fi_at_nb_med:+.3f} ms / "
f"{(fi_at_med / fi_at_nb_med - 1) * 100:+.1f}%)"
)
fi_med = fi_at_med # alias for downstream speedup print
# Marlin
if run_marlin:
try:
ml_prep = build_marlin_inputs(shape, w13, w2, w13_s, w2_s)
ml_call = make_marlin_runner(
shape, ml_prep, x, router_logits, topk_w, topk_i
)
ml_med, ml_min = time_call(ml_call)
print(
f" SGLang Marlin: median={ml_med:.3f} ms "
f"min={ml_min:.3f} ms"
)
print(f" speedup (Marlin / FI autotune): {ml_med / fi_at_med:.2f}x")
print(f" speedup (Marlin / FI AT no-bias): {ml_med / fi_at_nb_med:.2f}x")
except Exception as exc: # pylint: disable=broad-except
print(f" SGLang Marlin: SKIPPED ({type(exc).__name__}: {exc})")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--no-marlin", action="store_true", help="Skip Marlin path.")
args = parser.parse_args()
if not torch.cuda.is_available():
raise SystemExit("CUDA required.")
cap = torch.cuda.get_device_capability()
if cap[0] != 9:
print(f"WARNING: device cap {cap} is not SM90; SM90-specific kernel may fail.")
print(f"Device: {torch.cuda.get_device_name()} (cap {cap[0]}.{cap[1]})")
for shape in DEFAULT_SHAPES:
run_one_shape(shape, run_marlin=not args.no_marlin)
if __name__ == "__main__":
main()
@@ -21,7 +21,19 @@ from sglang.test.test_utils import (
try_cached_model,
)
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-8-gpu-h200")
register_cuda_ci(est_time=1800, suite="stage-c-test-dsv4-8-gpu-h200")
def _flashinfer_has_sm90_cutlass_mxfp4() -> bool:
try:
from flashinfer.fused_moe import ( # noqa: F401
interleave_moe_weights_for_sm90_mixed_gemm,
)
return True
except ImportError:
return False
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
MODEL_FP8 = "sgl-project/DeepSeek-V4-Flash-FP8"
@@ -77,5 +89,62 @@ class TestDSV4FlashFP4H200(ServerSanityMixin, CustomTestCase):
self.assertGreater(metrics["score"], 0.93)
@unittest.skipUnless(
_flashinfer_has_sm90_cutlass_mxfp4(),
"FlashInfer build lacks SM90 mixed-input MXFP4 helpers (PR #3084, >= 0.6.11)",
)
class TestDSV4FlashFP4H200FlashInferCutlass(ServerSanityMixin, CustomTestCase):
"""FlashInfer SM90 mixed-input cutlass MXFP4 backend (this PR): TP=4 + EAGLE.
Mirrors :class:`TestDSV4FlashFP4H200` but swaps `--moe-runner-backend marlin`
for `flashinfer_mxfp4`, exercising the SM90 cutlass path from FlashInfer PR
#3084 end-to-end on a real DSv4-Flash checkpoint.
"""
@classmethod
def setUpClass(cls):
cls.model = try_cached_model(MODEL)
cls.base_url = DEFAULT_URL_FOR_TEST
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=SERVER_LAUNCH_TIMEOUT,
other_args=[
"--trust-remote-code",
"--tp",
"4",
"--moe-runner-backend",
"flashinfer_mxfp4",
"--speculative-algorithm",
"EAGLE",
"--speculative-num-steps",
"3",
"--speculative-eagle-topk",
"1",
"--speculative-num-draft-tokens",
"4",
],
)
@classmethod
def tearDownClass(cls):
if hasattr(cls, "process") and cls.process:
kill_process_tree(cls.process.pid)
def test_gsm8k(self):
args = SimpleNamespace(
base_url=self.base_url,
model=self.model,
eval_name="gsm8k",
api="completion",
max_tokens=512,
num_examples=200,
num_threads=128,
)
metrics = run_eval(args)
print(f"[DSV4 Flash FP4 FlashInfer Cutlass H200] GSM8K {metrics=}")
self.assertGreater(metrics["score"], 0.93)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,544 @@
"""Unit test for the SM90 cutlass MXFP4 path in :class:`Mxfp4MoEMethod`.
Builds a single-layer GPT-OSS-style MoE with random MXFP4 weights, drives the
SGLang plumbing (``_process_weights_for_sm90_cutlass`` + ``_apply_sm90_cutlass``)
and compares against a direct FlashInfer ``cutlass_fused_moe`` call with the
same inputs. Both paths invoke the same SM90 kernel from FlashInfer PR #3084,
so outputs must be bit-exact.
Run on H100/H200:
python -m pytest test/registered/unit/layers/quantization/test_mxfp4_sm90_cutlass.py -v
"""
from __future__ import annotations
from contextlib import nullcontext
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=120, suite="stage-b-test-1-gpu-large")
flashinfer_fused_moe = pytest.importorskip("flashinfer.fused_moe")
if not hasattr(flashinfer_fused_moe, "interleave_moe_weights_for_sm90_mixed_gemm"):
pytest.skip(
"FlashInfer build does not include PR #3084 SM90 mixed-input helpers",
allow_module_level=True,
)
if not torch.cuda.is_available():
pytest.skip("CUDA required", allow_module_level=True)
from sglang.srt.utils import is_sm90_supported, is_sm100_supported
if not is_sm90_supported() or is_sm100_supported():
pytest.skip(
"SM90-only path; require Hopper without SM100 promotion",
allow_module_level=True,
)
from flashinfer.fused_moe import (
cutlass_fused_moe,
interleave_moe_scales_for_sm90_mixed_gemm,
interleave_moe_weights_for_sm90_mixed_gemm,
)
from flashinfer.fused_moe.core import ActivationType
GROUP_SIZE = 32 # MXFP4 block size
class _MockLayer:
"""Stand-in for ``FusedMoE`` carrying the attributes the SM90 helpers read.
We construct one by hand so the test stays out of SGLang's distributed init
path (``get_tp_group`` etc.).
"""
class _MockTopKOutput:
def __init__(self, weights, ids):
self.topk_weights = weights
self.topk_ids = ids
def _make_random_mxfp4(num_experts, hidden, inter, seed=0):
g = torch.Generator(device="cuda").manual_seed(seed)
w13 = torch.randint(
0,
256,
(num_experts, 2 * inter, hidden // 2),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w2 = torch.randint(
0,
256,
(num_experts, hidden, inter // 2),
dtype=torch.uint8,
device="cuda",
generator=g,
)
# E8M0 scales centered around 127 (= 2^0); narrow band keeps dequant values
# in a sane range so SwiGLU clamp doesn't dominate.
w13_s = torch.randint(
125,
130,
(num_experts, 2 * inter, hidden // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w2_s = torch.randint(
125,
130,
(num_experts, hidden, inter // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w13_b = (
torch.randn(
num_experts, 2 * inter, dtype=torch.float32, device="cuda", generator=g
).to(torch.bfloat16)
* 0.01
)
w2_b = (
torch.randn(
num_experts, hidden, dtype=torch.float32, device="cuda", generator=g
).to(torch.bfloat16)
* 0.01
)
return w13, w2, w13_s, w2_s, w13_b, w2_b
def _make_topk(tokens, num_experts, top_k, seed=1):
g = torch.Generator(device="cuda").manual_seed(seed)
logits = torch.randn(
tokens, num_experts, dtype=torch.float32, device="cuda", generator=g
)
weights, ids = torch.topk(torch.softmax(logits, dim=-1), top_k, dim=-1)
weights = weights / weights.sum(dim=-1, keepdim=True)
return weights.to(torch.float32), ids.to(torch.int32)
def _build_mock_layer(num_experts, hidden, inter, w13, w2, w13_s, w2_s, w13_b, w2_b):
layer = _MockLayer()
layer.w13_weight = torch.nn.Parameter(w13.clone(), requires_grad=False)
layer.w2_weight = torch.nn.Parameter(w2.clone(), requires_grad=False)
layer.w13_weight_scale = torch.nn.Parameter(w13_s.clone(), requires_grad=False)
layer.w2_weight_scale = torch.nn.Parameter(w2_s.clone(), requires_grad=False)
layer.w13_weight_bias = torch.nn.Parameter(w13_b.clone(), requires_grad=False)
layer.w2_weight_bias = torch.nn.Parameter(w2_b.clone(), requires_grad=False)
layer.num_local_experts = num_experts # tests run with EP size = 1
layer.moe_tp_size = 1
layer.moe_tp_rank = 0
layer.moe_ep_size = 1
layer.moe_ep_rank = 0
return layer
def _round_up(x, base):
return ((x + base - 1) // base) * base
def _build_method(num_experts, hidden, inter):
from sglang.srt.layers.quantization.mxfp4 import Mxfp4MoEMethod
method = Mxfp4MoEMethod.__new__(Mxfp4MoEMethod)
method._fi_kernel = "cutlass_sm90"
method.num_experts = num_experts
# The new SM90 cutlass path tracks padded sizes in dedicated attrs;
# ``hidden_size`` / ``intermediate_size_per_partition`` keep the unpadded
# values to mirror what ``create_weights`` records.
method.hidden_size = hidden
method.intermediate_size_per_partition = inter
method._padded_hidden = _round_up(hidden, 128)
method._padded_intermediate = _round_up(inter, 128)
method.use_flashinfer = True
return method
def _expected_w13_processed(w13_un, w13_s_un, w13_b_un, N_pad, K_pad, group_size):
"""Replicate ``_process_weights_for_sm90_cutlass`` for w13: de-interleave
HF's pair-wise ``[g_0, u_0, g_1, u_1, ...]`` layout into halved
``[up; gate]``, pad each half along its row dim from ``N_un -> N_pad``
and last dim from ``K_un -> K_pad`` with zeros, then run the FlashInfer
SM90 byte / scale interleave helpers."""
E, two_n_un, last_un_w = w13_un.shape
N_un = two_n_un // 2
K_un = last_un_w * 2 # packed 4-bit -> *2 for raw K
def _split_and_pad(unpadded, last_pad, last_un, dtype):
gate = unpadded[:, 0::2, :]
up = unpadded[:, 1::2, :]
out = torch.zeros(E, 2 * N_pad, last_pad, dtype=dtype, device=unpadded.device)
out[:, :N_un, :last_un] = up
out[:, N_pad : N_pad + N_un, :last_un] = gate
return out
w13_pad = _split_and_pad(
w13_un.view(torch.uint8), K_pad // 2, K_un // 2, w13_un.dtype
)
w13_s_pad = _split_and_pad(
w13_s_un, K_pad // group_size, K_un // group_size, w13_s_un.dtype
)
gate_b = w13_b_un[:, 0::2]
up_b = w13_b_un[:, 1::2]
w13_b_pad = torch.zeros(E, 2 * N_pad, dtype=w13_b_un.dtype, device=w13_b_un.device)
w13_b_pad[:, :N_un] = up_b
w13_b_pad[:, N_pad : N_pad + N_un] = gate_b
w13_il = interleave_moe_weights_for_sm90_mixed_gemm(w13_pad, "fp4")
w13_s_il = interleave_moe_scales_for_sm90_mixed_gemm(
w13_s_pad, group_size=group_size
)
return w13_il, w13_s_il, w13_b_pad
def _expected_w2_processed(w2_un, w2_s_un, w2_b_un, N_pad, K_pad, group_size):
"""w2 needs padding only (no halving / no de-interleave)."""
E, K_un, last_un_w = w2_un.shape
N_un = last_un_w * 2
def _pad(unpadded, last_pad, last_un):
out = torch.zeros(
E, K_pad, last_pad, dtype=unpadded.dtype, device=unpadded.device
)
out[:, :K_un, :last_un] = unpadded
return out
w2_pad = _pad(w2_un.view(torch.uint8), N_pad // 2, N_un // 2)
w2_s_pad = _pad(w2_s_un, N_pad // group_size, N_un // group_size)
w2_b_pad = torch.zeros(E, K_pad, dtype=w2_b_un.dtype, device=w2_b_un.device)
w2_b_pad[:, :K_un] = w2_b_un
w2_il = interleave_moe_weights_for_sm90_mixed_gemm(w2_pad, "fp4")
w2_s_il = interleave_moe_scales_for_sm90_mixed_gemm(w2_s_pad, group_size=group_size)
return w2_il, w2_s_il, w2_b_pad
@pytest.mark.parametrize(
"num_experts,hidden,inter",
[
# Aligned shapes (no padding needed).
(4, 256, 256),
(8, 768, 384),
(8, 1024, 1024),
# Non-aligned shapes (exercise the de-interleave + pad path).
# 192 % 128 = 64, so N_pad = K_pad = 256 (round_up(192, 128)).
(4, 192, 192),
# GPT-OSS-20B-like: hidden=2880, inter=2880 -> padded to 2944.
# Use smaller E to keep memory bounded.
(4, 2880, 2880),
],
)
def test_process_weights_matches_direct_interleave(num_experts, hidden, inter):
"""``_process_weights_for_sm90_cutlass`` must produce the same bytes as
a manual de-interleave + pad + halved-swap + interleave reference."""
w13, w2, w13_s, w2_s, w13_b, w2_b = _make_random_mxfp4(num_experts, hidden, inter)
layer = _build_mock_layer(
num_experts, hidden, inter, w13, w2, w13_s, w2_s, w13_b, w2_b
)
method = _build_method(num_experts, hidden, inter)
method._process_weights_for_sm90_cutlass(layer)
N_pad = _round_up(inter, 128)
K_pad = _round_up(hidden, 128)
ref_w13, ref_w13_s, ref_w13_b = _expected_w13_processed(
w13, w13_s, w13_b, N_pad, K_pad, GROUP_SIZE
)
ref_w2, ref_w2_s, ref_w2_b = _expected_w2_processed(
w2, w2_s, w2_b, N_pad, K_pad, GROUP_SIZE
)
assert torch.equal(layer.w13_weight.data, ref_w13)
assert torch.equal(layer.w2_weight.data, ref_w2)
assert torch.equal(layer.w13_weight_scale.data, ref_w13_s)
assert torch.equal(layer.w2_weight_scale.data, ref_w2_s)
assert torch.equal(layer.w13_weight_bias.data, ref_w13_b)
assert torch.equal(layer.w2_weight_bias.data, ref_w2_b)
# SwiGLU per-expert scalars seeded with GPT-OSS defaults.
assert torch.allclose(
layer.swiglu_alpha,
torch.full((num_experts,), 1.702, dtype=torch.float32, device="cuda"),
)
assert torch.allclose(
layer.swiglu_beta,
torch.full((num_experts,), 1.0, dtype=torch.float32, device="cuda"),
)
assert torch.allclose(
layer.swiglu_limit,
torch.full((num_experts,), 7.0, dtype=torch.float32, device="cuda"),
)
@pytest.mark.parametrize(
"tokens,num_experts,hidden,inter,top_k",
[
# Aligned shapes (no padding).
(4, 4, 256, 256, 2),
(16, 8, 768, 384, 2),
(32, 8, 1024, 1024, 4),
# Non-aligned (exercises pad x + trim output).
(8, 4, 192, 192, 2),
],
)
def test_apply_sm90_cutlass_matches_flashinfer_direct(
tokens, num_experts, hidden, inter, top_k, monkeypatch
):
"""End-to-end: SGLang's ``_apply_sm90_cutlass`` must produce the same
output as a direct FlashInfer ``cutlass_fused_moe`` call fed with the
same processed weights / scales / biases. The processing pipeline is
covered separately by ``test_process_weights_matches_direct_interleave``;
here we just verify that ``apply`` calls the kernel with the right
arguments (incl. input padding + output trim)."""
import sglang.srt.layers.quantization.mxfp4 as mxfp4_mod
# Bypass symmetric-memory / TP-group: not relevant to numerics.
monkeypatch.setattr(
mxfp4_mod, "use_symmetric_memory", lambda *a, **kw: nullcontext()
)
monkeypatch.setattr(mxfp4_mod, "is_allocation_symmetric", lambda: False)
monkeypatch.setattr(mxfp4_mod, "get_tp_group", lambda: None)
w13, w2, w13_s, w2_s, w13_b, w2_b = _make_random_mxfp4(num_experts, hidden, inter)
x = torch.randn(tokens, hidden, dtype=torch.bfloat16, device="cuda") * 0.1
topk_w, topk_i = _make_topk(tokens, num_experts, top_k)
# ---- SGLang path ----
layer = _build_mock_layer(
num_experts, hidden, inter, w13, w2, w13_s, w2_s, w13_b, w2_b
)
method = _build_method(num_experts, hidden, inter)
method._process_weights_for_sm90_cutlass(layer)
out_sglang = method._apply_sm90_cutlass(
layer, x.clone(), _MockTopKOutput(topk_w, topk_i)
).hidden_states
# ---- FlashInfer-direct reference using the same processed weights ----
K_pad = method._padded_hidden
if K_pad != hidden:
x_padded = torch.nn.functional.pad(
x.clone(), (0, K_pad - hidden), mode="constant", value=0.0
)
else:
x_padded = x.clone()
out_ref_padded = torch.empty(tokens, K_pad, dtype=torch.bfloat16, device="cuda")
cutlass_fused_moe(
input=x_padded,
token_selected_experts=topk_i.to(torch.int),
token_final_scales=topk_w,
fc1_expert_weights=layer.w13_weight,
fc2_expert_weights=layer.w2_weight,
output_dtype=torch.bfloat16,
quant_scales=[
layer.w13_weight_scale.view(torch.int32),
layer.w2_weight_scale.view(torch.int32),
],
fc1_expert_biases=layer.w13_weight_bias,
fc2_expert_biases=layer.w2_weight_bias,
swiglu_alpha=layer.swiglu_alpha,
swiglu_beta=layer.swiglu_beta,
swiglu_limit=layer.swiglu_limit,
use_w4_group_scaling=True,
activation_type=ActivationType.Swiglu,
output=out_ref_padded,
)
out_ref = (
out_ref_padded[:, :hidden].contiguous() if K_pad != hidden else out_ref_padded
)
assert torch.equal(out_sglang, out_ref), (
f"SGLang vs FlashInfer-direct mismatch; "
f"max abs diff = {(out_sglang.float() - out_ref.float()).abs().max().item():.4g}"
)
# =============================================================================
# DeepSeek-V4 path: Mxfp4FlashinferCutlassMoEMethod (sibling of Marlin /
# trtllm-gen). Wired into fp8.py's get_quant_method when SM90 +
# is_flashinfer_mxfp4 + is_fp4_experts.
# =============================================================================
def _make_random_dsv4_mxfp4(num_experts, hidden, inter, seed=0):
"""Mirrors the fp8 base method's allocation for fp4 experts: int8-packed
4-bit weights, fp32 scales (containing 2**e values, not raw E8M0 bytes)."""
g = torch.Generator(device="cuda").manual_seed(seed)
# int8 storage (signed) -- matches Fp8MoEMethod.create_weights for fp4_experts.
w13 = torch.randint(
-128,
128,
(num_experts, 2 * inter, hidden // 2),
dtype=torch.int8,
device="cuda",
generator=g,
)
w2 = torch.randint(
-128,
128,
(num_experts, hidden, inter // 2),
dtype=torch.int8,
device="cuda",
generator=g,
)
# fp32 scales whose bit pattern after .to(float8_e8m0fnu).view(uint8) lands
# in a sane E8M0 band -- generate exponents around 0 (= 2**0).
raw_e = torch.randint(
125,
130,
(num_experts, 2 * inter, hidden // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
raw_e2 = torch.randint(
125,
130,
(num_experts, hidden, inter // GROUP_SIZE),
dtype=torch.uint8,
device="cuda",
generator=g,
)
w13_s = raw_e.view(torch.float8_e8m0fnu).to(torch.float32)
w2_s = raw_e2.view(torch.float8_e8m0fnu).to(torch.float32)
return w13, w2, w13_s, w2_s
@pytest.mark.parametrize(
"tokens,num_experts,hidden,inter,top_k",
[
(4, 4, 256, 256, 2),
(16, 8, 768, 384, 2),
(256, 8, 1024, 1024, 4),
],
)
def test_dsv4_apply_matches_flashinfer_direct(
tokens, num_experts, hidden, inter, top_k, monkeypatch
):
"""End-to-end: SGLang's DSv4 ``Mxfp4FlashinferCutlassMoEMethod.apply``
output must match a direct FlashInfer ``cutlass_fused_moe`` call with
the equivalent reorder + scale-cast + interleave applied manually."""
from types import SimpleNamespace
import sglang.srt.layers.quantization.mxfp4_flashinfer_cutlass_moe as ds_mod
from sglang.srt.layers.quantization.utils import reorder_w1w3_to_w3w1
# Bypass symmetric-memory / TP-group stack -- not relevant to numerics.
monkeypatch.setattr(ds_mod, "use_symmetric_memory", lambda *a, **kw: nullcontext())
monkeypatch.setattr(ds_mod, "is_allocation_symmetric", lambda: False)
monkeypatch.setattr(ds_mod, "get_tp_group", lambda: None)
w13, w2, w13_s, w2_s = _make_random_dsv4_mxfp4(num_experts, hidden, inter)
x = torch.randn(tokens, hidden, dtype=torch.bfloat16, device="cuda") * 0.1
topk_w, topk_i = _make_topk(tokens, num_experts, top_k)
# ---- SGLang DSv4 path ----
method = ds_mod.Mxfp4FlashinferCutlassMoEMethod.__new__(
ds_mod.Mxfp4FlashinferCutlassMoEMethod
)
method._fp8 = SimpleNamespace(
process_weights_after_loading=lambda layer: None,
)
method.prefix = "test"
# plain SiLU * up — all three SwiGLU scalars None (no clamp configured).
method._swiglu_alpha_tensor = None
method._swiglu_beta_tensor = None
method._swiglu_limit_tensor = None
layer = _MockLayer()
layer.w13_weight = torch.nn.Parameter(w13.clone(), requires_grad=False)
layer.w2_weight = torch.nn.Parameter(w2.clone(), requires_grad=False)
layer.w13_weight_scale_inv = torch.nn.Parameter(w13_s.clone(), requires_grad=False)
layer.w2_weight_scale_inv = torch.nn.Parameter(w2_s.clone(), requires_grad=False)
layer.num_local_experts = num_experts
layer.moe_tp_size = 1
layer.moe_tp_rank = 0
layer.moe_ep_size = 1
layer.moe_ep_rank = 0
method.process_weights_after_loading(layer)
out_sglang = method.apply(
layer, _MockDispatchOutput(x.clone(), topk_w, topk_i)
).hidden_states
# ---- Direct FlashInfer reference ----
w13_re, w13_s_re = reorder_w1w3_to_w3w1(w13, w13_s)
w13_s_u8 = w13_s_re.to(torch.float8_e8m0fnu).view(torch.uint8).contiguous()
w2_s_u8 = w2_s.to(torch.float8_e8m0fnu).view(torch.uint8).contiguous()
ref_w13 = interleave_moe_weights_for_sm90_mixed_gemm(
w13_re.view(torch.uint8).contiguous(), "fp4"
)
ref_w2 = interleave_moe_weights_for_sm90_mixed_gemm(
w2.view(torch.uint8).contiguous(), "fp4"
)
ref_w13_s = interleave_moe_scales_for_sm90_mixed_gemm(
w13_s_u8, group_size=GROUP_SIZE
)
ref_w2_s = interleave_moe_scales_for_sm90_mixed_gemm(w2_s_u8, group_size=GROUP_SIZE)
out_ref = torch.empty(tokens, hidden, dtype=torch.bfloat16, device="cuda")
cutlass_fused_moe(
input=x.clone(),
token_selected_experts=topk_i,
token_final_scales=topk_w,
fc1_expert_weights=ref_w13,
fc2_expert_weights=ref_w2,
output_dtype=torch.bfloat16,
quant_scales=[ref_w13_s.view(torch.int32), ref_w2_s.view(torch.int32)],
fc1_expert_biases=None,
fc2_expert_biases=None,
swiglu_alpha=None,
swiglu_beta=None,
swiglu_limit=None,
use_w4_group_scaling=True,
activation_type=ActivationType.Swiglu,
output=out_ref,
)
assert torch.equal(out_sglang, out_ref), (
f"DSv4 SGLang vs FlashInfer-direct mismatch; "
f"max abs diff = "
f"{(out_sglang.float() - out_ref.float()).abs().max().item():.4g}"
)
class _MockDispatchOutput:
"""Stand-in for StandardDispatchOutput. ``topk_output`` is a real
``StandardTopKOutput`` so ``TopKOutputChecker.format_is_standard``
(an isinstance check) returns True without distributed init."""
def __init__(self, hidden_states, topk_weights, topk_ids):
from sglang.srt.layers.moe.topk import StandardTopKOutput
self.hidden_states = hidden_states
# router_logits is unused by Mxfp4FlashinferCutlassMoEMethod.apply;
# supply a placeholder of the right shape to keep the NamedTuple happy.
router_logits = torch.zeros(
topk_ids.shape[0],
int(topk_ids.max().item()) + 1 if topk_ids.numel() else 1,
dtype=torch.float32,
device=topk_ids.device,
)
self.topk_output = StandardTopKOutput(
topk_weights=topk_weights,
topk_ids=topk_ids,
router_logits=router_logits,
)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))