[Bugfix][ROCm] AMD-Quark mixed-precision DeepSeek-V4.1 support (#57071)
Signed-off-by: Xiao Yu <xiao.yu.dc@outlook.com> Co-authored-by: Cursor <cursoragent@cursor.com>
xiao-llm committed
Sep 25, 2026 at 21:51 UTC
5840d95284fe3ae9f56f2366074f58d277d69ad8
11 files changed
+647
-102
tests/quantization/test_fp8.py
+79
@@ -228,6 +228,85 @@ def test_deepseek_v41_vl_mapper_routes_linear_scales(
228
]
229
230
231
+def test_deepseek_v41_vl_exposes_quant_mappings_on_class():
232
+ """``configure_quant_config`` reads both mappings off the class, before
233
+ ``__init__`` builds the instance mapper. Without them a Quark config's
234
+ per-layer keys never match this wrapper's ``language_model.``-rooted
235
+ prefixes and every attention shard falls back to the global spec."""
236
+ from vllm.models.deepseek_v41.amd.vl_model import DeepseekV41ForCausalLM
237
+
238
+ assert "fused_wqa_wkv" in DeepseekV41ForCausalLM.packed_modules_mapping
239
+ mapper = DeepseekV41ForCausalLM.hf_to_vllm_mapper.get_rename_mapper()
240
+ assert mapper.apply_list(["layers.0.attn.wq_a"]) == [
241
+ "language_model.model.layers.0.attn.wq_a"
242
+ ]
243
+
244
+
245
+def test_deepseek_v41_engram_scale_accepts_quark_name():
246
+ """Quark exports name the engram scale ``embed.weight_scale``; the
247
+ ``\\.scale$`` rules only match a literal ``.scale`` suffix, so without an
248
+ explicit rule the tensor is never routed and loading fails."""
249
+ from vllm.models.deepseek_v41.amd import model as model_module
250
+
251
+ mapper = model_module._make_deepseek_v4_weights_mapper("fp4", "weight_scale")
252
+ weight = torch.empty(0)
253
+ mapped = [
254
+ name
255
+ for name, _ in mapper.apply(
256
+ [
257
+ ("layers.1.engram.embed.weight_scale", weight),
258
+ ("layers.1.engram.embed.scale", weight),
259
+ ]
260
+ )
261
+ ]
262
+ assert mapped == [
263
+ "model.layers.1.engram.embed_tokens.weight_scale_inv",
264
+ "model.layers.1.engram.embed_tokens.weight_scale_inv",
265
+ ]
266
+
267
+
268
+def test_deepseek_v41_declines_quark_configs():
269
+ """``from_config`` rewrites a Quark config into a single global FP8 scheme,
270
+ which would discard the per-layer specs of a mixed-precision export (MXFP4
271
+ experts + 2-D block MXFP8 attention in DeepSeek-V4.1-Flash). QuarkConfig
272
+ knows how to dispatch each scheme, so it must handle every Quark export."""
273
+ from vllm.models.deepseek_v41.quant_config import DeepseekV4FP8Config
274
+
275
+ hf_config = SimpleNamespace(model_type="deepseek_v41")
276
+ mxfp4_global = {
277
+ "global_quant_config": {
278
+ "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32}
279
+ },
280
+ "quant_method": "quark",
281
+ }
282
+
283
+ assert (
284
+ DeepseekV4FP8Config.override_quantization_method(
285
+ mxfp4_global, None, hf_config=hf_config
286
+ )
287
+ is None
288
+ )
289
+
290
+ mixed = {
291
+ **mxfp4_global,
292
+ "layer_quant_config": {
293
+ "layers.0.attn.wkv": {
294
+ "weight": {
295
+ "dtype": "fp8_e4m3",
296
+ "qscheme": "per_block",
297
+ "block_size": [32, 32],
298
+ }
299
+ }
300
+ },
301
+ }
302
+ assert (
303
+ DeepseekV4FP8Config.override_quantization_method(
304
+ mixed, None, hf_config=hf_config
305
+ )
306
+ is None
307
+ )
308
+
309
+
310
@pytest.mark.skipif(not current_platform.is_cuda(), reason="DeepGEMM requires CUDA")
311
@pytest.mark.parametrize("scale_dtype", [torch.uint8, torch.float8_e8m0fnu])
312
@pytest.mark.parametrize(
tests/quantization/test_quark.py
+282
@@ -909,6 +909,184 @@ def test_quant_method_dispatch_target(case):
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
+ # The OCP MX MoE constructor should fail loudly for MXFP8.
1085
+ fake_moe_config = MagicMock()
1086
+ with pytest.raises(ValueError, match="MXFP8 experts are not supported"):
1087
+ QuarkOCP_MX_MoEMethod(fake_moe_config, wk, ak)
1088
+
1089
+
1090
@pytest.mark.parametrize(
1091
("weight", "input_tensors"),
1092
[
@@ -2313,3 +2491,107 @@ def test_quark_int4_canonicalizes_pack_for_kernel_layout(pack_method, symmetric)
2491
)
2492
2493
assert torch.equal(actual, expected)
2494
+
2495
+
2496
+# ---------------------------------------------------------------------------
2497
+# override_quantization_method: Quark always delegates to QuarkConfig
2498
+# ---------------------------------------------------------------------------
2499
+
2500
+_QUARK_MXFP4_CFG = {
2501
+ "quant_method": "quark",
2502
+ "global_quant_config": {
2503
+ "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32},
2504
+ },
2505
+}
2506
+
2507
+_QUARK_MXFP8_CFG = {
2508
+ "quant_method": "quark",
2509
+ "global_quant_config": {
2510
+ "weight": {
2511
+ "dtype": "fp8_e4m3",
2512
+ "qscheme": "per_block",
2513
+ "block_size": [32, 32],
2514
+ "scale_type": "float8_e8m0fnu",
2515
+ "symmetric": True,
2516
+ },
2517
+ },
2518
+}
2519
+
2520
+_QUARK_MIXED_CFG = {
2521
+ "quant_method": "quark",
2522
+ "global_quant_config": {
2523
+ "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32},
2524
+ },
2525
+ "layer_quant_config": {"layers.0.attn": {"weight": {"dtype": "fp8_e4m3"}}},
2526
+}
2527
+
2528
+_FP8_CFG = {"quant_method": "fp8"}
2529
+
2530
+_DSV4_FP8_CFG = {"quant_method": "deepseek_v4_fp8"}
2531
+
2532
+
2533
+@pytest.mark.parametrize(
2534
+ "quant_config_cls",
2535
+ [
2536
+ "deepseek_v41",
2537
+ "deepseek_v4",
2538
+ ],
2539
+)
2540
+@pytest.mark.parametrize(
2541
+ "hf_quant_cfg, expect_none",
2542
+ [
2543
+ (_QUARK_MXFP4_CFG, True),
2544
+ (_QUARK_MXFP8_CFG, True),
2545
+ (_QUARK_MIXED_CFG, True),
2546
+ (_FP8_CFG, False),
2547
+ (_DSV4_FP8_CFG, False),
2548
+ ],
2549
+ ids=[
2550
+ "quark-mxfp4",
2551
+ "quark-mxfp8",
2552
+ "quark-mixed",
2553
+ "native-fp8",
2554
+ "deepseek-v4-fp8",
2555
+ ],
2556
+)
2557
+def test_quark_override_delegates_to_quark_config(
2558
+ quant_config_cls, hf_quant_cfg, expect_none
2559
+):
2560
+ """Any quant_method=='quark' must return None (delegate to QuarkConfig).
2561
+
2562
+ Native FP8 configs should still be claimed by the model-specific config.
2563
+ """
2564
+ import importlib
2565
+
2566
+ if quant_config_cls == "deepseek_v41":
2567
+ # Module was renamed from deepseek_v4_1 to deepseek_v41 upstream.
2568
+ for mod_name in (
2569
+ "vllm.models.deepseek_v41.quant_config",
2570
+ "vllm.models.deepseek_v4_1.quant_config",
2571
+ ):
2572
+ try:
2573
+ mod = importlib.import_module(mod_name)
2574
+ break
2575
+ except ModuleNotFoundError:
2576
+ continue
2577
+ else:
2578
+ pytest.skip("deepseek_v41 quant_config not found")
2579
+ model_type = "deepseek_v41"
2580
+ else:
2581
+ mod = importlib.import_module("vllm.models.deepseek_v4.quant_config")
2582
+ model_type = "deepseek_v4"
2583
+
2584
+ config_cls = mod.DeepseekV4FP8Config
2585
+ hf_config = SimpleNamespace(model_type=model_type)
2586
+ result = config_cls.override_quantization_method(
2587
+ hf_quant_cfg, None, hf_config=hf_config
2588
+ )
2589
+
2590
+ if expect_none:
2591
+ assert result is None, (
2592
+ f"Quark config should delegate to QuarkConfig (return None), got {result!r}"
2593
+ )
2594
+ else:
2595
+ assert result == "deepseek_v4_fp8", (
2596
+ f"Native FP8 should be claimed by DeepseekV4FP8Config, got {result!r}"
2597
+ )
vllm/model_executor/layers/quantization/quark/quark.py
+41
-42
@@ -45,6 +45,8 @@ from vllm.model_executor.layers.quantization.quark.utils import (
45
from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
46
_ACTIVATION_QUANT_KEY_MAP,
47
_WEIGHT_QUANT_KEY_MAP,
48
+ OCP_MX_BLOCK_SIZE,
49
+ ocp_mx_weight_dtype_and_rows,
50
)
51
from vllm.model_executor.layers.quantization.utils.quant_utils import (
52
QuantKey,
@@ -158,6 +160,20 @@ class QuarkConfig(QuantizationConfig):
160
161
self.quant_config = quant_config_with_hf_to_vllm_mapper
162
163
+ def _scale_block_rows(self, prefix: str, layer_type: type[torch.nn.Module]) -> int:
164
+ """How many weight rows one checkpoint scale row covers.
165
+
166
+ 1 for the canonical 1-D per-group MX spelling; ``block_size[0]`` for
167
+ MXFP8 exports that spell the scale 2-D (DeepSeek-V4.1 uses 32). The
168
+ scheme cannot work this out itself: it has no handle on the layer name,
169
+ and tensor-parallel sharding narrows the loaded scale so the ratio is
170
+ not recoverable from shapes either.
171
+ """
172
+ config = self._find_matched_config(prefix, layer_type)
173
+ weight = self._unwrap_single_quant_config(config.get("weight"))
174
+ normalized = ocp_mx_weight_dtype_and_rows(weight)
175
+ return normalized[1] if normalized is not None else 1
176
+
177
def get_quant_method(
178
self, layer: torch.nn.Module, prefix: str
179
) -> "QuantizeMethodBase | None":
@@ -175,6 +191,11 @@ class QuarkConfig(QuantizationConfig):
191
scheme_cls,
192
weight_quant_key=weight_quant_key,
193
activation_quant_key=activation_quant_key,
194
+ scale_block_rows=(
195
+ self._scale_block_rows(prefix, type(layer))
196
+ if scheme_cls is QuarkOCP_MX
197
+ else 1
198
+ ),
199
weight_config=self._find_matched_config(prefix, type(layer)).get(
200
"weight"
201
),
@@ -640,52 +661,18 @@ class QuarkConfig(QuantizationConfig):
661
The rationale for checking only the weight type is that
662
the model loading concept and process primarily concerns the weights themselves.
663
"""
643
- # Confirm weights quantized.
644
- if not isinstance(weight_quant, dict):
645
- logger.debug(
646
- "Quark model's weight quantization is incompatible with OCP_MX format: "
647
- "weight_quant is not a dictionary."
648
- )
649
- return QuantKeyMatch(False, None, None)
650
-
651
- # Input and weight qscheme needs to be per group.
652
- if weight_quant.get("qscheme") != "per_group":
653
- logger.debug(
654
- "Quark model's weight quantization is incompatible with OCP MX format: "
655
- "weight is not per_group."
656
- )
657
- return QuantKeyMatch(False, None, None)
658
-
659
- # Input and weight group size needs to be 32.
660
- if weight_quant.get("group_size") != 32:
661
- logger.debug(
662
- "Quark model's weight quantization is incompatible with OCP MX format: "
663
- "group_size of weight is not 32."
664
- )
665
- return QuantKeyMatch(False, None, None)
666
-
667
- # Activations and weight scales need to be in e8m0 format.
668
- if weight_quant.get("scale_format") != "e8m0":
664
+ # Confirm weights quantized, and normalize the two OCP MX config
665
+ # dialects (1-D per_group, and MXFP8's 2-D per_block) to a dtype.
666
+ normalized = ocp_mx_weight_dtype_and_rows(weight_quant)
667
+ if normalized is None:
668
logger.debug(
670
- "Quark model's weight quantization is incompatible with OCP MX format: "
671
- "scale_format of weight is not e8m0."
669
+ "Quark model's weight quantization is incompatible with OCP MX "
670
+ "format: %s",
671
+ weight_quant,
672
)
673
return QuantKeyMatch(False, None, None)
674
675
- # Input and weight dtypes need to be any of fp4,
676
- # fp6_e3m2 or fp6_e3m2, possibly mixed.
677
- if weight_quant.get("dtype") not in {
678
- "fp4",
679
- "fp6_e3m2",
680
- "fp6_e2m3",
681
- }:
682
- logger.debug(
683
- "Quark model's weight quantization is incompatible with OCP MX format: "
684
- "dtype is not in {fp4, fp6_e3m2, fp6_e2m3}."
685
- )
686
- return QuantKeyMatch(False, None, None)
687
-
688
- weight_dtype = weight_quant["dtype"].replace("fp", "mxfp")
675
+ weight_dtype, _ = normalized
676
weight_quant_key = _WEIGHT_QUANT_KEY_MAP[weight_dtype]
677
if input_quant is None:
678
activation_quant_key = None
@@ -703,6 +690,14 @@ class QuarkConfig(QuantizationConfig):
690
"input scales."
691
)
692
return QuantKeyMatch(False, None, None)
693
+ elif (
694
+ input_quant["dtype"] == "fp8_e4m3"
695
+ and input_quant.get("qscheme") == "per_group"
696
+ and input_quant.get("group_size") == OCP_MX_BLOCK_SIZE
697
+ ):
698
+ # MXFP8 activations. Must be checked before the per-tensor FP8
699
+ # branch below, which would otherwise claim them.
700
+ activation_quant_key = _ACTIVATION_QUANT_KEY_MAP["mxfp8_e4m3"]
701
elif input_quant["dtype"] == "fp8_e4m3":
702
activation_quant_key = kFp8DynamicTensorSym
703
else:
@@ -903,6 +898,7 @@ class QuarkConfig(QuantizationConfig):
898
scheme_cls: type["QuarkScheme"],
899
weight_quant_key: QuantKey | None,
900
activation_quant_key: QuantKey | None,
901
+ scale_block_rows: int = 1,
902
weight_config: dict[str, Any] | None = None,
903
) -> "QuarkScheme":
904
"""Construct a Quark scheme selected by get_scheme_cls."""
@@ -942,6 +938,9 @@ class QuarkConfig(QuantizationConfig):
938
):
939
kwargs["weight_quant_key"] = weight_quant_key
940
941
+ if scheme_cls is QuarkOCP_MX:
942
+ kwargs["scale_block_rows"] = scale_block_rows
943
+
944
scheme = scheme_cls(**kwargs)
945
946
# Raise error if device does not support the scheme
vllm/model_executor/layers/quantization/quark/quark_moe.py
+10
-2
@@ -1587,7 +1587,9 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1587
raise ValueError(
1588
f"Unsupported OCP MX dtype combination for MoE: "
1589
f"input_dtype={self.input_dtype}, weight_dtype={self.weight_dtype}. "
1590
- f"Please check that the combination is supported in OCP_MX_Scheme."
1590
+ f"MXFP8 experts are not supported through the MoE path; only "
1591
+ f"the linear path handles MXFP8. Supported MoE weight dtypes "
1592
+ f"are mxfp4, mxfp6_e3m2, and mxfp6_e2m3."
1593
)
1594
1595
# TODO(bowenbao): refactor and introduce backends for other OCP MX schemes,
@@ -1676,10 +1678,16 @@ class QuarkOCP_MX_MoEMethod(QuarkMoEMethod):
1678
if quant_dtype == "mxfp4":
1679
assert dim % 2 == 0
1680
return dim // 2
1679
- else:
1681
+ elif quant_dtype in {"mxfp6_e3m2", "mxfp6_e2m3"}:
1682
# FP6 packs 4 * 6 = 24 bits on 3 bytes.
1683
assert (dim * 3) % 4 == 0
1684
return (dim * 3) // 4
1685
+ else:
1686
+ raise NotImplementedError(
1687
+ f"Unsupported quant_dtype in QuarkOCP_MX_MoEMethod."
1688
+ f"get_packed_dim: {quant_dtype}. MXFP8 is not supported for "
1689
+ f"MoE experts."
1690
+ )
1691
1692
def create_weights(
1693
self,
vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py
+119
-35
@@ -10,22 +10,31 @@ from vllm.logger import init_logger
10
from vllm.model_executor.kernels.linear import (
11
MxFp4LinearKernel,
12
MxFp6LinearKernel,
13
+ Mxfp8LinearKernel,
14
init_mxfp4_linear_kernel,
15
init_mxfp6_linear_kernel,
16
+ init_mxfp8_linear_kernel,
17
+)
18
+from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
19
+ MXFP8_SCALE_DTYPE,
20
+ MXFP8_VALUE_DTYPE,
21
)
22
from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
23
_ACTIVATION_QUANT_KEY_MAP,
24
_WEIGHT_QUANT_KEY_MAP,
25
OCP_MX_BLOCK_SIZE,
26
+ OCP_MX_UNPACKED_DTYPES,
27
)
28
from vllm.model_executor.layers.quantization.utils.quant_utils import (
29
QuantKey,
30
kMxfp4Static,
31
kMxfp6E2M3Static,
32
kMxfp6E3M2Static,
33
+ kMxfp8Static,
34
)
35
from vllm.model_executor.parameter import (
36
GroupQuantScaleParameter,
37
+ ModelWeightParameter,
38
PackedvLLMParameter,
39
)
40
from vllm.platforms import current_platform
@@ -36,7 +45,7 @@ logger = init_logger(__name__)
45
46
47
class QuarkOCP_MX(QuarkScheme):
39
- ocp_mx_linear: MxFp6LinearKernel | MxFp4LinearKernel
48
+ ocp_mx_linear: MxFp6LinearKernel | MxFp4LinearKernel | Mxfp8LinearKernel
49
supported_activation_quant_keys = [*_ACTIVATION_QUANT_KEY_MAP.values(), None]
50
supported_weight_quant_keys = [*_WEIGHT_QUANT_KEY_MAP.values()]
51
@@ -44,9 +53,10 @@ class QuarkOCP_MX(QuarkScheme):
53
self,
54
weight_quant_key: QuantKey,
55
activation_quant_key: QuantKey | None,
56
+ scale_block_rows: int = 1,
57
):
58
super().__init__(weight_quant_key, activation_quant_key)
49
-
59
+ self.scale_block_rows = scale_block_rows
60
self.weight_dtype = next(
61
dtype
62
for dtype, quant_key in _WEIGHT_QUANT_KEY_MAP.items()
@@ -62,11 +72,21 @@ class QuarkOCP_MX(QuarkScheme):
72
else None
73
)
74
65
- if self.weight_dtype == "mxfp4":
66
- self.packed_factor: int | Fraction = 2
75
+ self.is_unpacked = self.weight_dtype in OCP_MX_UNPACKED_DTYPES
76
+
77
+ if self.is_unpacked:
78
+ self.packed_factor: int | Fraction = 1
79
+ elif self.weight_dtype == "mxfp4":
80
+ self.packed_factor = 2
81
else:
82
self.packed_factor = Fraction(numerator=8, denominator=6)
83
84
+ if self.is_unpacked:
85
+ # MXFP8 runs on the shared kernel abstraction, which picks a native
86
+ # backend where one exists and emulates otherwise. The MXFP4/MXFP6
87
+ # warnings below do not apply.
88
+ return
89
+
90
if not current_platform.supports_mx():
91
logger.warning_once(
92
"The current platform does not support native MXFP4/MXFP6 "
@@ -88,7 +108,9 @@ class QuarkOCP_MX(QuarkScheme):
108
)
109
110
def get_packed_dim(self, dim: int, quant_dtype: str):
91
- if quant_dtype == "mxfp4":
111
+ if quant_dtype in OCP_MX_UNPACKED_DTYPES:
112
+ return dim
113
+ elif quant_dtype == "mxfp4":
114
assert dim % 2 == 0
115
return dim // 2
116
elif quant_dtype in {"mxfp6_e3m2", "mxfp6_e2m3"}:
@@ -102,6 +124,26 @@ class QuarkOCP_MX(QuarkScheme):
124
"open an issue."
125
)
126
127
+ def _expanding_scale_loader(self, weight_loader: Callable) -> Callable:
128
+ """Expand a 2-D block scale to one row per weight row.
129
+
130
+ A ``block_size=[R, 32]`` export carries one e8m0 value per R weight
131
+ rows; the kernels want one per row. At ``R == 1`` the layouts already
132
+ coincide, so the loader is passed through untouched.
133
+ """
134
+ if self.scale_block_rows == 1:
135
+ return weight_loader
136
+
137
+ rows = self.scale_block_rows
138
+
139
+ def expanding_loader(param, loaded_weight, *args, **kwargs):
140
+ loaded_weight = loaded_weight.view(torch.uint8).repeat_interleave(
141
+ rows, dim=0
142
+ )
143
+ return weight_loader(param, loaded_weight, *args, **kwargs)
144
+
145
+ return expanding_loader
146
+
147
@classmethod
148
def get_min_capability(cls) -> int:
149
return 70
@@ -130,36 +172,68 @@ class QuarkOCP_MX(QuarkScheme):
172
"tensor-parallel sharding for this layer."
173
)
174
133
- output_size_per_partition = sum(output_partition_sizes)
134
- layer.logical_widths = output_partition_sizes
135
-
136
- # WEIGHT
137
- weight = PackedvLLMParameter(
138
- data=torch.empty(
139
- output_size_per_partition,
140
- self.get_packed_dim(input_size_per_partition, self.weight_dtype),
141
- dtype=torch.uint8,
142
- ),
143
- input_dim=1,
144
- output_dim=0,
145
- packed_dim=1,
146
- packed_factor=self.packed_factor,
147
- weight_loader=weight_loader,
148
- )
149
- layer.register_parameter("weight", weight)
150
-
151
- # WEIGHT SCALE
152
- weight_scale = GroupQuantScaleParameter(
153
- data=torch.empty(
154
- output_size_per_partition,
155
- input_size_per_partition // OCP_MX_BLOCK_SIZE,
156
- dtype=torch.uint8,
157
- ),
158
- input_dim=1,
159
- output_dim=0,
160
- weight_loader=weight_loader,
161
- )
162
- layer.register_parameter("weight_scale", weight_scale)
175
+ if self.is_unpacked:
176
+ output_size_per_partition = sum(output_partition_sizes)
177
+ layer.logical_widths = output_partition_sizes
178
+ layer.input_size_per_partition = input_size_per_partition
179
+ layer.output_size_per_partition = output_size_per_partition
180
+ layer.params_dtype = params_dtype
181
+
182
+ weight = ModelWeightParameter(
183
+ data=torch.empty(
184
+ output_size_per_partition,
185
+ input_size_per_partition,
186
+ dtype=MXFP8_VALUE_DTYPE,
187
+ ),
188
+ input_dim=1,
189
+ output_dim=0,
190
+ weight_loader=weight_loader,
191
+ )
192
+ layer.register_parameter("weight", weight)
193
+
194
+ weight_scale = GroupQuantScaleParameter(
195
+ data=torch.empty(
196
+ output_size_per_partition,
197
+ input_size_per_partition // OCP_MX_BLOCK_SIZE,
198
+ dtype=MXFP8_SCALE_DTYPE,
199
+ ),
200
+ input_dim=1,
201
+ output_dim=0,
202
+ weight_loader=self._expanding_scale_loader(weight_loader),
203
+ )
204
+ layer.register_parameter("weight_scale", weight_scale)
205
+ layer.weight_block_size = [1, OCP_MX_BLOCK_SIZE]
206
+ else:
207
+ output_size_per_partition = sum(output_partition_sizes)
208
+ layer.logical_widths = output_partition_sizes
209
+
210
+ # WEIGHT
211
+ weight = PackedvLLMParameter(
212
+ data=torch.empty(
213
+ output_size_per_partition,
214
+ self.get_packed_dim(input_size_per_partition, self.weight_dtype),
215
+ dtype=torch.uint8,
216
+ ),
217
+ input_dim=1,
218
+ output_dim=0,
219
+ packed_dim=1,
220
+ packed_factor=self.packed_factor,
221
+ weight_loader=weight_loader,
222
+ )
223
+ layer.register_parameter("weight", weight)
224
+
225
+ # WEIGHT SCALE
226
+ weight_scale = GroupQuantScaleParameter(
227
+ data=torch.empty(
228
+ output_size_per_partition,
229
+ input_size_per_partition // OCP_MX_BLOCK_SIZE,
230
+ dtype=torch.uint8,
231
+ ),
232
+ input_dim=1,
233
+ output_dim=0,
234
+ weight_loader=weight_loader,
235
+ )
236
+ layer.register_parameter("weight_scale", weight_scale)
237
238
if self.weight_quant_key == kMxfp4Static:
239
self.ocp_mx_linear = init_mxfp4_linear_kernel(
@@ -170,6 +244,16 @@ class QuarkOCP_MX(QuarkScheme):
244
weight_quant_key=self.weight_quant_key,
245
activation_quant_key=self.activation_quant_key,
246
)
247
+ elif self.weight_quant_key == kMxfp8Static:
248
+ self.ocp_mx_linear = init_mxfp8_linear_kernel(
249
+ weight_shape=layer.weight.shape
250
+ )
251
+ else:
252
+ raise NotImplementedError(
253
+ "No OCP MX linear kernel for weight_quant_key="
254
+ f"{self.weight_quant_key}. Something is wrong, please open an "
255
+ "issue."
256
+ )
257
258
def apply_weights(
259
self,
vllm/model_executor/layers/quantization/utils/config_utils.py
+6
-1
@@ -44,7 +44,12 @@ def get_quark_ocp_mx_group_size(
44
return None
45
46
assert weight_quant is not None
47
- return int(weight_quant["group_size"])
47
+ group_size = weight_quant.get("group_size")
48
+ if group_size is None:
49
+ block_size = weight_quant.get("block_size")
50
+ if isinstance(block_size, list | tuple) and len(block_size) >= 2:
51
+ group_size = block_size[1]
52
+ return int(group_size) if group_size is not None else None
53
54
55
def is_shared_expert_quant_fse_compatible(
vllm/model_executor/layers/quantization/utils/ocp_mx_utils.py
+58
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from enum import Enum
4
+from typing import Any
5
6
from vllm.logger import init_logger
7
from vllm.model_executor.layers.quantization.utils.quant_utils import (
@@ -11,6 +12,8 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
12
kMxfp6E2M3Static,
13
kMxfp6E3M2Dynamic,
14
kMxfp6E3M2Static,
15
+ kMxfp8Dynamic,
16
+ kMxfp8Static,
17
)
18
19
logger = init_logger(__name__)
@@ -21,14 +24,19 @@ _WEIGHT_QUANT_KEY_MAP: dict[str, QuantKey] = {
24
"mxfp4": kMxfp4Static,
25
"mxfp6_e3m2": kMxfp6E3M2Static,
26
"mxfp6_e2m3": kMxfp6E2M3Static,
27
+ "mxfp8_e4m3": kMxfp8Static,
28
}
29
30
_ACTIVATION_QUANT_KEY_MAP: dict[str, QuantKey] = {
31
"mxfp4": kMxfp4Dynamic,
32
"mxfp6_e3m2": kMxfp6E3M2Dynamic,
33
"mxfp6_e2m3": kMxfp6E2M3Dynamic,
34
+ "mxfp8_e4m3": kMxfp8Dynamic,
35
}
36
37
+# Unpacked OCP MX dtypes: one byte per element, no `packed_factor`.
38
+OCP_MX_UNPACKED_DTYPES = {"mxfp8_e4m3"}
39
+
40
OCP_MX_DTYPES = {
41
"mxfp4",
42
"mxfp6_e3m2",
@@ -39,6 +47,56 @@ OCP_MX_DTYPES = {
47
}
48
49
50
+def ocp_mx_weight_dtype_and_rows(
51
+ weight_quant: dict[str, Any] | None,
52
+) -> tuple[str, int] | None:
53
+ """Normalize a Quark weight quant config to ``(mx_dtype, scale_block_rows)``.
54
+
55
+ Quark spells OCP MX weights two ways. The canonical one is 1-D per-group:
56
+ ``qscheme="per_group"``, ``group_size=32``, ``scale_format="e8m0"``. MXFP8
57
+ checkpoints may instead use a 2-D per-block spelling
58
+ (``qscheme="per_block"``, ``block_size=[R, 32]``,
59
+ ``scale_type="float8_e8m0fnu"``), which carries one scale per ``R`` weight
60
+ rows rather than one per row. ``scale_block_rows`` is that ``R``; it is 1
61
+ for the canonical spelling, where the two layouts coincide.
62
+
63
+ Returns ``None`` when the config is not an OCP MX weight quantization.
64
+ """
65
+ if not isinstance(weight_quant, dict):
66
+ return None
67
+
68
+ dtype = weight_quant.get("dtype")
69
+ if not isinstance(dtype, str):
70
+ return None
71
+ mx_dtype = dtype.replace("fp", "mxfp")
72
+ if mx_dtype not in _WEIGHT_QUANT_KEY_MAP:
73
+ return None
74
+
75
+ qscheme = weight_quant.get("qscheme")
76
+ if qscheme == "per_group":
77
+ if weight_quant.get("group_size") != OCP_MX_BLOCK_SIZE:
78
+ return None
79
+ if weight_quant.get("scale_format") != "e8m0":
80
+ return None
81
+ return mx_dtype, 1
82
+
83
+ if qscheme == "per_block":
84
+ # Only the unpacked dtypes are accepted here: expanding a 2-D scale
85
+ # over a sub-byte packed weight has no checkpoint to validate against.
86
+ if mx_dtype not in OCP_MX_UNPACKED_DTYPES:
87
+ return None
88
+ block_size = list(weight_quant.get("block_size") or [])
89
+ if len(block_size) != 2 or block_size[1] != OCP_MX_BLOCK_SIZE:
90
+ return None
91
+ if weight_quant.get("scale_type") != "float8_e8m0fnu":
92
+ return None
93
+ if weight_quant.get("symmetric") is not True or weight_quant.get("is_dynamic"):
94
+ return None
95
+ return mx_dtype, block_size[0]
96
+
97
+ return None
98
+
99
+
100
class OCP_MX_Scheme(str, Enum):
101
w_mxfp4 = "w_mxfp4"
102
w_mxfp4_a_mxfp4 = "w_mxfp4_a_mxfp4"
vllm/models/deepseek_v4/quant_config.py
+7
-7
@@ -135,15 +135,15 @@ class DeepseekV4FP8Config(Fp8Config):
135
def override_quantization_method(
136
cls, hf_quant_cfg, user_quant, hf_config=None
137
) -> QuantizationMethods | None:
138
+ # Quark checkpoints are always handled by QuarkConfig.
139
+ if (
140
+ isinstance(hf_quant_cfg, dict)
141
+ and hf_quant_cfg.get("quant_method") == "quark"
142
+ ):
143
+ return None
144
if not (
145
isinstance(hf_quant_cfg, dict)
140
- and (
141
- hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
142
- or (
143
- hf_quant_cfg.get("quant_method") == "quark"
144
- and cls._is_quark_mxfp4_ocp(hf_quant_cfg)
145
- )
146
- )
146
+ and hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
147
):
148
return None
149
model_type = getattr(hf_config, "model_type", None)
vllm/models/deepseek_v41/amd/model.py
+10
@@ -895,6 +895,11 @@ def _make_deepseek_v4_weights_mapper(
895
# renames the engram fp8 table but not its scale; route the
896
# scale explicitly to the same module.
897
re.compile(r"(engram\.embed)\.scale$"): r"\1_tokens.weight_scale_inv",
898
+ # Quark exports spell the same tensor ``embed.weight_scale``,
899
+ # which the ``\.scale$`` rules never match.
900
+ re.compile(
901
+ r"(engram\.embed)\.weight_scale$"
902
+ ): r"\1_tokens.weight_scale_inv",
903
re.compile(r"\.scale$"): f".{linear_scale_name}",
904
}
905
else:
@@ -908,6 +913,11 @@ def _make_deepseek_v4_weights_mapper(
913
): r"\1.weight_scale_inv",
914
# Same engram reroute as the fp4 branch above.
915
re.compile(r"(engram\.embed)\.scale$"): r"\1_tokens.weight_scale_inv",
916
+ # Quark exports spell the same tensor ``embed.weight_scale``,
917
+ # which the ``\.scale$`` rules never match.
918
+ re.compile(
919
+ r"(engram\.embed)\.weight_scale$"
920
+ ): r"\1_tokens.weight_scale_inv",
921
re.compile(r"\.scale$"): f".{linear_scale_name}",
922
}
923
return WeightsMapper(
vllm/models/deepseek_v41/amd/vl_model.py
+26
-8
@@ -79,20 +79,22 @@ class DeepseekV4VLImagePixelInputs(TensorSchema):
79
types: Annotated[torch.Tensor, TensorShape("ns", dynamic_dims={"ns"})]
80
81
82
+_VL_PREFIX_MAPPING: dict[str, str | None] = {
83
+ "layers.": "language_model.model.layers.",
84
+ "embed.": "language_model.model.embed.",
85
+ "norm.": "language_model.model.norm.",
86
+ "hc_head": "language_model.model.hc_head",
87
+ "mtp.": "language_model.model.mtp.",
88
+}
89
+
90
+
91
def _make_deepseek_v4_vl_weights_mapper(
92
expert_dtype: str, linear_scale_name: str
93
) -> WeightsMapper:
94
"""Text-checkpoint mapping rules re-rooted under ``language_model.``."""
95
base = _make_deepseek_v4_weights_mapper(expert_dtype, linear_scale_name)
87
- orig_to_new_prefix: dict[str, str | None] = {
88
- "layers.": "language_model.model.layers.",
89
- "embed.": "language_model.model.embed.",
90
- "norm.": "language_model.model.norm.",
91
- "hc_head": "language_model.model.hc_head",
92
- "mtp.": "language_model.model.mtp.",
93
- }
96
return WeightsMapper(
95
- orig_to_new_prefix=orig_to_new_prefix,
97
+ orig_to_new_prefix=dict(_VL_PREFIX_MAPPING),
98
orig_to_new_regex=base.orig_to_new_regex,
99
orig_to_new_suffix={
100
"head.weight": "language_model.lm_head.weight",
@@ -131,6 +133,22 @@ class DeepseekV41ForCausalLM(
133
134
supports_encoder_tp_data = True
135
136
+ # Both of these are read off the *class* by
137
+ # ``configure_quant_config``/``SupportsQuant``, before ``__init__`` runs,
138
+ # to translate a quantization config's checkpoint-style module names into
139
+ # runtime prefixes. ``__init__`` later overwrites the instance
140
+ # ``hf_to_vllm_mapper`` with the full weight-loading mapper; only the
141
+ # prefix rules matter here. Without the class attributes, per-layer specs
142
+ # naming ``layers.N.attn.wq_a`` never match this wrapper's
143
+ # ``language_model.model.layers.N.attn.wq_a`` and silently fall back to
144
+ # the global spec.
145
+ packed_modules_mapping = DeepseekV41LLMForCausalLM.packed_modules_mapping
146
+
147
+ hf_to_vllm_mapper = WeightsMapper(
148
+ orig_to_new_prefix=dict(_VL_PREFIX_MAPPING),
149
+ orig_to_new_substr={".shared_experts.w2": ".shared_experts.down_proj"},
150
+ )
151
+
152
# The MoE router needs raw token ids to detect image-span tokens
153
# (all carrying image_token_id, see common/mm_preprocess.py) and apply
154
# bias_vl.
vllm/models/deepseek_v41/quant_config.py
+9
-7
@@ -139,15 +139,17 @@ class DeepseekV4FP8Config(Fp8Config):
139
def override_quantization_method(
140
cls, hf_quant_cfg, user_quant, hf_config=None
141
) -> QuantizationMethods | None:
142
+ # Quark checkpoints are always handled by QuarkConfig, which knows
143
+ # how to dispatch each per-layer scheme (MXFP4, MXFP8, etc.).
144
+ # ``from_config`` below rewrites the config into a single global FP8
145
+ # scheme, which would discard per-layer specs — so never claim Quark.
146
+ if isinstance(hf_quant_cfg, dict) and (
147
+ hf_quant_cfg.get("quant_method") == "quark"
148
+ ):
149
+ return None
150
if not (
151
isinstance(hf_quant_cfg, dict)
144
- and (
145
- hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
146
- or (
147
- hf_quant_cfg.get("quant_method") == "quark"
148
- and cls._is_quark_mxfp4_ocp(hf_quant_cfg)
149
- )
150
- )
152
+ and hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
153
):
154
return None
155
model_type = getattr(hf_config, "model_type", None)