Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/kernels/core/test_fused_q_kv_rmsnorm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand Down
12 changes: 9 additions & 3 deletions tests/kernels/quantization/test_flashinfer_mxfp8_trtllm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion tests/kernels/quantization/test_marlin_tile_padding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
135 changes: 135 additions & 0 deletions tests/kernels/quantization/test_mxfp8_kernel_selection.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions tests/kernels/quantization/test_rocm_mxfp8_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions tests/kernels/test_minimax_m3_amd_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -482,7 +482,9 @@ def make_layer():
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
Expand Down
4 changes: 2 additions & 2 deletions tests/model_executor/kernels/test_b12x_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"MarlinMxfp8LinearKernel",
"EmulationMxfp8LinearKernel",
init_mxfp8_linear_kernel,
{},
{"weight_shape": (2048, 2048)},
),
(
B12xTensorFP8ScaledMMLinearKernel,
Expand Down Expand Up @@ -365,7 +365,7 @@ def mm(

def test_b12x_mxfp8_can_implement_supported_config() -> None:
can_implement, reason = B12xMxfp8LinearKernel.can_implement(
Mxfp8LinearLayerConfig()
Mxfp8LinearLayerConfig(weight_shape=(256, 512))
)

assert can_implement
Expand Down
4 changes: 2 additions & 2 deletions tests/quantization/test_auto_round.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,7 @@ def apply_weights(self, layer, x, bias=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",
Expand Down Expand Up @@ -1288,7 +1288,7 @@ def apply_weights(self, layer, x, bias=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()

Expand Down
11 changes: 8 additions & 3 deletions vllm/model_executor/kernels/linear/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
5 changes: 5 additions & 0 deletions vllm/model_executor/kernels/linear/mxfp8/Mxfp8LinearKernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment thread
samuelkim7 marked this conversation as resolved.
bmm_batch_size: int | None = None


Expand Down
53 changes: 24 additions & 29 deletions vllm/model_executor/kernels/linear/mxfp8/flashinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
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+)."""

Expand All @@ -43,6 +57,15 @@ def is_supported(

@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
Comment thread
samuelkim7 marked this conversation as resolved.

def input_quant_key(self) -> QuantKey:
Expand Down Expand Up @@ -72,20 +95,6 @@ def apply_weights(
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
Expand Down Expand Up @@ -136,7 +145,7 @@ def is_supported(

@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.
Expand Down Expand Up @@ -166,20 +175,6 @@ def apply_weights(
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
Expand Down
Loading
Loading