# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Test model set-up and inference for quantized HF models supported on the AutoRound. Validating the configuration and printing results for manual checking. Run `pytest tests/quantization/test_auto_round.py`. """ import pytest from vllm.model_executor.layers.fused_moe import RoutedExperts from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod from vllm.model_executor.layers.quantization.auto_gptq import AutoGPTQConfig from vllm.model_executor.layers.quantization.inc import INCConfig from vllm.model_executor.layers.quantization.inc.config_parser import INCLayerConfig from vllm.model_executor.layers.quantization.inc.inc_linear import INCLinearMethod from vllm.model_executor.layers.quantization.inc.schemes import ( INCWna16Scheme, resolve_scheme, ) from vllm.model_executor.layers.quantization.inc.schemes.inc_scheme import ( INCLinearScheme, ) from vllm.model_executor.layers.quantization.inc.schemes.inc_wna16_linear import ( INCARKLinearMethod, INCWNA16LinearScheme, INCXPULinearMethod, ) from vllm.model_executor.layers.quantization.inc.schemes.inc_wna16_scheme import ( _resolve_awq_moe, _resolve_gptq_moe, ) from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead from vllm.platforms import current_platform MODELS = [ pytest.param( "OPEA/Qwen2.5-0.5B-Instruct-int4-sym-inc", id="auto_round:auto_gptq", ), pytest.param( "Intel/Qwen2-0.5B-Instruct-int4-sym-AutoRound", marks=pytest.mark.skipif( not (current_platform.is_cuda() or current_platform.is_xpu()), reason="AWQ AutoRound model only supports CUDA/XPU backend for now.", ), id="auto_round:auto_awq", ), ] @pytest.mark.skipif( not ( current_platform.is_cpu() or current_platform.is_xpu() or current_platform.is_cuda() ), reason="Only supports CPU/XPU/CUDA backend.", ) @pytest.mark.parametrize("model", MODELS) def test_auto_round_model(vllm_runner, model): with vllm_runner(model) as llm: output = llm.generate_greedy(["The capital of France is"], max_tokens=8) assert output print(output[0][1]) # --------------------------------------------------------------------------- # Unit tests for INCConfig and related classes # --------------------------------------------------------------------------- class DummyLayer: pass class DummyFusedMoE: pass def make_config(**overrides) -> INCConfig: kwargs = { "weight_bits": 4, "group_size": 128, "sym": True, "packing_format": "auto_round:auto_gptq", "block_name_to_quantize": None, "extra_config": None, "data_type": "int", "backend": "auto", } kwargs.update(overrides) return INCConfig(**kwargs) def make_layer_config(**overrides) -> INCLayerConfig: kwargs = { "bits": 4, "group_size": 128, "sym": True, "packing_format": "auto_round:auto_gptq", "backend": "auto", "data_type": "int", "quantized": True, } kwargs.update(overrides) return INCLayerConfig(**kwargs) def test_inc_config_parser_exact_match() -> None: config = make_config( extra_config={ "layers.0.self_attn.q_proj": { "bits": 8, "group_size": 64, "sym": False, } } ) layer_config = config.config_parser.resolve( DummyLayer(), "layers.0.self_attn.q_proj" ) assert layer_config.bits == 8 assert layer_config.group_size == 64 assert layer_config.sym is False assert layer_config.quantized is True def test_inc_model_prefix_early_exit() -> None: """extra_config keys with model. prefix trigger early unquantized return.""" config = make_config( extra_config={ "model.layers.1.mlp.gate_proj": { "bits": 16, }, } ) # get_quant_method checks model. prefix for unquantized early-exit result = config.get_quant_method(DummyLayer(), "layers.1.mlp.gate_proj") assert isinstance(result, UnquantizedLinearMethod) def test_inc_config_parser_regex_match() -> None: config = make_config( extra_config={ r"layers\.\d+\.self_attn\.(q|k|v)_proj": { "bits": 8, "group_size": 64, "sym": False, } } ) layer_config = config.config_parser.resolve( DummyLayer(), "layers.3.self_attn.q_proj" ) assert layer_config.bits == 8 assert layer_config.group_size == 64 assert layer_config.sym is False def test_inc_config_parser_invalid_regex_ignored() -> None: config = make_config( extra_config={ "[invalid": { "bits": 8, "group_size": 64, "sym": False, } } ) layer_config = config.config_parser.resolve( DummyLayer(), "layers.0.self_attn.q_proj" ) assert layer_config.bits == 4 assert layer_config.group_size == 128 assert layer_config.sym is True def test_inc_config_parser_block_name_to_quantize_marks_unquantized() -> None: config = make_config(block_name_to_quantize=["layers.1"]) layer_config = config.config_parser.resolve( DummyLayer(), "layers.0.self_attn.q_proj" ) assert layer_config.bits == 16 assert layer_config.group_size == -1 assert layer_config.sym is True assert layer_config.quantized is False def test_inc_config_parser_parallel_lm_head_defaults_to_unquantized() -> None: layer = object.__new__(ParallelLMHead) config = make_config() layer_config = config.config_parser.resolve(layer, "lm_head") assert layer_config.quantized is False assert layer_config.bits == 16 def test_inc_config_parser_fused_moe_requires_consistent_configs() -> None: config = make_config( extra_config={ "layers.0.block_sparse_moe.experts.0.w1": { "bits": 4, "group_size": 128, "sym": True, }, "layers.0.block_sparse_moe.experts.0.w2": { "bits": 8, "group_size": 128, "sym": True, }, } ) with pytest.raises(ValueError, match="requires consistent quant config"): config.config_parser.resolve(DummyFusedMoE(), "layers.0.block_sparse_moe") def test_inc_config_parser_fused_module_requires_consistent_configs() -> None: config = make_config( extra_config={ "layers.0.self_attn.q_proj": { "bits": 4, "group_size": 128, "sym": True, }, "layers.0.self_attn.k_proj": { "bits": 8, "group_size": 128, "sym": True, }, "layers.0.self_attn.v_proj": { "bits": 4, "group_size": 128, "sym": True, }, } ) config.packed_modules_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]} with pytest.raises(ValueError, match="requires consistent quant config"): config.config_parser.resolve(DummyLayer(), "layers.0.self_attn.qkv_proj") def test_inc_layer_config_mx_fp_helpers() -> None: layer_config = INCLayerConfig( bits=4, group_size=32, sym=True, packing_format="", backend="", data_type="mx_fp", quantized=True, ) assert layer_config.is_mxfp4 is True assert layer_config.is_mxfp8 is False def test_inc_resolve_scheme_selects_wna16() -> None: layer_config = INCLayerConfig( bits=4, group_size=128, sym=True, packing_format="auto_round:auto_gptq", backend="auto", data_type="int", quantized=True, ) scheme = resolve_scheme(layer_config) assert isinstance(scheme, INCWna16Scheme) class DummyLinearScheme(INCLinearScheme): def __init__(self) -> None: self.calls: list[tuple] = [] @classmethod def get_min_capability(cls) -> int: return 0 def create_weights(self, *args, **kwargs) -> None: self.calls.append(("create_weights", args, kwargs)) def process_weights_after_loading(self, layer) -> None: self.calls.append(("process_weights_after_loading", layer)) def apply_weights(self, layer, x, bias=None): self.calls.append(("apply_weights", layer, x, bias)) return "applied" def test_inc_linear_method_delegates() -> None: scheme = DummyLinearScheme() method = INCLinearMethod(scheme) layer = DummyLayer() method.create_weights( layer, input_size_per_partition=1, output_partition_sizes=[2], input_size=1, output_size=2, params_dtype=None, ) method.process_weights_after_loading(layer) result = method.apply(layer, "x", "b") assert result == "applied" assert [call[0] for call in scheme.calls] == [ "create_weights", "process_weights_after_loading", "apply_weights", ] def test_wna16_xpu_prefers_ark_when_available(monkeypatch) -> None: class DummyQuantLinear: pass monkeypatch.setattr(current_platform, "is_xpu", lambda: True) monkeypatch.setattr(current_platform, "is_cpu", lambda: False) monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_ark_ops.get_ark_state", lambda: (True, None, object(), DummyQuantLinear), ) method = INCWna16Scheme().get_linear_method( make_config(), object(), "layer", make_layer_config(), ) assert isinstance(method, INCLinearMethod) assert isinstance(method.scheme, INCARKLinearMethod) def test_wna16_xpu_falls_back_when_ark_unavailable(monkeypatch) -> None: monkeypatch.setattr(current_platform, "is_xpu", lambda: True) monkeypatch.setattr(current_platform, "is_cpu", lambda: False) monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_ark_ops.get_ark_state", lambda: (False, "missing", None, None), ) method = INCWna16Scheme().get_linear_method( make_config(), object(), "layer", make_layer_config(), ) assert isinstance(method, INCLinearMethod) assert isinstance(method.scheme, INCXPULinearMethod) def test_wna16_cpu_gptq_prefers_ark_when_available(monkeypatch) -> None: class DummyQuantLinear: pass monkeypatch.setattr(current_platform, "is_xpu", lambda: False) monkeypatch.setattr(current_platform, "is_cpu", lambda: True) monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_ark_ops.get_ark_state", lambda: (True, None, object(), DummyQuantLinear), ) method = INCWna16Scheme().get_linear_method( make_config(), object(), "layer", make_layer_config(), ) assert isinstance(method, INCLinearMethod) assert isinstance(method.scheme, INCARKLinearMethod) def test_wna16_cpu_gptq_raises_when_ark_and_marlin_unavailable( monkeypatch, ) -> None: monkeypatch.setattr(current_platform, "is_xpu", lambda: False) monkeypatch.setattr(current_platform, "is_cpu", lambda: True) monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_ark_ops.get_ark_state", lambda: (False, "missing", None, None), ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_wna16_linear.check_marlin_supported", lambda *args, **kwargs: False, ) with pytest.raises(NotImplementedError, match="Only 4-bit and 8-bit symmetric"): INCWna16Scheme().get_linear_method( make_config(), object(), "layer", make_layer_config(), ) def test_wna16_linear_gptq_uses_auto_gptq_when_supported(monkeypatch) -> None: captured = {} class DummyMethod: def __init__(self, cfg): captured["cfg"] = cfg monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.inc_wna16_linear." "check_marlin_supported", lambda *args, **kwargs: True, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.auto_gptq.AutoGPTQLinearMethod", DummyMethod, ) scheme = INCWNA16LinearScheme(make_layer_config()) assert isinstance(scheme.inner_method, DummyMethod) assert isinstance(captured["cfg"], AutoGPTQConfig) assert captured["cfg"].weight_bits == 4 assert captured["cfg"].group_size == 128 assert captured["cfg"].is_sym is True def test_wna16_linear_gptq_unsupported_config_raises() -> None: with pytest.raises(NotImplementedError, match="Only 4-bit and 8-bit symmetric"): INCWNA16LinearScheme(make_layer_config(sym=False)) def test_wna16_xpu_unsupported_config_still_raises(monkeypatch) -> None: monkeypatch.setattr(current_platform, "is_xpu", lambda: True) monkeypatch.setattr(current_platform, "is_cpu", lambda: False) with pytest.raises(NotImplementedError, match="unsupported config"): INCWna16Scheme().get_linear_method( make_config(sym=False), object(), "layer", make_layer_config(sym=False), ) def test_inc_get_quant_method_unquantized_linear_returns_unquantized() -> None: config = make_config(extra_config={"layer": {"bits": 16}}) layer = object.__new__(LinearBase) method = config.get_quant_method(layer, "layer") assert isinstance(method, UnquantizedLinearMethod) def test_inc_get_quant_method_unquantized_moe_returns_unquantized( monkeypatch, ) -> None: """Early-exit returns UnquantizedFusedMoEMethod for FusedMoE layers when extra_config has bits >= 16.""" config = make_config(extra_config={"layer": {"bits": 16}}) layer = object.__new__(RoutedExperts) layer.moe_config = None # UnquantizedFusedMoEMethod accepts moe_config class DummyUnquantizedFusedMoEMethod: def __init__(self, moe_config) -> None: self.moe_config = moe_config monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.inc.UnquantizedFusedMoEMethod", DummyUnquantizedFusedMoEMethod, ) method = config.get_quant_method(layer, "layer") assert isinstance(method, DummyUnquantizedFusedMoEMethod) assert method.moe_config is None def test_inc_get_quant_method_linear_uses_resolved_scheme(monkeypatch) -> None: config = make_config() layer = object.__new__(LinearBase) sentinel = object() class DummyScheme: def get_linear_method(self, _config, _layer, _prefix, _layer_config): return sentinel monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.factory.resolve_scheme", lambda _layer_config: DummyScheme(), ) method = config.get_quant_method(layer, "layer") assert method is sentinel def test_inc_get_quant_method_moe_uses_resolved_scheme(monkeypatch) -> None: config = make_config() layer = object.__new__(RoutedExperts) sentinel = object() class DummyScheme: def get_moe_method(self, _config, _layer, _prefix, _layer_config): return sentinel monkeypatch.setattr( "vllm.model_executor.layers.quantization.inc.schemes.factory.resolve_scheme", lambda _layer_config: DummyScheme(), ) method = config.get_quant_method(layer, "layer") assert method is sentinel def test_resolve_gptq_moe_falls_back_to_moe_wna16(monkeypatch) -> None: captured = {} class DummyMoeConfig: pass class DummyLayer: moe_config = DummyMoeConfig() class DummyBuiltConfig: pass built_config = DummyBuiltConfig() class DummyMethod: def __init__(self, cfg, moe): captured["cfg"] = cfg captured["moe"] = moe monkeypatch.setattr( "vllm.model_executor.layers.quantization.utils.marlin_utils.check_marlin_supported", lambda *args, **kwargs: False, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.moe_wna16.MoeWNA16Config.from_config", lambda cfg: captured.update({"from_config": cfg}) or built_config, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.moe_wna16.MoeWNA16Method", DummyMethod, ) layer_config = INCLayerConfig( bits=4, group_size=128, sym=True, packing_format="auto_round:auto_gptq", backend="auto", data_type="int", quantized=True, ) _resolve_gptq_moe(DummyLayer(), layer_config) assert captured["from_config"] == { "quant_method": "gptq", "bits": 4, "group_size": 128, "sym": True, "lm_head": False, } assert captured["cfg"] is built_config assert captured["moe"] is DummyLayer.moe_config def test_resolve_gptq_moe_uses_auto_gptq_when_supported(monkeypatch) -> None: captured = {} class DummyMoeConfig: pass class DummyLayer: moe_config = DummyMoeConfig() class DummyMethod: def __init__(self, cfg, moe): captured["cfg"] = cfg captured["moe"] = moe monkeypatch.setattr( "vllm.model_executor.layers.quantization.utils.marlin_utils.check_marlin_supported", lambda *args, **kwargs: True, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.utils.marlin_utils." "check_moe_marlin_supports_layer", lambda *args, **kwargs: True, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.auto_gptq.AutoGPTQMoEMethod", DummyMethod, ) _resolve_gptq_moe(DummyLayer(), make_layer_config()) assert isinstance(captured["cfg"], AutoGPTQConfig) assert captured["cfg"].weight_bits == 4 assert captured["cfg"].group_size == 128 assert captured["cfg"].is_sym is True assert captured["moe"] is DummyLayer.moe_config def test_resolve_awq_moe_uses_marlin_when_supported(monkeypatch) -> None: captured = {} class DummyMoeConfig: pass class DummyLayer: moe_config = DummyMoeConfig() class DummyMethod: def __init__(self, cfg, moe): captured["cfg"] = cfg captured["moe"] = moe monkeypatch.setattr( "vllm.model_executor.layers.quantization.utils.marlin_utils.check_marlin_supported", lambda *args, **kwargs: True, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.utils.marlin_utils.check_moe_marlin_supports_layer", lambda *args, **kwargs: True, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.auto_awq.verify_marlin_supported", lambda *args, **kwargs: None, ) monkeypatch.setattr( "vllm.model_executor.layers.quantization.auto_awq.AutoAWQMoEMethod", DummyMethod, ) layer_config = INCLayerConfig( bits=4, group_size=128, sym=False, packing_format="auto_round:auto_awq", backend="auto", data_type="int", quantized=True, ) _resolve_awq_moe(DummyLayer(), layer_config) assert captured["cfg"].weight_bits == 4 assert captured["cfg"].zero_point is True assert captured["moe"] is DummyLayer.moe_config # --------------------------------------------------------------------------- # Tests for get_layer_config step 4 (fused QKV / packed_modules_mapping) # --------------------------------------------------------------------------- class TestGetLayerConfigFusedQKV: """Tests for step-4 (fused QKV / packed_modules_mapping) logic. Focused on preventing false-positive substring matches. """ def test_exact_fusion_key_match(self): """A layer whose name contains 'qkv' maps to its extra_config entry.""" config = make_config( extra_config={ "model.layers.0.self_attn.qkv_proj": {"bits": 8}, } ) config.packed_modules_mapping = { "qkv_proj": ["q_proj", "k_proj", "v_proj"], } bits, _, _ = config.get_layer_config( DummyLayer(), "model.layers.0.self_attn.qkv_proj" ) assert bits == 8 def test_false_substring_match_does_not_override(self): """Regression test for the false-substring-match bug. Scenario (Qwen3.6-35B-A3B VLM): - packed_modules_mapping has "qkv" → ["qkv"] (from vision encoder). - The GDN text-attention layer is named "in_proj_qkvz". - "qkv" is a substring of "in_proj_qkvz", so old code would enter step 4 and generate sub_name "in_proj_qkvz" (replacing "qkv" with "qkv"). That name is NOT in extra_config, so get_config() falls back to the global default (bits=4), even though correct is 16. - Fix: skip the fusion key when none of the generated sub_names actually exist in extra_config. """ config = make_config( extra_config={ "model.layers.0.in_proj_qkv": {"bits": 16}, "model.layers.0.in_proj_z": {"bits": 16}, } ) config.packed_modules_mapping = { "qkv": ["qkv"], } bits, _, _ = config.get_layer_config( DummyLayer(), "model.layers.0.in_proj_qkvz" ) # bits should be the global default (4) – no erroneous fusion match assert bits == 4 def test_real_qkv_fusion_key_still_resolves(self): """The true "qkv" fusion (vision encoder) still resolves correctly.""" config = make_config( extra_config={ "vision_model.encoder.layers.0.self_attn.qkv": {"bits": 8}, } ) config.packed_modules_mapping = { "qkv": ["qkv"], } bits, _, _ = config.get_layer_config( DummyLayer(), "vision_model.encoder.layers.0.self_attn.qkv" ) assert bits == 8 def test_mixed_fp16_and_int4_fused_layer(self): """All sub-keys must agree; inconsistent configs raise ValueError.""" config = make_config( extra_config={ "model.layers.0.self_attn.q_proj": {"bits": 16}, "model.layers.0.self_attn.k_proj": {"bits": 4}, "model.layers.0.self_attn.v_proj": {"bits": 4}, } ) config.packed_modules_mapping = { "qkv_proj": ["q_proj", "k_proj", "v_proj"], } with pytest.raises(ValueError, match="consistent quant config"): config.get_layer_config(DummyLayer(), "model.layers.0.self_attn.qkv_proj") def test_fusion_triggered_by_regex_configured_sub_name(self): """Fusion step 4 is still triggered when sub_names match via regex. Ensures the guard does not regress when extra_config uses regex patterns instead of exact keys to configure sub-modules. """ config = make_config( extra_config={ r"model\.layers\.\d+\.self_attn\.(q|k|v)_proj": {"bits": 8}, } ) config.packed_modules_mapping = { "qkv_proj": ["q_proj", "k_proj", "v_proj"], } bits, _, _ = config.get_layer_config( DummyLayer(), "model.layers.0.self_attn.qkv_proj" ) assert bits == 8