[Quantization] Honor `--linear-backend` for ModelOpt W4A16 (#50273)
Signed-off-by: Netanel Haber <58652339+netanel-haber@users.noreply.github.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Netanel Haber committed
Jul 30, 2026 at 23:19 UTC
70bd10930bbb91af38fc243c53df4b94da80a8c9
3 files changed
+42
-21
tests/quantization/test_modelopt.py
+23
-4
@@ -13,7 +13,12 @@ import pytest
13
import torch
14
15
from tests.quantization.utils import is_quant_method_supported
16
+from vllm.config import VllmConfig, set_current_vllm_config
17
from vllm.config.model import ModelConfig
18
+from vllm.model_executor.kernels.linear import (
19
+ HummingNvFp4LinearKernel,
20
+ MarlinNvFp4LinearKernel,
21
+)
22
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
23
from vllm.model_executor.layers.quantization.modelopt import (
24
ModelOptFp8Config,
@@ -21,6 +26,7 @@ from vllm.model_executor.layers.quantization.modelopt import (
26
ModelOptMxFp8Config,
27
ModelOptNvFp4Config,
28
ModelOptNvFp4LinearMethod,
29
+ ModelOptNvFp4W4A16LinearMethod,
30
)
31
from vllm.model_executor.layers.vocab_parallel_embedding import (
32
ParallelLMHead,
@@ -468,15 +474,13 @@ def test_modelopt_nvfp4_config_dispatches_w4a4_method():
474
475
476
def test_modelopt_nvfp4_config_dispatches_w4a16_method():
471
- """``quant_method="W4A16_NVFP4"`` routes to the new
477
+ """``quant_method="W4A16_NVFP4"`` routes to
478
``ModelOptNvFp4W4A16LinearMethod`` instead of the W4A4 sibling.
479
480
Mirrors the FP8 dispatch precedent (``ModelOptFp8Config`` selects
481
one of three FP8 LinearMethods on ``quant_method``); a regression
482
here would mean a W4A16 NVFP4 checkpoint silently loaded under the
477
- W4A4 method, which would try to register an ``input_scale`` runtime
478
- parameter and (more importantly) call the cutlass W4A4 NVFP4 GEMM
479
- instead of FP4 Marlin.
483
+ W4A4 activation-quantization path.
484
"""
485
from vllm.model_executor.layers.quantization.modelopt import (
486
ModelOptNvFp4Config,
@@ -495,6 +499,21 @@ def test_modelopt_nvfp4_config_dispatches_w4a16_method():
499
assert config.quant_method == "W4A16_NVFP4"
500
501
502
+@pytest.mark.parametrize(
503
+ ("linear_backend", "kernel_cls"),
504
+ [("auto", MarlinNvFp4LinearKernel), ("humming", HummingNvFp4LinearKernel)],
505
+)
506
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA only")
507
+def test_modelopt_w4a16_respects_linear_backend(linear_backend, kernel_cls):
508
+ vllm_config = VllmConfig()
509
+ vllm_config.kernel_config.linear_backend = linear_backend
510
+ with set_current_vllm_config(vllm_config):
511
+ method = ModelOptNvFp4W4A16LinearMethod(
512
+ ModelOptNvFp4Config(quant_method="W4A16_NVFP4")
513
+ )
514
+ assert isinstance(method.kernel, kernel_cls)
515
+
516
+
517
@pytest.mark.parametrize(
518
"quant_method, expected_use_a16, act_key_is_none",
519
[
vllm/model_executor/kernels/linear/__init__.py
+5
@@ -891,6 +891,7 @@ def init_nvfp4_linear_kernel(use_a16: bool = False) -> NvFp4LinearKernel:
891
"""Select and instantiate the best NVFP4 linear kernel for the
892
current platform."""
893
config = NvFp4LinearLayerConfig()
894
+ a16_kernels = (MarlinNvFp4LinearKernel, HummingNvFp4LinearKernel)
895
896
# VLLM_BATCH_INVARIANT forces deterministic execution. Prefer the
897
# batch-invariant CUTLASS implementation when available, otherwise fall
@@ -931,6 +932,8 @@ def init_nvfp4_linear_kernel(use_a16: bool = False) -> NvFp4LinearKernel:
932
force_kernel = MarlinNvFp4LinearKernel
933
934
if force_kernel is not None:
935
+ if use_a16 and force_kernel not in a16_kernels:
936
+ raise ValueError(f"{force_kernel.__name__} does not support W4A16")
937
is_supported, reason = force_kernel.is_supported()
938
if not is_supported:
939
raise ValueError(
@@ -943,6 +946,8 @@ def init_nvfp4_linear_kernel(use_a16: bool = False) -> NvFp4LinearKernel:
946
# Auto-select from registry (or --linear-backend filtered).
947
platform = current_platform._enum
948
possible = list(_POSSIBLE_NVFP4_KERNELS.get(platform, []))
949
+ if use_a16:
950
+ possible = [kernel for kernel in possible if kernel in a16_kernels]
951
952
# Apply --linear-backend filtering when set.
953
if linear_backend != "auto":
vllm/model_executor/layers/quantization/modelopt.py
+14
-17
@@ -12,8 +12,6 @@ import vllm.model_executor.layers.fused_moe.modular_kernel as mk
12
from vllm.config import get_current_vllm_config
13
from vllm.logger import init_logger
14
from vllm.model_executor.kernels.linear import (
15
- MarlinNvFp4LinearKernel,
16
- NvFp4LinearLayerConfig,
15
init_fp8_linear_kernel,
16
init_mxfp8_linear_kernel,
17
init_nvfp4_linear_kernel,
@@ -1247,16 +1245,16 @@ class ModelOptNvFp4W4A16LinearMethod(LinearMethodBase):
1245
"""Linear method for ModelOpt NVFP4 W4A16.
1246
1247
4-bit NVFP4 weights, fp16/bf16 activations. Loads ModelOpt-style names
1250
- directly (no on-disk conversion) and dispatches to the FP4 Marlin GEMM:
1248
+ directly (no on-disk conversion) and dispatches to a W4A16 GEMM:
1249
1250
weight uint8 packed NVFP4 (2 nibbles/byte along input dim)
1251
weight_scale fp8-e4m3 per 16-elem group along input dim
1252
weight_scale_2 fp32 per-tensor global scale = amax / (6.0 * 448.0)
1253
1256
- No activation quantization. Marlin expects the global scale in the same
1257
- form ModelOpt stores (amax/2688), so we rename weight_scale_2 ->
1258
- weight_global_scale **without reciprocation** -- the CT W4A16 path
1259
- reciprocates only because CT stores the inverse on disk.
1254
+ No activation quantization. ModelOpt stores the global scale as
1255
+ amax/2688, so we rename weight_scale_2 -> weight_global_scale without
1256
+ reciprocation. The selected kernel converts it to its runtime format.
1257
+ The CT W4A16 path reciprocates because CT stores the inverse on disk.
1258
1259
We also register a placeholder input_scale parameter so that W4A4-shaped
1260
checkpoints (which contain *_proj.input_scale tensors) can be loaded
@@ -1267,17 +1265,12 @@ class ModelOptNvFp4W4A16LinearMethod(LinearMethodBase):
1265
1266
def __init__(self, quant_config: ModelOptNvFp4Config) -> None:
1267
self.quant_config = quant_config
1270
- # Vestigial slot mirrored from ModelOptNvFp4LinearMethod: the parent
1271
- # config's get_quant_method only fills marlin_input_dtype when
1272
- # backend == "marlin"; we don't set that since we pin the kernel
1273
- # below, but we keep the attribute for shape parity.
1268
self.marlin_input_dtype = None
1275
- # Direct-instantiate the Marlin NVFP4 adapter rather than going through
1276
- # init_nvfp4_linear_kernel(): the latter's priority list returns a
1277
- # cutlass W4A4 kernel as first-pick on this hardware, which would
1278
- # silently try to quantize activations (we have no input_scale). For
1279
- # W4A16 there is exactly one valid kernel, so we pin it.
1280
- self.kernel = MarlinNvFp4LinearKernel(NvFp4LinearLayerConfig())
1269
+ # `init_nvfp4_linear_kernel(use_a16=True)` is best of both worlds:
1270
+ # 1. `use_a16=True` forces `Marlin`: https://github.com/vllm-project/vllm/commit/e68988a#diff-7135ab92aa94dfacb1ad3c77fc13f9c4ffe0b977f8eac5d86c2afe243e5f92a6R842-R889
1271
+ # for `--linear-backend=auto`, avoiding a W4A4 kernel that requires input_scale.
1272
+ # 2. Specifying e.g. `--linear-backend=humming` will override.
1273
+ self.kernel = init_nvfp4_linear_kernel(use_a16=True)
1274
1275
def create_weights(
1276
self,
@@ -1300,6 +1293,7 @@ class ModelOptNvFp4W4A16LinearMethod(LinearMethodBase):
1293
layer.logical_widths = output_partition_sizes
1294
layer.input_size_per_partition = input_size_per_partition
1295
layer.output_size_per_partition = output_size_per_partition
1296
+ layer.output_partition_sizes = output_partition_sizes
1297
1298
if input_size_per_partition % 16 != 0:
1299
raise ValueError(
@@ -1355,6 +1349,9 @@ class ModelOptNvFp4W4A16LinearMethod(LinearMethodBase):
1349
layer.register_parameter("input_scale", input_scale)
1350
1351
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1352
+ if not hasattr(layer, "has_bias"):
1353
+ layer.has_bias = getattr(layer, "bias", None) is not None
1354
+
1355
# Discard the input_scale placeholder. Whether it carries values
1356
# (W4A4 ckpt loaded as W4A16) or is uninitialized (native W4A16
1357
# ckpt), W4A16 mode does not quantize activations, so this is unused.