main
py 326 lines 11.7 KB
Raw
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
10
11 if TYPE_CHECKING:
12 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
13
14
15 def get_quark_ocp_mx_group_size(
16 quant_config: "QuantizationConfig | None",
17 layer_name: str,
18 ) -> int | None:
19 """Return the OCP MX group size for a quantized Quark linear layer."""
20 if quant_config is None:
21 return None
22
23 from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
24 from vllm.model_executor.layers.quantization.quark.utils import should_ignore_layer
25
26 if not isinstance(quant_config, QuarkConfig):
27 return None
28
29 if should_ignore_layer(
30 layer_name,
31 ignore=quant_config.quant_config.get("exclude") or [],
32 fused_mapping=quant_config.packed_modules_mapping,
33 ):
34 return None
35
36 layer_quant_config = (
37 quant_config.get_layer_quant_config_from_name(layer_name)
38 or quant_config.quant_config.get("global_quant_config")
39 or {}
40 )
41 weight_quant = layer_quant_config.get("weight")
42 input_quant = layer_quant_config.get("input_tensors")
43 if not quant_config._is_w_ocp_mx_a_x(weight_quant, input_quant):
44 return None
45
46 assert weight_quant is not None
47 group_size = weight_quant.get("group_size")
48 if group_size is None:
49 block_size = weight_quant.get("block_size")
50 if isinstance(block_size, list | tuple) and len(block_size) >= 2:
51 group_size = block_size[1]
52 return int(group_size) if group_size is not None else None
53
54
55 def is_shared_expert_quant_fse_compatible(
56 quant_config: "QuantizationConfig | None",
57 expert_prefix: str,
58 shared_expert_prefix: str,
59 projection_names: list[str] | None = None,
60 ) -> tuple[bool, str | None]:
61 """Check whether quantization permits fused shared-expert execution.
62
63 Args:
64 quant_config: Model quantization configuration.
65 expert_prefix: Routed-expert module prefix.
66 shared_expert_prefix: Shared-expert module prefix.
67 projection_names: Shared-expert projection names.
68
69 Returns:
70 A compatibility flag and, when incompatible, the reason.
71
72 """
73 if projection_names is None:
74 projection_names = ["gate_up_proj", "down_proj"]
75
76 if quant_config is None:
77 return True, None
78
79 from vllm.model_executor.layers.quantization.fp8 import Fp8Config
80 from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
81 from vllm.model_executor.layers.quantization.utils.quant_utils import (
82 is_layer_skipped,
83 )
84 from vllm.models.deepseek_v4.quant_config import DeepseekV4FP8Config
85
86 if isinstance(quant_config, DeepseekV4FP8Config):
87 from vllm.config import get_current_vllm_config
88 from vllm.model_executor.models.utils import extract_layer_index
89
90 if quant_config.expert_dtype != "fp4":
91 return False, "DeepSeek-V4 routed experts are not MXFP4"
92
93 hf_config = get_current_vllm_config().model_config.hf_config
94
95 # TODO: This is adapted from former `_shared_experts_are_fp4`, and
96 # needs to be cleaned up this . There should not be Quark-specific
97 # logic in DeepseekV4FP8Config.
98 quantization_config = getattr(hf_config, "quantization_config", None)
99 if quantization_config is None:
100 return False, "DeepSeek-V4 has no quantization configuration"
101
102 if not quant_config._is_quark_mxfp4_ocp(quantization_config):
103 return False, "DeepSeek-v4 FSE is only implemented/tested with Quark MXFP4"
104
105 layer_idx = extract_layer_index(shared_expert_prefix)
106 if layer_idx >= hf_config.num_hidden_layers:
107 shared_expert_prefix = (
108 f"mtp.{layer_idx - hf_config.num_hidden_layers}.ffn.shared_experts"
109 )
110 else:
111 shared_expert_prefix = f"layers.{layer_idx}.ffn.shared_experts"
112
113 if any(
114 entry.startswith(shared_expert_prefix)
115 for entry in quantization_config.get("exclude") or []
116 if isinstance(entry, str)
117 ):
118 return (
119 False,
120 f"DeepSeek-V4 excludes shared experts at {shared_expert_prefix}",
121 )
122
123 shared_expert_weight_name = f"{shared_expert_prefix}.w1"
124 layer_quant_config = quantization_config.get("layer_quant_config") or {}
125 layer_config = layer_quant_config.get(shared_expert_weight_name)
126 if layer_config is None:
127 layer_config = next(
128 (
129 config
130 for pattern, config in layer_quant_config.items()
131 if isinstance(pattern, str)
132 and pattern.startswith("re:")
133 and re.fullmatch(
134 pattern.removeprefix("re:"), shared_expert_weight_name
135 )
136 ),
137 None,
138 )
139 shared_weight_config = (
140 layer_config or quantization_config.get("global_quant_config") or {}
141 ).get("weight") or {}
142 if shared_weight_config.get("dtype") == "fp4":
143 return True, None
144 return (
145 False,
146 f"DeepSeek-V4 shared experts at {shared_expert_prefix} are not MXFP4",
147 )
148
149 if isinstance(quant_config, QuarkConfig):
150 from vllm.model_executor.layers.quantization.quark.utils import (
151 should_ignore_layer,
152 )
153
154 # TODO: layer_type_quant_config is not taken into account here.
155 assert "exclude" in quant_config.quant_config
156 assert "global_quant_config" in quant_config.quant_config
157
158 exclude = quant_config.quant_config["exclude"]
159
160 # should_ignore_layer raises a rightful ValueError in case different shards
161 # have a different ignore policy, as unsupported.
162 is_excluded = any(
163 should_ignore_layer(
164 f"{shared_expert_prefix}.{projection_name}",
165 ignore=exclude,
166 fused_mapping=quant_config.packed_modules_mapping,
167 )
168 for projection_name in projection_names
169 )
170 if is_excluded:
171 return False, f"Quark excludes shared experts at {shared_expert_prefix}"
172
173 global_quant_config = quant_config.quant_config["global_quant_config"]
174
175 def get_projection_quant_configs(layer_name: str) -> list[object]:
176 module_prefix, _, projection_name = layer_name.rpartition(".")
177 packed_projection_names = quant_config.packed_modules_mapping.get(
178 projection_name, [projection_name]
179 )
180 return [
181 quant_config.get_layer_quant_config_from_name(
182 f"{module_prefix}.{packed_projection_name}"
183 )
184 or global_quant_config
185 for packed_projection_name in packed_projection_names
186 ]
187
188 expert_quant_config = (
189 quant_config.get_layer_quant_config_from_name(expert_prefix)
190 or global_quant_config
191 )
192 shared_expert_quant_configs = [
193 config
194 for projection_name in projection_names
195 for config in get_projection_quant_configs(
196 f"{shared_expert_prefix}.{projection_name}"
197 )
198 ]
199 if all(config == expert_quant_config for config in shared_expert_quant_configs):
200 return True, None
201 return (
202 False,
203 "Quark uses different quantization configurations for routed and "
204 f"shared experts at {shared_expert_prefix}",
205 )
206
207 if isinstance(quant_config, Fp8Config):
208 if quant_config.store_dtype is not None:
209 return (
210 False,
211 f"FP8 stores routed experts as {quant_config.store_dtype}, which "
212 f"is not supported for fused shared experts at "
213 f"{shared_expert_prefix}",
214 )
215
216 # Serialized per-tensor checkpoints store 0-D or size-1 scales, which
217 # the shared-expert weight chunker cannot slice into the appended expert
218 # slots; online FP8 is simply untested. Both lack a weight block size.
219 if quant_config.weight_block_size is None:
220 return (
221 False,
222 "FP8 shared-expert FSE is only implemented for block-quantized "
223 "checkpoints",
224 )
225
226 def is_ignored(layer_name: str) -> bool:
227 return is_layer_skipped(
228 prefix=layer_name,
229 ignored_layers=quant_config.ignored_layers,
230 fused_mapping=quant_config.packed_modules_mapping,
231 match_mode=quant_config.ignored_layers_match_mode,
232 )
233
234 expert_ignored = is_ignored(expert_prefix)
235 if any(
236 is_ignored(f"{shared_expert_prefix}.{projection_name}") != expert_ignored
237 for projection_name in projection_names
238 ):
239 return (
240 False,
241 "FP8 ignores routed and shared experts inconsistently at "
242 f"{shared_expert_prefix}",
243 )
244
245 return True, None
246
247 # TODO: Extend FSE support detection to other quantization methods. Typically,
248 # one would check that the experts and shared_experts use the same
249 # quantization config. This may be refactored as part of QuantizationConfig later.
250
251 return (
252 False,
253 "shared-expert FSE quantization compatibility is not implemented for "
254 f"{type(quant_config).__name__}",
255 )
256
257
258 def find_matching_patterns(
259 layer_name: str,
260 patterns: Iterable[str],
261 fused_mapping: Mapping[str, list[str]] = MappingProxyType({}),
262 use_fnmatch: bool = False,
263 ) -> list[set[str]]:
264 """Return matching patterns for a layer or each shard of a fused layer.
265
266 A pattern matching the fused layer directly takes precedence. Otherwise,
267 return one set of matching patterns for every shard.
268 """
269 patterns = list(patterns)
270 matches = [
271 pattern
272 for pattern in patterns
273 if is_equal_or_regex_match(layer_name, pattern, use_fnmatch=use_fnmatch)
274 ]
275 if matches:
276 return [set(matches)]
277
278 proj_name = layer_name.split(".")[-1]
279 if proj_name not in fused_mapping:
280 return [set()]
281
282 shard_names = [
283 layer_name.replace(proj_name, shard_proj_name)
284 for shard_proj_name in fused_mapping[proj_name]
285 ]
286 per_shard_matches = [
287 {
288 pattern
289 for pattern in patterns
290 if is_equal_or_regex_match(shard_name, pattern, use_fnmatch=use_fnmatch)
291 }
292 for shard_name in shard_names
293 ]
294 return per_shard_matches
295
296
297 def get_layer_name_after_index(layer_name: str) -> str:
298 """Return the suffix following the final numeric component of a layer name."""
299 parts = layer_name.split(".")
300 for index in range(len(parts) - 1, -1, -1):
301 if parts[index].isdigit():
302 return ".".join(parts[index + 1 :])
303 return layer_name
304
305
306 def is_equal_or_regex_match(
307 value: str,
308 target: str,
309 check_contains: bool = False,
310 use_fnmatch: bool = False,
311 ) -> bool:
312 """Checks whether a value is exactly equal or a regex match for target
313 if target starts with 're:'. If check_contains is set to True,
314 additionally checks if the target string is contained within the value.
315 If use_fnmatch is set, supports shell-style patterns in target.
316 """
317 if target.startswith("re:"):
318 pattern = target[3:]
319 if re.match(pattern, value):
320 return True
321 elif check_contains:
322 if target.lower() in value.lower():
323 return True
324 elif target == value or use_fnmatch and fnmatch(value, target):
325 return True
326 return False