main
py 1,046 lines 39.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import fnmatch
5 from typing import TYPE_CHECKING, Any, NamedTuple, cast
6
7 import torch
8
9 from vllm.logger import init_logger
10 from vllm.model_executor.layers.attention import Attention
11 from vllm.model_executor.layers.fused_moe import (
12 RoutedExperts,
13 UnquantizedFusedMoEMethod,
14 )
15 from vllm.model_executor.layers.linear import (
16 LinearBase,
17 LinearMethodBase,
18 UnquantizedLinearMethod,
19 )
20 from vllm.model_executor.layers.quantization import QuantizationMethods
21 from vllm.model_executor.layers.quantization.base_config import ( # noqa: E501
22 QuantizationConfig,
23 QuantizeMethodBase,
24 )
25 from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
26 from vllm.model_executor.layers.quantization.quark.quark_moe import ( # noqa: E501
27 QuarkMoEMethod,
28 )
29 from vllm.model_executor.layers.quantization.quark.schemes import (
30 QuarkNVFP4,
31 QuarkOCP_MX,
32 QuarkScheme,
33 QuarkW4A8_MXFP4_FP8,
34 QuarkW4A16Int4,
35 QuarkW8A8Fp8,
36 QuarkW8A8Fp8PerBlock,
37 QuarkW8A8Int8,
38 )
39 from vllm.model_executor.layers.quantization.quark.utils import (
40 QuarkQTensorHint,
41 deep_compare,
42 parse_w4a16_int4_weight_config,
43 should_ignore_layer,
44 )
45 from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
46 _ACTIVATION_QUANT_KEY_MAP,
47 _WEIGHT_QUANT_KEY_MAP,
48 OCP_MX_BLOCK_SIZE,
49 ocp_mx_weight_dtype_and_rows,
50 )
51 from vllm.model_executor.layers.quantization.utils.quant_utils import (
52 QuantKey,
53 kFp8Dynamic128Sym,
54 kFp8DynamicTensorSym,
55 kFp8DynamicTokenSym,
56 kFp8Static128BlockE8M0Sym,
57 kFp8Static128BlockSym,
58 kFp8StaticChannelSym,
59 kFp8StaticTensorSym,
60 kInt4Static,
61 kInt4Static32,
62 kInt4Static32Asym,
63 kInt4StaticAsym,
64 kInt4W4A8StaticChannelSym,
65 kInt8DynamicTensorAsym,
66 kInt8DynamicTensorSym,
67 kInt8DynamicTokenAsym,
68 kInt8DynamicTokenSym,
69 kInt8StaticChannelSym,
70 kInt8StaticTensorAsym,
71 kInt8StaticTensorSym,
72 kMxfp4Static,
73 kNvfp4Dynamic,
74 kNvfp4Static,
75 )
76 from vllm.model_executor.models.utils import WeightsMapper
77 from vllm.platforms import current_platform
78
79 if TYPE_CHECKING:
80 from vllm.model_executor.models.utils import WeightsMapper
81
82 __all__ = ["QuarkLinearMethod"]
83
84 logger = init_logger(__name__)
85
86
87 class QuantKeyMatch(NamedTuple):
88 matches: bool
89 activation_quant_key: QuantKey | None
90 weight_quant_key: QuantKey | None
91
92 def __bool__(self) -> bool:
93 return self.matches
94
95
96 class QuarkConfig(QuantizationConfig):
97 def __init__(
98 self,
99 quant_config: dict[str, Any],
100 kv_cache_group: list[str] | None = None,
101 kv_cache_config: dict[str, Any] | None = None,
102 pack_method: str = "reorder",
103 ):
104 super().__init__()
105 if kv_cache_group is None:
106 kv_cache_group = []
107 self.quant_config = quant_config
108 # Copy the class-level default (which a subclass may override, e.g. the
109 # DeepSeek-V4 Quark config) so per-instance edits don't mutate the class.
110 # Read from the class rather than ``self`` because the base
111 # ``QuantizationConfig.__init__`` sets an empty instance-level
112 # ``packed_modules_mapping`` that would otherwise shadow the override.
113 self.packed_modules_mapping = dict(
114 getattr(type(self), "packed_modules_mapping", {})
115 )
116 self.kv_cache_group = kv_cache_group
117 self.kv_cache_config = kv_cache_config
118 self.pack_method = pack_method
119
120 def get_linear_method(self) -> "QuarkLinearMethod":
121 return QuarkLinearMethod(self)
122
123 def get_supported_act_dtypes(cls) -> list[torch.dtype]:
124 return [torch.float16, torch.bfloat16]
125
126 @classmethod
127 def get_min_capability(cls) -> int:
128 return 70
129
130 def get_name(self) -> QuantizationMethods:
131 return "quark"
132
133 def apply_vllm_mapper( # noqa: B027
134 self, hf_to_vllm_mapper: "WeightsMapper"
135 ):
136 """Interface for models to update module names referenced in
137 quantization configs in order to reflect the vllm model structure
138
139 Args:
140 hf_to_vllm_mapper: maps from hf model structure (the assumed
141 structure of the qconfig) to vllm model structure
142
143 """
144 quant_config_with_hf_to_vllm_mapper: dict[str, Any] = {}
145
146 for k, v in self.quant_config.items():
147 if isinstance(v, list) and all(isinstance(item, str) for item in v):
148 quant_config_with_hf_to_vllm_mapper[k] = hf_to_vllm_mapper.apply_list(v)
149 elif isinstance(v, list):
150 quant_config_with_hf_to_vllm_mapper[k] = v
151 elif isinstance(v, dict):
152 quant_config_with_hf_to_vllm_mapper[k] = hf_to_vllm_mapper.apply_dict(v)
153 else:
154 if isinstance(v, str):
155 mapped_v_list = hf_to_vllm_mapper.apply_list([v])
156 if mapped_v_list:
157 quant_config_with_hf_to_vllm_mapper[k] = mapped_v_list[0]
158 else:
159 quant_config_with_hf_to_vllm_mapper[k] = v
160
161 self.quant_config = quant_config_with_hf_to_vllm_mapper
162
163 def _scale_block_rows(self, prefix: str, layer_type: type[torch.nn.Module]) -> int:
164 """How many weight rows one checkpoint scale row covers.
165
166 1 for the canonical 1-D per-group MX spelling; ``block_size[0]`` for
167 MXFP8 exports that spell the scale 2-D (DeepSeek-V4.1 uses 32). The
168 scheme cannot work this out itself: it has no handle on the layer name,
169 and tensor-parallel sharding narrows the loaded scale so the ratio is
170 not recoverable from shapes either.
171 """
172 config = self._find_matched_config(prefix, layer_type)
173 weight = self._unwrap_single_quant_config(config.get("weight"))
174 normalized = ocp_mx_weight_dtype_and_rows(weight)
175 return normalized[1] if normalized is not None else 1
176
177 def get_quant_method(
178 self, layer: torch.nn.Module, prefix: str
179 ) -> "QuantizeMethodBase | None":
180 weight_quant_key, activation_quant_key, method_cls = (
181 self.get_quant_method_target(prefix, type(layer))
182 )
183
184 if method_cls is UnquantizedFusedMoEMethod:
185 return UnquantizedFusedMoEMethod(layer.moe_config)
186 if method_cls is UnquantizedLinearMethod:
187 return UnquantizedLinearMethod()
188 if method_cls is QuarkLinearMethod:
189 _, _, scheme_cls = self.get_scheme_cls(type(layer), prefix)
190 scheme = self.init_scheme(
191 scheme_cls,
192 weight_quant_key=weight_quant_key,
193 activation_quant_key=activation_quant_key,
194 scale_block_rows=(
195 self._scale_block_rows(prefix, type(layer))
196 if scheme_cls is QuarkOCP_MX
197 else 1
198 ),
199 weight_config=self._find_matched_config(prefix, type(layer)).get(
200 "weight"
201 ),
202 )
203 layer.scheme = scheme
204 return QuarkLinearMethod(self)
205 if method_cls is QuarkKVCacheMethod:
206 return QuarkKVCacheMethod(self)
207 if method_cls is not None and issubclass(method_cls, QuarkMoEMethod):
208 return QuarkMoEMethod.get_moe_method(
209 self,
210 module=layer,
211 method_cls=method_cls,
212 weight_quant_key=weight_quant_key,
213 activation_quant_key=activation_quant_key,
214 layer_name=prefix,
215 )
216
217 return None
218
219 def get_quant_method_target(
220 self, prefix: str, layer_type: type[torch.nn.Module]
221 ) -> tuple[
222 QuantKey | None,
223 QuantKey | None,
224 type[QuantizeMethodBase]
225 | type[UnquantizedLinearMethod]
226 | type[UnquantizedFusedMoEMethod]
227 | None,
228 ]:
229 """Return weight key, activation key, and quant method class for
230 the given ``prefix`` and ``layer_type``.
231
232 This is the counterpart of ``get_quant_method`` without quant method
233 instantiation.
234
235 TODO: integrate as part of base QuantizationConfig.get_quant_method_target
236 in the future.
237
238 ``None`` denotes an unquantized/BF16 activation.
239 """
240 is_routed_experts = issubclass(layer_type, RoutedExperts)
241 if should_ignore_layer(
242 prefix,
243 ignore=cast(list[str], self.quant_config.get("exclude")),
244 fused_mapping=self.packed_modules_mapping,
245 check_children=is_routed_experts,
246 ):
247 if is_routed_experts:
248 return None, None, UnquantizedFusedMoEMethod
249 if issubclass(layer_type, LinearBase):
250 return None, None, UnquantizedLinearMethod
251 return None, None, None
252 if issubclass(layer_type, LinearBase):
253 weight_quant_key, activation_key, _ = self.get_scheme_cls(
254 layer_type, prefix
255 )
256 return weight_quant_key, activation_key, QuarkLinearMethod
257 if issubclass(layer_type, Attention):
258 return None, None, QuarkKVCacheMethod
259 if is_routed_experts:
260 weight_quant_key, activation_quant_key, quant_method_cls = (
261 QuarkMoEMethod.get_moe_method_target(self, layer_type, prefix)
262 )
263 if quant_method_cls is None:
264 return None, None, None
265 return weight_quant_key, activation_quant_key, quant_method_cls
266 return None, None, None
267
268 @classmethod
269 def from_config(cls, config: dict[str, Any]) -> "QuarkConfig":
270 export_config = config.get("export")
271 if export_config is None:
272 raise ValueError(
273 "The export key should be included in "
274 "the configurations of Quark quantized model"
275 )
276 kv_cache_group = cast(list[str], export_config.get("kv_cache_group"))
277 pack_method = cast(str, export_config.get("pack_method"))
278
279 # In the export model of quark, the quantization configuration
280 # of kv_cache is stored in layer_quant_config. First, it is
281 # judged whether kv_cache_group exists, and then it is judged
282 # whether layer_quant_config has a quantization configuration
283 # that matches kv_cache.
284 if len(kv_cache_group) == 0:
285 kv_cache_config = None
286 else:
287 kv_cache_set = set(kv_cache_group)
288 layer_quant_config = cast(dict[str, Any], config.get("layer_quant_config"))
289 layer_quant_names = list(layer_quant_config.keys())
290 layer_quant_set = set(layer_quant_names)
291
292 if not (
293 kv_cache_set.issubset(layer_quant_set)
294 or any(
295 fnmatch.fnmatchcase(layer_quant, pat)
296 for layer_quant in list(layer_quant_set)
297 for pat in list(kv_cache_set)
298 )
299 ):
300 raise ValueError(
301 "The Quark quantized model has the "
302 "kv_cache_group parameter setting, "
303 "but no kv_cache quantization settings "
304 "were found in the quantization "
305 "configuration."
306 )
307
308 q_configs = [
309 quant_cfg
310 for name, quant_cfg in layer_quant_config.items()
311 if any(fnmatch.fnmatchcase(name, pattern) for pattern in kv_cache_group)
312 ]
313
314 if not all(
315 deep_compare(q_config["output_tensors"], q_configs[0]["output_tensors"])
316 for q_config in q_configs
317 ):
318 raise ValueError(
319 "The quantization method used for kv_cache should "
320 "be the same, but the quantization method for the "
321 "kv_cache layer in the config is different."
322 )
323 kv_cache_config = q_configs[0].get("output_tensors")
324 if kv_cache_config is None:
325 raise ValueError("The kv_cache quantization configuration is empty.")
326
327 # Since we have already set kv_cache quantization configurations,
328 # we will remove the quantization configuration for the
329 # output_tensors corresponding to the kv_cache layer.
330 for q_config in q_configs:
331 q_config["output_tensors"] = None
332
333 # In case q_proj output is also quantized, remove the configuration
334 # to keep qkv consistency.
335 q_proj_q_config = cast(dict[str, Any], layer_quant_config.get("*q_proj"))
336 if q_proj_q_config is not None:
337 q_proj_q_config["output_tensors"] = None
338
339 return cls(
340 quant_config=config,
341 kv_cache_group=kv_cache_group,
342 kv_cache_config=kv_cache_config,
343 pack_method=pack_method,
344 )
345
346 @classmethod
347 def get_config_filenames(cls) -> list[str]:
348 return []
349
350 def _check_scheme_supported(self, min_capability: int, error: bool = True) -> bool:
351 capability_tuple = current_platform.get_device_capability()
352
353 if capability_tuple is not None:
354 capability = capability_tuple.to_int()
355 supported = capability >= min_capability
356 if error and not supported:
357 raise RuntimeError(
358 "Quantization scheme is not supported for the current GPU. "
359 f"Min capability: {min_capability}. "
360 f"Current capability: {capability}."
361 )
362 return supported
363 else:
364 return False
365
366 def _is_fp8_w4a8(
367 self,
368 weight_quant: QuarkQTensorHint,
369 input_quant: QuarkQTensorHint,
370 ) -> QuantKeyMatch:
371 # Confirm weights and input quantized.
372 if isinstance(input_quant, list):
373 if len(input_quant) != 1:
374 return QuantKeyMatch(False, None, None)
375 input_quant = input_quant[0]
376 if (
377 not isinstance(weight_quant, list)
378 or len(weight_quant) != 2
379 or not isinstance(input_quant, dict)
380 ):
381 return QuantKeyMatch(False, None, None)
382
383 # Confirm weight scheme is supported
384 is_w4a8_dtype = (
385 weight_quant[0].get("dtype") == "fp8_e4m3"
386 and weight_quant[1].get("dtype") == "int4"
387 and input_quant.get("dtype") == "fp8_e4m3"
388 )
389 is_static_weight = not weight_quant[0].get("is_dynamic") and not weight_quant[
390 1
391 ].get("is_dynamic")
392 is_per_tensor_fp8_and_per_channel_int4_weight = (
393 weight_quant[0].get("qscheme") == "per_tensor"
394 and weight_quant[1].get("qscheme") == "per_channel"
395 and weight_quant[1].get("symmetric") is True
396 and weight_quant[1].get("ch_axis") == 0
397 )
398
399 if not (
400 is_w4a8_dtype
401 and is_static_weight
402 and is_per_tensor_fp8_and_per_channel_int4_weight
403 ):
404 return QuantKeyMatch(False, None, None)
405
406 # Dynamic quantization is always supported if weights supported.
407 if input_quant.get("is_dynamic"):
408 return QuantKeyMatch(True, kFp8DynamicTokenSym, kInt4W4A8StaticChannelSym)
409
410 # Confirm activation scheme is supported.
411 is_per_tensor_activation = input_quant.get("qscheme") == "per_tensor"
412 if not is_per_tensor_activation:
413 return QuantKeyMatch(False, None, None)
414 return QuantKeyMatch(True, kFp8StaticTensorSym, kInt4W4A8StaticChannelSym)
415
416 def _is_fp8_w8a8(
417 self,
418 weight_quant: dict[str, Any] | None,
419 input_quant: dict[str, Any] | None,
420 ) -> QuantKeyMatch:
421 # Confirm weights and input quantized.
422 if not isinstance(weight_quant, dict) or not isinstance(input_quant, dict):
423 return QuantKeyMatch(False, None, None)
424
425 # Confirm weight scheme is supported
426 is_fp8_dtype = (
427 weight_quant.get("dtype") == "fp8_e4m3"
428 and input_quant.get("dtype") == "fp8_e4m3"
429 )
430 is_static_weight = not weight_quant.get("is_dynamic")
431 is_per_tensor_or_channel_weight = weight_quant.get("qscheme") in [
432 "per_tensor",
433 "per_channel",
434 ]
435 block_size = list(weight_quant.get("block_size") or [])
436 is_per_block_weight = (
437 weight_quant.get("qscheme") == "per_block"
438 and block_size == [128, 128]
439 and weight_quant.get("symmetric") is True
440 )
441
442 if not is_fp8_dtype or not is_static_weight:
443 return QuantKeyMatch(False, None, None)
444
445 if is_per_block_weight:
446 if not input_quant.get("is_dynamic"):
447 return QuantKeyMatch(False, None, None)
448 matches = (
449 input_quant.get("qscheme") == "per_group"
450 and input_quant.get("group_size") == block_size[1]
451 and input_quant.get("symmetric") is True
452 )
453 if not matches:
454 return QuantKeyMatch(False, None, None)
455 weight_quant_key = (
456 kFp8Static128BlockE8M0Sym
457 if weight_quant.get("scale_type") == "float8_e8m0fnu"
458 else kFp8Static128BlockSym
459 )
460 return QuantKeyMatch(True, kFp8Dynamic128Sym, weight_quant_key)
461
462 if not is_per_tensor_or_channel_weight:
463 return QuantKeyMatch(False, None, None)
464
465 # Dynamic quantization is always supported if tensor/channel weights
466 # are supported.
467 if input_quant.get("is_dynamic"):
468 activation_quant_key = (
469 kFp8DynamicTokenSym
470 if input_quant.get("qscheme") == "per_channel"
471 else kFp8DynamicTensorSym
472 )
473 weight_quant_key = (
474 kFp8StaticChannelSym
475 if weight_quant.get("qscheme") == "per_channel"
476 else kFp8StaticTensorSym
477 )
478 return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
479
480 # Confirm activation scheme is supported.
481 is_per_tensor_activation = input_quant.get("qscheme") == "per_tensor"
482 if not is_per_tensor_activation:
483 return QuantKeyMatch(False, None, None)
484 weight_quant_key = (
485 kFp8StaticChannelSym
486 if weight_quant.get("qscheme") == "per_channel"
487 else kFp8StaticTensorSym
488 )
489 return QuantKeyMatch(True, kFp8StaticTensorSym, weight_quant_key)
490
491 def _is_w8a8_int8(
492 self,
493 weight_quant: dict[str, Any] | None,
494 input_quant: dict[str, Any] | None,
495 ) -> QuantKeyMatch:
496 # Confirm weights and input quantized.
497 if not isinstance(weight_quant, dict) or not isinstance(input_quant, dict):
498 return QuantKeyMatch(False, None, None)
499
500 is_int8_dtype = (
501 weight_quant.get("dtype") == "int8" and input_quant.get("dtype") == "int8"
502 )
503
504 is_valid_weight_scheme = weight_quant.get("qscheme") in [
505 "per_tensor",
506 "per_channel",
507 ]
508 is_static_input = input_quant.get(
509 "qscheme"
510 ) == "per_tensor" and not input_quant.get("is_dynamic")
511 is_dynamic_input = (
512 input_quant.get("qscheme") == "per_channel"
513 and input_quant.get("is_dynamic") is True
514 )
515 is_weight_symmetric = weight_quant.get("symmetric") is True
516
517 # Both symmetric and asymmetric input quantization supported.
518 # Only symmetric weight quantization supported.
519 matches = (
520 is_int8_dtype
521 and is_valid_weight_scheme
522 and is_weight_symmetric
523 and (
524 (is_static_input and not weight_quant.get("is_dynamic"))
525 or is_dynamic_input
526 )
527 )
528 if not matches:
529 return QuantKeyMatch(False, None, None)
530 weight_quant_key = (
531 kInt8StaticChannelSym
532 if weight_quant.get("qscheme") == "per_channel"
533 else kInt8StaticTensorSym
534 )
535 if is_static_input:
536 activation_quant_key = (
537 kInt8StaticTensorSym
538 if input_quant.get("symmetric") is True
539 else kInt8StaticTensorAsym
540 )
541 elif weight_quant.get("qscheme") == "per_channel":
542 activation_quant_key = (
543 kInt8DynamicTokenSym
544 if input_quant.get("symmetric") is True
545 else kInt8DynamicTokenAsym
546 )
547 else:
548 activation_quant_key = (
549 kInt8DynamicTensorSym
550 if input_quant.get("symmetric") is True
551 else kInt8DynamicTensorAsym
552 )
553 return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
554
555 def _is_w4a16_int4(
556 self,
557 weight_quant: dict[str, Any] | None,
558 input_quant: dict[str, Any] | None,
559 ) -> QuantKeyMatch:
560 if weight_quant is None or input_quant is not None:
561 return QuantKeyMatch(False, None, None)
562
563 is_int4 = weight_quant.get("dtype") in ("int4", "uint4")
564 is_packed = self.pack_method in ("order", "reorder")
565 if not (is_int4 and is_packed):
566 return QuantKeyMatch(False, None, None)
567
568 group_size, is_symmetric = parse_w4a16_int4_weight_config(weight_quant)
569 if is_symmetric:
570 weight_key = kInt4Static32 if group_size == 32 else kInt4Static
571 else:
572 weight_key = kInt4Static32Asym if group_size == 32 else kInt4StaticAsym
573 return QuantKeyMatch(True, None, weight_key)
574
575 def _is_w4a8_mxfp4_fp8(
576 self,
577 weight_quant: dict[str, Any] | None,
578 input_quant: dict[str, Any] | None,
579 ) -> QuantKeyMatch:
580 if weight_quant is None or input_quant is None:
581 return QuantKeyMatch(False, None, None)
582
583 is_weight_mxfp4 = (
584 weight_quant.get("dtype") == "fp4"
585 and weight_quant.get("qscheme") == "per_group"
586 and weight_quant.get("group_size") == 32
587 and weight_quant.get("scale_format") == "e8m0"
588 and not weight_quant.get("is_dynamic")
589 )
590
591 is_input_fp8 = (
592 input_quant.get("dtype") == "fp8_e4m3"
593 and input_quant.get("qscheme") == "per_tensor"
594 and not input_quant.get("is_dynamic") # Static per-tensor
595 and input_quant.get("symmetric") is True # Symmetric quantization
596 )
597
598 if not (is_weight_mxfp4 and is_input_fp8):
599 return QuantKeyMatch(False, None, None)
600 return QuantKeyMatch(True, kFp8StaticTensorSym, kMxfp4Static)
601
602 def _is_nvfp4(
603 self,
604 weight_quant: QuarkQTensorHint,
605 input_quant: QuarkQTensorHint,
606 ) -> QuantKeyMatch:
607 # Confirm weights and input quantized.
608 if weight_quant is None or input_quant is None:
609 return QuantKeyMatch(False, None, None)
610
611 # Confirm both weight_quant and input_quant are lists with 2 elements
612 if not isinstance(weight_quant, list) or len(weight_quant) != 2:
613 return QuantKeyMatch(False, None, None)
614 if not isinstance(input_quant, list) or len(input_quant) != 2:
615 return QuantKeyMatch(False, None, None)
616
617 # First element should be fp4 with per_group quantization
618 is_fp4_per_group_weight = (
619 weight_quant[0].get("dtype") == "fp4"
620 and weight_quant[0].get("qscheme") == "per_group"
621 and weight_quant[0].get("group_size") == 16
622 and not weight_quant[0].get("is_dynamic")
623 )
624 is_fp4_per_group_input = (
625 input_quant[0].get("dtype") == "fp4"
626 and input_quant[0].get("qscheme") == "per_group"
627 and input_quant[0].get("group_size") == 16
628 and input_quant[0].get("is_dynamic")
629 )
630
631 # Second element should be fp8_e4m3 with per_tensor quantization
632 is_fp8_per_tensor_weight = (
633 weight_quant[1].get("dtype") == "fp8_e4m3"
634 and weight_quant[1].get("qscheme") == "per_tensor"
635 and not weight_quant[1].get("is_dynamic")
636 )
637 is_fp8_per_tensor_input = (
638 input_quant[1].get("dtype") == "fp8_e4m3"
639 and input_quant[1].get("qscheme") == "per_tensor"
640 and not input_quant[1].get("is_dynamic")
641 )
642
643 matches = (
644 is_fp4_per_group_weight # type: ignore[return-value]
645 and is_fp4_per_group_input
646 and is_fp8_per_tensor_weight
647 and is_fp8_per_tensor_input
648 )
649 if not matches:
650 return QuantKeyMatch(False, None, None)
651 return QuantKeyMatch(True, kNvfp4Dynamic, kNvfp4Static)
652
653 def _is_w_ocp_mx_a_x(
654 self,
655 weight_quant: dict[str, Any] | None,
656 input_quant: dict[str, Any] | None,
657 allow_static_fp8: bool = False,
658 ) -> QuantKeyMatch:
659 """This check returns True only if it is an OCP-MX weight quantization.
660 The activation can be FP16/BF16, OCP MXFP4, MXFP8, FP8.
661 The rationale for checking only the weight type is that
662 the model loading concept and process primarily concerns the weights themselves.
663 """
664 # Confirm weights quantized, and normalize the two OCP MX config
665 # dialects (1-D per_group, and MXFP8's 2-D per_block) to a dtype.
666 normalized = ocp_mx_weight_dtype_and_rows(weight_quant)
667 if normalized is None:
668 logger.debug(
669 "Quark model's weight quantization is incompatible with OCP MX "
670 "format: %s",
671 weight_quant,
672 )
673 return QuantKeyMatch(False, None, None)
674
675 weight_dtype, _ = normalized
676 weight_quant_key = _WEIGHT_QUANT_KEY_MAP[weight_dtype]
677 if input_quant is None:
678 activation_quant_key = None
679 elif not input_quant.get("is_dynamic"):
680 if (
681 allow_static_fp8
682 and input_quant.get("dtype") == "fp8_e4m3"
683 and input_quant.get("qscheme") == "per_tensor"
684 and input_quant.get("symmetric") is True
685 ):
686 activation_quant_key = kFp8StaticTensorSym
687 else:
688 logger.debug(
689 "Quark model's OCP MX quantization is incompatible with static "
690 "input scales."
691 )
692 return QuantKeyMatch(False, None, None)
693 elif (
694 input_quant["dtype"] == "fp8_e4m3"
695 and input_quant.get("qscheme") == "per_group"
696 and input_quant.get("group_size") == OCP_MX_BLOCK_SIZE
697 ):
698 # MXFP8 activations. Must be checked before the per-tensor FP8
699 # branch below, which would otherwise claim them.
700 activation_quant_key = _ACTIVATION_QUANT_KEY_MAP["mxfp8_e4m3"]
701 elif input_quant["dtype"] == "fp8_e4m3":
702 activation_quant_key = kFp8DynamicTensorSym
703 else:
704 input_dtype = input_quant["dtype"].replace("fp", "mxfp")
705 if input_dtype not in _ACTIVATION_QUANT_KEY_MAP:
706 raise ValueError(
707 f"Unsupported input_dtype={input_dtype} in Quark's vLLM "
708 "integration. Supported activation dtypes are "
709 f"{_ACTIVATION_QUANT_KEY_MAP.keys()}, or None for "
710 "weight-only quantization."
711 )
712 activation_quant_key = _ACTIVATION_QUANT_KEY_MAP[input_dtype]
713 return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
714
715 @staticmethod
716 def _unwrap_single_quant_config(
717 quant_config: QuarkQTensorHint,
718 ) -> dict[str, Any] | None:
719 """Unwrap one-entry quantization-config lists."""
720 if isinstance(quant_config, list):
721 return quant_config[0] if len(quant_config) == 1 else None
722 return quant_config
723
724 def get_layer_quant_config_from_name(
725 self, layer_name: str
726 ) -> dict[str, Any] | None:
727 proj_name = layer_name.split(".")[-1]
728 if proj_name in self.packed_modules_mapping:
729 shard_proj_names = self.packed_modules_mapping[proj_name]
730 shard_configs = []
731 for shard_proj_name in shard_proj_names:
732 shard_name = layer_name.replace(proj_name, shard_proj_name)
733 if shard_name != layer_name:
734 config = self.get_layer_quant_config_from_name(shard_name)
735 else:
736 config = None
737 shard_configs.append(config)
738
739 matched_configs = [config for config in shard_configs if config is not None]
740 if matched_configs and not all(
741 deep_compare(config, matched_configs[0]) for config in matched_configs
742 ):
743 raise ValueError(
744 f"Found a different quantization configuration for "
745 f"{shard_proj_names} in {layer_name}. vLLM "
746 "requires all to use the same scheme."
747 )
748 if matched_configs:
749 return matched_configs[0]
750 return None
751 else:
752 layer_quant_config = cast(
753 dict[str, Any], self.quant_config.get("layer_quant_config") or {}
754 )
755 for name_pattern, config in layer_quant_config.items():
756 if "*" not in name_pattern:
757 matches = layer_name in name_pattern
758 else:
759 matches = fnmatch.fnmatch(layer_name, name_pattern)
760 if matches:
761 return config
762 return None
763
764 def _find_matched_config(
765 self, layer_name: str, module: torch.nn.Module | type[torch.nn.Module]
766 ) -> dict[str, Any]:
767 # Priority order:
768 # 1. layer_quant_config,
769 # 2. layer_type_quant_config,
770 # 3. global_quant_config.
771
772 layer_type = cast(str, module if isinstance(module, type) else type(module))
773 layer_type_quant_config = cast(
774 dict[str, Any], self.quant_config.get("layer_type_quant_config")
775 )
776 global_quant_config = cast(
777 dict[str, Any], self.quant_config.get("global_quant_config")
778 )
779 fallback_config = layer_type_quant_config.get(layer_type, global_quant_config)
780
781 proj_name = layer_name.split(".")[-1]
782 if proj_name in self.packed_modules_mapping:
783 shard_proj_names = self.packed_modules_mapping[proj_name]
784 shard_configs = []
785 for shard_proj_name in shard_proj_names:
786 shard_name = layer_name.replace(proj_name, shard_proj_name)
787 if shard_name == layer_name:
788 config = fallback_config
789 else:
790 config = self.get_layer_quant_config_from_name(shard_name)
791 if config is None:
792 config = fallback_config
793 shard_configs.append(config)
794
795 if not all(
796 deep_compare(config, shard_configs[0]) for config in shard_configs
797 ):
798 raise ValueError(
799 f"Found a different quantization configuration for "
800 f"{shard_proj_names} in {layer_name}. vLLM requires all "
801 "to use the same scheme."
802 )
803 return shard_configs[0]
804 else:
805 layer_quant_config = self.get_layer_quant_config_from_name(layer_name)
806 if layer_quant_config is not None:
807 return layer_quant_config
808 return fallback_config
809
810 def _get_scheme_cls_from_config(
811 self, config: dict[str, Any]
812 ) -> tuple[QuantKey | None, QuantKey | None, type["QuarkScheme"]]:
813 if config.get("output_tensors") or config.get("bias"):
814 raise NotImplementedError(
815 "Currently, Quark models with output_tensors "
816 "and bias quantized are not supported"
817 )
818 weight_config = cast(QuarkQTensorHint, config.get("weight"))
819 input_config = cast(QuarkQTensorHint, config.get("input_tensors"))
820
821 if (
822 isinstance(weight_config, list)
823 and len(weight_config) > 1
824 or isinstance(input_config, list)
825 and len(input_config) > 1
826 ):
827 if match := self._is_nvfp4(weight_config, input_config):
828 return (
829 match.weight_quant_key,
830 match.activation_quant_key,
831 QuarkNVFP4,
832 )
833
834 raise NotImplementedError(
835 "Multi-entry weight or activation quantization configs are only "
836 "supported for NVFP4."
837 )
838
839 weight_config = self._unwrap_single_quant_config(weight_config)
840 input_config = self._unwrap_single_quant_config(input_config)
841
842 if (
843 isinstance(weight_config, dict)
844 and weight_config.get("qscheme") == "per_block"
845 and not weight_config.get("block_size")
846 ):
847 raise ValueError(
848 "Quark W8A8 FP8 per-block weight quantization requires "
849 "`block_size` in the weight quantization config."
850 )
851
852 if match := self._is_fp8_w8a8(weight_config, input_config):
853 assert weight_config is not None
854 if weight_config.get("qscheme") == "per_block":
855 return (
856 match.weight_quant_key,
857 match.activation_quant_key,
858 QuarkW8A8Fp8PerBlock,
859 )
860 return match.weight_quant_key, match.activation_quant_key, QuarkW8A8Fp8
861 elif match := self._is_w8a8_int8(weight_config, input_config):
862 return (
863 match.weight_quant_key,
864 match.activation_quant_key,
865 QuarkW8A8Int8,
866 )
867 elif match := self._is_w4a16_int4(weight_config, input_config):
868 return (
869 match.weight_quant_key,
870 match.activation_quant_key,
871 QuarkW4A16Int4,
872 )
873 elif match := self._is_w4a8_mxfp4_fp8(weight_config, input_config):
874 return (
875 match.weight_quant_key,
876 match.activation_quant_key,
877 QuarkW4A8_MXFP4_FP8,
878 )
879 elif match := self._is_w_ocp_mx_a_x(weight_config, input_config):
880 return match.weight_quant_key, match.activation_quant_key, QuarkOCP_MX
881
882 raise NotImplementedError(
883 "No quark compatible scheme was found. "
884 f"Weight config: {weight_config}, "
885 f"Input config: {input_config}"
886 )
887
888 def get_scheme_cls(
889 self, layer_type: type[torch.nn.Module], layer_name: str
890 ) -> tuple[QuantKey | None, QuantKey | None, type["QuarkScheme"]]:
891 """Return quantization keys and scheme class without initializing it."""
892 return self._get_scheme_cls_from_config(
893 self._find_matched_config(layer_name, layer_type)
894 )
895
896 def init_scheme(
897 self,
898 scheme_cls: type["QuarkScheme"],
899 weight_quant_key: QuantKey | None,
900 activation_quant_key: QuantKey | None,
901 scale_block_rows: int = 1,
902 weight_config: dict[str, Any] | None = None,
903 ) -> "QuarkScheme":
904 """Construct a Quark scheme selected by get_scheme_cls."""
905 if scheme_cls not in (
906 QuarkW8A8Fp8,
907 QuarkW8A8Fp8PerBlock,
908 QuarkW8A8Int8,
909 QuarkOCP_MX,
910 QuarkNVFP4,
911 QuarkW4A8_MXFP4_FP8,
912 QuarkW4A16Int4,
913 ):
914 raise AssertionError(f"Unsupported Quark scheme class: {scheme_cls}")
915
916 scheme: QuarkScheme
917 if scheme_cls is QuarkW4A16Int4:
918 normalized_weight_config = self._unwrap_single_quant_config(weight_config)
919 assert normalized_weight_config is not None
920 assert weight_quant_key is not None
921 scheme = QuarkW4A16Int4(
922 weight_quant_key=weight_quant_key,
923 activation_quant_key=activation_quant_key,
924 pack_method=self.pack_method,
925 weight_config=normalized_weight_config,
926 )
927 self._check_scheme_supported(scheme.get_min_capability())
928 return scheme
929
930 kwargs: dict[str, Any] = {"activation_quant_key": activation_quant_key}
931
932 # These classes handle different possible `weight_quant_key`.
933 if scheme_cls in (
934 QuarkW8A8Fp8,
935 QuarkW8A8Fp8PerBlock,
936 QuarkW8A8Int8,
937 QuarkOCP_MX,
938 ):
939 kwargs["weight_quant_key"] = weight_quant_key
940
941 if scheme_cls is QuarkOCP_MX:
942 kwargs["scale_block_rows"] = scale_block_rows
943
944 scheme = scheme_cls(**kwargs)
945
946 # Raise error if device does not support the scheme
947 # (e.g. fp8 needs ada lovelace)
948 self._check_scheme_supported(scheme.get_min_capability())
949 return scheme
950
951 @staticmethod
952 def get_cache_scale_mapper() -> "WeightsMapper":
953 """Map Quark KV-cache scale names to vLLM names."""
954 orig_to_new_suffix = {
955 ".k_proj.output_scale": ".attn.k_scale",
956 ".v_proj.output_scale": ".attn.v_scale",
957 ".q_proj.output_scale": ".attn.q_scale",
958 ".self_attn.prob_output_scale": ".self_attn.attn.prob_scale",
959 }
960 cache_scale_mapper = WeightsMapper(orig_to_new_suffix=orig_to_new_suffix)
961 return cache_scale_mapper | QuantizationConfig.get_cache_scale_mapper()
962
963
964 class QuarkLinearMethod(LinearMethodBase):
965 def __init__(self, quantization_config: QuarkConfig):
966 self.quantization_config = quantization_config
967
968 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
969 layer.scheme.process_weights_after_loading(layer)
970
971 def create_weights(
972 self,
973 layer: torch.nn.Module,
974 input_size_per_partition: int,
975 output_partition_sizes: list[int],
976 input_size: int,
977 output_size: int,
978 params_dtype: torch.dtype,
979 **extra_weight_attrs,
980 ):
981 """Use the CompressedTensorsScheme associated with each layer to create
982 the necessary parameters for the layer. See LinearMethodBase for param
983 details
984 """
985 weight_loader = extra_weight_attrs.get("weight_loader")
986 layer.scheme.create_weights(
987 layer=layer,
988 input_size=input_size,
989 input_size_per_partition=input_size_per_partition,
990 output_partition_sizes=output_partition_sizes,
991 output_size=output_size,
992 params_dtype=params_dtype,
993 weight_loader=weight_loader,
994 )
995
996 def apply(
997 self,
998 layer: torch.nn.Module,
999 x: torch.Tensor,
1000 bias: torch.Tensor | None = None,
1001 ):
1002 """Use the output of create_weights and the CompressedTensorsScheme
1003 associated with the layer to apply the forward pass with the
1004 layer input. See LinearMethodBase for param details
1005
1006 """
1007 scheme = layer.scheme
1008 if scheme is None:
1009 raise ValueError("A scheme must be defined for each layer")
1010
1011 return scheme.apply_weights(layer, x, bias=bias)
1012
1013
1014 class QuarkKVCacheMethod(BaseKVCacheMethod):
1015 """Supports loading kv-cache scaling factors from quark checkpoints."""
1016
1017 def __init__(self, quant_config: QuarkConfig):
1018 self.validate_kv_cache_config(quant_config.kv_cache_config)
1019 super().__init__(quant_config)
1020
1021 @staticmethod
1022 def validate_kv_cache_config(kv_cache_config: dict[str, Any] | None):
1023 """Validator for the kv cache configuration. Useful for controlling the
1024 kv cache quantization schemes, that are being supported in vLLM
1025
1026 Args:
1027 kv_cache_config: the quark kv cache scheme
1028
1029 """
1030 if kv_cache_config is None:
1031 return
1032
1033 dtype = kv_cache_config.get("dtype")
1034 if dtype != "fp8_e4m3":
1035 raise NotImplementedError(
1036 "Currently supported kv cache quantization is "
1037 f"dtype=fp8_e4m3, however received {dtype}"
1038 )
1039
1040 qscheme = kv_cache_config.get("qscheme")
1041 if qscheme != "per_tensor":
1042 raise NotImplementedError(
1043 "Only support per-tensor scaling factor "
1044 "for quark KV cache. "
1045 f"Expected qscheme: per_tensor, found qscheme: {qscheme}"
1046 )