[Quantization] Select linear backends per quantization (#51204)
Signed-off-by: Netanel Haber <58652339+netanel-haber@users.noreply.github.com>
Netanel Haber committed
Sep 14, 2026 at 23:24 UTC
d4ee7fe7a98b3012f64309e18dd425e565069a01
6 files changed
+154
-17
docs/features/quantization/README.md
+17
@@ -23,6 +23,23 @@ The following are the supported quantization formats for vLLM:
23
- [TorchAO](torchao.md)
24
- [FP8 ViT Encoder Attention](fp8_vit_attn.md)
25
26
+## Selecting Linear Backends per Quantization
27
+
28
+`--linear-backend` selects one backend for all quantized linear layers. For
29
+mixed-precision models that use more than one linear quantization scheme, use
30
+`linear_backend_per_quant` to override the backend for individual schemes:
31
+
32
+```bash
33
+vllm serve <model> \
34
+ --linear-backend cutlass \
35
+ --kernel-config '{"linear_backend_per_quant":{"nvfp4_w4a16":"humming"}}'
36
+```
37
+
38
+Here, NVFP4 W4A16 linear layers use Humming, while all other quantized linear
39
+layers use CUTLASS. Per-quantization overrides take precedence over
40
+`--linear-backend`; schemes without an override continue to use the global
41
+setting, including automatic selection when it is `auto`.
42
+
43
## Supported Hardware
44
45
The table below shows the compatibility of various quantization implementations with different hardware platforms in vLLM:
tests/kernels/quantization/test_scaled_mm_kernel_selection.py
+70
@@ -12,20 +12,90 @@ from unittest.mock import patch
12
import pytest
13
import torch
14
15
+from vllm.config import KernelConfig, VllmConfig, set_current_vllm_config
16
from vllm.model_executor.kernels.linear import (
17
AiterInt8ScaledMMLinearKernel,
18
CPUInt8ScaledMMLinearKernel,
19
+ HummingFP8ScaledMMLinearKernel,
20
Int8ScaledMMLinearKernel,
21
Int8ScaledMMLinearLayerConfig,
22
ScaledMMLinearKernel,
23
+ _get_linear_backend,
24
+ _resolve_backend_kernels,
25
+ init_fp8_linear_kernel,
26
init_int8_linear_kernel,
27
register_linear_kernel,
28
)
29
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
30
+ kFp8StaticTensorSym,
31
+)
32
from vllm.platforms import PlatformEnum
33
34
pytestmark = pytest.mark.cpu_test
35
36
37
+def test_auto_linear_backend_is_unchanged_without_overrides():
38
+ kernels = [object]
39
+ quantizations = (
40
+ "fp8_block_w8a8",
41
+ "fp8_w8a8",
42
+ "int8_w8a8",
43
+ "mixed_precision",
44
+ "mxfp8",
45
+ "mxfp4",
46
+ "mxfp6",
47
+ "w8a16_fp8",
48
+ "nvfp4_w4a4",
49
+ "nvfp4_w4a16",
50
+ )
51
+ config = VllmConfig(kernel_config=KernelConfig(linear_backend="auto"))
52
+
53
+ with set_current_vllm_config(config):
54
+ for quantization in quantizations:
55
+ assert _get_linear_backend(quantization=quantization) == "auto"
56
+ assert (
57
+ _resolve_backend_kernels(kernels, "test", quantization=quantization)
58
+ is kernels
59
+ )
60
+
61
+
62
+def test_linear_backend_override_is_quantization_specific():
63
+ config = VllmConfig(
64
+ kernel_config=KernelConfig(
65
+ linear_backend="cutlass",
66
+ linear_backend_per_quant={"nvfp4_w4a16": "humming"},
67
+ )
68
+ )
69
+
70
+ with set_current_vllm_config(config):
71
+ assert _get_linear_backend(quantization="nvfp4_w4a16") == "humming"
72
+ assert _get_linear_backend(quantization="nvfp4_w4a4") == "cutlass"
73
+ assert _get_linear_backend(quantization="fp8_w8a8") == "cutlass"
74
+
75
+
76
+@patch.object(HummingFP8ScaledMMLinearKernel, "is_supported", return_value=(True, None))
77
+@patch("vllm.model_executor.kernels.linear.current_platform")
78
+def test_fp8_linear_backend_override(platform_mock, _):
79
+ platform_mock._enum = PlatformEnum.CUDA
80
+ config = VllmConfig(
81
+ kernel_config=KernelConfig(
82
+ linear_backend="cutlass",
83
+ linear_backend_per_quant={"fp8_w8a8": "humming"},
84
+ )
85
+ )
86
+
87
+ with set_current_vllm_config(config):
88
+ kernel = init_fp8_linear_kernel(
89
+ activation_quant_key=kFp8StaticTensorSym,
90
+ weight_quant_key=kFp8StaticTensorSym,
91
+ input_dtype=torch.bfloat16,
92
+ out_dtype=torch.bfloat16,
93
+ weight_shape=(128, 128),
94
+ )
95
+
96
+ assert type(kernel) is HummingFP8ScaledMMLinearKernel
97
+
98
+
99
def test_is_supported_is_abstract():
100
"""Test that is_supported() is properly defined as abstract."""
101
assert issubclass(ScaledMMLinearKernel, ABC)
tests/model_executor/kernels/test_b12x_linear.py
+6
-2
@@ -115,7 +115,9 @@ def test_b12x_backend_registration_priority_and_selection(
115
assert names.index(before) < names.index(kernel_cls.__name__) < names.index(after)
116
117
monkeypatch.setattr(linear_mod.current_platform, "_enum", PlatformEnum.CUDA)
118
- monkeypatch.setattr(linear_mod, "_get_linear_backend", lambda: "b12x")
118
+ monkeypatch.setattr(
119
+ linear_mod, "_get_linear_backend", lambda *, quantization: "b12x"
120
+ )
121
monkeypatch.setattr(
122
kernel_cls,
123
"is_supported",
@@ -784,7 +786,9 @@ def test_b12x_backend_preserves_w4a16_fallback(monkeypatch) -> None:
786
import vllm.model_executor.kernels.linear as linear_mod
787
788
monkeypatch.setattr(linear_mod.current_platform, "_enum", PlatformEnum.CUDA)
787
- monkeypatch.setattr(linear_mod, "_get_linear_backend", lambda: "b12x")
789
+ monkeypatch.setattr(
790
+ linear_mod, "_get_linear_backend", lambda *, quantization: "b12x"
791
+ )
792
monkeypatch.setitem(
793
linear_mod._POSSIBLE_NVFP4_KERNELS,
794
PlatformEnum.CUDA,
vllm/config/kernel.py
+9
@@ -326,6 +326,13 @@ class KernelConfig:
326
- "xpu_woq": Use XPU kernels for weight-only quantization (e.g. W8A16)
327
"""
328
329
+ linear_backend_per_quant: dict[str, LinearBackend] | None = Field(
330
+ default=None, min_length=1
331
+ )
332
+ """Backend overrides keyed by linear quantization scheme. Overrides take
333
+ precedence over ``linear_backend``; for example,
334
+ ``{"nvfp4_w4a16": "humming"}``."""
335
+
336
@field_validator("moe_backend", mode="before")
337
@classmethod
338
def _normalize_moe_backend(cls, value: Any) -> Any:
@@ -359,6 +366,8 @@ class KernelConfig:
366
"enable_flashinfer_autotune",
367
"ir_op_priority", # handled separately below
368
}
369
+ if self.linear_backend_per_quant is None:
370
+ ignored_factors.add("linear_backend_per_quant")
371
factors = get_hash_factors(self, ignored_factors)
372
factors["ir_op_priority"] = self.ir_op_priority.compute_hash()
373
return hash_factors(factors)
vllm/model_executor/kernels/linear/__init__.py
+51
-14
@@ -235,14 +235,19 @@ from vllm.platforms import PlatformEnum, current_platform
235
logger = init_logger(__name__)
236
237
238
-def _get_linear_backend() -> str:
238
+def _get_linear_backend(*, quantization: str) -> str:
239
"""Get the linear_backend setting from the current vllm config."""
240
from vllm.config import get_current_vllm_config_or_none
241
242
- config = get_current_vllm_config_or_none()
243
- if config is not None:
244
- return config.kernel_config.linear_backend
245
- return "auto"
242
+ if (config := get_current_vllm_config_or_none()) is None:
243
+ return "auto"
244
+ overrides = config.kernel_config.linear_backend_per_quant
245
+ if overrides is not None and (override := overrides.get(quantization)):
246
+ logger.info_once(
247
+ "Applied linear backend override for %r: %r", quantization, override
248
+ )
249
+ return override
250
+ return config.kernel_config.linear_backend
251
252
253
# Kernel classes covered by each --linear-backend value.
@@ -370,6 +375,8 @@ def _filter_kernels_by_backend(
375
def _resolve_backend_kernels(
376
kernels: list[type],
377
layer_desc: str,
378
+ *,
379
+ quantization: str,
380
) -> list[type]:
381
"""Apply --linear-backend filtering to one layer type's kernel list.
382
@@ -380,7 +387,7 @@ def _resolve_backend_kernels(
387
layer types (e.g. NVFP4 MoE projections next to FP8 attention
388
projections).
389
"""
383
- linear_backend = _get_linear_backend()
390
+ linear_backend = _get_linear_backend(quantization=quantization)
391
if linear_backend == "auto":
392
return kernels
393
filtered = _filter_kernels_by_backend(linear_backend, kernels)
@@ -625,6 +632,8 @@ def choose_scaled_mm_linear_kernel(
632
possible_kernels: dict[PlatformEnum, list[type[_KernelT]]],
633
compute_capability: int | None = None,
634
force_kernel: type[_KernelT] | None = None,
635
+ *,
636
+ quantization: str,
637
) -> type[_KernelT]:
638
"""
639
Choose a _KernelT that can implement the given config for the
@@ -642,6 +651,7 @@ def choose_scaled_mm_linear_kernel(
651
force_kernel (Optional[type[_KernelT]]): An Optional forced kernel to override
652
the possible_kernels if it can be implemented. If None, it will only try the
653
possible kernels.
654
+ quantization: Quantization scheme used to select a backend override.
655
656
Raises:
657
ValueError: If no kernel can implement the given config.
@@ -668,7 +678,11 @@ def choose_scaled_mm_linear_kernel(
678
platform_kernels = possible_kernels.get(current_platform._enum, [])
679
680
# Apply --linear-backend filtering when set.
671
- platform_kernels = _resolve_backend_kernels(platform_kernels, "scaled-mm")
681
+ platform_kernels = _resolve_backend_kernels(
682
+ platform_kernels,
683
+ "scaled-mm",
684
+ quantization=quantization,
685
+ )
686
687
for kernel in platform_kernels:
688
is_supported_and_can_implement, failure_reason = (
@@ -705,6 +719,7 @@ def init_fp8_linear_kernel(
719
kernel_type = choose_scaled_mm_linear_kernel(
720
config=scaled_mm_linear_kernel_config,
721
possible_kernels=_POSSIBLE_FP8_BLOCK_KERNELS, # type: ignore[misc]
722
+ quantization="fp8_block_w8a8",
723
force_kernel=force_kernel,
724
)
725
if module_name:
@@ -736,6 +751,7 @@ def init_fp8_linear_kernel(
751
kernel_type = choose_scaled_mm_linear_kernel(
752
config=scaled_mm_linear_kernel_config,
753
possible_kernels=_POSSIBLE_FP8_KERNELS, # type: ignore[arg-type]
754
+ quantization="fp8_w8a8",
755
force_kernel=force_kernel,
756
)
757
if module_name:
@@ -772,6 +788,7 @@ def init_int8_linear_kernel(
788
kernel_type = choose_scaled_mm_linear_kernel(
789
config,
790
_POSSIBLE_INT8_KERNELS,
791
+ quantization="int8_w8a8",
792
)
793
794
logger.info_once(
@@ -824,7 +841,11 @@ def choose_mp_linear_kernel(
841
platform_kernels = _POSSIBLE_KERNELS.get(current_platform._enum, [])
842
843
# Apply --linear-backend filtering when set.
827
- platform_kernels = _resolve_backend_kernels(platform_kernels, "mixed-precision")
844
+ platform_kernels = _resolve_backend_kernels(
845
+ platform_kernels,
846
+ "mixed-precision",
847
+ quantization="mixed_precision",
848
+ )
849
850
failure_reasons = []
851
for kernel in platform_kernels:
@@ -875,7 +896,11 @@ def init_mxfp8_linear_kernel(*, bmm_batch_size: int | None = None) -> Mxfp8Linea
896
possible = list(_POSSIBLE_MXFP8_KERNELS.get(platform, []))
897
898
# Apply --linear-backend filtering when set.
878
- possible = _resolve_backend_kernels(possible, "MXFP8")
899
+ possible = _resolve_backend_kernels(
900
+ possible,
901
+ "MXFP8",
902
+ quantization="mxfp8",
903
+ )
904
905
failure_reasons = []
906
for kernel_cls in possible:
@@ -917,7 +942,11 @@ def init_mxfp4_linear_kernel(
942
possible = list(_POSSIBLE_MXFP4_KERNELS.get(platform, []))
943
944
# Apply --linear-backend filtering when set.
920
- possible = _resolve_backend_kernels(possible, "MXFP4")
945
+ possible = _resolve_backend_kernels(
946
+ possible,
947
+ "MXFP4",
948
+ quantization="mxfp4",
949
+ )
950
951
failure_reasons = []
952
for kernel_cls in possible:
@@ -957,7 +986,7 @@ def init_mxfp6_linear_kernel(
986
activation_quant_key=activation_quant_key,
987
)
988
960
- linear_backend = _get_linear_backend()
989
+ linear_backend = _get_linear_backend(quantization="mxfp6")
990
991
platform = current_platform._enum
992
possible = list(_POSSIBLE_MXFP6_KERNELS.get(platform, []))
@@ -1017,7 +1046,10 @@ def init_wfp8_a16_linear_kernel(
1046
)
1047
1048
kernel_type = choose_scaled_mm_linear_kernel(
1020
- config, _POSSIBLE_WFP8A16_KERNELS, force_kernel=force_kernel
1049
+ config,
1050
+ _POSSIBLE_WFP8A16_KERNELS,
1051
+ quantization="w8a16_fp8",
1052
+ force_kernel=force_kernel,
1053
)
1054
1055
if module_name:
@@ -1048,7 +1080,8 @@ def init_nvfp4_linear_kernel(use_a16: bool = False) -> NvFp4LinearKernel:
1080
# batch-invariant CUTLASS implementation when available, otherwise fall
1081
# back to emulation. It overrides --linear-backend.
1082
force_kernel: type[NvFp4LinearKernel] | None = None
1051
- linear_backend = _get_linear_backend()
1083
+ quantization = "nvfp4_w4a16" if use_a16 else "nvfp4_w4a4"
1084
+ linear_backend = _get_linear_backend(quantization=quantization)
1085
if envs.VLLM_BATCH_INVARIANT:
1086
bi_supported, reason = CutlassNvFp4LinearKernel.is_supported()
1087
if bi_supported:
@@ -1110,7 +1143,11 @@ def init_nvfp4_linear_kernel(use_a16: bool = False) -> NvFp4LinearKernel:
1143
possible = [kernel for kernel in possible if kernel in a16_kernels]
1144
1145
# Apply --linear-backend filtering when set.
1113
- possible = _resolve_backend_kernels(possible, "NVFP4")
1146
+ possible = _resolve_backend_kernels(
1147
+ possible,
1148
+ "NVFP4",
1149
+ quantization=quantization,
1150
+ )
1151
1152
failure_reasons = []
1153
for kernel_cls in possible:
vllm/model_executor/kernels/linear/mxfp4/aiter.py
+1
-1
@@ -158,7 +158,7 @@ class AiterMxfp4LinearKernel(MxFp4LinearKernel):
158
from vllm._aiter_ops import is_aiter_found_and_supported
159
from vllm.model_executor.kernels.linear import _get_linear_backend
160
161
- linear_backend = _get_linear_backend()
161
+ linear_backend = _get_linear_backend(quantization="mxfp4")
162
163
if (
164
current_platform.is_rocm()