[Online quantization] Add targeted online quantization configuration based on user patterns (#51285)

fxmarty-amd committed Sep 2, 2026 at 14:55 UTC 7894394b0759ba0f2d9c9cca249a6e49ecdd814f
11 files changed +892 -172
docs/features/quantization/online.md
+47 -3
@@ -67,7 +67,7 @@ quantization_config:
67 moe:
68 weight: <name>
69 activation: <name>
70 - ignore: [<layer-name-or-regex>, ...]
70 + ignore: [<layer-name-or-regex-or-fnmatch-pattern>, ...]
71 ```
72
73 `linear` and `moe` accept a full `{weight, activation}` dict, or a bare
@@ -133,7 +133,7 @@ llm = LLM(
133
134 ### Excluding Layers from Quantization
135
136 -Use the `ignore` parameter to skip specific layers. It accepts exact layer names and regex patterns (prefixed with `re:`):
136 +Use the `ignore` parameter to skip specific layers. It accepts exact layer names, regex patterns (prefixed with `re:`), and patterns understood by [`fnmatch.fnmatch`](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch):
137
138 ```python
139 from vllm import LLM
@@ -147,10 +147,54 @@ llm = LLM(
147 "model.layers.1.self_attn.o_proj",
148 # regex: skip all QKV projections
149 "re:.*[qkv]_proj",
150 + # fnmatch: skip all MoE experts
151 + "*mlp.experts*",
152 ],
153 },
154 )
155 ```
156
157 !!! note
156 - For fused layers (e.g., `qkv_proj` which fuses `q_proj`, `k_proj`, `v_proj`), the ignore pattern must match the **unfused** shard names (`q_proj`, `k_proj`, `v_proj`), not the fused name.
158 + For fused layers (e.g., `qkv_proj` which fuses `q_proj`, `k_proj`, `v_proj`), patterns may match the fused name directly or all of its unfused shard names.
159 +
160 +### Fine-Grained Per-Layer Quantization Schemes
161 +
162 +Use the `targets` parameter to apply different online shorthands to different layers, instead of one scheme applied everywhere via `linear`/`moe`. Keys are exact layer names, regex patterns (prefixed with `re:`), or patterns understood by [`fnmatch.fnmatch`](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch); values are shorthand names (`fp8_per_tensor`, `fp8_per_block`, `fp8_per_channel`, `mxfp8`, `int8_per_channel_weight_only`, `nvfp4_per_token`).
163 +
164 +Example:
165 +
166 +```python
167 +from vllm import LLM
168 +
169 +llm = LLM(
170 + "Qwen/Qwen3.5-35B-A3B",
171 + quantization="online",
172 + quantization_config={
173 + "targets": {
174 + # exact layer name
175 + "model.layers.0.self_attn.o_proj": "fp8_per_tensor",
176 + # regex: quantize all QKV projections
177 + r"re:.*self_attn\.qkv_proj.*": "mxfp8",
178 + # fnmatch: quantize all MoE experts
179 + "*mlp.experts*": "mxfp4",
180 + },
181 + },
182 +)
183 +```
184 +
185 +Or with the CLI:
186 +
187 +```bash
188 +vllm serve Qwen/Qwen3.5-35B-A3B \
189 + --quantization online \
190 + --quantization-config '{"targets":{"model.layers.0.self_attn.o_proj":"fp8_per_tensor","re:.*self_attn\\.qkv_proj.*":"mxfp8","*mlp.experts*":"mxfp4"}}'
191 +```
192 +
193 +!!! info
194 +
195 + - `targets` is mutually exclusive with online `linear` and `moe`: set one or the other, not both.
196 + - A layer that matches no `targets` pattern is left unchanged from its checkpoint dtype.
197 + - A layer name may not match both `targets` and `ignore`, raising an error.
198 + - A layer may not match more than one `targets` pattern, raising an error.
199 + - fnmatch-style patterns are supported only by online quantization; they are
200 + not applied to Quark or compressed-tensors configurations.
tests/quantization/test_config_utils.py new
+195
@@ -0,0 +1,195 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Tests quantization configuration matching utilities."""
4 +
5 +from unittest.mock import Mock
6 +
7 +import pytest
8 +
9 +from vllm.config.quantization import QuantizationConfigArgs
10 +from vllm.model_executor.layers.quantization.compressed_tensors.utils import (
11 + should_ignore_layer,
12 +)
13 +from vllm.model_executor.layers.quantization.online.base import (
14 + OnlineQuantizationConfig,
15 + _find_matching_targets,
16 +)
17 +from vllm.model_executor.layers.quantization.quark.utils import (
18 + should_ignore_layer as quark_should_ignore_layer,
19 +)
20 +from vllm.model_executor.layers.quantization.utils.config_utils import (
21 + find_matching_patterns,
22 + get_layer_name_after_index,
23 + is_equal_or_regex_match,
24 +)
25 +
26 +
27 +def test_is_equal_or_regex_match():
28 + assert is_equal_or_regex_match(
29 + "model.layers.0.mlp.down_proj", "model.layers.0.mlp.down_proj"
30 + )
31 + assert is_equal_or_regex_match("model.layers.0.mlp.down_proj", r"re:.*down_proj")
32 + assert not is_equal_or_regex_match("model.layers.0.mlp.down_proj", "other")
33 +
34 +
35 +def test_fnmatch_is_opt_in():
36 + layer_name = "model.layers.0.mlp.experts"
37 + pattern = "*mlp.experts*"
38 +
39 + assert not is_equal_or_regex_match(layer_name, pattern)
40 + assert is_equal_or_regex_match(layer_name, pattern, use_fnmatch=True)
41 + assert not should_ignore_layer(layer_name, [pattern])
42 + assert not quark_should_ignore_layer(layer_name, [pattern])
43 + assert should_ignore_layer(layer_name, [pattern], use_fnmatch=True)
44 +
45 +
46 +def test_online_targets_support_fnmatch_patterns():
47 + layer_name = "model.layers.0.mlp.experts"
48 + targets = {"*mlp.experts*": "mxfp4"}
49 +
50 + assert _find_matching_targets(layer_name, targets) == ["*mlp.experts*"]
51 +
52 +
53 +def test_online_ignore_supports_fnmatch_patterns():
54 + layer_name = "model.layers.0.mlp.experts"
55 + ignore = ["*mlp.experts*"]
56 + config = OnlineQuantizationConfig(
57 + QuantizationConfigArgs(targets={"*mlp.experts*": "mxfp4"}, ignore=ignore)
58 + )
59 +
60 + with pytest.raises(ValueError, match="matches both quantization_config.ignore"):
61 + config._dispatch_target(layer_name, Mock())
62 +
63 +
64 +@pytest.mark.parametrize(
65 + "layer_name,expected",
66 + [
67 + ("model.layers.1.self_attn.qkv_proj", "self_attn.qkv_proj"),
68 + ("model.layers.2.mlp.down_proj", "mlp.down_proj"),
69 + ("lm_head", "lm_head"),
70 + ],
71 +)
72 +def test_get_layer_name_after_index(layer_name, expected):
73 + assert get_layer_name_after_index(layer_name) == expected
74 +
75 +
76 +@pytest.mark.parametrize(
77 + "patterns",
78 + [
79 + [r"re:.*qkv_proj.*"],
80 + [r"re:.*\.[qkv]_proj$"],
81 + ],
82 + ids=["direct_fused_regex", "fused_shard_regexes"],
83 +)
84 +def test_find_matching_patterns_for_fused_regexes(patterns):
85 + layer_name = "model.layers.0.self_attn.qkv_proj"
86 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
87 +
88 + matches = find_matching_patterns(layer_name, patterns, fused_mapping)
89 + assert all(len(shard_matches) == 1 for shard_matches in matches)
90 +
91 +
92 +@pytest.mark.parametrize(
93 + "patterns,expected",
94 + [
95 + (
96 + [r"re:.*\.qkv_proj$"],
97 + [{r"re:.*\.qkv_proj$"}],
98 + ),
99 + (
100 + [r"re:.*\.q_proj$", r"re:.*\.k_proj$", r"re:.*\.v_proj$"],
101 + [
102 + {r"re:.*\.q_proj$"},
103 + {r"re:.*\.k_proj$"},
104 + {r"re:.*\.v_proj$"},
105 + ],
106 + ),
107 + ],
108 + ids=["direct_fused_match", "individual_shard_matches"],
109 +)
110 +def test_find_matching_patterns_distinguishes_direct_and_shard_matches(
111 + patterns, expected
112 +):
113 + layer_name = "model.layers.0.self_attn.qkv_proj"
114 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
115 +
116 + assert find_matching_patterns(layer_name, patterns, fused_mapping) == expected
117 + assert should_ignore_layer(layer_name, patterns, fused_mapping)
118 +
119 +
120 +def test_should_ignore_layer_rejects_partially_matched_fused_layer():
121 + layer_name = "model.layers.0.self_attn.qkv_proj"
122 + patterns = [r"re:.*\.q_proj$"]
123 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
124 +
125 + assert find_matching_patterns(layer_name, patterns, fused_mapping) == [
126 + {r"re:.*\.q_proj$"},
127 + set(),
128 + set(),
129 + ]
130 + with pytest.raises(ValueError, match="different quantization schemes"):
131 + should_ignore_layer(layer_name, patterns, fused_mapping)
132 +
133 +
134 +def test_should_ignore_layer_returns_false_when_no_fused_pattern_matches():
135 + layer_name = "model.layers.0.self_attn.qkv_proj"
136 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
137 +
138 + assert find_matching_patterns(layer_name, [], fused_mapping) == [
139 + set(),
140 + set(),
141 + set(),
142 + ]
143 + assert not should_ignore_layer(layer_name, [], fused_mapping)
144 +
145 +
146 +@pytest.mark.parametrize(
147 + "patterns",
148 + [
149 + [r"re:.*qkv_proj.*"],
150 + [r"re:.*\.[qkv]_proj$"],
151 + ],
152 + ids=["direct_fused_regex", "fused_shard_regexes"],
153 +)
154 +def test_ignore_and_targets_match_fused_regexes_identically(patterns):
155 + layer_name = "model.layers.0.self_attn.qkv_proj"
156 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
157 + targets = {pattern: "fp8_per_block" for pattern in patterns}
158 +
159 + assert should_ignore_layer(layer_name, patterns, fused_mapping)
160 + matches = _find_matching_targets(layer_name, targets, fused_mapping)
161 + assert len(matches) == 1
162 + assert targets[matches[0]] == "fp8_per_block"
163 +
164 +
165 +def test_ignore_allows_individually_matched_fused_shards():
166 + layer_name = "model.layers.0.self_attn.qkv_proj"
167 + patterns = [
168 + "model.layers.0.self_attn.q_proj",
169 + "model.layers.0.self_attn.k_proj",
170 + "model.layers.0.self_attn.v_proj",
171 + ]
172 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
173 +
174 + assert should_ignore_layer(layer_name, patterns, fused_mapping)
175 +
176 +
177 +@pytest.mark.parametrize(
178 + "patterns",
179 + [
180 + ["model.layers.0.self_attn.qkv_proj"],
181 + [r"re:.*\.qkv_proj$"],
182 + [
183 + "model.layers.0.self_attn.q_proj",
184 + "model.layers.0.self_attn.k_proj",
185 + "model.layers.0.self_attn.v_proj",
186 + ],
187 + ],
188 + ids=["direct_fused_name", "direct_fused_regex", "individual_shards"],
189 +)
190 +def test_quark_and_compressed_tensors_ignore_fused_layers_identically(patterns):
191 + layer_name = "model.layers.0.self_attn.qkv_proj"
192 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
193 +
194 + assert should_ignore_layer(layer_name, patterns, fused_mapping)
195 + assert quark_should_ignore_layer(layer_name, patterns, fused_mapping)
tests/quantization/test_online.py
+198 -5
@@ -18,10 +18,21 @@ from tests.quantization.utils import (
18 from vllm import _custom_ops as ops
19 from vllm._aiter_ops import rocm_aiter_ops
20 from vllm._custom_ops import scaled_fp4_quant
21 -from vllm.config.quantization import resolve_quantization_config
21 +from vllm.config import ModelConfig
22 +from vllm.config.quantization import (
23 + QuantizationConfigArgs,
24 + resolve_quantization_config,
25 +)
26 from vllm.forward_context import set_forward_context
27 from vllm.model_executor.layers.attention import Attention
24 -from vllm.model_executor.layers.linear import UnquantizedLinearMethod
28 +from vllm.model_executor.layers.linear import (
29 + ColumnParallelLinear,
30 + LinearBase,
31 + UnquantizedLinearMethod,
32 +)
33 +from vllm.model_executor.layers.quantization.online.base import (
34 + OnlineQuantizationConfig,
35 +)
36 from vllm.model_executor.layers.quantization.online.fp8 import (
37 Fp8PerBlockOnlineLinearMethod,
38 Fp8PerBlockOnlineMoEMethod,
@@ -49,6 +60,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
60 amax_for_tp_weight_quant,
61 weight_amax,
62 )
63 +from vllm.model_executor.layers.vocab_parallel_embedding import (
64 + VocabParallelEmbedding,
65 +)
66 +from vllm.model_executor.model_loader.base_loader import log_online_quantization
67 from vllm.model_executor.model_loader.dummy_loader import DummyModelLoader
68 from vllm.model_executor.models.granitemoe import (
69 GraniteMoeModel,
@@ -154,12 +169,23 @@ def test_online_nvfp4_reuses_kernel_when_weights_are_reprocessed(
169 Fp8PerBlockOnlineLinearMethod,
170 Fp8PerTensorOnlineMoEMethod,
171 ),
172 + # quantization='online' with per-layer target patterns
173 + (
174 + "online",
175 + {
176 + "targets": {
177 + r"re:.*self_attn\.o_proj": "fp8_per_block",
178 + r"re:.*block_sparse_moe\.experts": "fp8_per_tensor",
179 + }
180 + },
181 + Fp8PerBlockOnlineLinearMethod,
182 + Fp8PerTensorOnlineMoEMethod,
183 + ),
184 # ignore with direct layer name
185 (
186 "fp8_per_tensor",
160 - # qkv_proj is fused from q_proj/k_proj/v_proj, so currently the
161 - # ignore regex must match the unfused shard names
162 - # TODO(future PR): also make 're:.*qkv_proj.*' work
187 + # qkv_proj is fused from q_proj/k_proj/v_proj. The shard regex
188 + # remains supported alongside direct fused-name regexes.
189 {"ignore": ["model.layers.1.self_attn.o_proj", "re:.*[qkv]_proj"]},
190 Fp8PerTensorOnlineLinearMethod,
191 Fp8PerTensorOnlineMoEMethod,
@@ -171,6 +197,15 @@ def test_online_nvfp4_reuses_kernel_when_weights_are_reprocessed(
197 Mxfp4OnlineMoEMethod,
198 ),
199 ],
200 + ids=[
201 + "fp8_per_tensor",
202 + "fp8_per_block",
203 + "fp8_per_channel",
204 + "per_layer_kind_overrides",
205 + "targets",
206 + "ignore",
207 + "mxfp4",
208 + ],
209 )
210 @pytest.mark.parametrize(
211 "use_rocm_aiter", [True, False] if current_platform.is_rocm() else [False]
@@ -266,6 +301,12 @@ def test_online_quantization(
301 layer.self_attn.qkv_proj.quant_method, UnquantizedLinearMethod
302 )
303
304 + if isinstance(online_quant_args, dict) and "targets" in online_quant_args:
305 + for layer in model.model.layers:
306 + assert isinstance(
307 + layer.self_attn.qkv_proj.quant_method, UnquantizedLinearMethod
308 + )
309 +
310 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE)
311 positions = torch.arange(input_ids.numel(), device=DEVICE)
312 with set_forward_context(None, vllm_config, num_tokens=input_ids.numel()):
@@ -321,6 +362,158 @@ def test_online_quantization_loads_real_weights(vllm_runner, monkeypatch) -> Non
362 assert outputs
363
364
365 +@pytest.mark.skipif(
366 + not is_quant_method_supported("fp8"),
367 + reason="FP8 is not supported on this GPU type.",
368 +)
369 +@pytest.mark.parametrize(
370 + "targets,prefix,expected_method_cls,unmatched_prefix,expected_metadata",
371 + [
372 + (
373 + {r"re:.*self_attn\.o_proj": "fp8_per_block"},
374 + "model.layers.0.self_attn.o_proj",
375 + Fp8PerBlockOnlineLinearMethod,
376 + "model.layers.0.self_attn.qkv_proj",
377 + ("targets", "fp8_per_block", r"re:.*self_attn\.o_proj"),
378 + ),
379 + (
380 + {r"re:.*qkv_proj.*": "fp8_per_tensor"},
381 + "model.layers.0.self_attn.qkv_proj",
382 + Fp8PerTensorOnlineLinearMethod,
383 + "model.layers.0.self_attn.o_proj",
384 + ("targets", "fp8_per_tensor", r"re:.*qkv_proj.*"),
385 + ),
386 + (
387 + {r"re:.*[qkv]_proj": "fp8_per_tensor"},
388 + "model.layers.0.self_attn.qkv_proj",
389 + Fp8PerTensorOnlineLinearMethod,
390 + "model.layers.0.self_attn.o_proj",
391 + ("targets", "fp8_per_tensor", r"re:.*[qkv]_proj"),
392 + ),
393 + ],
394 + ids=["linear_regex", "direct_fused_regex", "legacy_fused_regex"],
395 +)
396 +def test_online_quantization_targets(
397 + default_vllm_config,
398 + dist_init,
399 + targets: dict[str, str],
400 + prefix: str,
401 + expected_method_cls,
402 + unmatched_prefix: str,
403 + expected_metadata: tuple[str, str, str],
404 +) -> None:
405 + """Target patterns select the real online linear methods."""
406 + default_vllm_config.model_config = ModelConfig()
407 + config = OnlineQuantizationConfig(QuantizationConfigArgs(targets=targets))
408 + config.packed_modules_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
409 +
410 + layer = ColumnParallelLinear(
411 + input_size=1,
412 + output_size=1,
413 + bias=False,
414 + disable_tp=True,
415 + )
416 +
417 + method = config.get_quant_method(layer, prefix)
418 + assert isinstance(method, expected_method_cls)
419 + assert config.quantized_layers == {prefix: expected_metadata}
420 +
421 + unmatched_method = config.get_quant_method(layer, unmatched_prefix)
422 + assert isinstance(unmatched_method, UnquantizedLinearMethod)
423 + assert config.quantized_layers == {prefix: expected_metadata}
424 +
425 +
426 +@pytest.mark.skipif(
427 + not is_quant_method_supported("fp8"),
428 + reason="FP8 is not supported on this GPU type.",
429 +)
430 +def test_online_quantization_records_global_config(
431 + default_vllm_config, dist_init
432 +) -> None:
433 + default_vllm_config.model_config = ModelConfig()
434 + config = OnlineQuantizationConfig(QuantizationConfigArgs(linear="fp8_per_block"))
435 + prefix = "model.layers.0.self_attn.o_proj"
436 + layer = ColumnParallelLinear(
437 + input_size=1,
438 + output_size=1,
439 + bias=False,
440 + disable_tp=True,
441 + )
442 +
443 + method = config.get_quant_method(layer, prefix)
444 +
445 + assert isinstance(method, Fp8PerBlockOnlineLinearMethod)
446 + assert config.quantized_layers == {
447 + prefix: ("linear", str(config.args.linear), None)
448 + }
449 +
450 +
451 +def test_online_quantization_targets_ignore_collision() -> None:
452 + """A targets/ignore collision is reported when the layer is dispatched."""
453 + config = OnlineQuantizationConfig(
454 + QuantizationConfigArgs(
455 + targets={"model.layers.0.self_attn.o_proj": "fp8_per_tensor"},
456 + ignore=["model.layers.0.self_attn.o_proj"],
457 + )
458 + )
459 + with pytest.raises(ValueError, match="matches both quantization_config.ignore"):
460 + config._dispatch_target(
461 + "model.layers.0.self_attn.o_proj", Mock(spec=LinearBase)
462 + )
463 +
464 +
465 +def test_online_quantization_targets_reject_unsupported_layer() -> None:
466 + """A targets match on a non-linear, non-MoE layer is rejected."""
467 + config = OnlineQuantizationConfig(
468 + QuantizationConfigArgs(targets={"lm_head": "fp8_per_tensor"})
469 + )
470 + lm_head = VocabParallelEmbedding(
471 + num_embeddings=1,
472 + embedding_dim=1,
473 + disable_tp=True,
474 + )
475 +
476 + with pytest.raises(
477 + ValueError,
478 + match=(
479 + "Layer lm_head was matched by quantization_config.targets "
480 + r"\(lm_head\), but online quantization is not supported for "
481 + "VocabParallelEmbedding."
482 + ),
483 + ):
484 + config.get_quant_method(lm_head, "lm_head")
485 +
486 +
487 +def test_log_online_quantization(default_vllm_config, monkeypatch) -> None:
488 + config = OnlineQuantizationConfig(QuantizationConfigArgs(linear="fp8_per_tensor"))
489 + config.quantized_layers = {
490 + "model.layers.0.mlp.down_proj": ("linear", "fp8_per_tensor", None),
491 + "model.layers.1.mlp.down_proj": ("linear", "fp8_per_tensor", None),
492 + "model.layers.0.self_attn.qkv_proj": (
493 + "targets",
494 + "mxfp4",
495 + r"re:.*qkv_proj.*",
496 + ),
497 + }
498 + default_vllm_config.quant_config = config
499 +
500 + logged_messages: list[str] = []
501 +
502 + def record_info(message: str, *args: object) -> None:
503 + logged_messages.append(message % args)
504 +
505 + monkeypatch.setattr(
506 + "vllm.model_executor.model_loader.base_loader.logger.info", record_info
507 + )
508 + log_online_quantization(default_vllm_config)
509 +
510 + assert logged_messages == [
511 + "Quantized 3 layers of types: mlp.down_proj: 2 (from linear: "
512 + "fp8_per_tensor); self_attn.qkv_proj: 1 (from targets: "
513 + "re:.*qkv_proj.*, mxfp4)"
514 + ]
515 +
516 +
517 @pytest.mark.skipif(
518 not (
519 current_platform.is_cuda()
tests/quantization/test_quantization_config_args.py
+77
@@ -2,6 +2,8 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for QuantizationConfigArgs parsing."""
4
5 +from unittest.mock import Mock
6 +
7 import pytest
8
9 from vllm.config.quantization import (
@@ -10,6 +12,11 @@ from vllm.config.quantization import (
12 QuantSpec,
13 resolve_quantization_config,
14 )
15 +from vllm.model_executor.layers.linear import LinearBase
16 +from vllm.model_executor.layers.quantization.online.base import (
17 + OnlineQuantizationConfig,
18 + _find_matching_targets,
19 +)
20 from vllm.model_executor.layers.quantization.utils.quant_utils import (
21 kFp8Dynamic128Sym,
22 kFp8DynamicTokenSym,
@@ -34,6 +41,10 @@ def test_quant_spec_accepts_quant_key_directly():
41 assert spec.activation is None
42
43
44 +def test_quant_spec_string_representation_uses_quantization_name():
45 + assert str(QuantSpec(weight="mxfp4")) == "mxfp4"
46 +
47 +
48 def test_quant_spec_rejects_unknown_name():
49 with pytest.raises(ValueError, match="unknown quantization name"):
50 QuantSpec(weight="not_a_real_format")
@@ -70,6 +81,11 @@ def test_args_accepts_dict_form():
81 assert args.moe == QuantSpec(weight=None, activation=kMxfp8Dynamic)
82
83
84 +def test_targets_reject_non_string_keys():
85 + with pytest.raises(ValueError, match="targets keys must be strings"):
86 + QuantizationConfigArgs._validate_targets({123: "mxfp8"})
87 +
88 +
89 # ---- resolve_quantization_config -----------------------------------------
90
91
@@ -132,3 +148,64 @@ def test_static_block_weight_paired_with_dynamic_block_activation():
148 spec = QuantSpec(weight="fp8_per_block_static", activation="fp8_per_block_dynamic")
149 assert spec.weight == kFp8Static128BlockSym
150 assert spec.activation == kFp8Dynamic128Sym
151 +
152 +
153 +def test_targets_allow_distinct_patterns_with_the_same_shorthand():
154 + layer_name = "model.layers.0.self_attn.qkv_proj"
155 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
156 + targets = {
157 + r"re:.*\.q_proj$": "mxfp8",
158 + r"re:.*\.k_proj$": "mxfp8",
159 + r"re:.*\.v_proj$": "mxfp8",
160 + }
161 +
162 + matches = _find_matching_targets(layer_name, targets, fused_mapping)
163 + assert len(matches) == 1
164 + assert targets[matches[0]] == "mxfp8"
165 +
166 +
167 +def test_targets_reject_overlapping_patterns():
168 + targets = {
169 + r"re:.*o_proj": "fp8_per_tensor",
170 + "model.layers.0.self_attn.o_proj": "fp8_per_block",
171 + }
172 +
173 + with pytest.raises(ValueError, match="multiple quantization_config.targets"):
174 + _find_matching_targets("model.layers.0.self_attn.o_proj", targets)
175 +
176 +
177 +def test_targets_reject_partially_matched_fused_layer():
178 + targets = {r"re:.*q_proj": "fp8_per_tensor"}
179 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
180 +
181 + with pytest.raises(ValueError, match="unmatched shards"):
182 + _find_matching_targets(
183 + "model.layers.0.self_attn.qkv_proj", targets, fused_mapping
184 + )
185 +
186 +
187 +def test_targets_reject_fused_shards_with_different_schemes():
188 + targets = {
189 + r"re:.*\.q_proj$": "fp8_per_tensor",
190 + r"re:.*\.k_proj$": "fp8_per_block",
191 + r"re:.*\.v_proj$": "fp8_per_tensor",
192 + }
193 + fused_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
194 +
195 + with pytest.raises(ValueError, match="different quantization_config.targets"):
196 + _find_matching_targets(
197 + "model.layers.0.self_attn.qkv_proj", targets, fused_mapping
198 + )
199 +
200 +
201 +def test_targets_reject_moe_only_shorthand_for_linear_layer():
202 + config = OnlineQuantizationConfig(
203 + QuantizationConfigArgs(
204 + targets={"model.layers.0.self_attn.o_proj": "nvfp4_per_token"}
205 + )
206 + )
207 +
208 + with pytest.raises(ValueError, match="does not define a QuantSpec"):
209 + config._dispatch_target(
210 + "model.layers.0.self_attn.o_proj", Mock(spec=LinearBase)
211 + )
tests/quantization/test_quark.py
+21 -1
@@ -50,7 +50,10 @@ from vllm.model_executor.layers.quantization.quark.quark_moe import ( # noqa: E
50 QuarkW8A8Int8MoEMethod,
51 )
52 from vllm.model_executor.layers.quantization.quark.schemes import QuarkScheme
53 -from vllm.model_executor.layers.quantization.quark.utils import QuarkQTensorHint
53 +from vllm.model_executor.layers.quantization.quark.utils import (
54 + QuarkQTensorHint,
55 + should_ignore_layer,
56 +)
57 from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
58 quant_dequant_mxfp4,
59 )
@@ -1294,6 +1297,23 @@ FUSED_MAPPING = {
1297 }
1298
1299
1300 +def test_quark_should_ignore_layer_checks_children():
1301 + assert should_ignore_layer(
1302 + "model.layers.78.mlp.experts",
1303 + ["model.layers.78.mlp.experts.0.down_proj"],
1304 + check_children=True,
1305 + )
1306 +
1307 +
1308 +def test_quark_should_ignore_layer_rejects_partial_fused_matches():
1309 + with pytest.raises(ValueError, match="different quantization schemes"):
1310 + should_ignore_layer(
1311 + "model.layers.0.self_attn.qkv_proj",
1312 + ["model.layers.0.self_attn.q_proj"],
1313 + FUSED_MAPPING,
1314 + )
1315 +
1316 +
1317 def test_fused_name_listed_directly_is_skipped():
1318 # Regression for Step-3.5-Flash-FP8: the checkpoint lists the fused
1319 # name (``qkv_proj``) directly in ``modules_to_not_convert``. When a
vllm/config/quantization.py
+74 -2
@@ -3,7 +3,14 @@
3
4 from typing import Annotated, Any
5
6 -from pydantic import Field, GetPydanticSchema, ValidationInfo, field_validator
6 +import regex as re
7 +from pydantic import (
8 + Field,
9 + GetPydanticSchema,
10 + ValidationInfo,
11 + field_validator,
12 + model_validator,
13 +)
14 from pydantic_core import core_schema
15
16 from vllm.config.utils import config
@@ -76,6 +83,21 @@ class QuantSpec:
83 activation: QuantKeyField = None
84 """Activation quantization key, or a name from QUANT_KEY_NAMES."""
85
86 + def __str__(self) -> str:
87 + def quant_key_str(quant_key: QuantKey | None) -> str:
88 + if quant_key is None:
89 + return "None"
90 + return next(
91 + (
92 + name
93 + for name, known_quant_key in QUANT_KEY_NAMES.items()
94 + if known_quant_key == quant_key
95 + ),
96 + str(quant_key),
97 + )
98 +
99 + return quant_key_str(self.weight)
100 +
101
102 @config
103 class QuantizationConfigArgs:
@@ -92,7 +114,16 @@ class QuantizationConfigArgs:
114 """Spec applied to ``FusedMoEFactory`` layers."""
115
116 ignore: list[str] = Field(default_factory=list)
95 - """Layers to skip quantization for."""
117 + """Layers to skip quantization for. Online quantization also supports
118 + fnmatch-style patterns."""
119 +
120 + targets: dict[str, str] | None = None
121 + """Per-layer online quantization overrides, keyed by exact layer name or
122 + regex patterns with a `re:`, or fnmatch-style patterns for online
123 + quantization, mapping to an online shorthand name (see
124 + `_ONLINE_SHORTHANDS`). A layer that matches no pattern is left unquantized.
125 + Mutually exclusive with `linear` and `moe`.
126 + """
127
128 @field_validator("linear", "moe", mode="before")
129 @classmethod
@@ -110,6 +141,46 @@ class QuantizationConfigArgs:
141 return spec
142 return QuantSpec(weight=_coerce_quant_key(v))
143
144 + @field_validator("targets", mode="before")
145 + @classmethod
146 + def _validate_targets(cls, v: Any) -> Any:
147 + if v is None:
148 + return v
149 + if not isinstance(v, dict):
150 + raise TypeError(f"targets must be a dict, got {type(v).__name__}")
151 + for pattern, shorthand in v.items():
152 + if not isinstance(pattern, str):
153 + raise ValueError(
154 + f"targets keys must be strings, got {type(pattern).__name__}"
155 + )
156 + if not isinstance(shorthand, str) or shorthand not in _ONLINE_SHORTHANDS:
157 + raise ValueError(
158 + f"targets[{pattern}] = {shorthand} is not a valid "
159 + f"online shorthand name; expected one of "
160 + f"{sorted(_ONLINE_SHORTHANDS)}"
161 + )
162 + if pattern.startswith("re:"):
163 + try:
164 + re.compile(pattern[3:])
165 + except re.error as e:
166 + raise ValueError(
167 + f"targets key {pattern} is not a valid regex: {e}"
168 + ) from e
169 + return v
170 +
171 + @model_validator(mode="after")
172 + def _validate_targets_exclusivity(self) -> "QuantizationConfigArgs":
173 + if self.targets is None:
174 + return self
175 + if self.linear is not None or self.moe is not None:
176 + raise ValueError(
177 + "quantization_config.targets is mutually exclusive with "
178 + f"quantization_config.linear/moe, got "
179 + f"targets={self.targets}, linear={self.linear}, "
180 + f"moe={self.moe}."
181 + )
182 + return self
183 +
184
185 # CLI shorthands accepted by `--quantization`. Each desugars to a full
186 # QuantizationConfigArgs; activation overrides go through quantization_config.
@@ -191,4 +262,5 @@ def resolve_quantization_config(
262 linear=quantization_config.linear or base.linear,
263 moe=quantization_config.moe or base.moe,
264 ignore=quantization_config.ignore or base.ignore,
265 + targets=quantization_config.targets or base.targets,
266 )
vllm/model_executor/layers/quantization/compressed_tensors/utils.py
+16 -77
@@ -4,11 +4,14 @@
4 from collections.abc import Iterable, Mapping
5 from types import MappingProxyType
6
7 -import regex as re
7 from compressed_tensors import CompressionFormat
8 from compressed_tensors.quantization import QuantizationStrategy
9 from torch.nn import Module
10
11 +from vllm.model_executor.layers.quantization.utils.config_utils import (
12 + find_matching_patterns,
13 + is_equal_or_regex_match,
14 +)
15 from vllm.model_executor.layers.quantization.utils.quant_utils import (
16 kFp8Static128BlockSym,
17 kFp8StaticChannelSym,
@@ -51,63 +54,20 @@ def should_ignore_layer(
54 layer_name: str | None,
55 ignore: Iterable[str] = tuple(),
56 fused_mapping: Mapping[str, list[str]] = MappingProxyType({}),
57 + use_fnmatch: bool = False,
58 ) -> bool:
59 if layer_name is None:
60 return False
57 -
58 - # layer_name = model.layers.0.self_attn.qkv_proj
59 - # proj_name = qkv_proj
60 - proj_name = layer_name.split(".")[-1]
61 -
62 - # Fused layers like gate_up_proj or qkv_proj will not be fused
63 - # in the safetensors checkpoint. So, we convert the name
64 - # from the fused version to unfused + check to make sure that
65 - # each shard of the fused layer has the same scheme.
66 - if proj_name in fused_mapping and layer_name not in ignore:
67 - shard_proj_names = fused_mapping[proj_name]
68 -
69 - # Convert fused_name --> [shard_names]
70 - shard_names = [
71 - layer_name.replace(proj_name, shard_proj_name)
72 - for shard_proj_name in shard_proj_names
73 - ]
74 -
75 - # Layer should be ignored if shards are ignored.
76 - should_ignore_layer = None
77 - for shard_name in shard_names:
78 - should_ignore_shard = check_equal_or_regex_match(
79 - layer_name=shard_name, targets=ignore
80 - )
81 -
82 - # If shard_idx=0, set layer ignore to match shard.
83 - if should_ignore_layer is None:
84 - should_ignore_layer = should_ignore_shard
85 -
86 - # If shard_idx=1+ confirm scheme matches prior shards.
87 - elif should_ignore_shard != should_ignore_layer:
88 - raise ValueError(
89 - f"Found a different quantization schemes for "
90 - f"{shard_proj_names} in {layer_name}. vLLM "
91 - "requires all to use the same scheme."
92 - )
93 -
94 - # Unfused layers like down_proj and o_proj will match
95 - # the safetensors checkpoint already.
96 - else:
97 - should_ignore_layer = check_equal_or_regex_match(
98 - layer_name=layer_name, targets=ignore
61 + per_shard_matches = find_matching_patterns(
62 + layer_name, ignore, fused_mapping, use_fnmatch=use_fnmatch
63 + )
64 + shards_ignored = [len(matches) > 0 for matches in per_shard_matches]
65 + if any(shards_ignored) and not all(shards_ignored):
66 + raise ValueError(
67 + f"Found different quantization schemes for the shards of "
68 + f"{layer_name}. vLLM requires all to use the same scheme."
69 )
100 -
101 - assert should_ignore_layer is not None
102 - return should_ignore_layer
103 -
104 -
105 -def check_equal_or_regex_match(layer_name: str, targets: Iterable[str]) -> bool:
106 - """
107 - Checks whether a layer_name is exactly equal or a regex match for
108 - if target starts with 're:' to any target in list.
109 - """
110 - return any(_is_equal_or_regex_match(layer_name, target) for target in targets)
70 + return all(shards_ignored)
71
72
73 def find_matched_target(
@@ -167,32 +127,11 @@ def _find_first_match(
127 """
128
129 for target in targets:
170 - if _is_equal_or_regex_match(value, target, check_contains=check_contains):
130 + if is_equal_or_regex_match(value, target, check_contains=check_contains):
131 return target
132 return None
133
134
175 -def _is_equal_or_regex_match(
176 - value: str, target: str, check_contains: bool = False
177 -) -> bool:
178 - """
179 - Checks whether a value is exactly equal or a regex match for target
180 - if target starts with 're:'. If check_contains is set to True,
181 - additionally checks if the target string is contained within the value.
182 - """
183 -
184 - if target.startswith("re:"):
185 - pattern = target[3:]
186 - if re.match(pattern, value):
187 - return True
188 - elif check_contains:
189 - if target.lower() in value.lower():
190 - return True
191 - elif target == value:
192 - return True
193 - return False
194 -
195 -
135 def _match_fused_layer(
136 layer_name: str,
137 target_layers: Iterable[str],
@@ -230,7 +169,7 @@ def _match_fused_layer(
169 unfused_matches: list[str | None] = []
170 for unfused in unfused_paths:
171 for target in target_layers:
233 - if _is_equal_or_regex_match(unfused, target):
172 + if is_equal_or_regex_match(unfused, target):
173 unfused_matches.append(target)
174 break
175 else:
vllm/model_executor/layers/quantization/online/base.py
+155 -10
@@ -1,11 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from collections.abc import Mapping
5 +from types import MappingProxyType
6 from typing import Any
7
8 import torch
9
8 -from vllm.config.quantization import QuantizationConfigArgs, QuantSpec
10 +from vllm.config.quantization import (
11 + _ONLINE_SHORTHANDS,
12 + QuantizationConfigArgs,
13 + QuantSpec,
14 +)
15 from vllm.logger import init_logger
16 from vllm.model_executor.layers.fused_moe import (
17 RoutedExperts,
@@ -47,6 +53,10 @@ from vllm.model_executor.layers.quantization.online.mxfp8 import (
53 from vllm.model_executor.layers.quantization.online.nvfp4 import (
54 Nvfp4OnlineMoEMethod,
55 )
56 +from vllm.model_executor.layers.quantization.utils.config_utils import (
57 + find_matching_patterns,
58 + get_layer_name_after_index,
59 +)
60 from vllm.model_executor.layers.quantization.utils.quant_utils import (
61 QuantKey,
62 kFp8Static128BlockSym,
@@ -83,6 +93,39 @@ _ONLINE_MOE_METHODS: dict[QuantKey, type] = {
93 }
94
95
96 +def _find_matching_targets(
97 + prefix: str,
98 + targets: Mapping[str, str],
99 + fused_mapping: Mapping[str, list[str]] = MappingProxyType({}),
100 +) -> list[str]:
101 + per_shard_matches = find_matching_patterns(
102 + prefix, targets, fused_mapping, use_fnmatch=True
103 + )
104 + if all(len(matches) == 0 for matches in per_shard_matches):
105 + return []
106 + if any(len(matches) == 0 for matches in per_shard_matches):
107 + raise ValueError(
108 + f"Found unmatched shards for {prefix}: {per_shard_matches}. vLLM "
109 + "requires all shards of a fused layer to match a target."
110 + )
111 + if any(len(matches) > 1 for matches in per_shard_matches):
112 + raise ValueError(
113 + f"Found multiple quantization_config.targets matches for the "
114 + f"shards of {prefix}: {per_shard_matches}. Each shard may match "
115 + "at most one target."
116 + )
117 +
118 + matched_patterns = [next(iter(matches)) for matches in per_shard_matches]
119 + quant_key_strs = {targets[pattern] for pattern in matched_patterns}
120 + if len(quant_key_strs) > 1:
121 + raise ValueError(
122 + f"Found different quantization_config.targets values for the "
123 + f"shards of {prefix}: {matched_patterns}. vLLM requires all "
124 + "shards of a fused layer to use the same target."
125 + )
126 + return [matched_patterns[0]]
127 +
128 +
129 class OnlineQuantizationConfig(QuantizationConfig):
130 """Model-level config for online quantization (quantize fp16/bf16 weights
131 during model loading, without requiring a pre-quantized checkpoint)."""
@@ -92,14 +135,44 @@ class OnlineQuantizationConfig(QuantizationConfig):
135 args: QuantizationConfigArgs,
136 ) -> None:
137 super().__init__()
95 - if args.linear is None and args.moe is None:
138 + if args.linear is None and args.moe is None and args.targets is None:
139 raise ValueError(
140 "OnlineQuantizationConfig requires at least one of "
98 - "quantization_config.linear or quantization_config.moe "
99 - "to be set."
141 + "quantization_config.linear, quantization_config.moe, or "
142 + "quantization_config.targets to be set."
143 )
144 self.args = args
145 self.ignored_layers: list[str] = args.ignore
146 + self.quantized_layers: dict[str, tuple[str, str, str | None]] = {}
147 +
148 + @property
149 + def quantized_layer_summaries(self) -> list[str]:
150 + counts: dict[tuple[str, str, str | None, str], int] = {}
151 + for layer_name, (
152 + source,
153 + quant_key_str,
154 + target_pattern,
155 + ) in self.quantized_layers.items():
156 + key = (
157 + get_layer_name_after_index(layer_name),
158 + source,
159 + target_pattern,
160 + quant_key_str,
161 + )
162 + counts[key] = counts.get(key, 0) + 1
163 +
164 + summaries = []
165 + # Build summary entries as
166 + # `self_attn.o_proj: 24 (from targets: re:.*self_attn\.o_proj, mxfp4`
167 + for (layer_type, source, target_pattern, quant_key_str), count in sorted(
168 + counts.items()
169 + ):
170 + pattern_prefix = f"{target_pattern}, " if target_pattern else ""
171 + summaries.append(
172 + f"{layer_type}: {count} "
173 + f"(from {source}: {pattern_prefix}{quant_key_str})"
174 + )
175 + return summaries
176
177 @classmethod
178 def get_name(cls) -> QuantizationMethods:
@@ -154,29 +227,101 @@ class OnlineQuantizationConfig(QuantizationConfig):
227 return cls(layer=layer)
228 return cls()
229
230 + def _dispatch_target(
231 + self, prefix: str, layer: torch.nn.Module
232 + ) -> "QuantizeMethodBase | None":
233 + assert self.args.targets is not None
234 + ignored = should_ignore_layer(
235 + prefix,
236 + ignore=self.ignored_layers,
237 + fused_mapping=self.packed_modules_mapping,
238 + use_fnmatch=True,
239 + )
240 + matches = _find_matching_targets(
241 + prefix, self.args.targets, fused_mapping=self.packed_modules_mapping
242 + )
243 + if ignored and matches:
244 + raise ValueError(
245 + f"Layer {prefix} matches both quantization_config.ignore "
246 + f"and quantization_config.targets ({matches}); a layer may "
247 + f"not be referenced by both."
248 + )
249 + if ignored or not matches:
250 + return None
251 + if len(matches) > 1:
252 + raise ValueError(
253 + f"Layer {prefix} matches multiple quantization_config."
254 + f"targets patterns: {matches}. Each layer may match at most "
255 + f"one target."
256 + )
257 + quant_key_str = self.args.targets[matches[0]]
258 + shorthand = _ONLINE_SHORTHANDS[quant_key_str]
259 + if isinstance(layer, LinearBase):
260 + quant_spec = shorthand.linear
261 + table = _ONLINE_LINEAR_METHODS
262 + elif isinstance(layer, RoutedExperts):
263 + quant_spec = shorthand.moe
264 + table = _ONLINE_MOE_METHODS
265 + else:
266 + raise ValueError(
267 + f"Layer {prefix} was matched by quantization_config.targets "
268 + f"({matches[0]}), but online quantization is not supported for "
269 + f"{type(layer).__name__}."
270 + )
271 + if quant_spec is None:
272 + raise ValueError(
273 + f"targets pattern {matches[0]} = {quant_key_str} does "
274 + f"not define a QuantSpec for {type(layer).__name__} layers "
275 + f"(matched at {prefix})."
276 + )
277 + method = self._dispatch(quant_spec, table, layer)
278 + if method is not None:
279 + self.quantized_layers[prefix] = ("targets", quant_key_str, matches[0])
280 + return method
281 +
282 def get_quant_method(
283 self, layer: torch.nn.Module, prefix: str
284 ) -> "QuantizeMethodBase | None":
285 + # `targets` takes precedence over `moe` and `linear` and is exclusive.
286 + if self.args.targets is not None:
287 + method = self._dispatch_target(prefix, layer)
288 + if method is not None:
289 + return method
290 +
291 + if isinstance(layer, LinearBase):
292 + return UnquantizedLinearMethod()
293 + if isinstance(layer, RoutedExperts):
294 + return UnquantizedFusedMoEMethod(layer.moe_config)
295 + return None
296 +
297 if isinstance(layer, LinearBase):
298 if should_ignore_layer(
299 prefix,
300 ignore=self.ignored_layers,
301 fused_mapping=self.packed_modules_mapping,
302 + use_fnmatch=True,
303 ):
304 return UnquantizedLinearMethod()
305 method = self._dispatch(self.args.linear, _ONLINE_LINEAR_METHODS, layer)
168 - return method if method is not None else UnquantizedLinearMethod()
306 + if method is not None:
307 + self.quantized_layers[prefix] = (
308 + "linear",
309 + str(self.args.linear),
310 + None,
311 + )
312 + return method
313 + return UnquantizedLinearMethod()
314 elif isinstance(layer, RoutedExperts):
315 if should_ignore_layer(
316 prefix,
317 ignore=self.ignored_layers,
318 fused_mapping=self.packed_modules_mapping,
319 + use_fnmatch=True,
320 ):
321 return UnquantizedFusedMoEMethod(layer.moe_config)
322 method = self._dispatch(self.args.moe, _ONLINE_MOE_METHODS, layer)
177 - return (
178 - method
179 - if method is not None
180 - else UnquantizedFusedMoEMethod(layer.moe_config)
181 - )
323 + if method is not None:
324 + self.quantized_layers[prefix] = ("moe", str(self.args.moe), None)
325 + return method
326 + return UnquantizedFusedMoEMethod(layer.moe_config)
327 return None
vllm/model_executor/layers/quantization/quark/utils.py
+15 -74
@@ -5,9 +5,12 @@ from collections.abc import Iterable, Mapping
5 from types import MappingProxyType
6 from typing import Any
7
8 -import regex as re
8 import torch
9
10 +from vllm.model_executor.layers.quantization.utils.config_utils import (
11 + find_matching_patterns,
12 +)
13 +
14 QuarkQTensorHint = dict[str, Any] | list[dict[str, Any]] | None
15
16
@@ -49,80 +52,18 @@ def should_ignore_layer(
52 ):
53 return True
54
52 - # layer_name = model.layers.0.self_attn.qkv_proj
53 - # proj_name = qkv_proj
54 - proj_name = layer_name.split(".")[-1]
55 -
56 - # Fused layers like gate_up_proj or qkv_proj will not be fused
57 - # in the safetensors checkpoint. So, we convert the name
58 - # from the fused version to unfused + check to make sure that
59 - # each shard of the fused layer has the same scheme.
60 - if proj_name in fused_mapping:
61 - shard_proj_names = fused_mapping[proj_name]
62 -
63 - # Convert fused_name --> [shard_names]
64 - shard_names = [
65 - layer_name.replace(proj_name, shard_proj_name)
66 - for shard_proj_name in shard_proj_names
67 - ]
68 -
69 - # Layer should be ignored if shards are ignored.
70 - should_ignore_layer = None
71 - for shard_name in shard_names:
72 - should_ignore_shard = check_equal_or_regex_match(
73 - layer_name=shard_name, targets=ignore
74 - )
75 -
76 - # If shard_idx=0, set layer ignore to match shard.
77 - if should_ignore_layer is None:
78 - should_ignore_layer = should_ignore_shard
79 -
80 - # If shard_idx=1+ confirm scheme matches prior shards.
81 - elif should_ignore_shard != should_ignore_layer:
82 - raise ValueError(
83 - f"Found a different quantization schemes for "
84 - f"{shard_proj_names} in {layer_name}. vLLM "
85 - "requires all to use the same scheme."
86 - )
87 -
88 - # Unfused layers like down_proj and o_proj will match
89 - # the safetensors checkpoint already.
90 - else:
91 - should_ignore_layer = check_equal_or_regex_match(
92 - layer_name=layer_name, targets=ignore
55 + # A direct fused-layer pattern takes precedence over expansion. For
56 + # model.layers.0.self_attn.qkv_proj,
57 + # ignore=["re:.*qkv_proj.*"] yields [{"re:.*qkv_proj.*"}]. In contrast,
58 + # ignore=["re:.*[qkv]_proj"] yields one matching set per expanded shard.
59 + per_shard_matches = find_matching_patterns(layer_name, ignore, fused_mapping)
60 + shards_ignored = [len(matches) > 0 for matches in per_shard_matches]
61 + if any(shards_ignored) and not all(shards_ignored):
62 + raise ValueError(
63 + f"Found different quantization schemes for the shards of "
64 + f"{layer_name}. vLLM requires all to use the same scheme."
65 )
94 -
95 - assert should_ignore_layer is not None
96 - return should_ignore_layer
97 -
98 -
99 -def check_equal_or_regex_match(layer_name: str, targets: Iterable[str]) -> bool:
100 - """
101 - Checks whether a layer_name is exactly equal or a regex match for
102 - if target starts with 're:' to any target in list.
103 - """
104 - return any(_is_equal_or_regex_match(layer_name, target) for target in targets)
105 -
106 -
107 -def _is_equal_or_regex_match(
108 - value: str, target: str, check_contains: bool = False
109 -) -> bool:
110 - """
111 - Checks whether a value is exactly equal or a regex match for target
112 - if target starts with 're:'. If check_contains is set to True,
113 - additionally checks if the target string is contained within the value.
114 - """
115 -
116 - if target.startswith("re:"):
117 - pattern = target[3:]
118 - if re.match(pattern, value):
119 - return True
120 - elif check_contains:
121 - if target.lower() in value.lower():
122 - return True
123 - elif target == value:
124 - return True
125 - return False
66 + return all(shards_ignored)
67
68
69 # utility for tensor dims > 2 cases
vllm/model_executor/layers/quantization/utils/config_utils.py
+76
@@ -1,6 +1,9 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from collections.abc import Iterable, Mapping
5 +from fnmatch import fnmatch
6 +from types import MappingProxyType
7 from typing import TYPE_CHECKING
8
9 import regex as re
@@ -244,3 +247,76 @@ def is_shared_expert_quant_fse_compatible(
247 "shared-expert FSE quantization compatibility is not implemented for "
248 f"{type(quant_config).__name__}",
249 )
250 +
251 +
252 +def find_matching_patterns(
253 + layer_name: str,
254 + patterns: Iterable[str],
255 + fused_mapping: Mapping[str, list[str]] = MappingProxyType({}),
256 + use_fnmatch: bool = False,
257 +) -> list[set[str]]:
258 + """Return matching patterns for a layer or each shard of a fused layer.
259 +
260 + A pattern matching the fused layer directly takes precedence. Otherwise,
261 + return one set of matching patterns for every shard.
262 + """
263 + patterns = list(patterns)
264 + matches = [
265 + pattern
266 + for pattern in patterns
267 + if is_equal_or_regex_match(layer_name, pattern, use_fnmatch=use_fnmatch)
268 + ]
269 + if matches:
270 + return [set(matches)]
271 +
272 + proj_name = layer_name.split(".")[-1]
273 + if proj_name not in fused_mapping:
274 + return [set()]
275 +
276 + shard_names = [
277 + layer_name.replace(proj_name, shard_proj_name)
278 + for shard_proj_name in fused_mapping[proj_name]
279 + ]
280 + per_shard_matches = [
281 + {
282 + pattern
283 + for pattern in patterns
284 + if is_equal_or_regex_match(shard_name, pattern, use_fnmatch=use_fnmatch)
285 + }
286 + for shard_name in shard_names
287 + ]
288 + return per_shard_matches
289 +
290 +
291 +def get_layer_name_after_index(layer_name: str) -> str:
292 + """Return the suffix following the final numeric component of a layer name."""
293 + parts = layer_name.split(".")
294 + for index in range(len(parts) - 1, -1, -1):
295 + if parts[index].isdigit():
296 + return ".".join(parts[index + 1 :])
297 + return layer_name
298 +
299 +
300 +def is_equal_or_regex_match(
301 + value: str,
302 + target: str,
303 + check_contains: bool = False,
304 + use_fnmatch: bool = False,
305 +) -> bool:
306 + """
307 + Checks whether a value is exactly equal or a regex match for target
308 + if target starts with 're:'. If check_contains is set to True,
309 + additionally checks if the target string is contained within the value.
310 + If use_fnmatch is set, supports shell-style patterns in target.
311 + """
312 +
313 + if target.startswith("re:"):
314 + pattern = target[3:]
315 + if re.match(pattern, value):
316 + return True
317 + elif check_contains:
318 + if target.lower() in value.lower():
319 + return True
320 + elif target == value or use_fnmatch and fnmatch(value, target):
321 + return True
322 + return False
vllm/model_executor/model_loader/base_loader.py
+18
@@ -58,6 +58,7 @@ class BaseModelLoader(ABC):
58 prefix=prefix,
59 )
60
61 + log_online_quantization(vllm_config)
62 log_model_inspection(model)
63
64 logger.debug("Loading weights on %s ...", load_device)
@@ -92,6 +93,23 @@ def log_model_inspection(model: nn.Module) -> None:
93 logger.info("vLLM model structure:\n%s", format_model_inspection(model))
94
95
96 +def log_online_quantization(vllm_config: VllmConfig) -> None:
97 + """Log the online-quantized layer count and types, when applicable."""
98 + from vllm.model_executor.layers.quantization.online.base import (
99 + OnlineQuantizationConfig,
100 + )
101 +
102 + quant_config = vllm_config.quant_config
103 + if not isinstance(quant_config, OnlineQuantizationConfig):
104 + return
105 +
106 + logger.info(
107 + "Quantized %d layers of types: %s",
108 + len(quant_config.quantized_layers),
109 + "; ".join(quant_config.quantized_layer_summaries),
110 + )
111 +
112 +
113 def _has_online_quant(model: nn.Module):
114 for module in model.modules():
115 quant_method = getattr(module, "quant_method", None)