| 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.resolve_quant_method_cls(Mock(), layer_name) |
| 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) |