main
py 2,600 lines 85.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test model set-up and weight loading for quark-quantized models.
4
5 Run `pytest tests/quantization/test_quark.py`.
6
7 See also `tests/kernels/moe/test_ocp_mx_moe.py`.
8 """
9
10 import importlib.metadata
11 from dataclasses import dataclass
12 from importlib.util import find_spec
13 from types import SimpleNamespace
14 from unittest.mock import MagicMock, Mock, patch
15
16 import huggingface_hub
17 import lm_eval
18 import pytest
19 import torch
20 from packaging import version
21
22 from tests.quantization.utils import load_model_without_vllm_runner
23 from vllm._aiter_ops import is_aiter_found_and_supported, rocm_aiter_ops
24 from vllm.config import VllmConfig, set_current_vllm_config
25 from vllm.config.cache import CacheConfig, CacheDType
26 from vllm.forward_context import set_forward_context
27 from vllm.model_executor import parameter
28 from vllm.model_executor.kernels.linear.scaled_mm.aiter import (
29 AiterHipbMMPerTokenFp8ScaledMMLinearKernel,
30 AiterPerTokenFp8ScaledMMLinearKernel,
31 AiterPreshuffledPerTokenFp8ScaledMMLinearKernel,
32 )
33 from vllm.model_executor.kernels.linear.scaled_mm.ScaledMMLinearKernel import (
34 FP8ScaledMMLinearLayerConfig,
35 )
36 from vllm.model_executor.layers.attention import Attention
37 from vllm.model_executor.layers.fused_moe import (
38 FusedMoeWeightScaleSupported,
39 RoutedExperts,
40 UnquantizedFusedMoEMethod,
41 )
42 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
43 from vllm.model_executor.layers.fused_moe.config import (
44 FusedMoEConfig,
45 FusedMoEParallelConfig,
46 RoutingMethodType,
47 )
48 from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
49 from vllm.model_executor.layers.quantization.quark.quark import ( # noqa: E501
50 QuarkConfig,
51 QuarkLinearMethod,
52 QuarkNVFP4,
53 QuarkOCP_MX,
54 QuarkW8A8Fp8,
55 QuarkW8A8Fp8PerBlock,
56 QuarkW8A8Int8,
57 )
58 from vllm.model_executor.layers.quantization.quark.quark_moe import ( # noqa: E501
59 QuarkMoEMethod,
60 QuarkW4A8Fp8MoEMethod,
61 QuarkW4A16Int4MoEMethod,
62 QuarkW8A8Fp8MoEMethod,
63 QuarkW8A8Int8MoEMethod,
64 )
65 from vllm.model_executor.layers.quantization.quark.schemes import (
66 QuarkScheme,
67 QuarkW4A16Int4,
68 )
69 from vllm.model_executor.layers.quantization.quark.utils import (
70 QuarkQTensorHint,
71 canonicalize_quark_packed_int4,
72 should_ignore_layer,
73 )
74 from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
75 quant_dequant_mxfp4,
76 )
77 from vllm.model_executor.layers.quantization.utils.quant_utils import (
78 QuantKey,
79 is_layer_skipped,
80 kFp8Dynamic128Sym,
81 kFp8DynamicTensorSym,
82 kFp8DynamicTokenSym,
83 kFp8Static128BlockE8M0Sym,
84 kFp8Static128BlockSym,
85 kFp8StaticChannelSym,
86 kFp8StaticTensorSym,
87 kInt4W4A8StaticChannelSym,
88 kInt8DynamicTensorAsym,
89 kInt8DynamicTensorSym,
90 kInt8DynamicTokenAsym,
91 kInt8DynamicTokenSym,
92 kInt8StaticChannelSym,
93 kInt8StaticTensorAsym,
94 kInt8StaticTensorSym,
95 kMxfp4Dynamic,
96 kMxfp4Static,
97 kMxfp6E2M3Dynamic,
98 kMxfp6E2M3Static,
99 kMxfp6E3M2Dynamic,
100 kMxfp6E3M2Static,
101 kNvfp4Dynamic,
102 kNvfp4Static,
103 )
104 from vllm.model_executor.models.llama import LlamaForCausalLM
105 from vllm.model_executor.models.utils import WeightsMapper
106 from vllm.platforms import current_platform
107 from vllm.transformers_utils.repo_utils import hf_api
108
109 if current_platform.is_rocm():
110 from vllm.platforms.rocm import on_gfx942, on_gfx950
111 else:
112
113 def on_gfx942() -> bool:
114 return False
115
116 def on_gfx950() -> bool:
117 return False
118
119
120 from .reference_mxfp4 import dq_mxfp4_torch, qdq_mxfp4_torch
121
122 # Minimum amd-quark version for MXFP4/OCP_MX tests (single source of truth).
123 QUARK_MXFP4_MIN_VERSION = "0.12"
124
125 QUARK_MXFP4_AVAILABLE = find_spec("quark") is not None and version.parse(
126 importlib.metadata.version("amd-quark")
127 ) >= version.parse(QUARK_MXFP4_MIN_VERSION)
128
129 AITER_AVAILABLE = is_aiter_found_and_supported()
130
131 AITER_PTPC_KERNELS = (
132 AiterHipbMMPerTokenFp8ScaledMMLinearKernel,
133 AiterPreshuffledPerTokenFp8ScaledMMLinearKernel,
134 AiterPerTokenFp8ScaledMMLinearKernel,
135 )
136
137 DEVICE_TYPE = current_platform.device_type
138
139
140 @dataclass(frozen=True)
141 class QTensorConfig:
142 name: str
143 weight: QuarkQTensorHint
144 input_tensors: QuarkQTensorHint
145 weight_quant_key: QuantKey | None = None
146 act_quant_key: QuantKey | None = None
147 dispatch_cls: type[QuarkScheme] | type[QuarkMoEMethod] | None = None
148 expected_error: tuple[type[Exception], str] | None = None
149
150
151 QTENSOR_CONFIGS = [
152 QTensorConfig(
153 name="fp8_w8a8_static_tensor",
154 weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
155 input_tensors={
156 "dtype": "fp8_e4m3",
157 "qscheme": "per_tensor",
158 "is_dynamic": False,
159 },
160 weight_quant_key=kFp8StaticTensorSym,
161 act_quant_key=kFp8StaticTensorSym,
162 dispatch_cls=QuarkW8A8Fp8,
163 ),
164 QTensorConfig(
165 name="fp8_w8a8_static_tensor_single_entry_lists",
166 weight=[
167 {
168 "dtype": "fp8_e4m3",
169 "qscheme": "per_tensor",
170 "is_dynamic": False,
171 }
172 ],
173 input_tensors=[
174 {
175 "dtype": "fp8_e4m3",
176 "qscheme": "per_tensor",
177 "is_dynamic": False,
178 }
179 ],
180 weight_quant_key=kFp8StaticTensorSym,
181 act_quant_key=kFp8StaticTensorSym,
182 dispatch_cls=QuarkW8A8Fp8,
183 ),
184 QTensorConfig(
185 name="fp8_w8a8_dynamic_tensor",
186 weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
187 input_tensors={
188 "dtype": "fp8_e4m3",
189 "qscheme": "per_tensor",
190 "is_dynamic": True,
191 },
192 weight_quant_key=kFp8StaticTensorSym,
193 act_quant_key=kFp8DynamicTensorSym,
194 dispatch_cls=QuarkW8A8Fp8,
195 ),
196 QTensorConfig(
197 name="fp8_w8a8_dynamic_token",
198 weight={"dtype": "fp8_e4m3", "qscheme": "per_channel", "is_dynamic": False},
199 input_tensors={
200 "dtype": "fp8_e4m3",
201 "qscheme": "per_channel",
202 "is_dynamic": True,
203 },
204 weight_quant_key=kFp8StaticChannelSym,
205 act_quant_key=kFp8DynamicTokenSym,
206 dispatch_cls=QuarkW8A8Fp8,
207 ),
208 QTensorConfig(
209 name="fp8_w8a8_channel_static_tensor",
210 weight={"dtype": "fp8_e4m3", "qscheme": "per_channel", "is_dynamic": False},
211 input_tensors={
212 "dtype": "fp8_e4m3",
213 "qscheme": "per_tensor",
214 "is_dynamic": False,
215 },
216 weight_quant_key=kFp8StaticChannelSym,
217 act_quant_key=kFp8StaticTensorSym,
218 dispatch_cls=QuarkW8A8Fp8,
219 ),
220 QTensorConfig(
221 name="fp8_w8a8_tensor_dynamic_token",
222 weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
223 input_tensors={
224 "dtype": "fp8_e4m3",
225 "qscheme": "per_channel",
226 "is_dynamic": True,
227 },
228 weight_quant_key=kFp8StaticTensorSym,
229 act_quant_key=kFp8DynamicTokenSym,
230 dispatch_cls=QuarkW8A8Fp8,
231 ),
232 QTensorConfig(
233 name="fp8_w8a8_dynamic_block_fp32",
234 weight={
235 "dtype": "fp8_e4m3",
236 "qscheme": "per_block",
237 "is_dynamic": False,
238 "block_size": [128, 128],
239 "symmetric": True,
240 },
241 input_tensors={
242 "dtype": "fp8_e4m3",
243 "qscheme": "per_group",
244 "is_dynamic": True,
245 "group_size": 128,
246 "symmetric": True,
247 },
248 weight_quant_key=kFp8Static128BlockSym,
249 act_quant_key=kFp8Dynamic128Sym,
250 dispatch_cls=QuarkW8A8Fp8PerBlock,
251 ),
252 QTensorConfig(
253 name="fp8_w8a8_dynamic_block_e8m0",
254 weight={
255 "dtype": "fp8_e4m3",
256 "qscheme": "per_block",
257 "is_dynamic": False,
258 "block_size": [128, 128],
259 "symmetric": True,
260 "scale_type": "float8_e8m0fnu",
261 },
262 input_tensors={
263 "dtype": "fp8_e4m3",
264 "qscheme": "per_group",
265 "is_dynamic": True,
266 "group_size": 128,
267 "symmetric": True,
268 },
269 weight_quant_key=kFp8Static128BlockE8M0Sym,
270 act_quant_key=kFp8Dynamic128Sym,
271 dispatch_cls=QuarkW8A8Fp8PerBlock,
272 ),
273 QTensorConfig(
274 name="fp8_w8a8_dynamic_block_fp32_moe",
275 weight={
276 "dtype": "fp8_e4m3",
277 "qscheme": "per_block",
278 "is_dynamic": False,
279 "block_size": [128, 128],
280 "symmetric": True,
281 },
282 input_tensors={
283 "dtype": "fp8_e4m3",
284 "qscheme": "per_group",
285 "is_dynamic": True,
286 "group_size": 128,
287 "symmetric": True,
288 },
289 weight_quant_key=kFp8Static128BlockSym,
290 act_quant_key=kFp8Dynamic128Sym,
291 dispatch_cls=QuarkW8A8Fp8MoEMethod,
292 ),
293 QTensorConfig(
294 name="fp8_w8a8_block_static_input",
295 weight={
296 "dtype": "fp8_e4m3",
297 "qscheme": "per_block",
298 "is_dynamic": False,
299 "block_size": [128, 128],
300 "symmetric": True,
301 },
302 input_tensors={
303 "dtype": "fp8_e4m3",
304 "qscheme": "per_group",
305 "is_dynamic": False,
306 "group_size": 128,
307 "symmetric": True,
308 },
309 expected_error=(NotImplementedError, "No quark compatible scheme"),
310 ),
311 QTensorConfig(
312 name="fp8_w8a8_block_group_size_mismatch",
313 weight={
314 "dtype": "fp8_e4m3",
315 "qscheme": "per_block",
316 "is_dynamic": False,
317 "block_size": [128, 128],
318 "symmetric": True,
319 },
320 input_tensors={
321 "dtype": "fp8_e4m3",
322 "qscheme": "per_group",
323 "is_dynamic": True,
324 "group_size": 64,
325 "symmetric": True,
326 },
327 expected_error=(NotImplementedError, "No quark compatible scheme"),
328 ),
329 QTensorConfig(
330 name="fp8_w8a8_block_missing_block_size",
331 weight={
332 "dtype": "fp8_e4m3",
333 "qscheme": "per_block",
334 "is_dynamic": False,
335 "symmetric": True,
336 },
337 input_tensors={
338 "dtype": "fp8_e4m3",
339 "qscheme": "per_group",
340 "is_dynamic": True,
341 "group_size": 128,
342 "symmetric": True,
343 },
344 expected_error=(ValueError, "requires `block_size`"),
345 ),
346 QTensorConfig(
347 name="int8_w8a8_static_symmetric",
348 weight={
349 "dtype": "int8",
350 "qscheme": "per_tensor",
351 "is_dynamic": False,
352 "symmetric": True,
353 },
354 input_tensors={
355 "dtype": "int8",
356 "qscheme": "per_tensor",
357 "is_dynamic": False,
358 "symmetric": True,
359 },
360 weight_quant_key=kInt8StaticTensorSym,
361 act_quant_key=kInt8StaticTensorSym,
362 dispatch_cls=QuarkW8A8Int8,
363 ),
364 QTensorConfig(
365 name="int8_w8a8_static_asymmetric",
366 weight={
367 "dtype": "int8",
368 "qscheme": "per_tensor",
369 "is_dynamic": False,
370 "symmetric": True,
371 },
372 input_tensors={
373 "dtype": "int8",
374 "qscheme": "per_tensor",
375 "is_dynamic": False,
376 "symmetric": False,
377 },
378 weight_quant_key=kInt8StaticTensorSym,
379 act_quant_key=kInt8StaticTensorAsym,
380 dispatch_cls=QuarkW8A8Int8,
381 ),
382 QTensorConfig(
383 name="int8_w8a8_channel_static_symmetric",
384 weight={
385 "dtype": "int8",
386 "qscheme": "per_channel",
387 "is_dynamic": False,
388 "symmetric": True,
389 },
390 input_tensors={
391 "dtype": "int8",
392 "qscheme": "per_tensor",
393 "is_dynamic": False,
394 "symmetric": True,
395 },
396 weight_quant_key=kInt8StaticChannelSym,
397 act_quant_key=kInt8StaticTensorSym,
398 dispatch_cls=QuarkW8A8Int8,
399 ),
400 QTensorConfig(
401 name="int8_w8a8_channel_static_asymmetric",
402 weight={
403 "dtype": "int8",
404 "qscheme": "per_channel",
405 "is_dynamic": False,
406 "symmetric": True,
407 },
408 input_tensors={
409 "dtype": "int8",
410 "qscheme": "per_tensor",
411 "is_dynamic": False,
412 "symmetric": False,
413 },
414 weight_quant_key=kInt8StaticChannelSym,
415 act_quant_key=kInt8StaticTensorAsym,
416 dispatch_cls=QuarkW8A8Int8,
417 ),
418 QTensorConfig(
419 name="int8_w8a8_dynamic_tensor_symmetric",
420 weight={
421 "dtype": "int8",
422 "qscheme": "per_tensor",
423 "is_dynamic": False,
424 "symmetric": True,
425 },
426 input_tensors={
427 "dtype": "int8",
428 "qscheme": "per_channel",
429 "is_dynamic": True,
430 "symmetric": True,
431 },
432 weight_quant_key=kInt8StaticTensorSym,
433 act_quant_key=kInt8DynamicTensorSym,
434 dispatch_cls=QuarkW8A8Int8,
435 ),
436 QTensorConfig(
437 name="int8_w8a8_dynamic_tensor_asymmetric",
438 weight={
439 "dtype": "int8",
440 "qscheme": "per_tensor",
441 "is_dynamic": False,
442 "symmetric": True,
443 },
444 input_tensors={
445 "dtype": "int8",
446 "qscheme": "per_channel",
447 "is_dynamic": True,
448 "symmetric": False,
449 },
450 weight_quant_key=kInt8StaticTensorSym,
451 act_quant_key=kInt8DynamicTensorAsym,
452 dispatch_cls=QuarkW8A8Int8,
453 ),
454 QTensorConfig(
455 name="int8_w8a8_dynamic_token",
456 weight={
457 "dtype": "int8",
458 "qscheme": "per_channel",
459 "is_dynamic": False,
460 "symmetric": True,
461 },
462 input_tensors={
463 "dtype": "int8",
464 "qscheme": "per_channel",
465 "is_dynamic": True,
466 "symmetric": True,
467 },
468 weight_quant_key=kInt8StaticChannelSym,
469 act_quant_key=kInt8DynamicTokenSym,
470 dispatch_cls=QuarkW8A8Int8,
471 ),
472 QTensorConfig(
473 name="int8_w8a8_dynamic_token_asymmetric",
474 weight={
475 "dtype": "int8",
476 "qscheme": "per_channel",
477 "is_dynamic": False,
478 "symmetric": True,
479 },
480 input_tensors={
481 "dtype": "int8",
482 "qscheme": "per_channel",
483 "is_dynamic": True,
484 "symmetric": False,
485 },
486 weight_quant_key=kInt8StaticChannelSym,
487 act_quant_key=kInt8DynamicTokenAsym,
488 dispatch_cls=QuarkW8A8Int8,
489 ),
490 QTensorConfig(
491 name="ocp_mx_mxfp4_weight_only",
492 weight={
493 "dtype": "fp4",
494 "qscheme": "per_group",
495 "group_size": 32,
496 "scale_format": "e8m0",
497 "is_dynamic": False,
498 },
499 input_tensors=None,
500 weight_quant_key=kMxfp4Static,
501 act_quant_key=None,
502 dispatch_cls=QuarkOCP_MX,
503 ),
504 QTensorConfig(
505 name="ocp_mx_mxfp4_activation",
506 weight={
507 "dtype": "fp4",
508 "qscheme": "per_group",
509 "group_size": 32,
510 "scale_format": "e8m0",
511 "is_dynamic": False,
512 },
513 input_tensors={
514 "dtype": "fp4",
515 "qscheme": "per_group",
516 "group_size": 32,
517 "scale_format": "e8m0",
518 "is_dynamic": True,
519 },
520 weight_quant_key=kMxfp4Static,
521 act_quant_key=kMxfp4Dynamic,
522 dispatch_cls=QuarkOCP_MX,
523 ),
524 QTensorConfig(
525 name="ocp_mx_mxfp6_e3m2",
526 weight={
527 "dtype": "fp6_e3m2",
528 "qscheme": "per_group",
529 "group_size": 32,
530 "scale_format": "e8m0",
531 "is_dynamic": False,
532 },
533 input_tensors={
534 "dtype": "fp6_e3m2",
535 "qscheme": "per_group",
536 "group_size": 32,
537 "scale_format": "e8m0",
538 "is_dynamic": True,
539 },
540 weight_quant_key=kMxfp6E3M2Static,
541 act_quant_key=kMxfp6E3M2Dynamic,
542 dispatch_cls=QuarkOCP_MX,
543 ),
544 QTensorConfig(
545 name="ocp_mx_mxfp4_mxfp6_e3m2_activation",
546 weight={
547 "dtype": "fp4",
548 "qscheme": "per_group",
549 "group_size": 32,
550 "scale_format": "e8m0",
551 "is_dynamic": False,
552 },
553 input_tensors={
554 "dtype": "fp6_e3m2",
555 "qscheme": "per_group",
556 "group_size": 32,
557 "scale_format": "e8m0",
558 "is_dynamic": True,
559 },
560 weight_quant_key=kMxfp4Static,
561 act_quant_key=kMxfp6E3M2Dynamic,
562 dispatch_cls=QuarkOCP_MX,
563 ),
564 QTensorConfig(
565 name="ocp_mx_mxfp4_mxfp6_e2m3_activation",
566 weight={
567 "dtype": "fp4",
568 "qscheme": "per_group",
569 "group_size": 32,
570 "scale_format": "e8m0",
571 "is_dynamic": False,
572 },
573 input_tensors={
574 "dtype": "fp6_e2m3",
575 "qscheme": "per_group",
576 "group_size": 32,
577 "scale_format": "e8m0",
578 "is_dynamic": True,
579 },
580 weight_quant_key=kMxfp4Static,
581 act_quant_key=kMxfp6E2M3Dynamic,
582 dispatch_cls=QuarkOCP_MX,
583 ),
584 QTensorConfig(
585 name="ocp_mx_mxfp6_e2m3",
586 weight={
587 "dtype": "fp6_e2m3",
588 "qscheme": "per_group",
589 "group_size": 32,
590 "scale_format": "e8m0",
591 "is_dynamic": False,
592 },
593 input_tensors={
594 "dtype": "fp6_e2m3",
595 "qscheme": "per_group",
596 "group_size": 32,
597 "scale_format": "e8m0",
598 "is_dynamic": True,
599 },
600 weight_quant_key=kMxfp6E2M3Static,
601 act_quant_key=kMxfp6E2M3Dynamic,
602 dispatch_cls=QuarkOCP_MX,
603 ),
604 QTensorConfig(
605 name="nvfp4",
606 weight=[
607 {
608 "dtype": "fp4",
609 "qscheme": "per_group",
610 "group_size": 16,
611 "is_dynamic": False,
612 },
613 {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
614 ],
615 input_tensors=[
616 {
617 "dtype": "fp4",
618 "qscheme": "per_group",
619 "group_size": 16,
620 "is_dynamic": True,
621 },
622 {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
623 ],
624 weight_quant_key=kNvfp4Static,
625 act_quant_key=kNvfp4Dynamic,
626 dispatch_cls=QuarkNVFP4,
627 ),
628 QTensorConfig(
629 name="w4a8_fp8_static",
630 weight=[
631 {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
632 {
633 "dtype": "int4",
634 "qscheme": "per_channel",
635 "is_dynamic": False,
636 "symmetric": True,
637 "ch_axis": 0,
638 },
639 ],
640 input_tensors={
641 "dtype": "fp8_e4m3",
642 "qscheme": "per_tensor",
643 "is_dynamic": False,
644 },
645 weight_quant_key=kInt4W4A8StaticChannelSym,
646 act_quant_key=kFp8StaticTensorSym,
647 dispatch_cls=QuarkW4A8Fp8MoEMethod,
648 ),
649 QTensorConfig(
650 name="w4a8_fp8_dynamic",
651 weight=[
652 {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
653 {
654 "dtype": "int4",
655 "qscheme": "per_channel",
656 "is_dynamic": False,
657 "symmetric": True,
658 "ch_axis": 0,
659 },
660 ],
661 input_tensors={
662 "dtype": "fp8_e4m3",
663 "qscheme": "per_channel",
664 "is_dynamic": True,
665 },
666 weight_quant_key=kInt4W4A8StaticChannelSym,
667 act_quant_key=kFp8DynamicTokenSym,
668 dispatch_cls=QuarkW4A8Fp8MoEMethod,
669 ),
670 QTensorConfig(
671 name="w4a8_fp8_static_single_entry_input",
672 weight=[
673 {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
674 {
675 "dtype": "int4",
676 "qscheme": "per_channel",
677 "is_dynamic": False,
678 "symmetric": True,
679 "ch_axis": 0,
680 },
681 ],
682 input_tensors=[
683 {
684 "dtype": "fp8_e4m3",
685 "qscheme": "per_tensor",
686 "is_dynamic": False,
687 }
688 ],
689 weight_quant_key=kInt4W4A8StaticChannelSym,
690 act_quant_key=kFp8StaticTensorSym,
691 dispatch_cls=QuarkW4A8Fp8MoEMethod,
692 ),
693 ]
694
695
696 def _make_qtensor_config(
697 weight: QuarkQTensorHint,
698 input_tensors: QuarkQTensorHint,
699 exclude: list[str] | None = None,
700 ) -> QuarkConfig:
701 return QuarkConfig(
702 {
703 "global_quant_config": {
704 "weight": weight,
705 "input_tensors": input_tensors,
706 },
707 "layer_type_quant_config": {},
708 "exclude": exclude or [],
709 }
710 )
711
712
713 def _make_test_moe_config() -> FusedMoEConfig:
714 return FusedMoEConfig(
715 num_experts=8,
716 experts_per_token=2,
717 hidden_dim=256,
718 intermediate_size=256,
719 num_local_experts=8,
720 num_logical_experts=8,
721 activation=MoEActivation.SILU,
722 device=current_platform.device_type,
723 routing_method=RoutingMethodType.Renormalize,
724 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
725 in_dtype=torch.bfloat16,
726 )
727
728
729 if QUARK_MXFP4_AVAILABLE:
730 from quark.torch.export.nn.modules.realquantizer import StaticScaledRealQuantizer
731 from quark.torch.kernel import mx as mx_kernel
732 from quark.torch.quantization.config.config import FP4PerGroupSpec
733
734 try:
735 hf_api().list_repo_refs(
736 "amd/Llama-3.3-70B-Instruct-WMXFP4-AMXFP4-KVFP8-Scale-UINT8-SQ"
737 )
738 HF_HUB_AMD_ORG_ACCESS = True
739 except huggingface_hub.errors.RepositoryNotFoundError:
740 HF_HUB_AMD_ORG_ACCESS = False
741
742
743 @pytest.fixture(scope="function", autouse=True)
744 def enable_pickle(monkeypatch):
745 """`LLM.apply_model` requires pickling a function."""
746 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
747
748
749 def test_quark_w8a8_fp8_per_block_registers_weight_scale(monkeypatch):
750 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
751 get_fp8_block_weight_scale,
752 )
753
754 monkeypatch.setattr(
755 "vllm.model_executor.layers.quantization.quark.schemes."
756 "quark_w8a8_fp8.get_current_vllm_config",
757 lambda: SimpleNamespace(model_config=SimpleNamespace(dtype=torch.bfloat16)),
758 )
759 scheme = QuarkW8A8Fp8PerBlock(kFp8Static128BlockSym, kFp8Dynamic128Sym)
760
761 layer = torch.nn.Module()
762 layer.weight_scale = torch.tensor([2.0])
763 assert get_fp8_block_weight_scale(layer) is None
764 layer.scheme = scheme
765 assert get_fp8_block_weight_scale(layer) is layer.weight_scale
766 layer.weight_scale_inv = torch.tensor([3.0])
767 assert get_fp8_block_weight_scale(layer) is layer.weight_scale
768 layer.scheme = None
769 assert get_fp8_block_weight_scale(layer) is layer.weight_scale_inv
770
771 loaded = torch.nn.Module()
772
773 def weight_loader(param, loaded_weight):
774 return None
775
776 dummy_param = torch.nn.Parameter(torch.empty(1), requires_grad=False)
777 with (
778 patch(
779 "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
780 "validate_fp8_block_shape"
781 ),
782 patch(
783 "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
784 "create_fp8_weight_parameter",
785 return_value=dummy_param,
786 ),
787 patch(
788 "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
789 "create_fp8_scale_parameter",
790 return_value=dummy_param,
791 ),
792 patch(
793 "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
794 "init_fp8_linear_kernel",
795 return_value=MagicMock(),
796 ),
797 ):
798 scheme.create_weights(
799 loaded,
800 output_partition_sizes=[256],
801 input_size_per_partition=256,
802 params_dtype=torch.bfloat16,
803 weight_loader=weight_loader,
804 input_size=256,
805 output_size=256,
806 )
807 assert hasattr(loaded, "weight_scale")
808 assert not hasattr(loaded, "weight_scale_inv")
809
810
811 def test_quark_config_has_no_model_specific_fused_mappings():
812 config = QuarkConfig({})
813
814 assert "gate_up_proj" not in config.packed_modules_mapping
815 assert "fused_wqa_wkv" not in config.packed_modules_mapping
816
817
818 def test_quark_config_preserves_existing_packed_modules_mapping():
819 class CustomQuarkConfig(QuarkConfig):
820 packed_modules_mapping = {"custom_proj": ["a", "b"]}
821
822 config = CustomQuarkConfig({})
823
824 assert config.packed_modules_mapping["custom_proj"] == ["a", "b"]
825
826
827 def test_quant_method_dispatch_ignored(default_vllm_config):
828 config = _make_qtensor_config(None, None, exclude=["linear", "experts"])
829
830 class TestLinear(LinearBase):
831 def __init__(self):
832 torch.nn.Module.__init__(self)
833
834 class TestRoutedExperts(RoutedExperts):
835 def __init__(self):
836 torch.nn.Module.__init__(self)
837 self.moe_config = _make_test_moe_config()
838
839 assert config.get_quant_method_target("linear", LinearBase) == (
840 None,
841 None,
842 UnquantizedLinearMethod,
843 )
844 assert isinstance(
845 config.get_quant_method(TestLinear(), "linear"), UnquantizedLinearMethod
846 )
847
848 assert config.get_quant_method_target("experts", RoutedExperts) == (
849 None,
850 None,
851 UnquantizedFusedMoEMethod,
852 )
853 assert isinstance(
854 config.get_quant_method(TestRoutedExperts(), "experts"),
855 UnquantizedFusedMoEMethod,
856 )
857
858 mxfp4_config = _make_qtensor_config(
859 {
860 "dtype": "fp4",
861 "qscheme": "per_group",
862 "group_size": 32,
863 "scale_format": "e8m0",
864 "is_dynamic": False,
865 },
866 None,
867 exclude=["self_attn.q_proj", "mlp.down_proj"],
868 )
869 assert mxfp4_config.get_quant_method_target("self_attn.q_proj", LinearBase) == (
870 None,
871 None,
872 UnquantizedLinearMethod,
873 )
874 assert isinstance(
875 mxfp4_config.get_quant_method(TestLinear(), "self_attn.q_proj"),
876 UnquantizedLinearMethod,
877 )
878
879 assert mxfp4_config.get_quant_method_target("mlp.down_proj", LinearBase) == (
880 None,
881 None,
882 UnquantizedLinearMethod,
883 )
884 assert isinstance(
885 mxfp4_config.get_quant_method(TestLinear(), "mlp.down_proj"),
886 UnquantizedLinearMethod,
887 )
888
889
890 @pytest.mark.parametrize("case", QTENSOR_CONFIGS, ids=lambda case: case.name)
891 def test_quant_method_dispatch_target(case):
892 config = _make_qtensor_config(case.weight, case.input_tensors)
893 if case.expected_error is not None:
894 error_type, error_message = case.expected_error
895 with pytest.raises(error_type, match=error_message):
896 config.get_quant_method_target("linear", LinearBase)
897 return
898
899 assert case.dispatch_cls is not None
900 is_linear = issubclass(case.dispatch_cls, QuarkScheme)
901
902 weight_quant_key, act_quant_key, method_cls = config.get_quant_method_target(
903 "linear" if is_linear else "experts",
904 LinearBase if is_linear else RoutedExperts,
905 )
906
907 assert weight_quant_key == case.weight_quant_key
908 assert act_quant_key == case.act_quant_key
909 assert method_cls is (QuarkLinearMethod if is_linear else case.dispatch_cls)
910
911
912 def test_quant_method_dispatch_mxfp8_2d_block(default_vllm_config):
913 """Shape B (DeepSeek-V4.1): 32x32 per-block e8m0 routes to QuarkOCP_MX.
914
915 Verifies that get_quant_method and get_quant_method_target agree (the
916 short-circuit that previously caused them to diverge has been removed).
917 """
918 from vllm.model_executor.layers.quantization.quark.schemes import QuarkOCP_MX
919 from vllm.model_executor.layers.quantization.utils.quant_utils import kMxfp8Static
920
921 default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
922 mxfp8_spec = {
923 "weight": {
924 "dtype": "fp8_e4m3",
925 "qscheme": "per_block",
926 "block_size": [32, 32],
927 "symmetric": True,
928 "is_dynamic": False,
929 "scale_type": "float8_e8m0fnu",
930 },
931 "input_tensors": {
932 "dtype": "fp8_e4m3",
933 "qscheme": "per_group",
934 "group_size": 32,
935 "symmetric": True,
936 "is_dynamic": True,
937 },
938 }
939 config = QuarkConfig(
940 {
941 "global_quant_config": {
942 "weight": {
943 "dtype": "fp4",
944 "qscheme": "per_group",
945 "group_size": 32,
946 "scale_format": "e8m0",
947 "is_dynamic": False,
948 },
949 "input_tensors": {
950 "dtype": "fp4",
951 "qscheme": "per_group",
952 "group_size": 32,
953 "scale_format": "e8m0",
954 "is_dynamic": True,
955 },
956 },
957 "layer_type_quant_config": {},
958 "layer_quant_config": {"layers.0.attn.wkv": mxfp8_spec},
959 "exclude": [],
960 }
961 )
962
963 class TestLinear(LinearBase):
964 def __init__(self):
965 torch.nn.Module.__init__(self)
966
967 # get_quant_method_target now routes MXFP8 through the matcher chain.
968 wk, ak, mcls = config.get_quant_method_target("layers.0.attn.wkv", LinearBase)
969 assert wk == kMxfp8Static
970 assert mcls is QuarkLinearMethod
971
972 linear = TestLinear()
973 method = config.get_quant_method(linear, "layers.0.attn.wkv")
974 assert isinstance(method, QuarkLinearMethod)
975 assert isinstance(linear.scheme, QuarkOCP_MX)
976 assert linear.scheme.weight_quant_key == kMxfp8Static
977 assert linear.scheme.scale_block_rows == 32
978
979 # Experts still fall through to the global MXFP4 spec.
980 assert (
981 config.get_quant_method_target("layers.0.ffn.experts", RoutedExperts)[2]
982 is not QuarkLinearMethod
983 )
984
985 # 128x128 per-block FP8 (DeepSeek V4) keeps its existing Quark scheme.
986 v4_spec = {
987 "weight": {**mxfp8_spec["weight"], "block_size": [128, 128]},
988 "input_tensors": {**mxfp8_spec["input_tensors"], "group_size": 128},
989 }
990 v4_config = _make_qtensor_config(v4_spec["weight"], v4_spec["input_tensors"])
991 assert v4_config.get_quant_method_target("linear", LinearBase)[2] is (
992 QuarkLinearMethod
993 )
994 linear = TestLinear()
995 assert isinstance(v4_config.get_quant_method(linear, "linear"), QuarkLinearMethod)
996 assert isinstance(linear.scheme, QuarkW8A8Fp8PerBlock)
997
998
999 def test_quant_method_dispatch_mxfp8_canonical(default_vllm_config):
1000 """Shape A (canonical MX): per_group/group_size=32/e8m0 FP8 routes
1001 to QuarkOCP_MX with scale_block_rows == 1.
1002
1003 This covers the canonical MXFP8 spelling (no 2-D block). Validated by
1004 synthetic dispatch test only — no canonical-MXFP8 checkpoint on hand.
1005 """
1006 from vllm.model_executor.layers.quantization.quark.schemes import QuarkOCP_MX
1007 from vllm.model_executor.layers.quantization.utils.quant_utils import kMxfp8Static
1008
1009 default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
1010 mxfp8_canonical_spec = {
1011 "weight": {
1012 "dtype": "fp8_e4m3",
1013 "qscheme": "per_group",
1014 "group_size": 32,
1015 "scale_format": "e8m0",
1016 "is_dynamic": False,
1017 },
1018 "input_tensors": {
1019 "dtype": "fp8_e4m3",
1020 "qscheme": "per_group",
1021 "group_size": 32,
1022 "scale_format": "e8m0",
1023 "is_dynamic": True,
1024 },
1025 }
1026 config = QuarkConfig(
1027 {
1028 "global_quant_config": mxfp8_canonical_spec,
1029 "layer_type_quant_config": {},
1030 "layer_quant_config": {},
1031 "exclude": [],
1032 }
1033 )
1034
1035 wk, ak, mcls = config.get_quant_method_target("linear", LinearBase)
1036 assert wk == kMxfp8Static
1037 assert mcls is QuarkLinearMethod
1038
1039 class TestLinear(LinearBase):
1040 def __init__(self):
1041 torch.nn.Module.__init__(self)
1042
1043 linear = TestLinear()
1044 method = config.get_quant_method(linear, "linear")
1045 assert isinstance(method, QuarkLinearMethod)
1046 assert isinstance(linear.scheme, QuarkOCP_MX)
1047 assert linear.scheme.weight_quant_key == kMxfp8Static
1048 assert linear.scheme.scale_block_rows == 1
1049
1050
1051 def test_quant_method_dispatch_mxfp8_moe_raises(default_vllm_config):
1052 """MXFP8 in a MoE config raises ValueError — experts are unsupported."""
1053 from vllm.model_executor.layers.quantization.quark.quark_moe import (
1054 QuarkOCP_MX_MoEMethod,
1055 )
1056
1057 default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
1058 mxfp8_spec = {
1059 "weight": {
1060 "dtype": "fp8_e4m3",
1061 "qscheme": "per_group",
1062 "group_size": 32,
1063 "scale_format": "e8m0",
1064 "is_dynamic": False,
1065 },
1066 "input_tensors": {
1067 "dtype": "fp8_e4m3",
1068 "qscheme": "per_group",
1069 "group_size": 32,
1070 "scale_format": "e8m0",
1071 "is_dynamic": True,
1072 },
1073 }
1074 config = QuarkConfig(
1075 {
1076 "global_quant_config": mxfp8_spec,
1077 "layer_type_quant_config": {},
1078 "layer_quant_config": {},
1079 "exclude": [],
1080 }
1081 )
1082 wk, ak, mcls = config.get_quant_method_target("experts", RoutedExperts)
1083 assert mcls is QuarkOCP_MX_MoEMethod
1084 assert wk is not None
1085 # The OCP MX MoE constructor should fail loudly for MXFP8.
1086 fake_moe_config = MagicMock()
1087 with pytest.raises(ValueError, match="MXFP8 experts are not supported"):
1088 QuarkOCP_MX_MoEMethod(fake_moe_config, wk, ak)
1089
1090
1091 @pytest.mark.parametrize(
1092 ("weight", "input_tensors"),
1093 [
1094 pytest.param(
1095 {
1096 "dtype": "int8",
1097 "qscheme": "per_group",
1098 "is_dynamic": False,
1099 "symmetric": True,
1100 },
1101 {
1102 "dtype": "int8",
1103 "qscheme": "per_tensor",
1104 "is_dynamic": False,
1105 "symmetric": True,
1106 },
1107 id="single_entry",
1108 ),
1109 pytest.param(
1110 [
1111 {"dtype": "int8", "qscheme": "per_tensor"},
1112 {"dtype": "int8", "qscheme": "per_tensor"},
1113 ],
1114 [
1115 {"dtype": "int8", "qscheme": "per_tensor"},
1116 {"dtype": "int8", "qscheme": "per_tensor"},
1117 ],
1118 id="multi_entry",
1119 ),
1120 ],
1121 )
1122 def test_quant_method_dispatch_unsupported(weight, input_tensors):
1123 config = _make_qtensor_config(weight, input_tensors)
1124
1125 class TestRoutedExperts(RoutedExperts):
1126 def __init__(self):
1127 torch.nn.Module.__init__(self)
1128
1129 with pytest.raises(RuntimeError, match="^Unsupported FusedMoe scheme$"):
1130 config.get_quant_method_target("experts", RoutedExperts)
1131
1132 with pytest.raises(RuntimeError, match="^Unsupported FusedMoe scheme$"):
1133 config.get_quant_method(TestRoutedExperts(), "experts")
1134
1135
1136 @pytest.mark.parametrize(
1137 "case",
1138 [case for case in QTENSOR_CONFIGS if case.expected_error is None],
1139 ids=lambda case: case.name,
1140 )
1141 def test_quant_method_dispatch_instantiation(case, monkeypatch, default_vllm_config):
1142 config = _make_qtensor_config(case.weight, case.input_tensors)
1143 assert case.dispatch_cls is not None
1144 if issubclass(case.dispatch_cls, QuarkScheme):
1145
1146 class TestLinear(LinearBase):
1147 def __init__(self):
1148 torch.nn.Module.__init__(self)
1149
1150 monkeypatch.setattr(
1151 "vllm.model_executor.layers.quantization.quark.schemes."
1152 "quark_w8a8_fp8.get_current_vllm_config",
1153 lambda: SimpleNamespace(model_config=SimpleNamespace(dtype=torch.bfloat16)),
1154 )
1155 layer = TestLinear()
1156 method = config.get_quant_method(layer, "linear")
1157
1158 assert isinstance(method, QuarkLinearMethod)
1159 assert isinstance(layer.scheme, case.dispatch_cls)
1160 if case.weight_quant_key == kFp8Static128BlockE8M0Sym:
1161 # TODO: Remove once E8M0 quant key is properly handled in oracle
1162 assert layer.scheme.weight_quant_key == kFp8Static128BlockSym
1163 else:
1164 assert layer.scheme.weight_quant_key == case.weight_quant_key
1165 assert layer.scheme.activation_quant_key == case.act_quant_key
1166 else:
1167
1168 class TestRoutedExperts(RoutedExperts):
1169 def __init__(self):
1170 torch.nn.Module.__init__(self)
1171 self.moe_config = _make_test_moe_config()
1172
1173 for target in (
1174 "select_fp8_moe_backend",
1175 "select_int8_moe_backend",
1176 "select_mxfp4_moe_backend",
1177 "backend_to_kernel_cls",
1178 "select_nvfp4_moe_backend",
1179 ):
1180 monkeypatch.setattr(
1181 f"vllm.model_executor.layers.quantization.quark.quark_moe.{target}",
1182 lambda *args, **kwargs: (object(), object()),
1183 )
1184
1185 # AssertionError: W4A8 FP8 MoE requires ROCm AITER fused MoE support
1186 monkeypatch.setattr(
1187 "vllm.model_executor.layers.quantization.quark.quark_moe."
1188 "rocm_aiter_ops.is_fused_moe_enabled",
1189 lambda: True,
1190 )
1191
1192 # default_vllm_config carries no model, but the FP8 and OCP MX methods
1193 # read the model type off the HF config.
1194 monkeypatch.setattr(
1195 "vllm.model_executor.layers.quantization.quark.quark_moe."
1196 "get_current_vllm_config",
1197 lambda: SimpleNamespace(
1198 model_config=SimpleNamespace(hf_config=SimpleNamespace())
1199 ),
1200 )
1201
1202 layer = TestRoutedExperts()
1203 method = config.get_quant_method(layer, "experts")
1204
1205 assert isinstance(method, case.dispatch_cls)
1206
1207
1208 QUARK_MOE_MODULE = "vllm.model_executor.layers.quantization.quark.quark_moe"
1209 # validate_fp8_block_shape_moe imports this from vllm.distributed when called,
1210 # so the patch has to target the source module rather than a local binding.
1211 TP_WORLD_SIZE = "vllm.distributed.get_tensor_model_parallel_world_size"
1212
1213
1214 def _make_per_block_fp8_moe_method(
1215 activation_quant_key: QuantKey = kFp8Dynamic128Sym,
1216 ) -> QuarkW8A8Fp8MoEMethod:
1217 return QuarkW8A8Fp8MoEMethod(
1218 _make_test_moe_config(),
1219 kFp8Static128BlockSym,
1220 activation_quant_key,
1221 )
1222
1223
1224 def test_quark_w8a8_fp8_moe_per_block_requires_dynamic_group_input():
1225 with (
1226 patch(
1227 f"{QUARK_MOE_MODULE}.select_fp8_moe_backend",
1228 return_value=(Mock(), Mock()),
1229 ),
1230 patch(f"{QUARK_MOE_MODULE}.get_current_vllm_config"),
1231 pytest.raises(ValueError, match="per-block scales"),
1232 ):
1233 _make_per_block_fp8_moe_method(kFp8StaticTensorSym)
1234
1235
1236 def test_quark_w8a8_fp8_moe_per_block_weight_shapes():
1237 with (
1238 patch(
1239 f"{QUARK_MOE_MODULE}.select_fp8_moe_backend",
1240 return_value=(Mock(), Mock()),
1241 ),
1242 patch(f"{QUARK_MOE_MODULE}.get_current_vllm_config"),
1243 patch(TP_WORLD_SIZE, return_value=1),
1244 ):
1245 method = _make_per_block_fp8_moe_method()
1246 assert method.block_quant
1247 assert method.weight_block_size == [128, 128]
1248
1249 layer = torch.nn.Module()
1250 method.create_weights(
1251 layer,
1252 num_experts=4,
1253 hidden_size=512,
1254 intermediate_size_per_partition=256,
1255 params_dtype=torch.bfloat16,
1256 )
1257
1258 w13_num_shards = method.moe.w13_num_shards
1259 assert layer.weight_block_size == [128, 128]
1260 assert layer.w13_weight.shape == (4, w13_num_shards * 256, 512)
1261 assert layer.w2_weight.shape == (4, 512, 256)
1262 # Quark exports block scales as `weight_scale`, like the per-tensor and
1263 # per-channel schemes, so all schemes register the same parameter name.
1264 assert not hasattr(layer, "w13_weight_scale_inv")
1265 assert not hasattr(layer, "w2_weight_scale_inv")
1266 # One scale per 128x128 tile of each expert's weight.
1267 assert layer.w13_weight_scale.shape == (
1268 4,
1269 w13_num_shards * (256 // 128),
1270 512 // 128,
1271 )
1272 assert layer.w2_weight_scale.shape == (4, 512 // 128, 256 // 128)
1273 # The loader shards block scales on the block grid, not per row.
1274 for scale in (layer.w13_weight_scale, layer.w2_weight_scale):
1275 assert scale.quant_method == FusedMoeWeightScaleSupported.BLOCK.value
1276
1277
1278 def test_quark_w8a8_fp8_moe_per_block_rejects_misaligned_partition():
1279 with (
1280 patch(
1281 f"{QUARK_MOE_MODULE}.select_fp8_moe_backend",
1282 return_value=(Mock(), Mock()),
1283 ),
1284 patch(f"{QUARK_MOE_MODULE}.get_current_vllm_config"),
1285 patch(TP_WORLD_SIZE, return_value=1),
1286 pytest.raises(ValueError, match="not divisible by"),
1287 ):
1288 _make_per_block_fp8_moe_method().create_weights(
1289 torch.nn.Module(),
1290 num_experts=4,
1291 hidden_size=512,
1292 intermediate_size_per_partition=192,
1293 params_dtype=torch.bfloat16,
1294 )
1295
1296
1297 def test_quark_fp8_ptpc_exposes_kernel_input_quant_key(monkeypatch):
1298 """QuarkW8A8Fp8 must advertise the key its kernel consumes pre-quantized.
1299
1300 Only kernel selection runs, no GEMM. The shape matters: AITER PTPC
1301 kernels decline untuned shapes, leaving a torch kernel that has no key.
1302 """
1303 # Llama-3.1-70B qkv_proj at TP1 (aiter-tuned N, K on gfx950).
1304 N, K = 10240, 8192
1305 dtype = torch.bfloat16
1306 kernel_config = FP8ScaledMMLinearLayerConfig(
1307 weight_quant_key=kFp8StaticChannelSym,
1308 activation_quant_key=kFp8DynamicTokenSym,
1309 weight_shape=(N, K),
1310 input_dtype=dtype,
1311 out_dtype=dtype,
1312 )
1313 if not any(
1314 cls.is_supported()[0] and cls.can_implement(kernel_config)[0]
1315 for cls in AITER_PTPC_KERNELS
1316 ):
1317 pytest.skip("no AITER PTPC kernel is usable for this shape and environment")
1318
1319 monkeypatch.setattr(parameter, "get_tensor_model_parallel_rank", lambda: 0)
1320 monkeypatch.setattr(parameter, "get_tensor_model_parallel_world_size", lambda: 1)
1321
1322 scheme = QuarkW8A8Fp8.__new__(QuarkW8A8Fp8)
1323 scheme.weight_qscheme = "per_channel"
1324 scheme.is_static_input_scheme = False
1325 scheme.activation_quant_key = kFp8DynamicTokenSym
1326 scheme.weight_quant_key = kFp8StaticChannelSym
1327 scheme.out_dtype = dtype
1328 scheme.input_dtype = dtype
1329
1330 layer = torch.nn.Module()
1331 with set_current_vllm_config(VllmConfig()):
1332 scheme.create_weights(
1333 layer,
1334 output_partition_sizes=[N],
1335 input_size_per_partition=K,
1336 params_dtype=dtype,
1337 weight_loader=lambda *args, **kwargs: None,
1338 )
1339
1340 assert isinstance(scheme.fp8_linear, AITER_PTPC_KERNELS)
1341 assert layer.input_quant_key == kFp8DynamicTokenSym
1342
1343
1344 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
1345 def test_quark_fp8_w_per_tensor_a_per_tensor(
1346 kv_cache_dtype: CacheDType, monkeypatch, dist_init, workspace_init
1347 ):
1348 model_path = "amd/Llama-3.1-8B-Instruct-FP8-KV-Quark-test"
1349 checkpoint_scales = {}
1350 scale_names = {
1351 "model.layers.0.self_attn.k_proj.output_scale",
1352 "model.layers.0.self_attn.v_proj.output_scale",
1353 }
1354 original_load_weights = LlamaForCausalLM.load_weights
1355
1356 def load_weights(self, weights):
1357 def capture_scales():
1358 for name, weight in weights:
1359 if name in scale_names:
1360 checkpoint_scales[name] = weight.detach().cpu()
1361 yield name, weight
1362
1363 return original_load_weights(self, capture_scales())
1364
1365 monkeypatch.setattr(LlamaForCausalLM, "load_weights", load_weights)
1366 model, vllm_config = load_model_without_vllm_runner(
1367 model_path,
1368 model_config_kwargs={"hf_overrides": {"num_hidden_layers": 3}},
1369 vllm_config_kwargs={"cache_config": CacheConfig(cache_dtype=kv_cache_dtype)},
1370 )
1371
1372 qkv_proj = model.model.layers[0].self_attn.qkv_proj
1373 assert isinstance(qkv_proj.quant_method, QuarkLinearMethod)
1374 assert isinstance(qkv_proj.scheme, QuarkW8A8Fp8)
1375 assert len(qkv_proj.input_scale.shape) == 0
1376 assert qkv_proj.weight.dtype is current_platform.fp8_dtype()
1377 assert len(qkv_proj.weight_scale.shape) == 0
1378
1379 attn = model.model.layers[0].self_attn.attn
1380 if kv_cache_dtype == "fp8":
1381 assert checkpoint_scales.keys() == scale_names
1382 scale_multiplier = 2 if current_platform.is_fp8_fnuz() else 1
1383 assert attn._k_scale_float == (
1384 checkpoint_scales["model.layers.0.self_attn.k_proj.output_scale"].item()
1385 * scale_multiplier
1386 )
1387 assert attn._v_scale_float == (
1388 checkpoint_scales["model.layers.0.self_attn.v_proj.output_scale"].item()
1389 * scale_multiplier
1390 )
1391 else:
1392 assert attn._k_scale_float == 1.0
1393 assert attn._v_scale_float == 1.0
1394
1395 monkeypatch.setattr(Attention, "forward", lambda _, q, k, v: q.contiguous())
1396 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE_TYPE)
1397 positions = torch.arange(input_ids.numel(), device=DEVICE_TYPE)
1398 with (
1399 set_current_vllm_config(vllm_config),
1400 set_forward_context(None, vllm_config, num_tokens=input_ids.numel()),
1401 ):
1402 hidden_states = model(input_ids, positions, None)
1403 logits = model.compute_logits(hidden_states)
1404 assert torch.isfinite(logits).all()
1405
1406
1407 def test_quark_fp8_w_per_channel_a_per_token(monkeypatch, dist_init, workspace_init):
1408 model_path = "amd/Qwen2.5-1.5B-Instruct-ptpc-Quark-ts"
1409 model, vllm_config = load_model_without_vllm_runner(
1410 model_path,
1411 model_config_kwargs={"hf_overrides": {"num_hidden_layers": 3}},
1412 )
1413
1414 qkv_proj = model.model.layers[0].self_attn.qkv_proj
1415 assert isinstance(qkv_proj.quant_method, QuarkLinearMethod)
1416 assert isinstance(qkv_proj.scheme, QuarkW8A8Fp8)
1417 assert qkv_proj.weight.dtype is current_platform.fp8_dtype()
1418 assert qkv_proj.weight_scale.shape[0] == qkv_proj.weight.shape[1]
1419 assert qkv_proj.weight_scale.shape[1] == 1
1420
1421 monkeypatch.setattr(Attention, "forward", lambda _, q, k, v: q.contiguous())
1422 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE_TYPE)
1423 positions = torch.arange(input_ids.numel(), device=DEVICE_TYPE)
1424 with (
1425 set_current_vllm_config(vllm_config),
1426 set_forward_context(None, vllm_config, num_tokens=input_ids.numel()),
1427 ):
1428 hidden_states = model(input_ids, positions, None)
1429 logits = model.compute_logits(hidden_states)
1430 assert torch.isfinite(logits).all()
1431
1432
1433 def test_quark_int8_w_per_tensor_a_per_tensor(monkeypatch, dist_init, workspace_init):
1434 model_path = "amd/Llama-3.1-8B-Instruct-w-int8-a-int8-sym-test"
1435 model, vllm_config = load_model_without_vllm_runner(
1436 model_path,
1437 model_config_kwargs={"hf_overrides": {"num_hidden_layers": 3}},
1438 )
1439 with set_current_vllm_config(vllm_config):
1440 qkv_proj = model.model.layers[0].self_attn.qkv_proj
1441 assert isinstance(qkv_proj.quant_method, QuarkLinearMethod)
1442 assert isinstance(qkv_proj.scheme, QuarkW8A8Int8)
1443
1444 monkeypatch.setattr(Attention, "forward", lambda _, q, k, v: q.contiguous())
1445 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE_TYPE)
1446 positions = torch.arange(input_ids.numel(), device=DEVICE_TYPE)
1447 with set_forward_context(None, vllm_config, num_tokens=input_ids.numel()):
1448 hidden_states = model(input_ids, positions, None)
1449 logits = model.compute_logits(hidden_states)
1450 assert torch.isfinite(logits).all()
1451
1452
1453 @pytest.mark.parametrize("tp", [1])
1454 def test_quark_int8_w8a8_moe(vllm_runner, tp):
1455 """Test W8A8 INT8 MoE quantization with a tiny Qwen3 MoE model."""
1456 model_path = "amd/tiny-qwen3-moe-w8a8-int8"
1457 with vllm_runner(
1458 model_path,
1459 enforce_eager=True,
1460 tensor_parallel_size=tp,
1461 gpu_memory_utilization=0.1,
1462 ) as llm:
1463
1464 def check_model(model):
1465 layer = model.model.layers[0]
1466 # MoE experts should use QuarkW8A8Int8MoEMethod
1467 moe = layer.mlp.experts
1468 assert isinstance(moe._quant_method, QuarkW8A8Int8MoEMethod), (
1469 f"Expected QuarkW8A8Int8MoEMethod, got {type(moe._quant_method)}"
1470 )
1471 # Non-MoE linear layers should use QuarkW8A8Int8
1472 qkv_proj = layer.self_attn.qkv_proj
1473 assert isinstance(qkv_proj.scheme, QuarkW8A8Int8)
1474
1475 llm.apply_model(check_model)
1476
1477 output = llm.generate_greedy("Hello", max_tokens=4)
1478 assert output
1479
1480
1481 @pytest.mark.parametrize("tp", [1])
1482 def test_quark_fp8_w8a8_per_block_moe(vllm_runner, tp):
1483 """Test per-block (128x128) FP8 MoE quantization with a tiny Qwen3 MoE model."""
1484 model_path = "Adamji/tiny-qwen3-moe-fp8-per-block"
1485 with vllm_runner(
1486 model_path,
1487 enforce_eager=True,
1488 tensor_parallel_size=tp,
1489 gpu_memory_utilization=0.1,
1490 ) as llm:
1491
1492 def check_model(model):
1493 experts = model.model.layers[0].mlp.experts
1494 method = experts._quant_method
1495 assert isinstance(method, QuarkW8A8Fp8MoEMethod), (
1496 f"Expected QuarkW8A8Fp8MoEMethod, got {type(method)}"
1497 )
1498 assert method.weight_qscheme == "per_block"
1499 assert method.weight_block_size == [128, 128]
1500
1501 # hidden_size=128, moe_intermediate_size=256 and 4 experts, so one
1502 # scale per 128x128 tile, with w13 stacking gate on top of up.
1503 routed_experts = experts.routed_experts
1504 assert routed_experts.w13_weight_scale.shape == (4, 4, 1)
1505 assert routed_experts.w2_weight_scale.shape == (4, 1, 2)
1506 # Quark exports the block scales under the same name as the
1507 # per-tensor and per-channel schemes.
1508 assert not hasattr(routed_experts, "w13_weight_scale_inv")
1509 assert not hasattr(routed_experts, "w2_weight_scale_inv")
1510
1511 llm.apply_model(check_model)
1512
1513 output = llm.generate_greedy("Hello", max_tokens=4)
1514 assert output
1515
1516
1517 @pytest.mark.skipif(
1518 not (on_gfx950() or on_gfx942()),
1519 reason="Quark W4A8 (INT4-FP8) MoE requires the AITER kernel on gfx942/gfx950",
1520 )
1521 def test_quark_w4a8_fp8_moe(monkeypatch, dist_init, workspace_init):
1522 """Test W4A8 (INT4 weight + FP8 activation) MoE with a tiny Qwen3 MoE model.
1523
1524 W4A8 dispatches through the AITER fused MoE kernel, so AITER must be on.
1525 """
1526 monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
1527 monkeypatch.setenv("VLLM_ROCM_USE_AITER_MOE", "1")
1528 rocm_aiter_ops.refresh_env_variables()
1529
1530 model_path = "amd/tiny-qwen3-moe-w4a8"
1531 model, vllm_config = load_model_without_vllm_runner(
1532 model_path,
1533 )
1534 with set_current_vllm_config(vllm_config):
1535 moe = model.model.layers[0].mlp.experts
1536 assert isinstance(moe._quant_method, QuarkW4A8Fp8MoEMethod), (
1537 f"Expected QuarkW4A8Fp8MoEMethod, got {type(moe._quant_method)}"
1538 )
1539
1540 monkeypatch.setattr(Attention, "forward", lambda _, q, k, v: q.contiguous())
1541 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE_TYPE)
1542 positions = torch.arange(input_ids.numel(), device=DEVICE_TYPE)
1543 with set_forward_context(None, vllm_config, num_tokens=input_ids.numel()):
1544 hidden_states = model(input_ids, positions, None)
1545 logits = model.compute_logits(hidden_states)
1546 assert torch.isfinite(logits).all()
1547
1548
1549 def test_quark_fp8_parity(dist_init, workspace_init):
1550 quark_model_id = "amd-quark/llama-tiny-fp8-quark-quant-method"
1551 fp8_model_id = "amd-quark/llama-tiny-fp8-quant-method"
1552
1553 def load_state_dict(model_id: str) -> dict[str, torch.Tensor]:
1554 model, _ = load_model_without_vllm_runner(model_id)
1555 return {k: v.cpu() for k, v in model.state_dict().items()}
1556
1557 quark_state_dict = load_state_dict(quark_model_id)
1558 fp8_state_dict = load_state_dict(fp8_model_id)
1559
1560 assert fp8_state_dict.keys() == quark_state_dict.keys()
1561
1562 for key in fp8_state_dict:
1563 assert torch.equal(fp8_state_dict[key], quark_state_dict[key])
1564
1565
1566 @dataclass
1567 class AccuracyTestConfig:
1568 model_name: str
1569 excepted_value: float
1570
1571 def get_model_args(
1572 self,
1573 tp_size: int,
1574 model_max_len: int | None = None,
1575 kwargs: dict | None = None,
1576 ) -> dict:
1577 if kwargs is None:
1578 kwargs = {}
1579
1580 model_args = {
1581 "pretrained": self.model_name,
1582 "dtype": "auto",
1583 "add_bos_token": True,
1584 "tensor_parallel_size": tp_size,
1585 "gpu_memory_utilization": 0.7,
1586 **kwargs,
1587 }
1588 if model_max_len is not None:
1589 model_args["max_model_len"] = model_max_len
1590
1591 return model_args
1592
1593
1594 WIKITEXT_ACCURACY_CONFIGS = [
1595 AccuracyTestConfig(
1596 model_name="fxmarty/qwen1.5_moe_a2.7b_chat_w_fp4_a_fp6_e2m3",
1597 excepted_value=11.3,
1598 ),
1599 AccuracyTestConfig(
1600 model_name="fxmarty/qwen1.5_moe_a2.7b_chat_w_fp6_e3m2_a_fp6_e3m2",
1601 excepted_value=10.6,
1602 ),
1603 ]
1604
1605
1606 @pytest.mark.skipif(
1607 not QUARK_MXFP4_AVAILABLE,
1608 reason=f"amd-quark>={QUARK_MXFP4_MIN_VERSION} is not available",
1609 )
1610 @pytest.mark.parametrize(
1611 "config", WIKITEXT_ACCURACY_CONFIGS, ids=lambda config: config.model_name
1612 )
1613 @pytest.mark.parametrize("tp_size", [1, 2])
1614 def test_ocp_mx_wikitext_correctness(config: AccuracyTestConfig, tp_size: int):
1615 device_count = torch.accelerator.device_count()
1616 if device_count < tp_size:
1617 pytest.skip(f"This test requires >={tp_size} gpus, got only {device_count}")
1618
1619 results = lm_eval.simple_evaluate(
1620 model="vllm",
1621 model_args=config.get_model_args(
1622 tp_size=tp_size, kwargs={"cudagraph_capture_sizes": [16]}
1623 ),
1624 tasks="wikitext",
1625 batch_size=64,
1626 )
1627
1628 measured_value = results["results"]["wikitext"]["word_perplexity,none"]
1629 assert measured_value == pytest.approx(config.excepted_value, abs=0.1)
1630
1631
1632 GSM8K_ACCURACY_CONFIGS = [
1633 # Private model.
1634 AccuracyTestConfig(
1635 model_name="amd/DeepSeek-R1-WMXFP4-AMXFP4-Scale-UINT8-MoE-Quant",
1636 excepted_value=0.96,
1637 ),
1638 ]
1639
1640
1641 @pytest.mark.parametrize("config", GSM8K_ACCURACY_CONFIGS)
1642 @pytest.mark.skipif(
1643 not QUARK_MXFP4_AVAILABLE,
1644 reason=f"amd-quark>={QUARK_MXFP4_MIN_VERSION} is not available",
1645 )
1646 @pytest.mark.skipif(
1647 not HF_HUB_AMD_ORG_ACCESS,
1648 reason="Read access to huggingface.co/amd is required for this test.",
1649 )
1650 def test_mxfp4_gsm8k_correctness(config: AccuracyTestConfig):
1651 device_count = torch.accelerator.device_count()
1652 if device_count < 8:
1653 pytest.skip(f"This test requires >=8 gpus, got only {device_count}")
1654
1655 task = "gsm8k"
1656 rtol = 0.03
1657
1658 results = lm_eval.simple_evaluate(
1659 model="vllm",
1660 model_args=config.get_model_args(tp_size=8, model_max_len=38768),
1661 tasks=task,
1662 batch_size=64,
1663 num_fewshot=8,
1664 )
1665
1666 EXPECTED_VALUE = config.excepted_value
1667 measured_value = results["results"][task]["exact_match,strict-match"]
1668 assert (
1669 measured_value - rtol < EXPECTED_VALUE
1670 and measured_value + rtol > EXPECTED_VALUE
1671 ), f"Expected: {EXPECTED_VALUE} | Measured: {measured_value}"
1672
1673
1674 @pytest.mark.skipif(
1675 not QUARK_MXFP4_AVAILABLE,
1676 reason=f"amd-quark>={QUARK_MXFP4_MIN_VERSION} is not available",
1677 )
1678 @pytest.mark.parametrize("float_dtype", [torch.bfloat16, torch.float16])
1679 @pytest.mark.parametrize("scalings", [[2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]])
1680 def test_mxfp4_fused_qdq_match_quark(float_dtype: torch.dtype, scalings: list[int]):
1681 torch.manual_seed(0)
1682
1683 hidden_size = 64 * 32
1684 inp = (torch.rand(1, hidden_size, dtype=float_dtype, device=DEVICE_TYPE) - 0.5) * 2
1685 for i in range(hidden_size // 32):
1686 inp[:, i * 32 : (i + 1) * 32] = (
1687 inp[:, i * 32 : (i + 1) * 32] * scalings[i % len(scalings)]
1688 )
1689
1690 inp_kernel = inp.clone()
1691 inp_kernel_clone = inp_kernel.clone()
1692
1693 res_hip = mx_kernel.qdq_mxfp4_hip(inp_kernel_clone, "even")
1694 res_torch = qdq_mxfp4_torch(inp_kernel, "even")
1695
1696 for i in range(hidden_size // 32):
1697 assert torch.all(torch.isfinite(res_hip[:, i * 32 : (i + 1) * 32]))
1698 assert torch.all(torch.isfinite(res_torch[:, i * 32 : (i + 1) * 32]))
1699
1700 torch.testing.assert_close(
1701 res_hip[:, i * 32 : (i + 1) * 32], res_torch[:, i * 32 : (i + 1) * 32]
1702 )
1703
1704
1705 @pytest.mark.skipif(
1706 not QUARK_MXFP4_AVAILABLE,
1707 reason=f"amd-quark>={QUARK_MXFP4_MIN_VERSION} is not available",
1708 )
1709 @pytest.mark.parametrize("float_dtype", [torch.bfloat16, torch.float16])
1710 @pytest.mark.parametrize("scalings", [[2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]])
1711 def test_mxfp4_dequant_kernel_match_quark(
1712 float_dtype: torch.dtype, scalings: list[int]
1713 ):
1714 qspec = FP4PerGroupSpec(
1715 ch_axis=-1,
1716 group_size=32,
1717 scale_format="e8m0",
1718 scale_calculation_mode="even",
1719 is_dynamic=False,
1720 ).to_quantization_spec()
1721
1722 weight_quantizer = StaticScaledRealQuantizer(
1723 qspec=qspec,
1724 quantizer=None,
1725 reorder=False,
1726 real_quantized=True,
1727 float_dtype=float_dtype,
1728 device=DEVICE_TYPE,
1729 )
1730
1731 observer = qspec.observer_cls(qspec, device=DEVICE_TYPE)
1732
1733 hidden_size = 512
1734 shape = (11008, hidden_size)
1735
1736 w = (torch.rand(shape, device=DEVICE_TYPE, dtype=float_dtype) - 0.5) * 2
1737
1738 # Make it so that different groups have different scales.
1739 for i in range(hidden_size // 32):
1740 w[:, i * 32 : (i + 1) * 32] = (
1741 w[:, i * 32 : (i + 1) * 32] * scalings[i % len(scalings)]
1742 )
1743
1744 observer(w)
1745 scale, _ = observer._calculate_qparams()
1746 weight_quantizer.scale = scale
1747
1748 w_mxfp4 = weight_quantizer.to_real_quantize_params(w).to(DEVICE_TYPE)
1749 weight_quantizer.maybe_convert_and_transpose_scale()
1750
1751 scale = weight_quantizer.scale
1752
1753 out_hip = mx_kernel.dq_mxfp4_hip(w_mxfp4, scale, float_dtype)
1754
1755 out_torch = dq_mxfp4_torch(w_mxfp4, scale, float_dtype)
1756
1757 assert torch.equal(out_hip, out_torch)
1758
1759
1760 @pytest.mark.skipif(
1761 not QUARK_MXFP4_AVAILABLE,
1762 reason=f"amd-quark>={QUARK_MXFP4_MIN_VERSION} is not available",
1763 )
1764 @pytest.mark.skipif(
1765 not AITER_AVAILABLE,
1766 reason="AITER is not found or not supported on the current platform",
1767 )
1768 @pytest.mark.parametrize("float_dtype", [torch.bfloat16, torch.float16])
1769 @pytest.mark.parametrize("scalings", [[2.3, 0.03, 7.3, 0.1, 0.004, 17.3, 1e4, 1e-4]])
1770 def test_mxfp4_dynamic_quant_match_quark(
1771 float_dtype: torch.dtype, scalings: list[float]
1772 ):
1773 """`AiterMxfp4LinearKernel` quantizes weights dynamically through AITER's
1774 `dynamic_mxfp4_quant`, while the emulation path quantizes/dequantizes
1775 through Quark's `qdq_mxfp4`. Check that both agree on the same input.
1776 """
1777 from aiter.ops.triton.quant import dynamic_mxfp4_quant
1778
1779 torch.manual_seed(0)
1780
1781 hidden_size = 32 * 64
1782 inp = (torch.rand(48, hidden_size, dtype=float_dtype, device=DEVICE_TYPE) - 0.5) * 2
1783 for i in range(hidden_size // 32):
1784 inp[:, i * 32 : (i + 1) * 32] = (
1785 inp[:, i * 32 : (i + 1) * 32] * scalings[i % len(scalings)]
1786 )
1787
1788 x_q, x_s = dynamic_mxfp4_quant(inp)
1789 out_dynamic_quant = dq_mxfp4_torch(x_q, x_s, float_dtype)
1790
1791 out_quark_qdq = quant_dequant_mxfp4(inp)
1792
1793 assert torch.equal(out_dynamic_quant, out_quark_qdq)
1794
1795
1796 # Unit tests for ``is_layer_skipped`` fused-name handling.
1797
1798 FUSED_MAPPING = {
1799 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
1800 "gate_up_proj": ["gate_proj", "up_proj"],
1801 }
1802
1803
1804 def test_quark_should_ignore_layer_checks_children():
1805 assert should_ignore_layer(
1806 "model.layers.78.mlp.experts",
1807 ["model.layers.78.mlp.experts.0.down_proj"],
1808 check_children=True,
1809 )
1810
1811
1812 def test_quark_should_ignore_layer_rejects_partial_fused_matches():
1813 with pytest.raises(ValueError, match="different quantization schemes"):
1814 should_ignore_layer(
1815 "model.layers.0.self_attn.qkv_proj",
1816 ["model.layers.0.self_attn.q_proj"],
1817 FUSED_MAPPING,
1818 )
1819
1820
1821 def test_fused_name_listed_directly_is_skipped():
1822 # Regression for Step-3.5-Flash-FP8: the checkpoint lists the fused
1823 # name (``qkv_proj``) directly in ``modules_to_not_convert``. When a
1824 # ``packed_modules_mapping`` is registered on the model, the fused
1825 # match must still win over per-shard expansion.
1826 ignored = ["model.layers.0.self_attn.qkv_proj"]
1827 assert is_layer_skipped(
1828 prefix="model.layers.0.self_attn.qkv_proj",
1829 ignored_layers=ignored,
1830 fused_mapping=FUSED_MAPPING,
1831 )
1832 assert is_layer_skipped(
1833 prefix="model.layers.0.mlp.gate_up_proj",
1834 ignored_layers=["model.layers.0.mlp.gate_up_proj"],
1835 fused_mapping=FUSED_MAPPING,
1836 )
1837
1838
1839 def test_unfused_shards_listed_is_skipped():
1840 # Quark INT8 style: per-shard names listed; all shards present means
1841 # the fused layer is skipped via expansion.
1842 ignored = [
1843 "model.layers.0.self_attn.q_proj",
1844 "model.layers.0.self_attn.k_proj",
1845 "model.layers.0.self_attn.v_proj",
1846 ]
1847 assert is_layer_skipped(
1848 prefix="model.layers.0.self_attn.qkv_proj",
1849 ignored_layers=ignored,
1850 fused_mapping=FUSED_MAPPING,
1851 )
1852
1853
1854 def test_partial_shards_raises():
1855 # Only some shards listed -> ambiguous, must raise. Fused name is
1856 # not in ignored_layers, so we fall through to per-shard expansion.
1857 ignored = ["model.layers.0.self_attn.q_proj"]
1858 with pytest.raises(ValueError):
1859 is_layer_skipped(
1860 prefix="model.layers.0.self_attn.qkv_proj",
1861 ignored_layers=ignored,
1862 fused_mapping=FUSED_MAPPING,
1863 )
1864
1865
1866 def test_not_skipped_when_nothing_listed():
1867 assert not is_layer_skipped(
1868 prefix="model.layers.0.self_attn.qkv_proj",
1869 ignored_layers=["model.layers.0.mlp.gate_up_proj"],
1870 fused_mapping=FUSED_MAPPING,
1871 )
1872
1873
1874 def test_non_fused_layer_unaffected():
1875 assert is_layer_skipped(
1876 prefix="model.layers.0.self_attn.o_proj",
1877 ignored_layers=["model.layers.0.self_attn.o_proj"],
1878 fused_mapping=FUSED_MAPPING,
1879 )
1880 assert not is_layer_skipped(
1881 prefix="model.layers.0.self_attn.o_proj",
1882 ignored_layers=["model.layers.1.self_attn.o_proj"],
1883 fused_mapping=FUSED_MAPPING,
1884 )
1885
1886
1887 def test_substr_match_on_fused_name():
1888 # Substring matching: a fused-name match should also
1889 # short-circuit before shard expansion.
1890 assert is_layer_skipped(
1891 prefix="model.layers.0.self_attn.qkv_proj",
1892 ignored_layers=["self_attn.qkv_proj"],
1893 fused_mapping=FUSED_MAPPING,
1894 match_mode="substring",
1895 )
1896
1897
1898 @pytest.mark.parametrize(
1899 ("prefix", "ignored_layer", "expected"),
1900 [
1901 ("model.layers.0.self_attn.b_proj", "b_proj", True),
1902 ("model.layers.0.self_attn.q_b_proj", "b_proj", False),
1903 ("model.layers.0.self_attn.kv_b_proj", "b_proj", False),
1904 ("model.layers.5.self_attn.g_proj", "5.self_attn.g_proj", True),
1905 ("model.layers.6.self_attn.g_proj", "5.self_attn.g_proj", False),
1906 ],
1907 )
1908 def test_suffix_match_at_module_boundary(prefix, ignored_layer, expected):
1909 assert (
1910 is_layer_skipped(
1911 prefix=prefix,
1912 ignored_layers=[ignored_layer],
1913 match_mode="suffix",
1914 )
1915 is expected
1916 )
1917
1918
1919 _GLM5_MXFP4_WEIGHT = {
1920 "dtype": "fp4",
1921 "qscheme": "per_group",
1922 "group_size": 32,
1923 "scale_format": "e8m0",
1924 "is_dynamic": False,
1925 }
1926 _GLM5_BLOCK_FP8_WEIGHT = {
1927 "dtype": "fp8_e4m3",
1928 "qscheme": "per_block",
1929 "is_dynamic": False,
1930 "block_size": [128, 128],
1931 "symmetric": True,
1932 }
1933 _GLM5_BLOCK_FP8_INPUT = {
1934 "dtype": "fp8_e4m3",
1935 "qscheme": "per_group",
1936 "is_dynamic": True,
1937 "group_size": 128,
1938 "symmetric": True,
1939 }
1940 _GLM5_GATE_UP = "language_model.model.layers.0.mlp.gate_up_proj"
1941
1942
1943 def _glm5_mixed_precision_config() -> QuarkConfig:
1944 """Global MXFP4 with the dense-MLP gate/up shards marked block-FP8."""
1945 fp8 = {"weight": _GLM5_BLOCK_FP8_WEIGHT, "input_tensors": _GLM5_BLOCK_FP8_INPUT}
1946 return QuarkConfig(
1947 {
1948 "global_quant_config": {
1949 "weight": _GLM5_MXFP4_WEIGHT,
1950 "input_tensors": None,
1951 },
1952 "layer_type_quant_config": {},
1953 "layer_quant_config": {"*mlp.gate_proj": fp8, "*mlp.up_proj": fp8},
1954 "exclude": [],
1955 }
1956 )
1957
1958
1959 class _GLM5RecordingParam:
1960 """Stand-in for a BF16 target param that records what gets loaded."""
1961
1962 def __init__(self):
1963 self.loaded_weight: torch.Tensor | None = None
1964 self.loaded_shard: object = "unset"
1965
1966 def weight_loader(self, param, loaded_weight, shard_id=None):
1967 self.loaded_weight = loaded_weight
1968 self.loaded_shard = shard_id
1969
1970
1971 def _glm5_block_fp8(out_dim: int, in_dim: int, block: int = 128):
1972 """Return a (fp8 weight, f32 per-block scale) pair."""
1973 weight = (torch.randn(out_dim, in_dim) * 0.1).to(torch.float8_e4m3fn)
1974 scale = torch.rand(out_dim // block, in_dim // block, dtype=torch.float32) + 0.5
1975 return weight, scale
1976
1977
1978 def test_glm5next_gate_up_proj_mapping_is_genuine_fusion():
1979 from vllm.models.glm5next.common.model import Glm5NextForConditionalGeneration
1980
1981 mapping = Glm5NextForConditionalGeneration.packed_modules_mapping
1982 assert mapping["gate_up_proj"] == ["gate_proj", "up_proj"]
1983
1984
1985 def test_glm5next_genuine_fusion_resolves_gate_up_to_block_fp8():
1986 # The fused module must expand to its real shard names so each resolves to
1987 # the per-layer block-FP8 entry (rather than the global MXFP4 scheme).
1988 config = _glm5_mixed_precision_config()
1989 config.packed_modules_mapping = {"gate_up_proj": ["gate_proj", "up_proj"]}
1990 _, _, scheme_cls = config.get_scheme_cls(LinearBase, _GLM5_GATE_UP)
1991 assert scheme_cls is QuarkW8A8Fp8PerBlock
1992
1993
1994 def test_glm5next_attn_loader_accepts_quark_weight_scale():
1995 # Regression for KeyError on '...kv_a_proj_with_mqa.weight_scale': the fused
1996 # q_a/kv_a projection is kept BF16 and dequantized on load; the Quark scale
1997 # name must be recognized and routed to the correct fused shard.
1998 from vllm.models.glm5next.common.model import (
1999 _dequant_fp8_block,
2000 _try_load_fp8_attn_proj,
2001 )
2002
2003 prefix = "layers.3.self_attn"
2004 target = _GLM5RecordingParam()
2005 params_dict = {f"{prefix}.fused_qkv_a_proj.weight": target}
2006 buf: dict = {}
2007 loaded: set = set()
2008
2009 weight, scale = _glm5_block_fp8(256, 256)
2010 # fp8 weight arrives first -> buffered, nothing loaded yet.
2011 assert (
2012 _try_load_fp8_attn_proj(
2013 f"{prefix}.kv_a_proj_with_mqa.weight", weight, buf, params_dict, loaded, 0
2014 )
2015 is True
2016 )
2017 assert target.loaded_weight is None
2018 # Quark-named scale completes the pair -> dequantize + load.
2019 assert (
2020 _try_load_fp8_attn_proj(
2021 f"{prefix}.kv_a_proj_with_mqa.weight_scale",
2022 scale,
2023 buf,
2024 params_dict,
2025 loaded,
2026 0,
2027 )
2028 is True
2029 )
2030 assert target.loaded_weight is not None
2031 assert target.loaded_weight.dtype == torch.bfloat16
2032 assert target.loaded_shard == 1 # kv_a is shard 1 of fused_qkv_a_proj
2033 assert torch.equal(target.loaded_weight, _dequant_fp8_block(weight, scale, 128))
2034 assert f"{prefix}.fused_qkv_a_proj.weight" in loaded
2035
2036
2037 def test_glm5next_attn_loader_accepts_deepseek_weight_scale_inv():
2038 # DeepSeek scale name keeps working unchanged.
2039 from vllm.models.glm5next.common.model import (
2040 _dequant_fp8_block,
2041 _try_load_fp8_attn_proj,
2042 )
2043
2044 prefix = "layers.7.self_attn"
2045 target = _GLM5RecordingParam()
2046 params_dict = {f"{prefix}.o_proj.weight": target}
2047 buf: dict = {}
2048 loaded: set = set()
2049
2050 weight, scale = _glm5_block_fp8(128, 256)
2051 _try_load_fp8_attn_proj(
2052 f"{prefix}.o_proj.weight", weight, buf, params_dict, loaded, 0
2053 )
2054 assert (
2055 _try_load_fp8_attn_proj(
2056 f"{prefix}.o_proj.weight_scale_inv", scale, buf, params_dict, loaded, 0
2057 )
2058 is True
2059 )
2060 assert target.loaded_shard is None # o_proj is a direct (non-fused) proj
2061 assert torch.equal(target.loaded_weight, _dequant_fp8_block(weight, scale, 128))
2062
2063
2064 _REVERSE_AWQ_PACK_ORDER = [0, 4, 1, 5, 2, 6, 3, 7]
2065
2066
2067 def _quark_int4_config(
2068 *,
2069 pack_method: str = "reorder",
2070 symmetric: bool = True,
2071 exclude: list[str] | None = None,
2072 ) -> dict:
2073 return {
2074 "quant_method": "quark",
2075 "export": {"pack_method": pack_method, "kv_cache_group": []},
2076 "global_quant_config": {
2077 "weight": {
2078 "dtype": "int4",
2079 "group_size": 128,
2080 "symmetric": symmetric,
2081 }
2082 },
2083 "exclude": exclude or [],
2084 }
2085
2086
2087 def _sign_extend_int4_nibbles(t: torch.Tensor) -> torch.Tensor:
2088 mask = (t & 0x8).bool()
2089 t = t.clone()
2090 t[mask] = t[mask] | 0xF0
2091 return t
2092
2093
2094 def _dequantize_quark_signed_awq_torch(
2095 qweight: torch.Tensor,
2096 scales: torch.Tensor,
2097 qzeros: torch.Tensor,
2098 group_size: int,
2099 *,
2100 pack_reorder: bool = True,
2101 ) -> torch.Tensor:
2102 bits = 4
2103 shifts = torch.arange(0, 32, bits, device=qweight.device)
2104 iweights = ((qweight[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
2105 iweights = iweights.view(qweight.shape[0], -1)
2106 zeros = ((qzeros[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
2107 zeros = zeros.view(qzeros.shape[0], -1)
2108
2109 if pack_reorder:
2110 order = torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=qweight.device)
2111 else:
2112 order = torch.arange(8, device=qweight.device)
2113 iweights = iweights.view(qweight.shape[0], -1, 8)[:, :, order].reshape(
2114 qweight.shape[0], -1
2115 )
2116 zeros = zeros.view(qzeros.shape[0], -1, 8)[:, :, order].reshape(qzeros.shape[0], -1)
2117 iweights = _sign_extend_int4_nibbles(iweights & 0xF)
2118 zeros = _sign_extend_int4_nibbles(zeros & 0xF)
2119
2120 scales = scales.repeat_interleave(group_size, dim=0)
2121 zeros = zeros.repeat_interleave(group_size, dim=0)
2122 return (iweights - zeros) * scales
2123
2124
2125 def _dequantize_awq_unsigned_torch(
2126 qweight: torch.Tensor,
2127 scales: torch.Tensor,
2128 qzeros: torch.Tensor,
2129 group_size: int,
2130 *,
2131 pack_reorder: bool = True,
2132 ) -> torch.Tensor:
2133 bits = 4
2134 shifts = torch.arange(0, 32, bits, device=qweight.device)
2135 iweights = ((qweight[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
2136 iweights = iweights.view(qweight.shape[0], -1)
2137 zeros = ((qzeros[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
2138 zeros = zeros.view(qzeros.shape[0], -1)
2139
2140 if pack_reorder:
2141 order = torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=qweight.device)
2142 else:
2143 order = torch.arange(8, device=qweight.device)
2144 iweights = iweights.view(qweight.shape[0], -1, 8)[:, :, order].reshape(
2145 qweight.shape[0], -1
2146 )
2147 zeros = zeros.view(qzeros.shape[0], -1, 8)[:, :, order].reshape(qzeros.shape[0], -1)
2148
2149 scales = scales.repeat_interleave(group_size, dim=0)
2150 zeros = zeros.repeat_interleave(group_size, dim=0)
2151 return (iweights - zeros) * scales
2152
2153
2154 def _pack_int4_nibbles(nibbles: torch.Tensor, *, pack_reorder: bool) -> torch.Tensor:
2155 pack_order = (
2156 torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=nibbles.device)
2157 if pack_reorder
2158 else torch.arange(8, device=nibbles.device)
2159 )
2160 shifts = pack_order * 4
2161 return ((nibbles.to(torch.int64) & 0xF) << shifts).sum(dim=-1).to(torch.int32)
2162
2163
2164 class TestQuarkInt4Format:
2165 """Tests for Quark INT4 export format compatibility."""
2166
2167 def test_quark_order_pack_method_uses_native_int4_scheme(self):
2168 quant_config = QuarkConfig.from_config(_quark_int4_config(pack_method="order"))
2169 weight_config = quant_config.quant_config["global_quant_config"]
2170 weight_key, act_key, scheme_cls = quant_config._get_scheme_cls_from_config(
2171 weight_config
2172 )
2173 assert scheme_cls is QuarkW4A16Int4
2174
2175 scheme = quant_config.init_scheme(
2176 scheme_cls,
2177 weight_quant_key=weight_key,
2178 activation_quant_key=act_key,
2179 weight_config=weight_config["weight"],
2180 )
2181 assert isinstance(scheme, QuarkW4A16Int4)
2182 assert not scheme.pack_reorder
2183
2184 def test_quark_int4_scheme_supports_asymmetric_weights(self):
2185 quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2186 weight_config = quant_config.quant_config["global_quant_config"]
2187 weight_key, act_key, scheme_cls = quant_config._get_scheme_cls_from_config(
2188 weight_config
2189 )
2190 assert scheme_cls is QuarkW4A16Int4
2191
2192 scheme = quant_config.init_scheme(
2193 scheme_cls,
2194 weight_quant_key=weight_key,
2195 activation_quant_key=act_key,
2196 weight_config=weight_config["weight"],
2197 )
2198 assert isinstance(scheme, QuarkW4A16Int4)
2199 assert not scheme.is_symmetric
2200
2201 @pytest.mark.parametrize("missing_field", ["group_size", "symmetric"])
2202 def test_quark_int4_scheme_requires_weight_config_fields(self, missing_field):
2203 from vllm.model_executor.layers.quantization.utils.quant_utils import (
2204 kInt4Static,
2205 )
2206
2207 weight_config = {
2208 "dtype": "int4",
2209 "group_size": 128,
2210 "symmetric": True,
2211 }
2212 weight_config.pop(missing_field)
2213 quant_config = QuarkConfig.from_config(
2214 {
2215 "quant_method": "quark",
2216 "export": {"pack_method": "reorder", "kv_cache_group": []},
2217 "global_quant_config": {"weight": weight_config},
2218 "exclude": [],
2219 }
2220 )
2221
2222 with pytest.raises(ValueError, match=missing_field):
2223 quant_config.init_scheme(
2224 QuarkW4A16Int4,
2225 weight_quant_key=kInt4Static,
2226 activation_quant_key=None,
2227 weight_config=weight_config,
2228 )
2229
2230 def test_quark_int4_moe_uses_native_moe_method(self):
2231 from unittest.mock import MagicMock, patch
2232
2233 from vllm.model_executor.layers.quantization.utils.quant_utils import (
2234 kInt4StaticAsym,
2235 )
2236
2237 quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2238 moe_config = type("MoeConfig", (), {})()
2239 moe_config.has_bias = False
2240
2241 mock_backend = MagicMock()
2242 mock_experts_cls = MagicMock()
2243 with patch(
2244 "vllm.model_executor.layers.quantization.quark.quark_moe"
2245 ".select_wna16_moe_backend",
2246 return_value=(mock_backend, mock_experts_cls),
2247 ):
2248 method = QuarkW4A16Int4MoEMethod(
2249 kInt4StaticAsym,
2250 None,
2251 quant_config.quant_config["global_quant_config"]["weight"],
2252 quant_config.pack_method,
2253 moe_config,
2254 )
2255
2256 assert method.group_size == 128
2257 assert method.pack_reorder
2258 assert method.use_wna16_backend
2259
2260 def test_triton_wna16_experts_supports_asymmetric_int4(self):
2261 from vllm.model_executor.layers.fused_moe.experts.triton_moe import (
2262 TritonWNA16Experts,
2263 )
2264 from vllm.model_executor.layers.quantization.utils.quant_utils import (
2265 kInt4Static,
2266 kInt4Static32,
2267 kInt4Static32Asym,
2268 kInt4StaticAsym,
2269 )
2270
2271 for key in [kInt4Static, kInt4Static32, kInt4StaticAsym, kInt4Static32Asym]:
2272 assert TritonWNA16Experts._supports_quant_scheme(key, None), (
2273 f"{key} should be supported by TritonWNA16Experts"
2274 )
2275
2276 def test_quark_moe_loader_shards_w2_zero_point_across_tp_ranks(self):
2277 """w2 zero-points are sharded along the intermediate dim per TP rank.
2278
2279 Loading the same Quark checkpoint tensor at tensor_parallel_size=2 for
2280 both ranks must jointly reconstruct the tensor_parallel_size=1 (full)
2281 result, with no overlap or gap. This guards the custom loader's TP
2282 split for the packed zero-point layout.
2283 """
2284
2285 class _FakeMoEConfig:
2286 has_bias = False
2287
2288 def __init__(self, tp_size, tp_rank):
2289 self.tp_size = tp_size
2290 self.tp_rank = tp_rank
2291
2292 class _FakeLayer:
2293 def __init__(self, moe_config):
2294 self.moe_config = moe_config
2295 self.intermediate_size_per_partition = 64
2296 self.group_size_div_factor = 1
2297
2298 def load_w2_zero_point(raw_zp, tp_size, tp_rank):
2299 from unittest.mock import MagicMock, patch
2300
2301 from vllm.model_executor.layers.quantization.utils.quant_utils import (
2302 kInt4StaticAsym,
2303 )
2304
2305 quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2306 moe_config = _FakeMoEConfig(tp_size, tp_rank)
2307 with patch(
2308 "vllm.model_executor.layers.quantization.quark.quark_moe"
2309 ".select_wna16_moe_backend",
2310 return_value=(MagicMock(), MagicMock()),
2311 ):
2312 method = QuarkW4A16Int4MoEMethod(
2313 kInt4StaticAsym,
2314 None,
2315 quant_config.quant_config["global_quant_config"]["weight"],
2316 quant_config.pack_method,
2317 moe_config,
2318 )
2319
2320 layer = _FakeLayer(moe_config)
2321 # The loader converts the packed zero-point, then writes the
2322 # per-rank slice into param.data[expert_id]. A single expert dim is
2323 # enough; the last dim shrinks by tp_size after sharding.
2324 param = torch.nn.Parameter(
2325 torch.zeros(1, 16, 8 // tp_size, dtype=torch.uint8),
2326 requires_grad=False,
2327 )
2328 loader = method.get_weight_loader(
2329 layer,
2330 weight_loader=None,
2331 )
2332 loader(
2333 param,
2334 raw_zp.clone(),
2335 weight_name="w2_weight_zero_point",
2336 shard_id="w2",
2337 expert_id=0,
2338 )
2339 return param.data[0]
2340
2341 # Quark-packed zero-point for one expert: rows = hidden // pack_factor,
2342 # cols carry the (packed) intermediate dim, split evenly across ranks.
2343 raw_zp = torch.randint(0, 256, (8, 16), dtype=torch.uint8)
2344
2345 full = load_w2_zero_point(raw_zp, tp_size=1, tp_rank=0)
2346 shard0 = load_w2_zero_point(raw_zp, tp_size=2, tp_rank=0)
2347 shard1 = load_w2_zero_point(raw_zp, tp_size=2, tp_rank=1)
2348
2349 expected = full.view(full.size(0), 2, -1)
2350 torch.testing.assert_close(shard0, expected[:, 0])
2351 torch.testing.assert_close(shard1, expected[:, 1])
2352
2353 def test_quark_shared_expert_gate_keeps_quantized_tensors(self):
2354 quant_config = QuarkConfig.from_config(_quark_int4_config())
2355 mapper = quant_config.get_cache_scale_mapper()
2356
2357 output_names = {
2358 name
2359 for name, _ in mapper.apply(
2360 [
2361 (
2362 "model.language_model.layers.0.mlp.shared_expert_gate.weight",
2363 torch.zeros(1),
2364 ),
2365 (
2366 "model.language_model.layers.0.mlp.shared_expert_gate"
2367 ".weight_scale",
2368 torch.zeros(1),
2369 ),
2370 (
2371 "model.language_model.layers.0.mlp.shared_expert_gate"
2372 ".weight_zero_point",
2373 torch.zeros(1),
2374 ),
2375 ]
2376 )
2377 }
2378
2379 assert (
2380 "model.language_model.layers.0.mlp.shared_expert_gate.weight"
2381 in output_names
2382 )
2383 assert (
2384 "model.language_model.layers.0.mlp.shared_expert_gate.weight_scale"
2385 in output_names
2386 )
2387 assert (
2388 "model.language_model.layers.0.mlp.shared_expert_gate.weight_zero_point"
2389 in output_names
2390 )
2391
2392 def test_quark_apply_mapper_updates_exclude_and_layer_quant_config(self):
2393 quant_config = QuarkConfig.from_config(
2394 {
2395 **_quark_int4_config(),
2396 "exclude": ["lm_head"],
2397 "layer_quant_config": {
2398 "model.language_model.layers.0.mlp.gate": {
2399 "weight": {"dtype": "float16"},
2400 },
2401 },
2402 }
2403 )
2404 quant_config.apply_vllm_mapper(
2405 WeightsMapper(
2406 orig_to_new_prefix={
2407 "lm_head": "language_model.lm_head",
2408 "model.language_model.": "language_model.model.",
2409 },
2410 )
2411 )
2412
2413 layer_quant_config = quant_config.quant_config["layer_quant_config"]
2414 assert "language_model.model.layers.0.mlp.gate" in layer_quant_config
2415 assert "model.language_model.layers.0.mlp.gate" not in layer_quant_config
2416 assert quant_config.quant_config["exclude"] == [
2417 "language_model.lm_head",
2418 ]
2419
2420 def test_quark_apply_mapper_ignores_non_string_list_entries(self):
2421 quant_config = QuarkConfig.from_config(
2422 {
2423 **_quark_int4_config(),
2424 "algo_config": [
2425 {"name": "qronos", "inside_layer_modules": ["self_attn.q_proj"]}
2426 ],
2427 }
2428 )
2429 quant_config.apply_vllm_mapper(WeightsMapper())
2430
2431 assert quant_config.quant_config["algo_config"] == [
2432 {"name": "qronos", "inside_layer_modules": ["self_attn.q_proj"]}
2433 ]
2434
2435 def test_quark_exclude_matches_via_mapper_prefix(self):
2436 quant_config = QuarkConfig.from_config(_quark_int4_config(exclude=["lm_head"]))
2437 quant_config.apply_vllm_mapper(
2438 WeightsMapper(orig_to_new_prefix={"lm_head": "language_model.lm_head"})
2439 )
2440
2441 exclude_layers = quant_config.quant_config["exclude"]
2442 assert should_ignore_layer("language_model.lm_head", ignore=exclude_layers)
2443 assert not should_ignore_layer(
2444 "language_model.model.layers.0.mlp.gate", ignore=exclude_layers
2445 )
2446
2447
2448 @pytest.mark.parametrize("symmetric", [False, True])
2449 @pytest.mark.parametrize("pack_method", ["order", "reorder"])
2450 def test_quark_int4_canonicalizes_pack_for_kernel_layout(pack_method, symmetric):
2451 """Quark pack order is normalized to the layout expected by awq_* ops.
2452
2453 This reuses the existing AWQ dequant/gemm kernels for compute only; loading
2454 still goes through the native Quark quantization path, not AutoAWQ.
2455 """
2456 pack_reorder = pack_method == "reorder"
2457 group_size = 2
2458 packed_values = torch.tensor(
2459 [
2460 [0, 1, 7, 8, 9, 15, 2, 14],
2461 [15, 8, 0, 3, 12, 7, 1, 9],
2462 ],
2463 dtype=torch.int32,
2464 )
2465 packed_zeros = torch.zeros((1, 8), dtype=torch.int32)
2466 qweight = _pack_int4_nibbles(packed_values, pack_reorder=pack_reorder).view(2, 1)
2467 qzeros = _pack_int4_nibbles(packed_zeros, pack_reorder=pack_reorder).view(1, 1)
2468 scales = torch.ones((1, 8), dtype=torch.float16)
2469
2470 dequantize = (
2471 _dequantize_quark_signed_awq_torch
2472 if symmetric
2473 else _dequantize_awq_unsigned_torch
2474 )
2475 expected = dequantize(
2476 qweight,
2477 scales,
2478 qzeros,
2479 group_size,
2480 pack_reorder=pack_reorder,
2481 )
2482 canonical_weight = canonicalize_quark_packed_int4(
2483 qweight,
2484 pack_reorder=pack_reorder,
2485 is_symmetric=symmetric,
2486 )
2487 canonical_zero = canonicalize_quark_packed_int4(
2488 qzeros,
2489 pack_reorder=pack_reorder,
2490 is_symmetric=symmetric,
2491 )
2492 actual = _dequantize_awq_unsigned_torch(
2493 canonical_weight, scales, canonical_zero, group_size
2494 )
2495
2496 assert torch.equal(actual, expected)
2497
2498
2499 # ---------------------------------------------------------------------------
2500 # override_quantization_method: Quark always delegates to QuarkConfig
2501 # ---------------------------------------------------------------------------
2502
2503 _QUARK_MXFP4_CFG = {
2504 "quant_method": "quark",
2505 "global_quant_config": {
2506 "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32},
2507 },
2508 }
2509
2510 _QUARK_MXFP8_CFG = {
2511 "quant_method": "quark",
2512 "global_quant_config": {
2513 "weight": {
2514 "dtype": "fp8_e4m3",
2515 "qscheme": "per_block",
2516 "block_size": [32, 32],
2517 "scale_type": "float8_e8m0fnu",
2518 "symmetric": True,
2519 },
2520 },
2521 }
2522
2523 _QUARK_MIXED_CFG = {
2524 "quant_method": "quark",
2525 "global_quant_config": {
2526 "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32},
2527 },
2528 "layer_quant_config": {"layers.0.attn": {"weight": {"dtype": "fp8_e4m3"}}},
2529 }
2530
2531 _FP8_CFG = {"quant_method": "fp8"}
2532
2533 _DSV4_FP8_CFG = {"quant_method": "deepseek_v4_fp8"}
2534
2535
2536 @pytest.mark.parametrize(
2537 "quant_config_cls",
2538 [
2539 "deepseek_v41",
2540 "deepseek_v4",
2541 ],
2542 )
2543 @pytest.mark.parametrize(
2544 "hf_quant_cfg, expect_none",
2545 [
2546 (_QUARK_MXFP4_CFG, True),
2547 (_QUARK_MXFP8_CFG, True),
2548 (_QUARK_MIXED_CFG, True),
2549 (_FP8_CFG, False),
2550 (_DSV4_FP8_CFG, False),
2551 ],
2552 ids=[
2553 "quark-mxfp4",
2554 "quark-mxfp8",
2555 "quark-mixed",
2556 "native-fp8",
2557 "deepseek-v4-fp8",
2558 ],
2559 )
2560 def test_quark_override_delegates_to_quark_config(
2561 quant_config_cls, hf_quant_cfg, expect_none
2562 ):
2563 """Any quant_method=='quark' must return None (delegate to QuarkConfig).
2564
2565 Native FP8 configs should still be claimed by the model-specific config.
2566 """
2567 import importlib
2568
2569 if quant_config_cls == "deepseek_v41":
2570 # Module was renamed from deepseek_v4_1 to deepseek_v41 upstream.
2571 for mod_name in (
2572 "vllm.models.deepseek_v41.quant_config",
2573 "vllm.models.deepseek_v4_1.quant_config",
2574 ):
2575 try:
2576 mod = importlib.import_module(mod_name)
2577 break
2578 except ModuleNotFoundError:
2579 continue
2580 else:
2581 pytest.skip("deepseek_v41 quant_config not found")
2582 model_type = "deepseek_v41"
2583 else:
2584 mod = importlib.import_module("vllm.models.deepseek_v4.quant_config")
2585 model_type = "deepseek_v4"
2586
2587 config_cls = mod.DeepseekV4FP8Config
2588 hf_config = SimpleNamespace(model_type=model_type)
2589 result = config_cls.override_quantization_method(
2590 hf_quant_cfg, None, hf_config=hf_config
2591 )
2592
2593 if expect_none:
2594 assert result is None, (
2595 f"Quark config should delegate to QuarkConfig (return None), got {result!r}"
2596 )
2597 else:
2598 assert result == "deepseek_v4_fp8", (
2599 f"Native FP8 should be claimed by DeepseekV4FP8Config, got {result!r}"
2600 )