[Quantization][Refactor][1/N] Adopt `QuantKey` in `QuarkConfig` and methods, relying on `weight_quant_key`, `act_quant_key` for quant method dispatch (#52958)

Signed-off-by: Felix Marty <Felix.Marty@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>

fxmarty-amd committed Sep 1, 2026 at 09:55 UTC 63988f3c2d79db505ca2ac24dc1ed51f4b63249a
13 files changed +1573 -475
tests/quantization/test_online_mxfp4.py
+8 -4
@@ -35,6 +35,10 @@ from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
35 mxfp4_quantize,
36 quant_dequant_mxfp4,
37 )
38 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
39 + kMxfp4Dynamic,
40 + kMxfp4Static,
41 +)
42 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
43 from vllm.model_executor.utils import replace_parameter
44 from vllm.platforms import PlatformEnum, current_platform
@@ -346,9 +350,9 @@ def test_online_mxfp4_moe_matches_quark(
350 online_layer = make_layer("online_layer")
351
352 checkpoint_method = QuarkOCP_MX_MoEMethod(
349 - weight_config={"qscheme": "per_group", "dtype": "fp4"},
350 - input_config={"dtype": "fp4", "is_dynamic": True},
353 moe=checkpoint_layer.moe_config,
354 + weight_quant_key=kMxfp4Static,
355 + activation_quant_key=kMxfp4Dynamic,
356 )
357 online_method = Mxfp4OnlineMoEMethod(layer=online_layer)
358
@@ -566,8 +570,8 @@ def test_online_mxfp4_dense_matches_quark(
570 checkpoint_weight, checkpoint_weight_scale = mxfp4_quantize(weight)
571
572 checkpoint_scheme = QuarkOCP_MX(
569 - weight_quant_spec={"qscheme": "per_group", "dtype": "fp4"},
570 - input_quant_spec={"dtype": "fp4", "is_dynamic": True},
573 + weight_quant_key=kMxfp4Static,
574 + activation_quant_key=kMxfp4Dynamic,
575 )
576 checkpoint_scheme.create_weights(
577 layer=checkpoint_layer,
tests/quantization/test_quark.py
+792 -67
@@ -10,6 +10,7 @@ See also `tests/kernels/moe/test_ocp_mx_moe.py`.
10 import importlib.metadata
11 from dataclasses import dataclass
12 from importlib.util import find_spec
13 +from types import SimpleNamespace
14
15 import huggingface_hub
16 import lm_eval
@@ -18,22 +19,62 @@ import torch
19 from packaging import version
20
21 from vllm._aiter_ops import is_aiter_found_and_supported
22 +from vllm.model_executor.layers.fused_moe import (
23 + RoutedExperts,
24 + UnquantizedFusedMoEMethod,
25 +)
26 +from vllm.model_executor.layers.fused_moe.activation import MoEActivation
27 +from vllm.model_executor.layers.fused_moe.config import (
28 + FusedMoEConfig,
29 + FusedMoEParallelConfig,
30 + RoutingMethodType,
31 +)
32 +from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
33 from vllm.model_executor.layers.quantization.quark.quark import ( # noqa: E501
34 QuarkConfig,
35 QuarkLinearMethod,
36 + QuarkNVFP4,
37 + QuarkOCP_MX,
38 QuarkW8A8Fp8,
39 QuarkW8A8Fp8PerBlock,
40 QuarkW8A8Int8,
41 )
42 from vllm.model_executor.layers.quantization.quark.quark_moe import ( # noqa: E501
43 + QuarkMoEMethod,
44 QuarkW4A8Fp8MoEMethod,
45 QuarkW8A8Int8MoEMethod,
46 )
47 +from vllm.model_executor.layers.quantization.quark.schemes import QuarkScheme
48 +from vllm.model_executor.layers.quantization.quark.utils import QuarkQTensorHint
49 from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
50 quant_dequant_mxfp4,
51 )
52 from vllm.model_executor.layers.quantization.utils.quant_utils import (
53 + QuantKey,
54 is_layer_skipped,
55 + kFp8Dynamic128Sym,
56 + kFp8DynamicTensorSym,
57 + kFp8DynamicTokenSym,
58 + kFp8Static128BlockE8M0Sym,
59 + kFp8Static128BlockSym,
60 + kFp8StaticChannelSym,
61 + kFp8StaticTensorSym,
62 + kInt4W4A8StaticChannelSym,
63 + kInt8DynamicTensorAsym,
64 + kInt8DynamicTensorSym,
65 + kInt8DynamicTokenAsym,
66 + kInt8DynamicTokenSym,
67 + kInt8StaticChannelSym,
68 + kInt8StaticTensorAsym,
69 + kInt8StaticTensorSym,
70 + kMxfp4Dynamic,
71 + kMxfp4Static,
72 + kMxfp6E2M3Dynamic,
73 + kMxfp6E2M3Static,
74 + kMxfp6E3M2Dynamic,
75 + kMxfp6E3M2Static,
76 + kNvfp4Dynamic,
77 + kNvfp4Static,
78 )
79 from vllm.platforms import current_platform
80 from vllm.transformers_utils.repo_utils import hf_api
@@ -62,6 +103,576 @@ AITER_AVAILABLE = is_aiter_found_and_supported()
103
104 DEVICE_TYPE = current_platform.device_type
105
106 +
107 +@dataclass(frozen=True)
108 +class QTensorConfig:
109 + name: str
110 + weight: QuarkQTensorHint
111 + input_tensors: QuarkQTensorHint
112 + weight_quant_key: QuantKey | None = None
113 + act_quant_key: QuantKey | None = None
114 + dispatch_cls: type[QuarkScheme] | type[QuarkMoEMethod] | None = None
115 + expected_error: tuple[type[Exception], str] | None = None
116 +
117 +
118 +QTENSOR_CONFIGS = [
119 + QTensorConfig(
120 + name="fp8_w8a8_static_tensor",
121 + weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
122 + input_tensors={
123 + "dtype": "fp8_e4m3",
124 + "qscheme": "per_tensor",
125 + "is_dynamic": False,
126 + },
127 + weight_quant_key=kFp8StaticTensorSym,
128 + act_quant_key=kFp8StaticTensorSym,
129 + dispatch_cls=QuarkW8A8Fp8,
130 + ),
131 + QTensorConfig(
132 + name="fp8_w8a8_static_tensor_single_entry_lists",
133 + weight=[
134 + {
135 + "dtype": "fp8_e4m3",
136 + "qscheme": "per_tensor",
137 + "is_dynamic": False,
138 + }
139 + ],
140 + input_tensors=[
141 + {
142 + "dtype": "fp8_e4m3",
143 + "qscheme": "per_tensor",
144 + "is_dynamic": False,
145 + }
146 + ],
147 + weight_quant_key=kFp8StaticTensorSym,
148 + act_quant_key=kFp8StaticTensorSym,
149 + dispatch_cls=QuarkW8A8Fp8,
150 + ),
151 + QTensorConfig(
152 + name="fp8_w8a8_dynamic_tensor",
153 + weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
154 + input_tensors={
155 + "dtype": "fp8_e4m3",
156 + "qscheme": "per_tensor",
157 + "is_dynamic": True,
158 + },
159 + weight_quant_key=kFp8StaticTensorSym,
160 + act_quant_key=kFp8DynamicTensorSym,
161 + dispatch_cls=QuarkW8A8Fp8,
162 + ),
163 + QTensorConfig(
164 + name="fp8_w8a8_dynamic_token",
165 + weight={"dtype": "fp8_e4m3", "qscheme": "per_channel", "is_dynamic": False},
166 + input_tensors={
167 + "dtype": "fp8_e4m3",
168 + "qscheme": "per_channel",
169 + "is_dynamic": True,
170 + },
171 + weight_quant_key=kFp8StaticChannelSym,
172 + act_quant_key=kFp8DynamicTokenSym,
173 + dispatch_cls=QuarkW8A8Fp8,
174 + ),
175 + QTensorConfig(
176 + name="fp8_w8a8_channel_static_tensor",
177 + weight={"dtype": "fp8_e4m3", "qscheme": "per_channel", "is_dynamic": False},
178 + input_tensors={
179 + "dtype": "fp8_e4m3",
180 + "qscheme": "per_tensor",
181 + "is_dynamic": False,
182 + },
183 + weight_quant_key=kFp8StaticChannelSym,
184 + act_quant_key=kFp8StaticTensorSym,
185 + dispatch_cls=QuarkW8A8Fp8,
186 + ),
187 + QTensorConfig(
188 + name="fp8_w8a8_tensor_dynamic_token",
189 + weight={"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
190 + input_tensors={
191 + "dtype": "fp8_e4m3",
192 + "qscheme": "per_channel",
193 + "is_dynamic": True,
194 + },
195 + weight_quant_key=kFp8StaticTensorSym,
196 + act_quant_key=kFp8DynamicTokenSym,
197 + dispatch_cls=QuarkW8A8Fp8,
198 + ),
199 + QTensorConfig(
200 + name="fp8_w8a8_dynamic_block_fp32",
201 + weight={
202 + "dtype": "fp8_e4m3",
203 + "qscheme": "per_block",
204 + "is_dynamic": False,
205 + "block_size": [128, 128],
206 + "symmetric": True,
207 + },
208 + input_tensors={
209 + "dtype": "fp8_e4m3",
210 + "qscheme": "per_group",
211 + "is_dynamic": True,
212 + "group_size": 128,
213 + "symmetric": True,
214 + },
215 + weight_quant_key=kFp8Static128BlockSym,
216 + act_quant_key=kFp8Dynamic128Sym,
217 + dispatch_cls=QuarkW8A8Fp8PerBlock,
218 + ),
219 + QTensorConfig(
220 + name="fp8_w8a8_dynamic_block_e8m0",
221 + weight={
222 + "dtype": "fp8_e4m3",
223 + "qscheme": "per_block",
224 + "is_dynamic": False,
225 + "block_size": [128, 128],
226 + "symmetric": True,
227 + "scale_type": "float8_e8m0fnu",
228 + },
229 + input_tensors={
230 + "dtype": "fp8_e4m3",
231 + "qscheme": "per_group",
232 + "is_dynamic": True,
233 + "group_size": 128,
234 + "symmetric": True,
235 + },
236 + weight_quant_key=kFp8Static128BlockE8M0Sym,
237 + act_quant_key=kFp8Dynamic128Sym,
238 + dispatch_cls=QuarkW8A8Fp8PerBlock,
239 + ),
240 + QTensorConfig(
241 + name="fp8_w8a8_block_static_input",
242 + weight={
243 + "dtype": "fp8_e4m3",
244 + "qscheme": "per_block",
245 + "is_dynamic": False,
246 + "block_size": [128, 128],
247 + "symmetric": True,
248 + },
249 + input_tensors={
250 + "dtype": "fp8_e4m3",
251 + "qscheme": "per_group",
252 + "is_dynamic": False,
253 + "group_size": 128,
254 + "symmetric": True,
255 + },
256 + expected_error=(NotImplementedError, "No quark compatible scheme"),
257 + ),
258 + QTensorConfig(
259 + name="fp8_w8a8_block_group_size_mismatch",
260 + weight={
261 + "dtype": "fp8_e4m3",
262 + "qscheme": "per_block",
263 + "is_dynamic": False,
264 + "block_size": [128, 128],
265 + "symmetric": True,
266 + },
267 + input_tensors={
268 + "dtype": "fp8_e4m3",
269 + "qscheme": "per_group",
270 + "is_dynamic": True,
271 + "group_size": 64,
272 + "symmetric": True,
273 + },
274 + expected_error=(NotImplementedError, "No quark compatible scheme"),
275 + ),
276 + QTensorConfig(
277 + name="fp8_w8a8_block_missing_block_size",
278 + weight={
279 + "dtype": "fp8_e4m3",
280 + "qscheme": "per_block",
281 + "is_dynamic": False,
282 + "symmetric": True,
283 + },
284 + input_tensors={
285 + "dtype": "fp8_e4m3",
286 + "qscheme": "per_group",
287 + "is_dynamic": True,
288 + "group_size": 128,
289 + "symmetric": True,
290 + },
291 + expected_error=(ValueError, "requires `block_size`"),
292 + ),
293 + QTensorConfig(
294 + name="int8_w8a8_static_symmetric",
295 + weight={
296 + "dtype": "int8",
297 + "qscheme": "per_tensor",
298 + "is_dynamic": False,
299 + "symmetric": True,
300 + },
301 + input_tensors={
302 + "dtype": "int8",
303 + "qscheme": "per_tensor",
304 + "is_dynamic": False,
305 + "symmetric": True,
306 + },
307 + weight_quant_key=kInt8StaticTensorSym,
308 + act_quant_key=kInt8StaticTensorSym,
309 + dispatch_cls=QuarkW8A8Int8,
310 + ),
311 + QTensorConfig(
312 + name="int8_w8a8_static_asymmetric",
313 + weight={
314 + "dtype": "int8",
315 + "qscheme": "per_tensor",
316 + "is_dynamic": False,
317 + "symmetric": True,
318 + },
319 + input_tensors={
320 + "dtype": "int8",
321 + "qscheme": "per_tensor",
322 + "is_dynamic": False,
323 + "symmetric": False,
324 + },
325 + weight_quant_key=kInt8StaticTensorSym,
326 + act_quant_key=kInt8StaticTensorAsym,
327 + dispatch_cls=QuarkW8A8Int8,
328 + ),
329 + QTensorConfig(
330 + name="int8_w8a8_channel_static_symmetric",
331 + weight={
332 + "dtype": "int8",
333 + "qscheme": "per_channel",
334 + "is_dynamic": False,
335 + "symmetric": True,
336 + },
337 + input_tensors={
338 + "dtype": "int8",
339 + "qscheme": "per_tensor",
340 + "is_dynamic": False,
341 + "symmetric": True,
342 + },
343 + weight_quant_key=kInt8StaticChannelSym,
344 + act_quant_key=kInt8StaticTensorSym,
345 + dispatch_cls=QuarkW8A8Int8,
346 + ),
347 + QTensorConfig(
348 + name="int8_w8a8_channel_static_asymmetric",
349 + weight={
350 + "dtype": "int8",
351 + "qscheme": "per_channel",
352 + "is_dynamic": False,
353 + "symmetric": True,
354 + },
355 + input_tensors={
356 + "dtype": "int8",
357 + "qscheme": "per_tensor",
358 + "is_dynamic": False,
359 + "symmetric": False,
360 + },
361 + weight_quant_key=kInt8StaticChannelSym,
362 + act_quant_key=kInt8StaticTensorAsym,
363 + dispatch_cls=QuarkW8A8Int8,
364 + ),
365 + QTensorConfig(
366 + name="int8_w8a8_dynamic_tensor_symmetric",
367 + weight={
368 + "dtype": "int8",
369 + "qscheme": "per_tensor",
370 + "is_dynamic": False,
371 + "symmetric": True,
372 + },
373 + input_tensors={
374 + "dtype": "int8",
375 + "qscheme": "per_channel",
376 + "is_dynamic": True,
377 + "symmetric": True,
378 + },
379 + weight_quant_key=kInt8StaticTensorSym,
380 + act_quant_key=kInt8DynamicTensorSym,
381 + dispatch_cls=QuarkW8A8Int8,
382 + ),
383 + QTensorConfig(
384 + name="int8_w8a8_dynamic_tensor_asymmetric",
385 + weight={
386 + "dtype": "int8",
387 + "qscheme": "per_tensor",
388 + "is_dynamic": False,
389 + "symmetric": True,
390 + },
391 + input_tensors={
392 + "dtype": "int8",
393 + "qscheme": "per_channel",
394 + "is_dynamic": True,
395 + "symmetric": False,
396 + },
397 + weight_quant_key=kInt8StaticTensorSym,
398 + act_quant_key=kInt8DynamicTensorAsym,
399 + dispatch_cls=QuarkW8A8Int8,
400 + ),
401 + QTensorConfig(
402 + name="int8_w8a8_dynamic_token",
403 + weight={
404 + "dtype": "int8",
405 + "qscheme": "per_channel",
406 + "is_dynamic": False,
407 + "symmetric": True,
408 + },
409 + input_tensors={
410 + "dtype": "int8",
411 + "qscheme": "per_channel",
412 + "is_dynamic": True,
413 + "symmetric": True,
414 + },
415 + weight_quant_key=kInt8StaticChannelSym,
416 + act_quant_key=kInt8DynamicTokenSym,
417 + dispatch_cls=QuarkW8A8Int8,
418 + ),
419 + QTensorConfig(
420 + name="int8_w8a8_dynamic_token_asymmetric",
421 + weight={
422 + "dtype": "int8",
423 + "qscheme": "per_channel",
424 + "is_dynamic": False,
425 + "symmetric": True,
426 + },
427 + input_tensors={
428 + "dtype": "int8",
429 + "qscheme": "per_channel",
430 + "is_dynamic": True,
431 + "symmetric": False,
432 + },
433 + weight_quant_key=kInt8StaticChannelSym,
434 + act_quant_key=kInt8DynamicTokenAsym,
435 + dispatch_cls=QuarkW8A8Int8,
436 + ),
437 + QTensorConfig(
438 + name="ocp_mx_mxfp4_weight_only",
439 + weight={
440 + "dtype": "fp4",
441 + "qscheme": "per_group",
442 + "group_size": 32,
443 + "scale_format": "e8m0",
444 + "is_dynamic": False,
445 + },
446 + input_tensors=None,
447 + weight_quant_key=kMxfp4Static,
448 + act_quant_key=None,
449 + dispatch_cls=QuarkOCP_MX,
450 + ),
451 + QTensorConfig(
452 + name="ocp_mx_mxfp4_activation",
453 + weight={
454 + "dtype": "fp4",
455 + "qscheme": "per_group",
456 + "group_size": 32,
457 + "scale_format": "e8m0",
458 + "is_dynamic": False,
459 + },
460 + input_tensors={
461 + "dtype": "fp4",
462 + "qscheme": "per_group",
463 + "group_size": 32,
464 + "scale_format": "e8m0",
465 + "is_dynamic": True,
466 + },
467 + weight_quant_key=kMxfp4Static,
468 + act_quant_key=kMxfp4Dynamic,
469 + dispatch_cls=QuarkOCP_MX,
470 + ),
471 + QTensorConfig(
472 + name="ocp_mx_mxfp6_e3m2",
473 + weight={
474 + "dtype": "fp6_e3m2",
475 + "qscheme": "per_group",
476 + "group_size": 32,
477 + "scale_format": "e8m0",
478 + "is_dynamic": False,
479 + },
480 + input_tensors={
481 + "dtype": "fp6_e3m2",
482 + "qscheme": "per_group",
483 + "group_size": 32,
484 + "scale_format": "e8m0",
485 + "is_dynamic": True,
486 + },
487 + weight_quant_key=kMxfp6E3M2Static,
488 + act_quant_key=kMxfp6E3M2Dynamic,
489 + dispatch_cls=QuarkOCP_MX,
490 + ),
491 + QTensorConfig(
492 + name="ocp_mx_mxfp4_mxfp6_e3m2_activation",
493 + weight={
494 + "dtype": "fp4",
495 + "qscheme": "per_group",
496 + "group_size": 32,
497 + "scale_format": "e8m0",
498 + "is_dynamic": False,
499 + },
500 + input_tensors={
501 + "dtype": "fp6_e3m2",
502 + "qscheme": "per_group",
503 + "group_size": 32,
504 + "scale_format": "e8m0",
505 + "is_dynamic": True,
506 + },
507 + weight_quant_key=kMxfp4Static,
508 + act_quant_key=kMxfp6E3M2Dynamic,
509 + dispatch_cls=QuarkOCP_MX,
510 + ),
511 + QTensorConfig(
512 + name="ocp_mx_mxfp4_mxfp6_e2m3_activation",
513 + weight={
514 + "dtype": "fp4",
515 + "qscheme": "per_group",
516 + "group_size": 32,
517 + "scale_format": "e8m0",
518 + "is_dynamic": False,
519 + },
520 + input_tensors={
521 + "dtype": "fp6_e2m3",
522 + "qscheme": "per_group",
523 + "group_size": 32,
524 + "scale_format": "e8m0",
525 + "is_dynamic": True,
526 + },
527 + weight_quant_key=kMxfp4Static,
528 + act_quant_key=kMxfp6E2M3Dynamic,
529 + dispatch_cls=QuarkOCP_MX,
530 + ),
531 + QTensorConfig(
532 + name="ocp_mx_mxfp6_e2m3",
533 + weight={
534 + "dtype": "fp6_e2m3",
535 + "qscheme": "per_group",
536 + "group_size": 32,
537 + "scale_format": "e8m0",
538 + "is_dynamic": False,
539 + },
540 + input_tensors={
541 + "dtype": "fp6_e2m3",
542 + "qscheme": "per_group",
543 + "group_size": 32,
544 + "scale_format": "e8m0",
545 + "is_dynamic": True,
546 + },
547 + weight_quant_key=kMxfp6E2M3Static,
548 + act_quant_key=kMxfp6E2M3Dynamic,
549 + dispatch_cls=QuarkOCP_MX,
550 + ),
551 + QTensorConfig(
552 + name="nvfp4",
553 + weight=[
554 + {
555 + "dtype": "fp4",
556 + "qscheme": "per_group",
557 + "group_size": 16,
558 + "is_dynamic": False,
559 + },
560 + {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
561 + ],
562 + input_tensors=[
563 + {
564 + "dtype": "fp4",
565 + "qscheme": "per_group",
566 + "group_size": 16,
567 + "is_dynamic": True,
568 + },
569 + {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
570 + ],
571 + weight_quant_key=kNvfp4Static,
572 + act_quant_key=kNvfp4Dynamic,
573 + dispatch_cls=QuarkNVFP4,
574 + ),
575 + QTensorConfig(
576 + name="w4a8_fp8_static",
577 + weight=[
578 + {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
579 + {
580 + "dtype": "int4",
581 + "qscheme": "per_channel",
582 + "is_dynamic": False,
583 + "symmetric": True,
584 + "ch_axis": 0,
585 + },
586 + ],
587 + input_tensors={
588 + "dtype": "fp8_e4m3",
589 + "qscheme": "per_tensor",
590 + "is_dynamic": False,
591 + },
592 + weight_quant_key=kInt4W4A8StaticChannelSym,
593 + act_quant_key=kFp8StaticTensorSym,
594 + dispatch_cls=QuarkW4A8Fp8MoEMethod,
595 + ),
596 + QTensorConfig(
597 + name="w4a8_fp8_dynamic",
598 + weight=[
599 + {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
600 + {
601 + "dtype": "int4",
602 + "qscheme": "per_channel",
603 + "is_dynamic": False,
604 + "symmetric": True,
605 + "ch_axis": 0,
606 + },
607 + ],
608 + input_tensors={
609 + "dtype": "fp8_e4m3",
610 + "qscheme": "per_channel",
611 + "is_dynamic": True,
612 + },
613 + weight_quant_key=kInt4W4A8StaticChannelSym,
614 + act_quant_key=kFp8DynamicTokenSym,
615 + dispatch_cls=QuarkW4A8Fp8MoEMethod,
616 + ),
617 + QTensorConfig(
618 + name="w4a8_fp8_static_single_entry_input",
619 + weight=[
620 + {"dtype": "fp8_e4m3", "qscheme": "per_tensor", "is_dynamic": False},
621 + {
622 + "dtype": "int4",
623 + "qscheme": "per_channel",
624 + "is_dynamic": False,
625 + "symmetric": True,
626 + "ch_axis": 0,
627 + },
628 + ],
629 + input_tensors=[
630 + {
631 + "dtype": "fp8_e4m3",
632 + "qscheme": "per_tensor",
633 + "is_dynamic": False,
634 + }
635 + ],
636 + weight_quant_key=kInt4W4A8StaticChannelSym,
637 + act_quant_key=kFp8StaticTensorSym,
638 + dispatch_cls=QuarkW4A8Fp8MoEMethod,
639 + ),
640 +]
641 +
642 +
643 +def _make_qtensor_config(
644 + weight: QuarkQTensorHint,
645 + input_tensors: QuarkQTensorHint,
646 + exclude: list[str] | None = None,
647 +) -> QuarkConfig:
648 + return QuarkConfig(
649 + {
650 + "global_quant_config": {
651 + "weight": weight,
652 + "input_tensors": input_tensors,
653 + },
654 + "layer_type_quant_config": {},
655 + "exclude": exclude or [],
656 + }
657 + )
658 +
659 +
660 +def _make_test_moe_config() -> FusedMoEConfig:
661 + return FusedMoEConfig(
662 + num_experts=8,
663 + experts_per_token=2,
664 + hidden_dim=256,
665 + intermediate_size=256,
666 + num_local_experts=8,
667 + num_logical_experts=8,
668 + activation=MoEActivation.SILU,
669 + device=current_platform.device_type,
670 + routing_method=RoutingMethodType.Renormalize,
671 + moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
672 + in_dtype=torch.bfloat16,
673 + )
674 +
675 +
676 if QUARK_MXFP4_AVAILABLE:
677 from quark.torch.export.nn.modules.realquantizer import StaticScaledRealQuantizer
678 from quark.torch.kernel import mx as mx_kernel
@@ -98,83 +709,197 @@ def test_quark_config_preserves_existing_packed_modules_mapping():
709 assert config.packed_modules_mapping["custom_proj"] == ["a", "b"]
710
711
101 -def test_quark_fp8_w8a8_detects_per_block_config():
102 - config = QuarkConfig({})
103 - weight_config = {
104 - "dtype": "fp8_e4m3",
105 - "qscheme": "per_block",
106 - "is_dynamic": False,
107 - "block_size": [128, 128],
108 - "symmetric": True,
109 - }
110 - input_config = {
111 - "dtype": "fp8_e4m3",
112 - "qscheme": "per_group",
113 - "is_dynamic": True,
114 - "group_size": 128,
115 - "symmetric": True,
116 - }
712 +def test_quant_method_dispatch_ignored(default_vllm_config):
713 + config = _make_qtensor_config(None, None, exclude=["linear", "experts"])
714
118 - assert config._is_fp8_w8a8(weight_config, input_config)
715 + class TestLinear(LinearBase):
716 + def __init__(self):
717 + torch.nn.Module.__init__(self)
718
719 + class TestRoutedExperts(RoutedExperts):
720 + def __init__(self):
721 + torch.nn.Module.__init__(self)
722 + self.moe_config = _make_test_moe_config()
723
121 -def test_quark_fp8_w8a8_rejects_per_block_static_input():
122 - config = QuarkConfig({})
123 - weight_config = {
124 - "dtype": "fp8_e4m3",
125 - "qscheme": "per_block",
126 - "is_dynamic": False,
127 - "block_size": [128, 128],
128 - "symmetric": True,
129 - }
130 - input_config = {
131 - "dtype": "fp8_e4m3",
132 - "qscheme": "per_group",
133 - "is_dynamic": False,
134 - "group_size": 128,
135 - "symmetric": True,
136 - }
724 + assert config.get_quant_method_target("linear", LinearBase) == (
725 + None,
726 + None,
727 + UnquantizedLinearMethod,
728 + )
729 + assert isinstance(
730 + config.get_quant_method(TestLinear(), "linear"), UnquantizedLinearMethod
731 + )
732
138 - assert not config._is_fp8_w8a8(weight_config, input_config)
733 + assert config.get_quant_method_target("experts", RoutedExperts) == (
734 + None,
735 + None,
736 + UnquantizedFusedMoEMethod,
737 + )
738 + assert isinstance(
739 + config.get_quant_method(TestRoutedExperts(), "experts"),
740 + UnquantizedFusedMoEMethod,
741 + )
742
743 + dynamic_mxfp4_config = _make_qtensor_config(
744 + {
745 + "dtype": "fp4",
746 + "qscheme": "per_group",
747 + "group_size": 32,
748 + "scale_format": "e8m0",
749 + "is_dynamic": False,
750 + },
751 + None,
752 + exclude=["self_attn.q_proj", "mlp.down_proj"],
753 + )
754 + dynamic_mxfp4_config.dynamic_mxfp4_quant = True
755 +
756 + assert dynamic_mxfp4_config.get_quant_method_target(
757 + "self_attn.q_proj", LinearBase
758 + ) == (kMxfp4Static, None, QuarkLinearMethod)
759 + attention_proj = TestLinear()
760 + assert isinstance(
761 + dynamic_mxfp4_config.get_quant_method(attention_proj, "self_attn.q_proj"),
762 + QuarkLinearMethod,
763 + )
764 + assert isinstance(attention_proj.scheme, QuarkOCP_MX)
765 + assert attention_proj.scheme.dynamic_mxfp4_quant
766 +
767 + assert dynamic_mxfp4_config.get_quant_method_target(
768 + "mlp.down_proj", LinearBase
769 + ) == (None, None, UnquantizedLinearMethod)
770 + assert isinstance(
771 + dynamic_mxfp4_config.get_quant_method(TestLinear(), "mlp.down_proj"),
772 + UnquantizedLinearMethod,
773 + )
774
141 -def test_quark_fp8_w8a8_rejects_per_block_group_size_mismatch():
142 - config = QuarkConfig({})
143 - weight_config = {
144 - "dtype": "fp8_e4m3",
145 - "qscheme": "per_block",
146 - "is_dynamic": False,
147 - "block_size": [128, 128],
148 - "symmetric": True,
149 - }
150 - input_config = {
151 - "dtype": "fp8_e4m3",
152 - "qscheme": "per_group",
153 - "is_dynamic": True,
154 - "group_size": 64,
155 - "symmetric": True,
156 - }
775
158 - assert not config._is_fp8_w8a8(weight_config, input_config)
776 +@pytest.mark.parametrize("case", QTENSOR_CONFIGS, ids=lambda case: case.name)
777 +def test_quant_method_dispatch_target(case):
778 + config = _make_qtensor_config(case.weight, case.input_tensors)
779 + if case.expected_error is not None:
780 + error_type, error_message = case.expected_error
781 + with pytest.raises(error_type, match=error_message):
782 + config.get_quant_method_target("linear", LinearBase)
783 + return
784
785 + assert case.dispatch_cls is not None
786 + is_linear = issubclass(case.dispatch_cls, QuarkScheme)
787
161 -def test_quark_w8a8_fp8_per_block_requires_block_size():
162 - weight_config = {
163 - "dtype": "fp8_e4m3",
164 - "qscheme": "per_block",
165 - "is_dynamic": False,
166 - "symmetric": True,
167 - }
168 - input_config = {
169 - "dtype": "fp8_e4m3",
170 - "qscheme": "per_group",
171 - "is_dynamic": True,
172 - "group_size": 128,
173 - "symmetric": True,
174 - }
788 + weight_quant_key, act_quant_key, method_cls = config.get_quant_method_target(
789 + "linear" if is_linear else "experts",
790 + LinearBase if is_linear else RoutedExperts,
791 + )
792 +
793 + assert weight_quant_key == case.weight_quant_key
794 + assert act_quant_key == case.act_quant_key
795 + assert method_cls is (QuarkLinearMethod if is_linear else case.dispatch_cls)
796 +
797 +
798 +@pytest.mark.parametrize(
799 + ("weight", "input_tensors"),
800 + [
801 + pytest.param(
802 + {
803 + "dtype": "int8",
804 + "qscheme": "per_group",
805 + "is_dynamic": False,
806 + "symmetric": True,
807 + },
808 + {
809 + "dtype": "int8",
810 + "qscheme": "per_tensor",
811 + "is_dynamic": False,
812 + "symmetric": True,
813 + },
814 + id="single_entry",
815 + ),
816 + pytest.param(
817 + [
818 + {"dtype": "int8", "qscheme": "per_tensor"},
819 + {"dtype": "int8", "qscheme": "per_tensor"},
820 + ],
821 + [
822 + {"dtype": "int8", "qscheme": "per_tensor"},
823 + {"dtype": "int8", "qscheme": "per_tensor"},
824 + ],
825 + id="multi_entry",
826 + ),
827 + ],
828 +)
829 +def test_quant_method_dispatch_unsupported(weight, input_tensors):
830 + config = _make_qtensor_config(weight, input_tensors)
831 +
832 + class TestRoutedExperts(RoutedExperts):
833 + def __init__(self):
834 + torch.nn.Module.__init__(self)
835 +
836 + with pytest.raises(RuntimeError, match="^Unsupported FusedMoe scheme$"):
837 + config.get_quant_method_target("experts", RoutedExperts)
838 +
839 + with pytest.raises(RuntimeError, match="^Unsupported FusedMoe scheme$"):
840 + config.get_quant_method(TestRoutedExperts(), "experts")
841 +
842 +
843 +@pytest.mark.parametrize(
844 + "case",
845 + [case for case in QTENSOR_CONFIGS if case.expected_error is None],
846 + ids=lambda case: case.name,
847 +)
848 +def test_quant_method_dispatch_instantiation(case, monkeypatch, default_vllm_config):
849 + config = _make_qtensor_config(case.weight, case.input_tensors)
850 + assert case.dispatch_cls is not None
851 + if issubclass(case.dispatch_cls, QuarkScheme):
852 +
853 + class TestLinear(LinearBase):
854 + def __init__(self):
855 + torch.nn.Module.__init__(self)
856 +
857 + monkeypatch.setattr(
858 + "vllm.model_executor.layers.quantization.quark.schemes."
859 + "quark_w8a8_fp8.get_current_vllm_config",
860 + lambda: SimpleNamespace(model_config=SimpleNamespace(dtype=torch.bfloat16)),
861 + )
862 + layer = TestLinear()
863 + method = config.get_quant_method(layer, "linear")
864 +
865 + assert isinstance(method, QuarkLinearMethod)
866 + assert isinstance(layer.scheme, case.dispatch_cls)
867 + if case.weight_quant_key == kFp8Static128BlockE8M0Sym:
868 + # TODO: Remove once E8M0 quant key is properly handled in oracle
869 + assert layer.scheme.weight_quant_key == kFp8Static128BlockSym
870 + else:
871 + assert layer.scheme.weight_quant_key == case.weight_quant_key
872 + assert layer.scheme.activation_quant_key == case.act_quant_key
873 + else:
874 +
875 + class TestRoutedExperts(RoutedExperts):
876 + def __init__(self):
877 + torch.nn.Module.__init__(self)
878 + self.moe_config = _make_test_moe_config()
879 +
880 + for target in (
881 + "select_fp8_moe_backend",
882 + "select_int8_moe_backend",
883 + "select_mxfp4_moe_backend",
884 + "backend_to_kernel_cls",
885 + "select_nvfp4_moe_backend",
886 + ):
887 + monkeypatch.setattr(
888 + f"vllm.model_executor.layers.quantization.quark.quark_moe.{target}",
889 + lambda *args, **kwargs: (object(), object()),
890 + )
891 +
892 + # AssertionError: W4A8 FP8 MoE requires ROCm AITER fused MoE support
893 + monkeypatch.setattr(
894 + "vllm.model_executor.layers.quantization.quark.quark_moe."
895 + "rocm_aiter_ops.is_fused_moe_enabled",
896 + lambda: True,
897 + )
898 +
899 + layer = TestRoutedExperts()
900 + method = config.get_quant_method(layer, "experts")
901
176 - with pytest.raises(ValueError, match="requires `block_size`"):
177 - QuarkW8A8Fp8PerBlock(weight_config, input_config)
902 + assert isinstance(method, case.dispatch_cls)
903
904
905 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
vllm/model_executor/layers/quantization/quark/quark.py
+386 -161
@@ -2,7 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import fnmatch
5 -from typing import TYPE_CHECKING, Any, cast
5 +from typing import TYPE_CHECKING, Any, NamedTuple, cast
6
7 import torch
8 from transformers import PretrainedConfig
@@ -37,9 +37,35 @@ from vllm.model_executor.layers.quantization.quark.schemes import (
37 QuarkW8A8Int8,
38 )
39 from vllm.model_executor.layers.quantization.quark.utils import (
40 + QuarkQTensorHint,
41 deep_compare,
42 should_ignore_layer,
43 )
44 +from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
45 + _ACTIVATION_QUANT_KEY_MAP,
46 + _WEIGHT_QUANT_KEY_MAP,
47 +)
48 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
49 + QuantKey,
50 + kFp8Dynamic128Sym,
51 + kFp8DynamicTensorSym,
52 + kFp8DynamicTokenSym,
53 + kFp8Static128BlockE8M0Sym,
54 + kFp8Static128BlockSym,
55 + kFp8StaticChannelSym,
56 + kFp8StaticTensorSym,
57 + kInt4W4A8StaticChannelSym,
58 + kInt8DynamicTensorAsym,
59 + kInt8DynamicTensorSym,
60 + kInt8DynamicTokenAsym,
61 + kInt8DynamicTokenSym,
62 + kInt8StaticChannelSym,
63 + kInt8StaticTensorAsym,
64 + kInt8StaticTensorSym,
65 + kMxfp4Static,
66 + kNvfp4Dynamic,
67 + kNvfp4Static,
68 +)
69 from vllm.model_executor.models.utils import WeightsMapper
70 from vllm.platforms import current_platform
71
@@ -55,6 +81,15 @@ logger = init_logger(__name__)
81 _DEEPSEEK_V3_FAMILY_MODEL_TYPES = frozenset({"deepseek_v3", "deepseek_v32"})
82
83
84 +class QuantKeyMatch(NamedTuple):
85 + matches: bool
86 + activation_quant_key: QuantKey | None
87 + weight_quant_key: QuantKey | None
88 +
89 + def __bool__(self) -> bool:
90 + return self.matches
91 +
92 +
93 class QuarkConfig(QuantizationConfig):
94 def __init__(
95 self,
@@ -155,41 +190,102 @@ class QuarkConfig(QuantizationConfig):
190 def get_quant_method(
191 self, layer: torch.nn.Module, prefix: str
192 ) -> "QuantizeMethodBase | None":
158 - # Check if the layer is skipped for quantization.
193 + weight_quant_key, activation_quant_key, method_cls = (
194 + self.get_quant_method_target(prefix, type(layer))
195 + )
196 +
197 exclude_layers = cast(list[str], self.quant_config.get("exclude"))
160 - if should_ignore_layer(
198 + is_ignored = should_ignore_layer(
199 prefix,
200 ignore=exclude_layers,
201 fused_mapping=self.packed_modules_mapping,
202 check_children=isinstance(layer, RoutedExperts),
165 - ):
166 - if isinstance(layer, RoutedExperts):
167 - return UnquantizedFusedMoEMethod(layer.moe_config)
168 - if (
169 - "self_attn" not in prefix # only quantize attention projections
170 - or not getattr(self, "dynamic_mxfp4_quant", False)
171 - or not isinstance(layer, LinearBase) # Ignore other methods
172 - ):
173 - return UnquantizedLinearMethod()
203 + )
204 + dynamic_mxfp4_quant = (
205 + is_ignored and "self_attn" in prefix and self.dynamic_mxfp4_quant
206 + )
207
175 - scheme = self.get_scheme(
176 - layer=layer,
177 - layer_name=prefix,
178 - dynamic_mxfp4_quant=True,
208 + if method_cls is UnquantizedFusedMoEMethod:
209 + return UnquantizedFusedMoEMethod(layer.moe_config)
210 + if method_cls is UnquantizedLinearMethod:
211 + return UnquantizedLinearMethod()
212 + if method_cls is QuarkLinearMethod:
213 + _, _, scheme_cls = self.get_scheme_cls(type(layer), prefix)
214 + scheme = self.init_scheme(
215 + scheme_cls,
216 + weight_quant_key=weight_quant_key,
217 + activation_quant_key=activation_quant_key,
218 + dynamic_mxfp4_quant=dynamic_mxfp4_quant,
219 )
220 layer.scheme = scheme
221 return QuarkLinearMethod(self)
182 - if isinstance(layer, LinearBase):
183 - scheme = self.get_scheme(layer=layer, layer_name=prefix)
184 - layer.scheme = scheme
185 - return QuarkLinearMethod(self)
186 - if isinstance(layer, Attention):
222 + if method_cls is QuarkKVCacheMethod:
223 return QuarkKVCacheMethod(self)
224 + if method_cls is not None and issubclass(method_cls, QuarkMoEMethod):
225 + return QuarkMoEMethod.get_moe_method(
226 + self,
227 + module=layer,
228 + method_cls=method_cls,
229 + weight_quant_key=weight_quant_key,
230 + activation_quant_key=activation_quant_key,
231 + )
232
189 - if isinstance(layer, RoutedExperts):
190 - return QuarkMoEMethod.get_moe_method(self, module=layer, layer_name=prefix)
233 return None
234
235 + def get_quant_method_target(
236 + self, prefix: str, layer_type: type[torch.nn.Module]
237 + ) -> tuple[
238 + QuantKey | None,
239 + QuantKey | None,
240 + type[QuantizeMethodBase]
241 + | type[UnquantizedLinearMethod]
242 + | type[UnquantizedFusedMoEMethod]
243 + | None,
244 + ]:
245 + """Return weight key, activation key, and quant method class for
246 + the given ``prefix`` and ``layer_type``.
247 +
248 + This is the counterpart of ``get_quant_method`` without quant method
249 + instantiation.
250 +
251 + TODO: integrate as part of base QuantizationConfig.get_quant_method_target
252 + in the future.
253 +
254 + ``None`` denotes an unquantized/BF16 activation.
255 + """
256 + is_routed_experts = issubclass(layer_type, RoutedExperts)
257 + if should_ignore_layer(
258 + prefix,
259 + ignore=cast(list[str], self.quant_config.get("exclude")),
260 + fused_mapping=self.packed_modules_mapping,
261 + check_children=is_routed_experts,
262 + ):
263 + if is_routed_experts:
264 + return None, None, UnquantizedFusedMoEMethod
265 + if issubclass(layer_type, LinearBase):
266 + if "self_attn" in prefix and self.dynamic_mxfp4_quant:
267 + weight_quant_key, activation_key, _ = self.get_scheme_cls(
268 + layer_type, prefix
269 + )
270 + return weight_quant_key, activation_key, QuarkLinearMethod
271 + return None, None, UnquantizedLinearMethod
272 + return None, None, None
273 + if issubclass(layer_type, LinearBase):
274 + weight_quant_key, activation_key, _ = self.get_scheme_cls(
275 + layer_type, prefix
276 + )
277 + return weight_quant_key, activation_key, QuarkLinearMethod
278 + if issubclass(layer_type, Attention):
279 + return None, None, QuarkKVCacheMethod
280 + if is_routed_experts:
281 + weight_quant_key, activation_quant_key, quant_method_cls = (
282 + QuarkMoEMethod.get_moe_method_target(self, layer_type, prefix)
283 + )
284 + if quant_method_cls is None:
285 + return None, None, None
286 + return weight_quant_key, activation_quant_key, quant_method_cls
287 + return None, None, None
288 +
289 @classmethod
290 def from_config(cls, config: dict[str, Any]) -> "QuarkConfig":
291 export_config = config.get("export")
@@ -290,15 +386,20 @@ class QuarkConfig(QuantizationConfig):
386
387 def _is_fp8_w4a8(
388 self,
293 - weight_quant: list[dict[str, Any]] | None,
294 - input_quant: dict[str, Any] | None,
295 - ) -> bool:
389 + weight_quant: QuarkQTensorHint,
390 + input_quant: QuarkQTensorHint,
391 + ) -> QuantKeyMatch:
392 # Confirm weights and input quantized.
297 - if weight_quant is None or input_quant is None:
298 - return False
299 -
300 - if not isinstance(weight_quant, list) or len(weight_quant) != 2:
301 - return False
393 + if isinstance(input_quant, list):
394 + if len(input_quant) != 1:
395 + return QuantKeyMatch(False, None, None)
396 + input_quant = input_quant[0]
397 + if (
398 + not isinstance(weight_quant, list)
399 + or len(weight_quant) != 2
400 + or not isinstance(input_quant, dict)
401 + ):
402 + return QuantKeyMatch(False, None, None)
403
404 # Confirm weight scheme is supported
405 is_w4a8_dtype = (
@@ -321,24 +422,26 @@ class QuarkConfig(QuantizationConfig):
422 and is_static_weight
423 and is_per_tensor_fp8_and_per_channel_int4_weight
424 ):
324 - return False
425 + return QuantKeyMatch(False, None, None)
426
427 # Dynamic quantization is always supported if weights supported.
428 if input_quant.get("is_dynamic"):
328 - return True
429 + return QuantKeyMatch(True, kFp8DynamicTokenSym, kInt4W4A8StaticChannelSym)
430
431 # Confirm activation scheme is supported.
432 is_per_tensor_activation = input_quant.get("qscheme") == "per_tensor"
332 - return is_per_tensor_activation
433 + if not is_per_tensor_activation:
434 + return QuantKeyMatch(False, None, None)
435 + return QuantKeyMatch(True, kFp8StaticTensorSym, kInt4W4A8StaticChannelSym)
436
437 def _is_fp8_w8a8(
438 self,
439 weight_quant: dict[str, Any] | None,
440 input_quant: dict[str, Any] | None,
338 - ) -> bool:
441 + ) -> QuantKeyMatch:
442 # Confirm weights and input quantized.
340 - if weight_quant is None or input_quant is None:
341 - return False
443 + if not isinstance(weight_quant, dict) or not isinstance(input_quant, dict):
444 + return QuantKeyMatch(False, None, None)
445
446 # Confirm weight scheme is supported
447 is_fp8_dtype = (
@@ -358,64 +461,125 @@ class QuarkConfig(QuantizationConfig):
461 )
462
463 if not is_fp8_dtype or not is_static_weight:
361 - return False
464 + return QuantKeyMatch(False, None, None)
465
466 if is_per_block_weight:
467 if not input_quant.get("is_dynamic"):
365 - return False
366 - return (
468 + return QuantKeyMatch(False, None, None)
469 + matches = (
470 input_quant.get("qscheme") == "per_group"
471 and input_quant.get("group_size") == block_size[1]
472 and input_quant.get("symmetric") is True
473 )
474 + if not matches:
475 + return QuantKeyMatch(False, None, None)
476 + weight_quant_key = (
477 + kFp8Static128BlockE8M0Sym
478 + if weight_quant.get("scale_type") == "float8_e8m0fnu"
479 + else kFp8Static128BlockSym
480 + )
481 + return QuantKeyMatch(True, kFp8Dynamic128Sym, weight_quant_key)
482
483 if not is_per_tensor_or_channel_weight:
373 - return False
484 + return QuantKeyMatch(False, None, None)
485
486 # Dynamic quantization is always supported if tensor/channel weights
487 # are supported.
488 if input_quant.get("is_dynamic"):
378 - return True
489 + activation_quant_key = (
490 + kFp8DynamicTokenSym
491 + if input_quant.get("qscheme") == "per_channel"
492 + else kFp8DynamicTensorSym
493 + )
494 + weight_quant_key = (
495 + kFp8StaticChannelSym
496 + if weight_quant.get("qscheme") == "per_channel"
497 + else kFp8StaticTensorSym
498 + )
499 + return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
500
501 # Confirm activation scheme is supported.
502 is_per_tensor_activation = input_quant.get("qscheme") == "per_tensor"
382 - return is_per_tensor_activation
503 + if not is_per_tensor_activation:
504 + return QuantKeyMatch(False, None, None)
505 + weight_quant_key = (
506 + kFp8StaticChannelSym
507 + if weight_quant.get("qscheme") == "per_channel"
508 + else kFp8StaticTensorSym
509 + )
510 + return QuantKeyMatch(True, kFp8StaticTensorSym, weight_quant_key)
511
384 - def _is_static_tensor_w8a8(
512 + def _is_w8a8_int8(
513 self,
514 weight_quant: dict[str, Any] | None,
515 input_quant: dict[str, Any] | None,
388 - ) -> bool:
516 + ) -> QuantKeyMatch:
517 # Confirm weights and input quantized.
390 - if weight_quant is None or input_quant is None:
391 - return False
518 + if not isinstance(weight_quant, dict) or not isinstance(input_quant, dict):
519 + return QuantKeyMatch(False, None, None)
520
521 is_int8_dtype = (
522 weight_quant.get("dtype") == "int8" and input_quant.get("dtype") == "int8"
523 )
524
397 - is_tensor = (
398 - weight_quant.get("qscheme") in ["per_tensor", "per_channel"]
399 - and input_quant.get("qscheme") == "per_tensor"
400 - )
401 -
402 - is_static = not weight_quant.get("is_dynamic") and not input_quant.get(
403 - "is_dynamic"
525 + is_valid_weight_scheme = weight_quant.get("qscheme") in [
526 + "per_tensor",
527 + "per_channel",
528 + ]
529 + is_static_input = input_quant.get(
530 + "qscheme"
531 + ) == "per_tensor" and not input_quant.get("is_dynamic")
532 + is_dynamic_input = (
533 + input_quant.get("qscheme") == "per_channel"
534 + and input_quant.get("is_dynamic") is True
535 )
405 -
536 is_weight_symmetric = weight_quant.get("symmetric") is True
537
538 # Both symmetric and asymmetric input quantization supported.
539 # Only symmetric weight quantization supported.
410 - return is_int8_dtype and is_tensor and is_weight_symmetric and is_static
540 + matches = (
541 + is_int8_dtype
542 + and is_valid_weight_scheme
543 + and is_weight_symmetric
544 + and (
545 + (is_static_input and not weight_quant.get("is_dynamic"))
546 + or is_dynamic_input
547 + )
548 + )
549 + if not matches:
550 + return QuantKeyMatch(False, None, None)
551 + weight_quant_key = (
552 + kInt8StaticChannelSym
553 + if weight_quant.get("qscheme") == "per_channel"
554 + else kInt8StaticTensorSym
555 + )
556 + if is_static_input:
557 + activation_quant_key = (
558 + kInt8StaticTensorSym
559 + if input_quant.get("symmetric") is True
560 + else kInt8StaticTensorAsym
561 + )
562 + elif weight_quant.get("qscheme") == "per_channel":
563 + activation_quant_key = (
564 + kInt8DynamicTokenSym
565 + if input_quant.get("symmetric") is True
566 + else kInt8DynamicTokenAsym
567 + )
568 + else:
569 + activation_quant_key = (
570 + kInt8DynamicTensorSym
571 + if input_quant.get("symmetric") is True
572 + else kInt8DynamicTensorAsym
573 + )
574 + return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
575
576 def _is_w4a8_mxfp4_fp8(
577 self,
578 weight_quant: dict[str, Any] | None,
579 input_quant: dict[str, Any] | None,
416 - ) -> bool:
580 + ) -> QuantKeyMatch:
581 if weight_quant is None or input_quant is None:
418 - return False
582 + return QuantKeyMatch(False, None, None)
583
584 is_weight_mxfp4 = (
585 weight_quant.get("dtype") == "fp4"
@@ -432,53 +596,24 @@ class QuarkConfig(QuantizationConfig):
596 and input_quant.get("symmetric") is True # Symmetric quantization
597 )
598
435 - return is_weight_mxfp4 and is_input_fp8
436 -
437 - def _is_dynamic_per_token_w8a8(
438 - self,
439 - weight_quant: dict[str, Any] | None,
440 - input_quant: dict[str, Any] | None,
441 - ) -> bool:
442 - """Detect W8A8 INT8 with per-tensor or per-channel
443 - weights and dynamic per-token input."""
444 - if weight_quant is None or input_quant is None:
445 - return False
446 -
447 - is_int8_dtype = (
448 - weight_quant.get("dtype") == "int8" and input_quant.get("dtype") == "int8"
449 - )
450 -
451 - is_valid_weight_scheme = weight_quant.get("qscheme") in [
452 - "per_tensor",
453 - "per_channel",
454 - ]
455 - is_per_token_input = input_quant.get("qscheme") == "per_channel"
456 -
457 - is_dynamic_input = input_quant.get("is_dynamic") is True
458 - is_weight_symmetric = weight_quant.get("symmetric") is True
459 -
460 - return (
461 - is_int8_dtype
462 - and is_valid_weight_scheme
463 - and is_per_token_input
464 - and is_dynamic_input
465 - and is_weight_symmetric
466 - )
599 + if not (is_weight_mxfp4 and is_input_fp8):
600 + return QuantKeyMatch(False, None, None)
601 + return QuantKeyMatch(True, kFp8StaticTensorSym, kMxfp4Static)
602
603 def _is_nvfp4(
604 self,
470 - weight_quant: dict[str, Any] | list[dict[str, Any]] | None,
471 - input_quant: dict[str, Any] | list[dict[str, Any]] | None,
472 - ) -> bool:
605 + weight_quant: QuarkQTensorHint,
606 + input_quant: QuarkQTensorHint,
607 + ) -> QuantKeyMatch:
608 # Confirm weights and input quantized.
609 if weight_quant is None or input_quant is None:
475 - return False
610 + return QuantKeyMatch(False, None, None)
611
612 # Confirm both weight_quant and input_quant are lists with 2 elements
613 if not isinstance(weight_quant, list) or len(weight_quant) != 2:
479 - return False
614 + return QuantKeyMatch(False, None, None)
615 if not isinstance(input_quant, list) or len(input_quant) != 2:
481 - return False
616 + return QuantKeyMatch(False, None, None)
617
618 # First element should be fp4 with per_group quantization
619 is_fp4_per_group_weight = (
@@ -506,36 +641,35 @@ class QuarkConfig(QuantizationConfig):
641 and not input_quant[1].get("is_dynamic")
642 )
643
509 - return (
644 + matches = (
645 is_fp4_per_group_weight # type: ignore[return-value]
646 and is_fp4_per_group_input
647 and is_fp8_per_tensor_weight
648 and is_fp8_per_tensor_input
649 )
650 + if not matches:
651 + return QuantKeyMatch(False, None, None)
652 + return QuantKeyMatch(True, kNvfp4Dynamic, kNvfp4Static)
653
654 def _is_w_ocp_mx_a_x(
517 - self, weight_quant: dict[str, Any] | None, input_quant: dict[str, Any] | None
518 - ) -> bool:
655 + self,
656 + weight_quant: dict[str, Any] | None,
657 + input_quant: dict[str, Any] | None,
658 + allow_static_fp8: bool = False,
659 + ) -> QuantKeyMatch:
660 """
661 This check returns True only if it is an OCP-MX weight quantization.
521 - The activation can be any data type (e.g., FP16/BF16, FP8, or OCP-MX format).
662 + The activation can be FP16/BF16, OCP MXFP4, MXFP8, FP8.
663 The rationale for checking only the weight type is that
664 the model loading concept and process primarily concerns the weights themselves.
665 """
666 # Confirm weights quantized.
526 - if weight_quant is None:
667 + if not isinstance(weight_quant, dict):
668 logger.debug(
669 "Quark model's weight quantization is incompatible with OCP_MX format: "
529 - "weight_quant is not set."
670 + "weight_quant is not a dictionary."
671 )
531 - return False
532 -
533 - if isinstance(weight_quant, list):
534 - logger.debug(
535 - "Quark model's weight quantization is incompatible with OCP_MX format: "
536 - "weight_quant is a list (e.g. fp8_w4a8), OCP_MX requires a single dict."
537 - )
538 - return False
672 + return QuantKeyMatch(False, None, None)
673
674 # Input and weight qscheme needs to be per group.
675 if weight_quant.get("qscheme") != "per_group":
@@ -543,7 +677,7 @@ class QuarkConfig(QuantizationConfig):
677 "Quark model's weight quantization is incompatible with OCP MX format: "
678 "weight is not per_group."
679 )
546 - return False
680 + return QuantKeyMatch(False, None, None)
681
682 # Input and weight group size needs to be 32.
683 if weight_quant.get("group_size") != 32:
@@ -551,7 +685,7 @@ class QuarkConfig(QuantizationConfig):
685 "Quark model's weight quantization is incompatible with OCP MX format: "
686 "group_size of weight is not 32."
687 )
554 - return False
688 + return QuantKeyMatch(False, None, None)
689
690 # Activations and weight scales need to be in e8m0 format.
691 if weight_quant.get("scale_format") != "e8m0":
@@ -559,7 +693,7 @@ class QuarkConfig(QuantizationConfig):
693 "Quark model's weight quantization is incompatible with OCP MX format: "
694 "scale_format of weight is not e8m0."
695 )
562 - return False
696 + return QuantKeyMatch(False, None, None)
697
698 # Input and weight dtypes need to be any of fp4,
699 # fp6_e3m2 or fp6_e3m2, possibly mixed.
@@ -572,9 +706,48 @@ class QuarkConfig(QuantizationConfig):
706 "Quark model's weight quantization is incompatible with OCP MX format: "
707 "dtype is not in {fp4, fp6_e3m2, fp6_e2m3}."
708 )
575 - return False
709 + return QuantKeyMatch(False, None, None)
710 +
711 + weight_dtype = weight_quant["dtype"].replace("fp", "mxfp")
712 + weight_quant_key = _WEIGHT_QUANT_KEY_MAP[weight_dtype]
713 + if input_quant is None:
714 + activation_quant_key = None
715 + elif not input_quant.get("is_dynamic"):
716 + if (
717 + allow_static_fp8
718 + and input_quant.get("dtype") == "fp8_e4m3"
719 + and input_quant.get("qscheme") == "per_tensor"
720 + and input_quant.get("symmetric") is True
721 + ):
722 + activation_quant_key = kFp8StaticTensorSym
723 + else:
724 + logger.debug(
725 + "Quark model's OCP MX quantization is incompatible with static "
726 + "input scales."
727 + )
728 + return QuantKeyMatch(False, None, None)
729 + elif input_quant["dtype"] == "fp8_e4m3":
730 + activation_quant_key = kFp8DynamicTensorSym
731 + else:
732 + input_dtype = input_quant["dtype"].replace("fp", "mxfp")
733 + if input_dtype not in _ACTIVATION_QUANT_KEY_MAP:
734 + raise ValueError(
735 + f"Unsupported input_dtype={input_dtype} in Quark's vLLM "
736 + "integration. Supported activation dtypes are "
737 + f"{_ACTIVATION_QUANT_KEY_MAP.keys()}, or None for "
738 + "weight-only quantization."
739 + )
740 + activation_quant_key = _ACTIVATION_QUANT_KEY_MAP[input_dtype]
741 + return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
742
577 - return True
743 + @staticmethod
744 + def _unwrap_single_quant_config(
745 + quant_config: QuarkQTensorHint,
746 + ) -> dict[str, Any] | None:
747 + """Unwrap one-entry quantization-config lists."""
748 + if isinstance(quant_config, list):
749 + return quant_config[0] if len(quant_config) == 1 else None
750 + return quant_config
751
752 def get_layer_quant_config_from_name(
753 self, layer_name: str
@@ -617,14 +790,14 @@ class QuarkConfig(QuantizationConfig):
790 return None
791
792 def _find_matched_config(
620 - self, layer_name: str, module: torch.nn.Module
793 + self, layer_name: str, module: torch.nn.Module | type[torch.nn.Module]
794 ) -> dict[str, Any]:
795 # Priority order:
796 # 1. layer_quant_config,
797 # 2. layer_type_quant_config,
798 # 3. global_quant_config.
799
627 - layer_type = cast(str, type(module))
800 + layer_type = cast(str, module if isinstance(module, type) else type(module))
801 layer_type_quant_config = cast(
802 dict[str, Any], self.quant_config.get("layer_type_quant_config")
803 )
@@ -662,51 +835,71 @@ class QuarkConfig(QuantizationConfig):
835 return layer_quant_config
836 return fallback_config
837
665 - def _get_scheme_from_config(
666 - self, config: dict[str, Any], dynamic_mxfp4_quant: bool = False
667 - ) -> "QuarkScheme":
838 + def _get_scheme_cls_from_config(
839 + self, config: dict[str, Any]
840 + ) -> tuple[QuantKey | None, QuantKey | None, type["QuarkScheme"]]:
841 if config.get("output_tensors") or config.get("bias"):
842 raise NotImplementedError(
843 "Currently, Quark models with output_tensors "
844 "and bias quantized are not supported"
845 )
673 - weight_config = cast(dict[str, Any], config.get("weight"))
674 - input_config = cast(dict[str, Any], config.get("input_tensors"))
675 -
676 - if self._is_nvfp4(weight_config, input_config):
677 - return QuarkNVFP4()
678 - elif self._is_fp8_w8a8(weight_config, input_config):
679 - is_fp8_w8a8_supported = self._check_scheme_supported(
680 - QuarkW8A8Fp8.get_min_capability(), error=False
681 - )
682 - if is_fp8_w8a8_supported:
683 - if weight_config.get("qscheme") == "per_block":
684 - return QuarkW8A8Fp8PerBlock(weight_config, input_config)
685 - return QuarkW8A8Fp8(weight_config, input_config)
686 - elif self._is_static_tensor_w8a8(weight_config, input_config):
687 - weight_qscheme = cast(str, weight_config.get("qscheme"))
688 - return QuarkW8A8Int8(
689 - qscheme=weight_qscheme,
690 - is_static_input_scheme=True,
691 - input_symmetric=input_config.get("symmetric"),
846 + weight_config = cast(QuarkQTensorHint, config.get("weight"))
847 + input_config = cast(QuarkQTensorHint, config.get("input_tensors"))
848 +
849 + if (
850 + isinstance(weight_config, list)
851 + and len(weight_config) > 1
852 + or isinstance(input_config, list)
853 + and len(input_config) > 1
854 + ):
855 + if match := self._is_nvfp4(weight_config, input_config):
856 + return (
857 + match.weight_quant_key,
858 + match.activation_quant_key,
859 + QuarkNVFP4,
860 + )
861 +
862 + raise NotImplementedError(
863 + "Multi-entry weight or activation quantization configs are only "
864 + "supported for NVFP4."
865 )
693 - elif self._is_w4a8_mxfp4_fp8(weight_config, input_config):
694 - is_w4a8_supported = self._check_scheme_supported(
695 - QuarkW4A8_MXFP4_FP8.get_min_capability(), error=False
866 +
867 + weight_config = self._unwrap_single_quant_config(weight_config)
868 + input_config = self._unwrap_single_quant_config(input_config)
869 +
870 + if (
871 + isinstance(weight_config, dict)
872 + and weight_config.get("qscheme") == "per_block"
873 + and not weight_config.get("block_size")
874 + ):
875 + raise ValueError(
876 + "Quark W8A8 FP8 per-block weight quantization requires "
877 + "`block_size` in the weight quantization config."
878 )
697 - if is_w4a8_supported:
698 - return QuarkW4A8_MXFP4_FP8(weight_config, input_config)
699 - elif self._is_dynamic_per_token_w8a8(weight_config, input_config):
700 - weight_qscheme = cast(str, weight_config.get("qscheme"))
701 - return QuarkW8A8Int8(
702 - qscheme=weight_qscheme,
703 - is_static_input_scheme=False,
704 - input_symmetric=input_config.get("symmetric"),
879 +
880 + if match := self._is_fp8_w8a8(weight_config, input_config):
881 + assert weight_config is not None
882 + if weight_config.get("qscheme") == "per_block":
883 + return (
884 + match.weight_quant_key,
885 + match.activation_quant_key,
886 + QuarkW8A8Fp8PerBlock,
887 + )
888 + return match.weight_quant_key, match.activation_quant_key, QuarkW8A8Fp8
889 + elif match := self._is_w8a8_int8(weight_config, input_config):
890 + return (
891 + match.weight_quant_key,
892 + match.activation_quant_key,
893 + QuarkW8A8Int8,
894 )
706 - elif self._is_w_ocp_mx_a_x(weight_config, input_config):
707 - return QuarkOCP_MX(
708 - weight_config, input_config, dynamic_mxfp4_quant=dynamic_mxfp4_quant
895 + elif match := self._is_w4a8_mxfp4_fp8(weight_config, input_config):
896 + return (
897 + match.weight_quant_key,
898 + match.activation_quant_key,
899 + QuarkW4A8_MXFP4_FP8,
900 )
901 + elif match := self._is_w_ocp_mx_a_x(weight_config, input_config):
902 + return match.weight_quant_key, match.activation_quant_key, QuarkOCP_MX
903
904 raise NotImplementedError(
905 "No quark compatible scheme was found. "
@@ -714,19 +907,51 @@ class QuarkConfig(QuantizationConfig):
907 f"Input config: {input_config}"
908 )
909
717 - def get_scheme(
718 - self, layer: torch.nn.Module, layer_name: str, dynamic_mxfp4_quant: bool = False
910 + def get_scheme_cls(
911 + self, layer_type: type[torch.nn.Module], layer_name: str
912 + ) -> tuple[QuantKey | None, QuantKey | None, type["QuarkScheme"]]:
913 + """Return quantization keys and scheme class without initializing it."""
914 + return self._get_scheme_cls_from_config(
915 + self._find_matched_config(layer_name, layer_type)
916 + )
917 +
918 + def init_scheme(
919 + self,
920 + scheme_cls: type["QuarkScheme"],
921 + weight_quant_key: QuantKey | None,
922 + activation_quant_key: QuantKey | None,
923 + dynamic_mxfp4_quant: bool = False,
924 ) -> "QuarkScheme":
720 - layer_quant_config = self._find_matched_config(layer_name, layer)
925 + """Construct a Quark scheme selected by get_scheme_cls."""
926 + if scheme_cls not in (
927 + QuarkW8A8Fp8,
928 + QuarkW8A8Fp8PerBlock,
929 + QuarkW8A8Int8,
930 + QuarkOCP_MX,
931 + QuarkNVFP4,
932 + QuarkW4A8_MXFP4_FP8,
933 + ):
934 + raise AssertionError(f"Unsupported Quark scheme class: {scheme_cls}")
935 +
936 + kwargs: dict[str, Any] = {"activation_quant_key": activation_quant_key}
937 +
938 + # These classes handle different possible `weight_quant_key`.
939 + if scheme_cls in (
940 + QuarkW8A8Fp8,
941 + QuarkW8A8Fp8PerBlock,
942 + QuarkW8A8Int8,
943 + QuarkOCP_MX,
944 + ):
945 + kwargs["weight_quant_key"] = weight_quant_key
946 +
947 + if scheme_cls is QuarkOCP_MX:
948 + kwargs["dynamic_mxfp4_quant"] = dynamic_mxfp4_quant
949 +
950 + scheme = scheme_cls(**kwargs)
951
722 - # Find the quant_scheme
723 - scheme = self._get_scheme_from_config(
724 - layer_quant_config, dynamic_mxfp4_quant=dynamic_mxfp4_quant
725 - )
952 # Raise error if device does not support the scheme
953 # (e.g. fp8 needs ada lovelace)
954 self._check_scheme_supported(scheme.get_min_capability())
729 -
955 return scheme
956
957 @staticmethod
vllm/model_executor/layers/quantization/quark/quark_moe.py
+208 -111
@@ -1,7 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from typing import Any
4 +from typing import TYPE_CHECKING
5
6 import torch
7
@@ -56,19 +56,27 @@ from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
56 make_nvfp4_moe_quant_config,
57 select_nvfp4_moe_backend,
58 )
59 +from vllm.model_executor.layers.quantization.quark.utils import QuarkQTensorHint
60 from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
61 + _ACTIVATION_QUANT_KEY_MAP,
62 + _WEIGHT_QUANT_KEY_MAP,
63 OCP_MX_BLOCK_SIZE,
64 OCP_MX_Scheme,
65 )
66 from vllm.model_executor.layers.quantization.utils.quant_utils import (
67 GroupShape,
68 + QuantKey,
69 kFp8DynamicTensorSym,
70 kFp8DynamicTokenSym,
71 kFp8StaticChannelSym,
72 kFp8StaticTensorSym,
73 + kInt4W4A8StaticChannelSym,
74 + kInt8DynamicTensorAsym,
75 kInt8DynamicTensorSym,
76 + kInt8DynamicTokenAsym,
77 kInt8DynamicTokenSym,
78 kInt8StaticChannelSym,
79 + kInt8StaticTensorAsym,
80 kInt8StaticTensorSym,
81 kMxfp4Dynamic,
82 kNvfp4Dynamic,
@@ -82,6 +90,9 @@ from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
90 from vllm.model_executor.utils import replace_parameter, set_weight_attrs
91 from vllm.platforms import current_platform
92
93 +if TYPE_CHECKING:
94 + from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
95 +
96 logger = init_logger(__name__)
97
98 __all__ = [
@@ -93,67 +104,154 @@ __all__ = [
104
105
106 class QuarkMoEMethod(FusedMoEMethodBase):
96 - def __init__(self, moe: FusedMoEConfig):
107 + supported_activation_quant_keys: list[QuantKey | None] = []
108 + supported_weight_quant_keys: list[QuantKey] = []
109 +
110 + def __init__(
111 + self,
112 + moe: FusedMoEConfig,
113 + weight_quant_key: QuantKey,
114 + activation_quant_key: QuantKey | None,
115 + ):
116 super().__init__(moe)
117 + if activation_quant_key not in self.supported_activation_quant_keys:
118 + raise ValueError(
119 + f"Unsupported activation quant key: {activation_quant_key}"
120 + )
121 + if weight_quant_key not in self.supported_weight_quant_keys:
122 + raise ValueError(f"Unsupported weight quant key: {weight_quant_key}")
123 + self.activation_quant_key = activation_quant_key
124 + self.weight_quant_key = weight_quant_key
125 self.has_bias = self.moe.has_bias
126
127 @staticmethod
101 - def get_moe_method(
102 - quant_config: "QuarkConfig", # type: ignore # noqa E501 # noqa F821
103 - module: RoutedExperts,
128 + def get_moe_method_target(
129 + quant_config: "QuarkConfig",
130 + layer_type: type[torch.nn.Module],
131 layer_name: str,
105 - ) -> "QuarkMoEMethod":
106 - layer_quant_config = quant_config._find_matched_config(layer_name, module)
107 -
132 + ) -> tuple[
133 + QuantKey | None,
134 + QuantKey | None,
135 + type["QuarkMoEMethod"] | None,
136 + ]:
137 + """Return the quantization target for a routed-experts layer."""
138 + layer_quant_config = quant_config._find_matched_config(layer_name, layer_type)
139 if layer_quant_config.get("output_tensors") or layer_quant_config.get("bias"):
140 raise NotImplementedError(
141 "Currently, Quark models with "
142 "output_tensors and bias "
143 "quantized are not supported"
144 )
145 + weight_config: QuarkQTensorHint = layer_quant_config.get("weight")
146 + input_config: QuarkQTensorHint = layer_quant_config.get("input_tensors")
147 +
148 + if (
149 + isinstance(weight_config, list)
150 + and len(weight_config) > 1
151 + or isinstance(input_config, list)
152 + and len(input_config) > 1
153 + ):
154 + if match := quant_config._is_fp8_w4a8(weight_config, input_config):
155 + return (
156 + match.weight_quant_key,
157 + match.activation_quant_key,
158 + QuarkW4A8Fp8MoEMethod,
159 + )
160 + if match := quant_config._is_nvfp4(weight_config, input_config):
161 + return (
162 + match.weight_quant_key,
163 + match.activation_quant_key,
164 + QuarkNvfp4MoEMethod,
165 + )
166 + raise RuntimeError("Unsupported FusedMoe scheme")
167 +
168 + weight_config = quant_config._unwrap_single_quant_config(weight_config)
169 + input_config = quant_config._unwrap_single_quant_config(input_config)
170 +
171 + if match := quant_config._is_fp8_w8a8(weight_config, input_config):
172 + return (
173 + match.weight_quant_key,
174 + match.activation_quant_key,
175 + QuarkW8A8Fp8MoEMethod,
176 + )
177 + if match := quant_config._is_w_ocp_mx_a_x(
178 + weight_config, input_config, allow_static_fp8=True
179 + ):
180 + return (
181 + match.weight_quant_key,
182 + match.activation_quant_key,
183 + QuarkOCP_MX_MoEMethod,
184 + )
185 + if match := quant_config._is_w8a8_int8(weight_config, input_config):
186 + return (
187 + match.weight_quant_key,
188 + match.activation_quant_key,
189 + QuarkW8A8Int8MoEMethod,
190 + )
191 + raise RuntimeError("Unsupported FusedMoe scheme")
192
115 - weight_config = layer_quant_config.get("weight")
116 - input_config = layer_quant_config.get("input_tensors")
193 + @staticmethod
194 + def get_moe_method(
195 + quant_config: "QuarkConfig", # type: ignore # noqa E501 # noqa F821
196 + module: RoutedExperts,
197 + method_cls: type["QuarkMoEMethod"],
198 + weight_quant_key: QuantKey | None,
199 + activation_quant_key: QuantKey | None,
200 + ) -> "QuarkMoEMethod":
201 + if weight_quant_key is None:
202 + raise RuntimeError("Unsupported FusedMoe scheme")
203
118 - if quant_config._is_fp8_w4a8(weight_config, input_config):
119 - return QuarkW4A8Fp8MoEMethod(weight_config, input_config, module.moe_config)
120 - elif quant_config._is_nvfp4(weight_config, input_config):
204 + if method_cls is QuarkW4A8Fp8MoEMethod:
205 + return QuarkW4A8Fp8MoEMethod(module.moe_config, activation_quant_key)
206 + elif method_cls is QuarkNvfp4MoEMethod:
207 return QuarkNvfp4MoEMethod(
122 - weight_config, input_config, module.moe_config, quant_config
208 + module.moe_config, quant_config, activation_quant_key
209 + )
210 + elif method_cls is QuarkW8A8Fp8MoEMethod:
211 + return QuarkW8A8Fp8MoEMethod(
212 + module.moe_config, weight_quant_key, activation_quant_key
213 )
124 - elif quant_config._is_fp8_w8a8(weight_config, input_config):
125 - return QuarkW8A8Fp8MoEMethod(weight_config, input_config, module.moe_config)
126 - elif quant_config._is_w_ocp_mx_a_x(weight_config, input_config):
214 + elif method_cls is QuarkOCP_MX_MoEMethod:
215 # All OCP MX schemes (W4A16, W4A8, etc.) handled by QuarkOCP_MX_MoEMethod
216 # Backend selection happens inside via oracle
129 - return QuarkOCP_MX_MoEMethod(weight_config, input_config, module.moe_config)
130 - elif quant_config._is_static_tensor_w8a8(
131 - weight_config, input_config
132 - ) or quant_config._is_dynamic_per_token_w8a8(weight_config, input_config):
217 + return QuarkOCP_MX_MoEMethod(
218 + module.moe_config, weight_quant_key, activation_quant_key
219 + )
220 + elif method_cls is QuarkW8A8Int8MoEMethod:
221 return QuarkW8A8Int8MoEMethod(
134 - weight_config, input_config, module.moe_config
222 + module.moe_config, weight_quant_key, activation_quant_key
223 )
224 else:
225 raise RuntimeError("Unsupported FusedMoe scheme")
226
227
228 class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
229 + supported_activation_quant_keys = [
230 + kFp8StaticTensorSym,
231 + kFp8DynamicTensorSym,
232 + kFp8DynamicTokenSym,
233 + ]
234 + supported_weight_quant_keys = [
235 + kFp8StaticChannelSym,
236 + kFp8StaticTensorSym,
237 + ]
238 +
239 def __init__(
240 self,
143 - weight_config: dict[str, Any],
144 - input_config: dict[str, Any],
241 moe: FusedMoEConfig,
242 + weight_quant_key: QuantKey,
243 + activation_quant_key: QuantKey | None,
244 ):
147 - super().__init__(moe)
148 - self.weight_quant = weight_config
149 - self.input_quant = input_config
150 -
151 - self.weight_qscheme = self.weight_quant.get("qscheme")
152 - self.input_qscheme = self.input_quant.get("qscheme")
153 - self.weight_dtype = self.weight_quant.get("dtype", "").replace(
154 - "fp8_e4m3", "fp8"
245 + super().__init__(moe, weight_quant_key, activation_quant_key)
246 + self.weight_qscheme = (
247 + "per_channel" if weight_quant_key == kFp8StaticChannelSym else "per_tensor"
248 )
156 - self.input_dtype = self.input_quant.get("dtype", "").replace("fp8_e4m3", "fp8")
249 + self.input_qscheme = (
250 + "per_channel"
251 + if activation_quant_key == kFp8DynamicTokenSym
252 + else "per_tensor"
253 + )
254 + self.static_input_scales = activation_quant_key == kFp8StaticTensorSym
255 per_tensor = (
256 self.weight_qscheme == "per_tensor" and self.input_qscheme == "per_tensor"
257 )
@@ -170,28 +268,10 @@ class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
268 f"{self.weight_qscheme}, {self.input_qscheme}"
269 ) # noqa E501
270
173 - self.static_input_scales = not self.input_quant.get("is_dynamic")
174 - if self.static_input_scales and per_channel:
175 - raise ValueError(
176 - "For FP8 Fused MoE layer, we require either per tensor or "
177 - "channelwise, dynamic per token quantization."
178 - )
179 -
180 - # Determine quant keys for oracle backend selection
181 - if per_channel:
182 - weight_key = kFp8StaticChannelSym
183 - activation_key = kFp8DynamicTokenSym
184 - elif self.static_input_scales:
185 - weight_key = kFp8StaticTensorSym
186 - activation_key = kFp8StaticTensorSym
187 - else:
188 - weight_key = kFp8StaticTensorSym
189 - activation_key = kFp8DynamicTensorSym
190 -
271 self.fp8_backend, self.experts_cls = select_fp8_moe_backend(
272 config=moe,
193 - weight_key=weight_key,
194 - activation_key=activation_key,
273 + weight_key=self.weight_quant_key,
274 + activation_key=self.activation_quant_key,
275 )
276
277 self.model_type = getattr(
@@ -500,17 +580,41 @@ class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
580 class QuarkW8A8Int8MoEMethod(QuarkMoEMethod):
581 """Quark W8A8 INT8 MoE method."""
582
583 + supported_activation_quant_keys = [
584 + kInt8StaticTensorSym,
585 + kInt8StaticTensorAsym,
586 + kInt8DynamicTensorSym,
587 + kInt8DynamicTensorAsym,
588 + kInt8DynamicTokenSym,
589 + kInt8DynamicTokenAsym,
590 + ]
591 + supported_weight_quant_keys = [
592 + kInt8StaticChannelSym,
593 + kInt8StaticTensorSym,
594 + ]
595 +
596 def __init__(
597 self,
505 - weight_config: dict[str, Any],
506 - input_config: dict[str, Any],
598 moe: FusedMoEConfig,
599 + weight_quant_key: QuantKey,
600 + activation_quant_key: QuantKey | None,
601 ):
509 - super().__init__(moe)
510 - self.weight_quant = weight_config
511 - self.input_quant = input_config
512 - self.weight_qscheme = self.weight_quant.get("qscheme", "per_tensor")
513 - self.static_input_scales = not self.input_quant.get("is_dynamic", False)
602 + super().__init__(moe, weight_quant_key, activation_quant_key)
603 +
604 + # TODO: oracle needs to support asym int8.
605 + if self.activation_quant_key == kInt8StaticTensorAsym:
606 + self.activation_quant_key = kInt8StaticTensorSym
607 + if self.activation_quant_key == kInt8DynamicTensorAsym:
608 + self.activation_quant_key = kInt8DynamicTensorSym
609 + if self.activation_quant_key == kInt8DynamicTokenAsym:
610 + self.activation_quant_key = kInt8DynamicTokenSym
611 +
612 + self.weight_qscheme = (
613 + "per_channel" if weight_quant_key == kInt8StaticChannelSym else "per_tensor"
614 + )
615 +
616 + assert self.activation_quant_key is not None
617 + self.static_input_scales = self.activation_quant_key.scale.static
618
619 self.moe_quant_config: FusedMoEQuantConfig | None = None
620 self.moe_kernel: mk.FusedMoEKernel | None = None
@@ -528,17 +632,10 @@ class QuarkW8A8Int8MoEMethod(QuarkMoEMethod):
632 # Map the Quark weight scheme to oracle quant keys. Per-channel
633 # weights pair with dynamic per-token activations; per-tensor
634 # weights with dynamic per-tensor activations.
531 - if self.weight_qscheme == "per_channel":
532 - weight_key = kInt8StaticChannelSym
533 - activation_key = kInt8DynamicTokenSym
534 - else:
535 - weight_key = kInt8StaticTensorSym
536 - activation_key = kInt8DynamicTensorSym
537 -
635 self.int8_backend, self.experts_cls = select_int8_moe_backend(
636 config=moe,
540 - weight_key=weight_key,
541 - activation_key=activation_key,
637 + weight_key=self.weight_quant_key,
638 + activation_key=self.activation_quant_key,
639 )
640
641 def create_weights(
@@ -876,15 +973,18 @@ class QuarkW8A8Int8MoEMethod(QuarkMoEMethod):
973
974
975 class QuarkW4A8Fp8MoEMethod(QuarkMoEMethod):
976 + supported_activation_quant_keys = [
977 + kFp8DynamicTokenSym,
978 + kFp8StaticTensorSym,
979 + ]
980 + supported_weight_quant_keys = [kInt4W4A8StaticChannelSym]
981 +
982 def __init__(
983 self,
881 - weight_config: dict[str, Any],
882 - input_config: dict[str, Any],
984 moe: FusedMoEConfig,
985 + activation_quant_key: QuantKey | None,
986 ):
885 - super().__init__(moe)
886 - self.weight_quant = weight_config
887 - self.input_quant = input_config
987 + super().__init__(moe, kInt4W4A8StaticChannelSym, activation_quant_key)
988
989 assert rocm_aiter_ops.is_fused_moe_enabled(), (
990 "W4A8 FP8 MoE requires ROCm AITER fused MoE support."
@@ -1032,37 +1132,36 @@ class QuarkW4A8Fp8MoEMethod(QuarkMoEMethod):
1132
1133
1134 class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1135 + supported_activation_quant_keys = [
1136 + *_ACTIVATION_QUANT_KEY_MAP.values(),
1137 + kFp8DynamicTensorSym,
1138 + kFp8StaticTensorSym,
1139 + None,
1140 + ]
1141 + supported_weight_quant_keys = [*_WEIGHT_QUANT_KEY_MAP.values()]
1142 +
1143 def __init__(
1144 self,
1037 - weight_config: dict[str, Any],
1038 - input_config: dict[str, Any] | None,
1145 moe: FusedMoEConfig,
1146 + weight_quant_key: QuantKey,
1147 + activation_quant_key: QuantKey | None,
1148 ):
1041 - super().__init__(moe)
1042 - self.weight_quant = weight_config
1043 - self.input_quant = input_config
1044 -
1045 - weight_qscheme = self.weight_quant.get("qscheme")
1046 - if not weight_qscheme == "per_group":
1047 - raise ValueError(
1048 - "For MX(FP4) Fused MoE layers, only per-group scales "
1049 - f"for weights are supported. Found {weight_qscheme}."
1050 - ) # noqa E501
1051 -
1052 - self.weight_dtype = self.weight_quant["dtype"].replace("fp", "mxfp")
1053 - if self.input_quant is not None:
1054 - input_quant = self.input_quant["dtype"]
1055 - if input_quant in ["fp4", "fp6_e3m2", "fp6_e2m3"]:
1056 - self.input_dtype = input_quant.replace("fp", "mxfp")
1057 - elif input_quant == "fp8_e4m3":
1058 - self.input_dtype = input_quant.replace("fp8_e4m3", "fp8")
1059 - else:
1060 - raise NotImplementedError(
1061 - f"Current input dtype {input_quant} is not compatible \
1062 - with OCP MX (weight) MoE quantization. Please open an issue"
1063 - )
1064 - else:
1149 + super().__init__(moe, weight_quant_key, activation_quant_key)
1150 + self.weight_dtype = next(
1151 + dtype
1152 + for dtype, quant_key in _WEIGHT_QUANT_KEY_MAP.items()
1153 + if quant_key == weight_quant_key
1154 + )
1155 + if activation_quant_key in {kFp8DynamicTensorSym, kFp8StaticTensorSym}:
1156 + self.input_dtype: str | None = "fp8"
1157 + elif activation_quant_key is None:
1158 self.input_dtype = None
1159 + else:
1160 + self.input_dtype = next(
1161 + dtype
1162 + for dtype, quant_key in _ACTIVATION_QUANT_KEY_MAP.items()
1163 + if quant_key == activation_quant_key
1164 + )
1165
1166 self.ocp_mx_scheme = OCP_MX_Scheme.from_quant_dtype(
1167 self.input_dtype, self.weight_dtype
@@ -1085,10 +1184,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1184 self.w13_precision_config = None
1185 self.w2_precision_config = None
1186
1088 - if self.input_quant is not None:
1089 - self.static_input_scales = not self.input_quant.get("is_dynamic")
1090 - else:
1091 - self.static_input_scales = False
1187 + self.static_input_scales = activation_quant_key == kFp8StaticTensorSym
1188
1189 # Select backend based on OCP MX scheme
1190 if self.ocp_mx_scheme == "w_mxfp4":
@@ -1388,6 +1484,7 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1484 f"in vLLM for OCP MX scheme {self.ocp_mx_scheme}. Please open an issue."
1485 )
1486 else:
1487 + assert self.input_dtype is not None
1488 return ocp_mx_moe_quant_config(
1489 quant_dtype=self.input_dtype,
1490 weight_dtype=self.weight_dtype,
@@ -1459,24 +1556,24 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1556
1557
1558 class QuarkNvfp4MoEMethod(QuarkMoEMethod):
1559 + supported_activation_quant_keys = [kNvfp4Dynamic]
1560 + supported_weight_quant_keys = [kNvfp4Static]
1561 +
1562 def __init__(
1563 self,
1464 - weight_config: dict[str, Any],
1465 - input_config: dict[str, Any],
1564 moe: FusedMoEConfig,
1565 quant_config: "QuarkConfig", # type: ignore # noqa E501 # noqa F821
1566 + activation_quant_key: QuantKey | None,
1567 ):
1469 - super().__init__(moe)
1470 - self.weight_quant = weight_config
1471 - self.input_quant = input_config
1568 + super().__init__(moe, kNvfp4Static, activation_quant_key)
1569 self.quant_config = quant_config
1570 self.group_size = 16
1571
1572 # Select experts implementation.
1573 self.nvfp4_backend, self.experts_cls = select_nvfp4_moe_backend(
1574 config=self.moe,
1478 - weight_key=kNvfp4Static,
1479 - activation_key=kNvfp4Dynamic,
1575 + weight_key=self.weight_quant_key,
1576 + activation_key=self.activation_quant_key,
1577 )
1578
1579 def create_weights(
vllm/model_executor/layers/quantization/quark/schemes/quark_nvfp4.py
+10
@@ -14,6 +14,11 @@ from vllm.model_executor.kernels.linear.nvfp4.emulation import (
14 from vllm.model_executor.layers.quantization.quark.schemes.quark_scheme import (
15 QuarkScheme,
16 )
17 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
18 + QuantKey,
19 + kNvfp4Dynamic,
20 + kNvfp4Static,
21 +)
22 from vllm.model_executor.parameter import (
23 GroupQuantScaleParameter,
24 ModelWeightParameter,
@@ -36,9 +41,14 @@ class QuarkNVFP4(QuarkScheme):
41 - input_scale_2: bfloat16/float32, scalar (global input scale)
42 """
43
44 + supported_activation_quant_keys = [kNvfp4Dynamic]
45 + supported_weight_quant_keys = [kNvfp4Static]
46 +
47 def __init__(
48 self,
49 + activation_quant_key: QuantKey | None,
50 ):
51 + super().__init__(kNvfp4Static, activation_quant_key)
52 self.kernel = init_nvfp4_linear_kernel()
53 self.group_size = 16
54
vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py
+18 -44
@@ -3,7 +3,6 @@
3
4 from collections.abc import Callable
5 from fractions import Fraction
6 -from typing import Any
6
7 import torch
8
@@ -15,15 +14,14 @@ from vllm.model_executor.kernels.linear import (
14 init_mxfp6_linear_kernel,
15 )
16 from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
17 + _ACTIVATION_QUANT_KEY_MAP,
18 + _WEIGHT_QUANT_KEY_MAP,
19 OCP_MX_BLOCK_SIZE,
20 )
21 from vllm.model_executor.layers.quantization.utils.quant_utils import (
22 QuantKey,
22 - kMxfp4Dynamic,
23 kMxfp4Static,
24 - kMxfp6E2M3Dynamic,
24 kMxfp6E2M3Static,
26 - kMxfp6E3M2Dynamic,
25 kMxfp6E3M2Static,
26 )
27 from vllm.model_executor.parameter import (
@@ -38,45 +36,32 @@ from .quark_scheme import QuarkScheme
36
37 logger = init_logger(__name__)
38
41 -_WEIGHT_QUANT_KEY_MAP: dict[str, QuantKey] = {
42 - "mxfp4": kMxfp4Static,
43 - "mxfp6_e3m2": kMxfp6E3M2Static,
44 - "mxfp6_e2m3": kMxfp6E2M3Static,
45 -}
46 -
47 -_ACTIVATION_QUANT_KEY_MAP: dict[str, QuantKey] = {
48 - "mxfp4": kMxfp4Dynamic,
49 - "mxfp6_e3m2": kMxfp6E3M2Dynamic,
50 - "mxfp6_e2m3": kMxfp6E2M3Dynamic,
51 -}
52 -
39
40 class QuarkOCP_MX(QuarkScheme):
41 ocp_mx_linear: MxFp6LinearKernel | MxFp4LinearKernel
42 + supported_activation_quant_keys = [*_ACTIVATION_QUANT_KEY_MAP.values(), None]
43 + supported_weight_quant_keys = [*_WEIGHT_QUANT_KEY_MAP.values()]
44
45 def __init__(
46 self,
59 - weight_quant_spec: dict[str, Any],
60 - input_quant_spec: dict[str, Any] | None,
47 + weight_quant_key: QuantKey,
48 + activation_quant_key: QuantKey | None,
49 dynamic_mxfp4_quant: bool = False,
50 ):
51 + super().__init__(weight_quant_key, activation_quant_key)
52 self.dynamic_mxfp4_quant = dynamic_mxfp4_quant
64 - self.weight_dtype = weight_quant_spec["dtype"].replace("fp", "mxfp")
65 - self.input_dtype: str | None = None
66 - if input_quant_spec is not None:
67 - self.input_dtype = input_quant_spec["dtype"].replace("fp", "mxfp")
68 -
69 - if self.input_dtype not in [None, *_ACTIVATION_QUANT_KEY_MAP]:
70 - raise ValueError(
71 - f"Unsupported input_dtype={self.input_dtype} for QuarkOCP_MX. "
72 - f"Supported activation dtypes are {_ACTIVATION_QUANT_KEY_MAP.keys()}, "
73 - "or None for weight-only quantization."
53 + self.weight_dtype = next(
54 + dtype
55 + for dtype, quant_key in _WEIGHT_QUANT_KEY_MAP.items()
56 + if quant_key == weight_quant_key
57 + )
58 + self.input_dtype = (
59 + next(
60 + dtype
61 + for dtype, quant_key in _ACTIVATION_QUANT_KEY_MAP.items()
62 + if quant_key == activation_quant_key
63 )
75 -
76 - self.weight_quant_key = _WEIGHT_QUANT_KEY_MAP[self.weight_dtype]
77 - self.activation_quant_key = (
78 - _ACTIVATION_QUANT_KEY_MAP[self.input_dtype]
79 - if self.input_dtype is not None
64 + if activation_quant_key is not None
65 else None
66 )
67
@@ -85,17 +70,6 @@ class QuarkOCP_MX(QuarkScheme):
70 else:
71 self.packed_factor = Fraction(numerator=8, denominator=6)
72
88 - if input_quant_spec is None:
89 - self.static_input_scales = False
90 - else:
91 - self.static_input_scales = not input_quant_spec.get("is_dynamic")
92 -
93 - if self.static_input_scales:
94 - raise NotImplementedError(
95 - "QuarkOCP_MX with static input scales is currently not "
96 - "implemented. Please open an issue."
97 - )
98 -
73 if not current_platform.supports_mx():
74 logger.warning_once(
75 "The current platform does not support native MXFP4/MXFP6 "
vllm/model_executor/layers/quantization/quark/schemes/quark_scheme.py
+19
@@ -5,6 +5,8 @@ from abc import ABC, abstractmethod
5
6 import torch
7
8 +from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
9 +
10 __all__ = ["QuarkScheme"]
11
12
@@ -14,6 +16,23 @@ class QuarkScheme(ABC):
16 of different quantization schemes supported by Quark.
17 """
18
19 + supported_activation_quant_keys: list[QuantKey | None] = []
20 + supported_weight_quant_keys: list[QuantKey] = []
21 +
22 + def __init__(
23 + self,
24 + weight_quant_key: QuantKey,
25 + activation_quant_key: QuantKey | None,
26 + ):
27 + if activation_quant_key not in self.supported_activation_quant_keys:
28 + raise ValueError(
29 + f"Unsupported activation quant key: {activation_quant_key}"
30 + )
31 + if weight_quant_key not in self.supported_weight_quant_keys:
32 + raise ValueError(f"Unsupported weight quant key: {weight_quant_key}")
33 + self.activation_quant_key = activation_quant_key
34 + self.weight_quant_key = weight_quant_key
35 +
36 @classmethod
37 @abstractmethod
38 def get_min_capability(cls) -> int:
vllm/model_executor/layers/quantization/quark/schemes/quark_w4a8_mxfp4_fp8.py
+27 -38
@@ -3,7 +3,6 @@
3
4 from collections.abc import Callable
5 from fractions import Fraction
6 -from typing import Any
6
7 import torch
8 import torch.nn.functional as F
@@ -11,7 +10,10 @@ import torch.nn.functional as F
10 from vllm._aiter_ops import is_aiter_found_and_supported, rocm_aiter_ops
11 from vllm.logger import init_logger
12 from vllm.model_executor.layers.quantization.utils.quant_utils import (
13 + QuantKey,
14 get_fp8_min_max,
15 + kFp8StaticTensorSym,
16 + kMxfp4Static,
17 )
18 from vllm.model_executor.parameter import (
19 GroupQuantScaleParameter,
@@ -38,39 +40,28 @@ class QuarkW4A8_MXFP4_FP8(QuarkScheme):
40 Uses the AITER Triton kernel and falls back to emulation if AITER not available.
41 """
42
43 + supported_activation_quant_keys = [kFp8StaticTensorSym]
44 + supported_weight_quant_keys = [kMxfp4Static]
45 +
46 def __init__(
47 self,
43 - weight_quant_spec: dict[str, Any],
44 - input_quant_spec: dict[str, Any],
48 + activation_quant_key: QuantKey | None,
49 ):
50 + super().__init__(kMxfp4Static, activation_quant_key)
51 self.out_dtype = None
52
53 self.weight_dtype = "mxfp4"
54 self.packed_factor: Fraction = Fraction(2, 1) # 2 FP4 values per byte
55 self.weight_block_size = OCP_MX_BLOCK_SIZE
56
52 - self.is_static_input_scheme = not input_quant_spec.get("is_dynamic")
53 - self.input_qscheme = input_quant_spec.get("qscheme") # "per_tensor"
54 -
57 self.fp8_min, self.fp8_max = get_fp8_min_max()
58 self.fp8_dtype = current_platform.fp8_dtype()
59
58 - if not self.is_static_input_scheme:
59 - raise NotImplementedError(
60 - "Dynamic FP8 activation quantization is not yet supported "
61 - "for W4A8. The current implementation expects static per-tensor "
62 - "FP8 scales stored in the checkpoint."
63 - )
64 -
60 kernel_supported_gpu = False
61 if current_platform.is_rocm():
62 kernel_supported_gpu = current_platform.supports_mx()
63
69 - self.use_aiter_kernel = (
70 - is_aiter_found_and_supported()
71 - and self.is_static_input_scheme
72 - and kernel_supported_gpu
73 - )
64 + self.use_aiter_kernel = is_aiter_found_and_supported() and kernel_supported_gpu
65
66 if not self.use_aiter_kernel:
67 logger.warning_once(
@@ -128,17 +119,16 @@ class QuarkW4A8_MXFP4_FP8(QuarkScheme):
119 layer.register_parameter("weight_scale", weight_scale)
120
121 # INPUT SCALE (FP8 per-tensor static scale)
131 - if self.is_static_input_scheme:
132 - input_scale = PerTensorScaleParameter(
133 - data=torch.empty(
134 - len(output_partition_sizes),
135 - dtype=torch.float32,
136 - ),
137 - weight_loader=weight_loader,
138 - )
139 - # Initialize to avoid NaN
140 - input_scale[:] = torch.finfo(torch.float32).min
141 - layer.register_parameter("input_scale", input_scale)
122 + input_scale = PerTensorScaleParameter(
123 + data=torch.empty(
124 + len(output_partition_sizes),
125 + dtype=torch.float32,
126 + ),
127 + weight_loader=weight_loader,
128 + )
129 + # Initialize to avoid NaN
130 + input_scale[:] = torch.finfo(torch.float32).min
131 + layer.register_parameter("input_scale", input_scale)
132
133 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
134 # Ensuring weights & scales are non-trainable
@@ -147,16 +137,15 @@ class QuarkW4A8_MXFP4_FP8(QuarkScheme):
137 layer.weight_scale.data, requires_grad=False
138 )
139
150 - if self.is_static_input_scheme:
151 - input_scale = layer.input_scale.data
152 - # For fused modules (QKV), take the max scale
153 - if input_scale.numel() != 1:
154 - input_scale = input_scale.max()
140 + input_scale = layer.input_scale.data
141 + # For fused modules (QKV), take the max scale
142 + if input_scale.numel() != 1:
143 + input_scale = input_scale.max()
144
156 - layer.input_scale = torch.nn.Parameter(
157 - torch.tensor(input_scale, dtype=torch.float32),
158 - requires_grad=False,
159 - )
145 + layer.input_scale = torch.nn.Parameter(
146 + torch.tensor(input_scale, dtype=torch.float32),
147 + requires_grad=False,
148 + )
149
150 def apply_weights(
151 self,
vllm/model_executor/layers/quantization/quark/schemes/quark_w8a8_fp8.py
+41 -44
@@ -2,7 +2,6 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Callable
5 -from typing import Any, cast
5
6 import torch
7 from torch.nn import Parameter
@@ -18,8 +17,11 @@ from vllm.model_executor.layers.quantization.utils.fp8_utils import (
17 )
18 from vllm.model_executor.layers.quantization.utils.quant_utils import (
19 GroupShape,
20 + QuantKey,
21 kFp8Dynamic128Sym,
22 + kFp8DynamicTensorSym,
23 kFp8DynamicTokenSym,
24 + kFp8Static128BlockE8M0Sym,
25 kFp8Static128BlockSym,
26 kFp8StaticChannelSym,
27 kFp8StaticTensorSym,
@@ -42,36 +44,26 @@ logger = init_logger(__name__)
44
45
46 class QuarkW8A8Fp8(QuarkScheme):
47 + supported_activation_quant_keys: list[QuantKey | None] = [
48 + kFp8DynamicTensorSym,
49 + kFp8DynamicTokenSym,
50 + kFp8StaticTensorSym,
51 + ]
52 + supported_weight_quant_keys: list[QuantKey] = [
53 + kFp8StaticChannelSym,
54 + kFp8StaticTensorSym,
55 + ]
56 +
57 def __init__(
46 - self, weight_config: dict[str, Any], input_config: dict[str, Any] | None
58 + self,
59 + weight_quant_key: QuantKey,
60 + activation_quant_key: QuantKey | None,
61 ):
48 - self.weight_qscheme = cast(str, weight_config.get("qscheme"))
49 - self.is_static_input_scheme: bool = False
50 - self.input_qscheme: str | None = None
51 - if input_config is not None:
52 - self.is_static_input_scheme = not cast(bool, input_config.get("is_dynamic"))
53 - self.input_qscheme = cast(str, input_config.get("qscheme"))
54 -
55 - if self.weight_qscheme == "per_block":
56 - raise ValueError(
57 - "Quark W8A8 FP8 per-block weight quantization should use "
58 - "QuarkW8A8Fp8PerBlock."
59 - )
60 - per_token_activation = (
61 - not self.is_static_input_scheme and self.input_qscheme == "per_channel"
62 - )
63 - per_channel_weight = self.weight_qscheme == "per_channel"
64 -
65 - self.activation_quant_key = (
66 - kFp8DynamicTokenSym if per_token_activation else kFp8StaticTensorSym
67 - )
68 - # A per-output-channel weight scale is one fp32 value per weight row
69 - # (length N). Tag it as ``GroupShape.PER_CHANNEL`` to match the
70 - # canonical compressed-tensors CHANNEL strategy, so kernel selection
71 - # (e.g. AITER's pre-shuffled FP8 GEMM) treats it uniformly.
72 - self.weight_quant_key = (
73 - kFp8StaticChannelSym if per_channel_weight else kFp8StaticTensorSym
62 + super().__init__(weight_quant_key, activation_quant_key)
63 + self.weight_qscheme = (
64 + "per_channel" if weight_quant_key == kFp8StaticChannelSym else "per_tensor"
65 )
66 + self.is_static_input_scheme = activation_quant_key == kFp8StaticTensorSym
67 self.out_dtype = torch.get_default_dtype()
68 self.input_dtype = get_current_vllm_config().model_config.dtype
69
@@ -122,6 +114,8 @@ class QuarkW8A8Fp8(QuarkScheme):
114 layer.input_scale = Parameter(input_scale, requires_grad=False)
115 else:
116 weight_scale = layer.weight_scale.data
117 +
118 + assert self.activation_quant_key is not None
119 if self.activation_quant_key.scale.group_shape == GroupShape.PER_TOKEN:
120 weight_scale = weight_scale.view(-1, 1)
121 layer.weight = Parameter(weight.t(), requires_grad=False)
@@ -191,6 +185,7 @@ class QuarkW8A8Fp8(QuarkScheme):
185 input_scale[:] = torch.finfo(torch.float32).min
186 layer.register_parameter("input_scale", input_scale)
187
188 + assert self.activation_quant_key is not None
189 self.fp8_linear = init_fp8_linear_kernel(
190 activation_quant_key=self.activation_quant_key,
191 weight_quant_key=self.weight_quant_key,
@@ -210,22 +205,23 @@ class QuarkW8A8Fp8(QuarkScheme):
205
206
207 class QuarkW8A8Fp8PerBlock(QuarkScheme):
208 + supported_activation_quant_keys = [kFp8Dynamic128Sym]
209 + supported_weight_quant_keys = [
210 + kFp8Static128BlockSym,
211 + kFp8Static128BlockE8M0Sym,
212 + ]
213 +
214 def __init__(
214 - self, weight_config: dict[str, Any], input_config: dict[str, Any] | None
215 + self,
216 + weight_quant_key: QuantKey,
217 + activation_quant_key: QuantKey | None,
218 ):
216 - self.weight_qscheme = cast(str, weight_config.get("qscheme"))
217 - if self.weight_qscheme != "per_block":
218 - raise ValueError("QuarkW8A8Fp8PerBlock requires per-block weights.")
219 - block_size = weight_config.get("block_size")
220 - if not block_size:
221 - raise ValueError(
222 - "Quark W8A8 FP8 per-block weight quantization requires "
223 - "`block_size` in the weight quantization config."
224 - )
225 - self.weight_config = weight_config
226 - self.weight_block_size = list(block_size)
227 - self.activation_quant_key = kFp8Dynamic128Sym
228 - self.weight_quant_key = kFp8Static128BlockSym
219 + super().__init__(weight_quant_key, activation_quant_key)
220 + self.weight_scale_dtype = weight_quant_key.scale.dtype
221 + if self.weight_quant_key == kFp8Static128BlockE8M0Sym:
222 + # TODO: oracle needs to support kFp8Static128BlockE8M0Sym.
223 + self.weight_quant_key = kFp8Static128BlockSym
224 + self.weight_block_size = list(weight_quant_key.scale.group_shape)
225 self.out_dtype = torch.get_default_dtype()
226 self.input_dtype = get_current_vllm_config().model_config.dtype
227
@@ -272,8 +268,8 @@ class QuarkW8A8Fp8PerBlock(QuarkScheme):
268 layer.register_parameter("weight", weight)
269
270 scale_dtype = (
275 - torch.float8_e8m0fnu
276 - if self.weight_config.get("scale_type") == "float8_e8m0fnu"
271 + self.weight_scale_dtype
272 + if self.weight_scale_dtype != torch.float32
273 else None
274 )
275 weight_scale = create_fp8_scale_parameter(
@@ -287,6 +283,7 @@ class QuarkW8A8Fp8PerBlock(QuarkScheme):
283 # DeepSeek V4 weight mappers route checkpoint ".scale" tensors here.
284 layer.register_parameter("weight_scale_inv", weight_scale)
285
286 + assert self.activation_quant_key is not None
287 self.fp8_linear = init_fp8_linear_kernel(
288 activation_quant_key=self.activation_quant_key,
289 weight_quant_key=self.weight_quant_key,
vllm/model_executor/layers/quantization/quark/schemes/quark_w8a8_int8.py
+33 -6
@@ -10,6 +10,16 @@ from vllm.model_executor.kernels.linear import (
10 init_int8_linear_kernel,
11 )
12 from vllm.model_executor.layers.quantization.quark.schemes import QuarkScheme
13 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
14 + QuantKey,
15 + kInt8DynamicTensorAsym,
16 + kInt8DynamicTensorSym,
17 + kInt8DynamicTokenAsym,
18 + kInt8DynamicTokenSym,
19 + kInt8StaticChannelSym,
20 + kInt8StaticTensorAsym,
21 + kInt8StaticTensorSym,
22 +)
23 from vllm.model_executor.parameter import (
24 BasevLLMParameter,
25 ChannelQuantScaleParameter,
@@ -21,15 +31,32 @@ logger = init_logger(__name__)
31
32
33 class QuarkW8A8Int8(QuarkScheme):
34 + supported_activation_quant_keys = [
35 + kInt8StaticTensorSym,
36 + kInt8StaticTensorAsym,
37 + kInt8DynamicTensorSym,
38 + kInt8DynamicTensorAsym,
39 + kInt8DynamicTokenSym,
40 + kInt8DynamicTokenAsym,
41 + ]
42 + supported_weight_quant_keys = [
43 + kInt8StaticChannelSym,
44 + kInt8StaticTensorSym,
45 + ]
46 +
47 def __init__(
48 self,
26 - qscheme: str,
27 - is_static_input_scheme: bool | None,
28 - input_symmetric: bool | None,
49 + weight_quant_key: QuantKey,
50 + activation_quant_key: QuantKey | None,
51 ):
30 - self.qscheme = qscheme
31 - self.is_static_input_scheme = is_static_input_scheme
32 - self.input_symmetric = input_symmetric
52 + super().__init__(weight_quant_key, activation_quant_key)
53 + self.qscheme = (
54 + "per_channel" if weight_quant_key == kInt8StaticChannelSym else "per_tensor"
55 + )
56 +
57 + assert activation_quant_key is not None
58 + self.is_static_input_scheme = activation_quant_key.scale.static
59 + self.input_symmetric = activation_quant_key.symmetric
60
61 @classmethod
62 def get_min_capability(cls) -> int:
vllm/model_executor/layers/quantization/quark/utils.py
+2
@@ -8,6 +8,8 @@ from typing import Any
8 import regex as re
9 import torch
10
11 +QuarkQTensorHint = dict[str, Any] | list[dict[str, Any]] | None
12 +
13
14 def deep_compare(dict1: Any, dict2: Any) -> bool:
15 if type(dict1) is not type(dict2):
vllm/model_executor/layers/quantization/utils/ocp_mx_utils.py
+21
@@ -3,11 +3,32 @@
3 from enum import Enum
4
5 from vllm.logger import init_logger
6 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
7 + QuantKey,
8 + kMxfp4Dynamic,
9 + kMxfp4Static,
10 + kMxfp6E2M3Dynamic,
11 + kMxfp6E2M3Static,
12 + kMxfp6E3M2Dynamic,
13 + kMxfp6E3M2Static,
14 +)
15
16 logger = init_logger(__name__)
17
18 OCP_MX_BLOCK_SIZE = 32
19
20 +_WEIGHT_QUANT_KEY_MAP: dict[str, QuantKey] = {
21 + "mxfp4": kMxfp4Static,
22 + "mxfp6_e3m2": kMxfp6E3M2Static,
23 + "mxfp6_e2m3": kMxfp6E2M3Static,
24 +}
25 +
26 +_ACTIVATION_QUANT_KEY_MAP: dict[str, QuantKey] = {
27 + "mxfp4": kMxfp4Dynamic,
28 + "mxfp6_e3m2": kMxfp6E3M2Dynamic,
29 + "mxfp6_e2m3": kMxfp6E2M3Dynamic,
30 +}
31 +
32 OCP_MX_DTYPES = {
33 "mxfp4",
34 "mxfp6_e3m2",
vllm/model_executor/layers/quantization/utils/quant_utils.py
+8
@@ -222,6 +222,11 @@ kFp8Dynamic128Sym = QuantKey(FP8_DTYPE, kDynamic128Scale, symmetric=True)
222
223 kStatic128BlockScale = ScaleDesc(torch.float32, True, GroupShape(128, 128))
224 kFp8Static128BlockSym = QuantKey(FP8_DTYPE, kStatic128BlockScale, symmetric=True)
225 +kFp8Static128BlockE8M0Sym = QuantKey(
226 + FP8_DTYPE,
227 + ScaleDesc(torch.float8_e8m0fnu, True, GroupShape(128, 128)),
228 + symmetric=True,
229 +)
230
231 kMxfp8StaticScale = ScaleDesc(torch.uint8, True, GroupShape(1, 32))
232 kMxfp8Static = QuantKey(FP8_DTYPE, kMxfp8StaticScale, symmetric=True)
@@ -284,6 +289,9 @@ kInt8StaticChannelSym = QuantKey(torch.int8, kStaticChannelScale, symmetric=True
289 kInt8DynamicTokenSym = QuantKey(torch.int8, kDynamicTokenScale, symmetric=True)
290 kInt8StaticTensorSym = QuantKey(torch.int8, kStaticTensorScale, symmetric=True)
291 kInt8DynamicTensorSym = QuantKey(torch.int8, kDynamicTensorScale, symmetric=True)
292 +kInt8DynamicTokenAsym = QuantKey(torch.int8, kDynamicTokenScale, symmetric=False)
293 +kInt8StaticTensorAsym = QuantKey(torch.int8, kStaticTensorScale, symmetric=False)
294 +kInt8DynamicTensorAsym = QuantKey(torch.int8, kDynamicTensorScale, symmetric=False)
295
296 # INT4 W4A8 quantization keys
297