[Bugfix][Quantization] Fix MXFP8 startup crash on layers below mm_mxfp8 shape limits (#54223)

Signed-off-by: samuelkim7 <samuelmwkim@gmail.com>
Signed-off-by: mgoin <mgoin64@gmail.com>
Co-authored-by: mgoin <mgoin64@gmail.com>
This commit is contained in:
Samuel Kim
2026-09-25 02:54:11 +00:00
committed by GitHub
co-authored by mgoin
parent 6936e77ba4
commit f55419c43c
16 changed files with 221 additions and 57 deletions
@@ -223,7 +223,6 @@ def test_shared_query_quant_preserves_projection(
with set_current_vllm_config(
VllmConfig(compilation_config=CompilationConfig(mode=CompilationMode.NONE))
):
kernel = cls(Mxfp8LinearLayerConfig())
# Match the checkpoint's TP4 attention and replicated indexer widths.
if projection == "attention":
linear = ColumnParallelLinear.__new__(ColumnParallelLinear)
@@ -247,6 +246,7 @@ def test_shared_query_quant_preserves_projection(
)
linear.weight = torch.nn.Parameter(w, requires_grad=False)
linear.weight_scale = torch.nn.Parameter(ws, requires_grad=False)
kernel = cls(Mxfp8LinearLayerConfig(weight_shape=w.shape))
quant_config = DeepseekV4FP8Config.from_config(
{
"quant_method": "fp8",
@@ -50,7 +50,9 @@ def test_flashinfer_trtllm_mxfp8_linear_numerics(
x = torch.randn((m, k), dtype=torch.bfloat16, device="cuda")
weight = torch.randn((n, k), dtype=torch.bfloat16, device="cuda")
layer = _make_layer(weight)
kernel = FlashInferTrtllmMxfp8LinearKernel(Mxfp8LinearLayerConfig())
kernel = FlashInferTrtllmMxfp8LinearKernel(
Mxfp8LinearLayerConfig(weight_shape=(n, k))
)
kernel.process_weights_after_loading(layer)
output = kernel.apply_weights(layer, x)
@@ -69,7 +71,9 @@ def test_flashinfer_trtllm_mxfp8_custom_ops() -> None:
x = torch.randn((7, 512), dtype=torch.bfloat16, device="cuda")
weight = torch.randn((256, 512), dtype=torch.bfloat16, device="cuda")
layer = _make_layer(weight)
kernel = FlashInferTrtllmMxfp8LinearKernel(Mxfp8LinearLayerConfig())
kernel = FlashInferTrtllmMxfp8LinearKernel(
Mxfp8LinearLayerConfig(weight_shape=(256, 512))
)
kernel.process_weights_after_loading(layer)
torch.library.opcheck(
@@ -104,7 +108,9 @@ def test_flashinfer_trtllm_mxfp8_linear_cuda_graph() -> None:
m, n, k = 7, 130, 512
weight = torch.randn((n, k), dtype=torch.bfloat16, device="cuda")
layer = _make_layer(weight)
kernel = FlashInferTrtllmMxfp8LinearKernel(Mxfp8LinearLayerConfig())
kernel = FlashInferTrtllmMxfp8LinearKernel(
Mxfp8LinearLayerConfig(weight_shape=(n, k))
)
kernel.process_weights_after_loading(layer)
static_x = torch.randn((m, k), dtype=torch.bfloat16, device="cuda")
@@ -408,7 +408,9 @@ def test_fp8_block_marlin_padded_round_trip(shape):
_gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
reason="FP8 Marlin is not supported on this GPU type.",
)
@pytest.mark.parametrize("shape", [(200, 288), (4640, 512)])
@pytest.mark.parametrize(
"shape", [(32, 256), (96, 256), (130, 256), (200, 288), (4640, 512)]
)
def test_mxfp8_marlin_padded_round_trip(shape):
"""MXFP8 exercises the e8m0 scale path, where padded 0.0 scales clamp to
2^-127 instead of zero and must still contribute nothing."""
@@ -0,0 +1,135 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for MXFP8 linear kernel selection logic (CPU-only)
Run `pytest tests/kernels/quantization/test_mxfp8_kernel_selection.py`.
"""
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from vllm.model_executor.kernels.linear import (
FlashInferCutedslMxfp8LinearKernel,
FlashInferCutlassMxfp8LinearKernel,
MarlinMxfp8LinearKernel,
Mxfp8LinearLayerConfig,
init_mxfp8_linear_kernel,
)
from vllm.platforms import PlatformEnum
pytestmark = pytest.mark.cpu_test
# Kernels backed by FlashInfer mm_mxfp8, which requires N, K >= 128 and
# K % 32 == 0.
_MM_MXFP8_KERNELS = [
FlashInferCutedslMxfp8LinearKernel,
FlashInferCutlassMxfp8LinearKernel,
]
_SUPPORTED_SHAPE = (4096, 4096)
# (N, K): N < 128, K < 128, K % 32 != 0.
_UNSUPPORTED_SHAPES = [(64, 4096), (4096, 64), (4096, 4112)]
# (N, K) with N < 128 or K < 128, which only Marlin runs.
_MARLIN_ONLY_SHAPES = [(32, 256), (64, 256), (96, 256), (256, 64)]
# (N, K): N <= 0, K <= 0, K % 32 != 0.
_MARLIN_UNSUPPORTED_SHAPES = [(0, 256), (256, 0), (256, 100)]
_SELECTION_CASES = [
(100, (128, 128), FlashInferCutedslMxfp8LinearKernel),
(100, (130, 256), FlashInferCutedslMxfp8LinearKernel),
(103, (128, 128), FlashInferCutedslMxfp8LinearKernel),
(103, (130, 256), FlashInferCutedslMxfp8LinearKernel),
(120, (128, 128), FlashInferCutlassMxfp8LinearKernel),
(120, (130, 256), MarlinMxfp8LinearKernel),
(121, (128, 128), FlashInferCutlassMxfp8LinearKernel),
(121, (130, 256), MarlinMxfp8LinearKernel),
*[
(capability, shape, MarlinMxfp8LinearKernel)
for capability in (100, 103, 120, 121)
for shape in _MARLIN_ONLY_SHAPES
],
]
@pytest.mark.parametrize("kernel_cls", _MM_MXFP8_KERNELS)
def test_mm_mxfp8_kernels_accept_supported_shape(kernel_cls):
config = Mxfp8LinearLayerConfig(weight_shape=_SUPPORTED_SHAPE)
can_implement, reason = kernel_cls.can_implement(config)
assert can_implement, reason
@pytest.mark.parametrize("kernel_cls", _MM_MXFP8_KERNELS)
@pytest.mark.parametrize("weight_shape", _UNSUPPORTED_SHAPES)
def test_mm_mxfp8_kernels_reject_unsupported_shape(kernel_cls, weight_shape):
config = Mxfp8LinearLayerConfig(weight_shape=weight_shape)
can_implement, reason = kernel_cls.can_implement(config)
assert not can_implement
assert reason
@pytest.mark.parametrize("weight_shape", _MARLIN_UNSUPPORTED_SHAPES)
def test_marlin_mxfp8_rejects_unsupported_shape(weight_shape):
config = Mxfp8LinearLayerConfig(weight_shape=weight_shape)
can_implement, reason = MarlinMxfp8LinearKernel.can_implement(config)
assert not can_implement
assert reason
@contextmanager
def _patch_cuda_platform(capability: int):
"""Patch the platform so kernel selection runs as on a CUDA GPU with the
given compute capability. The FlashInfer and Marlin kernels report as
supported, CuTe-DSL only on SM100/103."""
with (
patch("vllm.model_executor.kernels.linear.current_platform") as platform,
patch(
"vllm.model_executor.kernels.linear.mxfp8.flashinfer.current_platform."
"is_device_capability_family",
side_effect=lambda cap, device_id=0: cap // 10 == capability // 10,
),
patch.object(
FlashInferCutedslMxfp8LinearKernel,
"is_supported",
return_value=(capability in (100, 103), None),
),
patch.object(
FlashInferCutlassMxfp8LinearKernel,
"is_supported",
return_value=(True, None),
),
patch.object(
MarlinMxfp8LinearKernel, "is_supported", return_value=(True, None)
),
):
platform._enum = PlatformEnum.CUDA
yield
@pytest.mark.parametrize(
("capability", "expected"),
[(100, True), (103, True), (120, False), (121, False)],
)
def test_cutlass_mxfp8_requires_aligned_n_only_on_sm12x(capability, expected):
"""(130, 256) passes the generic mm_mxfp8 limits. Only the SM12x CUTLASS
backend needs N % 32 == 0, so SM100/103 must keep accepting it."""
config = Mxfp8LinearLayerConfig(weight_shape=(130, 256))
with _patch_cuda_platform(capability):
can_implement, _ = FlashInferCutlassMxfp8LinearKernel.can_implement(config)
assert can_implement == expected
@pytest.mark.parametrize(
("capability", "weight_shape", "expected_kernel_cls"), _SELECTION_CASES
)
def test_init_mxfp8_linear_kernel(capability, weight_shape, expected_kernel_cls):
"""A layer that mm_mxfp8 cannot handle must fall through to the next
kernel in the CUDA priority list instead of being selected and failing
in apply_weights."""
with _patch_cuda_platform(capability):
kernel = init_mxfp8_linear_kernel(weight_shape=weight_shape)
assert isinstance(kernel, expected_kernel_cls)
assert kernel.config == Mxfp8LinearLayerConfig(weight_shape=weight_shape)
@@ -66,7 +66,7 @@ def test_rocm_mxfp8_linear_matches_dequant_reference(name, n, k, num_tokens):
torch.manual_seed(num_tokens)
device = "cuda"
layer = _make_layer(n, k, device)
kernel = RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig(bmm_batch_size=None))
kernel = RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig(weight_shape=(n, k)))
kernel.process_weights_after_loading(layer)
x = torch.randn(num_tokens, k, device=device, dtype=torch.bfloat16) * 0.5
@@ -103,7 +103,7 @@ def test_rocm_mxfp8_linear_handles_extreme_magnitudes():
device = "cuda"
n, k = 5120, 2048 # wo_b at TP=4
layer = _make_layer(n, k, device)
kernel = RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig(bmm_batch_size=None))
kernel = RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig(weight_shape=(n, k)))
kernel.process_weights_after_loading(layer)
x = torch.randn(8, k, device=device, dtype=torch.bfloat16)
+4 -2
View File
@@ -431,7 +431,7 @@ def test_mxfp8_linear_emulation_bf16_at_load(
layer.weight = torch.nn.Parameter(w_fp8.clone(), requires_grad=False)
layer.weight_scale = torch.nn.Parameter(w_scale.clone(), requires_grad=False)
kernel = EmulationMxfp8LinearKernel(Mxfp8LinearLayerConfig())
kernel = EmulationMxfp8LinearKernel(Mxfp8LinearLayerConfig(weight_shape=(N, K)))
kernel.process_weights_after_loading(layer)
if dequant_at_load:
@@ -482,7 +482,9 @@ def test_mxfp8_rocm_native_unaligned_k_dequantizes_at_load(shape):
layer.weight_scale = torch.nn.Parameter(w_scale.clone(), requires_grad=False)
return layer
kernel = rocm_native.RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig())
kernel = rocm_native.RocmDotScaledMxfp8LinearKernel(
Mxfp8LinearLayerConfig(weight_shape=(N, K))
)
layer = make_layer()
kernel.process_weights_after_loading(layer)
assert (layer.weight.element_size() >= 2) is not aligned
@@ -66,7 +66,7 @@ from vllm.platforms import PlatformEnum
"MarlinMxfp8LinearKernel",
"EmulationMxfp8LinearKernel",
init_mxfp8_linear_kernel,
{},
{"weight_shape": (2048, 2048)},
),
(
B12xTensorFP8ScaledMMLinearKernel,
@@ -365,7 +365,7 @@ def test_b12x_tensor_fp8_apply_quantizes_and_uses_packed_weight(
def test_b12x_mxfp8_can_implement_supported_config() -> None:
can_implement, reason = B12xMxfp8LinearKernel.can_implement(
Mxfp8LinearLayerConfig()
Mxfp8LinearLayerConfig(weight_shape=(256, 512))
)
assert can_implement
+2 -2
View File
@@ -1249,7 +1249,7 @@ def test_inc_mxfp8_linear_scheme_delegates_to_kernel(monkeypatch) -> None:
kernel = DummyKernel()
monkeypatch.setattr(
"vllm.model_executor.layers.quantization.inc.schemes.inc_mxfp8_linear.init_mxfp8_linear_kernel",
lambda: kernel,
lambda weight_shape: kernel,
)
monkeypatch.setattr(
"vllm.model_executor.layers.quantization.inc.schemes.inc_mxfp8_linear.ModelWeightParameter",
@@ -1288,7 +1288,7 @@ def test_inc_mxfp8_linear_scheme_delegates_to_kernel(monkeypatch) -> None:
def test_inc_mxfp8_linear_scheme_requires_block_32_input(monkeypatch) -> None:
monkeypatch.setattr(
"vllm.model_executor.layers.quantization.inc.schemes.inc_mxfp8_linear.init_mxfp8_linear_kernel",
lambda: object(),
lambda weight_shape: object(),
)
scheme = INCMxfp8LinearScheme()
@@ -880,10 +880,15 @@ def choose_mp_linear_kernel(
)
def init_mxfp8_linear_kernel(*, bmm_batch_size: int | None = None) -> Mxfp8LinearKernel:
def init_mxfp8_linear_kernel(
*, weight_shape: tuple[int, int], bmm_batch_size: int | None = None
) -> Mxfp8LinearKernel:
"""Select and instantiate the best MXFP8 linear kernel for the
current platform."""
config = Mxfp8LinearLayerConfig(bmm_batch_size=bmm_batch_size)
current platform and `(N, K)` weight shape."""
config = Mxfp8LinearLayerConfig(
weight_shape=weight_shape,
bmm_batch_size=bmm_batch_size,
)
platform = current_platform._enum
possible: list[type[Mxfp8LinearKernel]]
@@ -15,8 +15,13 @@ class Mxfp8LinearLayerConfig:
All MXFP8 layers share the same structure: FP8-E4M3 weights with
uint8 (E8M0) per-block scales at block size 32.
Attributes:
weight_shape: The layer's `(out_features, in_features)`, i.e. `(N, K)`.
"""
weight_shape: tuple[int, int]
bmm_batch_size: int | None = None
@@ -24,6 +24,20 @@ from vllm.utils.flashinfer import has_flashinfer, has_flashinfer_cutedsl
from .Mxfp8LinearKernel import Mxfp8LinearKernel, Mxfp8LinearLayerConfig
def _check_mm_mxfp8_shape(weight_shape: tuple[int, int]) -> tuple[bool, str | None]:
N, K = weight_shape
if K < 128:
return False, f"mm_mxfp8 requires K >= 128, got K={K}."
if K % MXFP8_BLOCK_SIZE != 0:
return (
False,
f"mm_mxfp8 requires K to be divisible by {MXFP8_BLOCK_SIZE}, got K={K}.",
)
if N < 128:
return False, f"mm_mxfp8 requires N >= 128, got N={N}."
return True, None
class FlashInferCutlassMxfp8LinearKernel(Mxfp8LinearKernel):
"""MXFP8 W8A8 GEMM via FlashInfer CUTLASS (SM100+)."""
@@ -43,6 +57,15 @@ class FlashInferCutlassMxfp8LinearKernel(Mxfp8LinearKernel):
@classmethod
def can_implement(cls, c: Mxfp8LinearLayerConfig) -> tuple[bool, str | None]:
can_implement, reason = _check_mm_mxfp8_shape(c.weight_shape)
if not can_implement:
return False, reason
N, _ = c.weight_shape
if current_platform.is_device_capability_family(120) and N % 32 != 0:
return (
False,
f"mm_mxfp8 on SM12x requires N to be divisible by 32, got N={N}.",
)
return True, None
def input_quant_key(self) -> QuantKey:
@@ -72,20 +95,6 @@ class FlashInferCutlassMxfp8LinearKernel(Mxfp8LinearKernel):
weight_scale = layer.weight_scale
N, K = weight.shape
min_dim = 128
assert min_dim <= K, (
f"mm_mxfp8 requires K >= {min_dim}, got K={K}. "
f"in_features is too small for mm_mxfp8."
)
assert K % MXFP8_BLOCK_SIZE == 0, (
f"mm_mxfp8 requires K to be divisible by {MXFP8_BLOCK_SIZE}, got K={K}."
)
assert min_dim <= N, (
f"mm_mxfp8 requires N >= {min_dim}, got N={N}. "
f"out_features is too small for mm_mxfp8."
)
qa = as_quantized_activation(x, self.input_quant_key())
if qa is not None:
input_mxfp8, input_scale = qa.data, qa.scale
@@ -136,7 +145,7 @@ class FlashInferCutedslMxfp8LinearKernel(Mxfp8LinearKernel):
@classmethod
def can_implement(cls, c: Mxfp8LinearLayerConfig) -> tuple[bool, str | None]:
return True, None
return _check_mm_mxfp8_shape(c.weight_shape)
def input_quant_key(self) -> QuantKey:
# Activations use FlashInfer's F8_128x4 swizzled scale layout.
@@ -166,20 +175,6 @@ class FlashInferCutedslMxfp8LinearKernel(Mxfp8LinearKernel):
weight_scale = layer.weight_scale
K, N = weight.shape
min_dim = 128
assert min_dim <= K, (
f"mm_mxfp8 requires K >= {min_dim}, got K={K}. "
f"in_features is too small for mm_mxfp8."
)
assert K % MXFP8_BLOCK_SIZE == 0, (
f"mm_mxfp8 requires K to be divisible by {MXFP8_BLOCK_SIZE}, got K={K}."
)
assert min_dim <= N, (
f"mm_mxfp8 requires N >= {min_dim}, got N={N}. "
f"out_features is too small for mm_mxfp8."
)
qa = as_quantized_activation(x, self.input_quant_key())
if qa is not None:
input_mxfp8, input_scale = qa.data, qa.scale
@@ -3,6 +3,10 @@
import torch
from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
MXFP8_BLOCK_SIZE,
)
from .Mxfp8LinearKernel import Mxfp8LinearKernel, Mxfp8LinearLayerConfig
@@ -23,6 +27,16 @@ class MarlinMxfp8LinearKernel(Mxfp8LinearKernel):
@classmethod
def can_implement(cls, c: Mxfp8LinearLayerConfig) -> tuple[bool, str | None]:
N, K = c.weight_shape
if N <= 0 or K <= 0:
return False, f"Marlin MXFP8 requires positive N and K, got N={N}, K={K}."
if K % MXFP8_BLOCK_SIZE != 0:
return (
False,
f"Marlin MXFP8 requires K to be divisible by {MXFP8_BLOCK_SIZE}, "
f"got K={K}.",
)
# Tile misalignment is fixed by zero-padding at weight prep.
return True, None
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
@@ -33,9 +33,6 @@ class CompressedTensorsW8A8Mxfp8(CompressedTensorsScheme):
- Activations dynamically quantized to MXFP8 during inference
"""
def __init__(self):
self.kernel = init_mxfp8_linear_kernel()
@classmethod
def get_min_capability(cls) -> int:
return 75
@@ -79,6 +76,8 @@ class CompressedTensorsW8A8Mxfp8(CompressedTensorsScheme):
)
layer.register_parameter("weight_scale", weight_scale)
self.kernel = init_mxfp8_linear_kernel(weight_shape=layer.weight.shape)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
self.kernel.process_weights_after_loading(layer)
@@ -17,9 +17,6 @@ from .inc_scheme import INCLinearScheme
class INCMxfp8LinearScheme(INCLinearScheme):
def __init__(self) -> None:
self.kernel = init_mxfp8_linear_kernel()
@classmethod
def get_min_capability(cls) -> int:
return 75
@@ -72,6 +69,8 @@ class INCMxfp8LinearScheme(INCLinearScheme):
)
layer.register_parameter("weight_scale", weight_scale)
self.kernel = init_mxfp8_linear_kernel(weight_shape=layer.weight.shape)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
self.kernel.process_weights_after_loading(layer)
@@ -2320,7 +2320,8 @@ def select_linear_kernel(
return init_nvfp4_linear_kernel(use_a16=spec.activation is None)
if w.scale.dtype == MXFP8_SCALE_DTYPE:
return init_mxfp8_linear_kernel(
bmm_batch_size=getattr(layer, "bmm_batch_size", None)
weight_shape=weight_shape or layer.weight.shape,
bmm_batch_size=getattr(layer, "bmm_batch_size", None),
)
# fp8 family: init_fp8 routes block-vs-plain itself off the activation key,
# and needs a real key -- weight-only fp8 is not a ModelOpt format.
@@ -2526,7 +2527,10 @@ class ModelOptLinearMethod(LinearMethodBase):
def process_weights_after_loading(self, layer) -> None:
if self.spec.weight == kMxfp8Static and getattr(layer, "is_bmm", False):
self.kernel = init_mxfp8_linear_kernel(bmm_batch_size=layer.bmm_batch_size)
self.kernel = init_mxfp8_linear_kernel(
weight_shape=(layer.weight.shape[-2], layer.weight.shape[-1]),
bmm_batch_size=layer.bmm_batch_size,
)
if is_weights_pre_processed():
if not self.supports_pre_processed_weights:
raise RuntimeError(
@@ -42,10 +42,6 @@ class Mxfp8OnlineLinearMethod(OnlineLinearBase):
FP8 with block-32 scales) during weight loading.
"""
def __init__(self):
super().__init__()
self.kernel = init_mxfp8_linear_kernel()
def create_weights(
self,
layer: torch.nn.Module,
@@ -73,6 +69,8 @@ class Mxfp8OnlineLinearMethod(OnlineLinearBase):
**extra_weight_attrs,
)
self.kernel = init_mxfp8_linear_kernel(weight_shape=layer.weight.shape)
def process_weights_after_loading(self, layer: Module) -> None:
if getattr(layer, "_already_called_process_weights_after_loading", False):
return