[Bugfix][Quantization] Register Quark per-block FP8 scales as weight_scale (#54770)

Signed-off-by: jimmy-adams <41593649+jimmy-adams@users.noreply.github.com> Co-authored-by: fxmarty-amd <felmarty@amd.com> Co-authored-by: Shanshan Shen <467638484@qq.com>

jimmy-adams committed Sep 5, 2026 at 10:44 UTC 385ba6b5b003238e559d9c1f18e0f1984320ee61
7 files changed +111 -25
tests/quantization/test_quark.py
+63
@@ -11,6 +11,7 @@ import importlib.metadata
11 from dataclasses import dataclass
12 from importlib.util import find_spec
13 from types import SimpleNamespace
14 +from unittest.mock import MagicMock, patch
15
16 import huggingface_hub
17 import lm_eval
@@ -702,6 +703,68 @@ def enable_pickle(monkeypatch):
703 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
704
705
706 +def test_quark_w8a8_fp8_per_block_registers_weight_scale(monkeypatch):
707 + from vllm.model_executor.layers.quantization.utils.fp8_utils import (
708 + get_fp8_block_weight_scale,
709 + )
710 +
711 + monkeypatch.setattr(
712 + "vllm.model_executor.layers.quantization.quark.schemes."
713 + "quark_w8a8_fp8.get_current_vllm_config",
714 + lambda: SimpleNamespace(model_config=SimpleNamespace(dtype=torch.bfloat16)),
715 + )
716 + scheme = QuarkW8A8Fp8PerBlock(kFp8Static128BlockSym, kFp8Dynamic128Sym)
717 +
718 + layer = torch.nn.Module()
719 + layer.weight_scale = torch.tensor([2.0])
720 + assert get_fp8_block_weight_scale(layer) is None
721 + layer.scheme = scheme
722 + assert get_fp8_block_weight_scale(layer) is layer.weight_scale
723 + layer.weight_scale_inv = torch.tensor([3.0])
724 + assert get_fp8_block_weight_scale(layer) is layer.weight_scale
725 + layer.scheme = None
726 + assert get_fp8_block_weight_scale(layer) is layer.weight_scale_inv
727 +
728 + loaded = torch.nn.Module()
729 +
730 + def weight_loader(param, loaded_weight):
731 + return None
732 +
733 + dummy_param = torch.nn.Parameter(torch.empty(1), requires_grad=False)
734 + with (
735 + patch(
736 + "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
737 + "validate_fp8_block_shape"
738 + ),
739 + patch(
740 + "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
741 + "create_fp8_weight_parameter",
742 + return_value=dummy_param,
743 + ),
744 + patch(
745 + "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
746 + "create_fp8_scale_parameter",
747 + return_value=dummy_param,
748 + ),
749 + patch(
750 + "vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8."
751 + "init_fp8_linear_kernel",
752 + return_value=MagicMock(),
753 + ),
754 + ):
755 + scheme.create_weights(
756 + loaded,
757 + output_partition_sizes=[256],
758 + input_size_per_partition=256,
759 + params_dtype=torch.bfloat16,
760 + weight_loader=weight_loader,
761 + input_size=256,
762 + output_size=256,
763 + )
764 + assert hasattr(loaded, "weight_scale")
765 + assert not hasattr(loaded, "weight_scale_inv")
766 +
767 +
768 def test_quark_config_has_no_model_specific_fused_mappings():
769 config = QuarkConfig({})
770
vllm/model_executor/layers/quantization/quark/schemes/quark_w8a8_fp8.py
+7 -2
@@ -230,6 +230,9 @@ class QuarkW8A8Fp8PerBlock(QuarkScheme):
230 return QuarkW8A8Fp8.get_min_capability()
231
232 def process_weights_after_loading(self, layer) -> None:
233 + # Quark exports the dequant multiplier as ``weight_scale`` (the same
234 + # numerical convention as DeepSeek's ``weight_scale_inv``). Kernels
235 + # multiply ``weight * scale``; do not invert here.
236 self.fp8_linear.process_weights_after_loading(layer)
237
238 def create_weights(
@@ -280,8 +283,10 @@ class QuarkW8A8Fp8PerBlock(QuarkScheme):
283 weight_loader,
284 scale_dtype=scale_dtype,
285 )
283 - # DeepSeek V4 weight mappers route checkpoint ".scale" tensors here.
284 - layer.register_parameter("weight_scale_inv", weight_scale)
286 + # Match Quark-exported checkpoints (``.weight_scale``).
287 + # ``Fp8LinearMethod.create_weights`` registers weight_scale_inv, which has the
288 + # same semantic meaning as Quark's `weight_scale`.
289 + layer.register_parameter("weight_scale", weight_scale)
290
291 assert self.activation_quant_key is not None
292 self.fp8_linear = init_fp8_linear_kernel(
vllm/model_executor/layers/quantization/utils/fp8_utils.py
+15
@@ -50,6 +50,21 @@ def is_fp8(x: torch.dtype | torch.Tensor) -> bool:
50 return x == torch.float8_e4m3fn or x == torch.float8_e4m3fnuz
51
52
53 +def get_fp8_block_weight_scale(layer: torch.nn.Module) -> torch.Tensor | None:
54 + """Return the block-FP8 weight scale for supported quant methods."""
55 + # Local import avoids a circular import: quark_w8a8_fp8 imports this module.
56 + from vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8 import (
57 + QuarkW8A8Fp8PerBlock,
58 + )
59 +
60 + if isinstance(
61 + getattr(layer, "scheme", None),
62 + QuarkW8A8Fp8PerBlock,
63 + ):
64 + return getattr(layer, "weight_scale", None)
65 + return getattr(layer, "weight_scale_inv", None)
66 +
67 +
68 def input_to_float8(
69 x: torch.Tensor, dtype: torch.dtype | None = None
70 ) -> tuple[torch.Tensor, torch.Tensor]:
vllm/models/deepseek_v4/amd/model.py
+13 -13
@@ -123,20 +123,20 @@ class DeepseekV4MLP(nn.Module):
123 # B-preshuffle the gate_up_proj weight in place (single weight).
124 if not self._gateup:
125 return
126 + from vllm.model_executor.layers.quantization.utils.fp8_utils import (
127 + _upcast_e8m0_to_fp32,
128 + get_fp8_block_weight_scale,
129 + )
130 from vllm.model_executor.utils import replace_parameter
131
132 w = getattr(self.gate_up_proj, "weight", None)
129 - ws = getattr(self.gate_up_proj, "weight_scale_inv", None) # per-block scale
133 + ws = get_fp8_block_weight_scale(self.gate_up_proj)
134 if w is None or ws is None or w.dim() != 2:
135 return
136 # K % 128 (group-128 quant) and N % 16 (shuffle_weight) must hold.
137 if w.shape[-1] % 128 != 0 or w.shape[0] % 16 != 0:
138 return
139 if ws.dtype == torch.float8_e8m0fnu:
136 - from vllm.model_executor.layers.quantization.utils.fp8_utils import (
137 - _upcast_e8m0_to_fp32,
138 - )
139 -
140 ws = _upcast_e8m0_to_fp32(ws).contiguous()
141 replace_parameter(
142 self.gate_up_proj,
@@ -774,6 +774,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
774 loaded_params: set[str] = set()
775
776 def _resolve_param_name(name: str) -> str:
777 + # Fp8LinearMethod registers block scales as ``weight_scale_inv``.
778 + # Quark checkpoints / QuarkW8A8Fp8PerBlock use ``weight_scale``.
779 + # Alias only when the registered param is the ``_inv`` name.
780 inv_name = f"{name}_inv"
781 if name not in params_dict and inv_name in params_dict:
782 return inv_name
@@ -922,20 +925,17 @@ def _make_deepseek_v4_weights_mapper(
925 ) -> WeightsMapper:
926 if expert_dtype == "fp4":
927 # MXFP4 experts use Mxfp4MoEMethod, which registers scales as
925 - # ``w{1,2,3}_weight_scale`` (no _inv suffix). FP8 linear and
926 - # (non-fused) shared experts use Fp8LinearMethod's block scales,
927 - # which register as ``weight_scale_inv``.
928 + # ``w{1,2,3}_weight_scale`` (no _inv suffix). Native DeepSeek FP8
929 + # linear / shared experts use Fp8LinearMethod's ``weight_scale_inv``.
930 + # Quark linear block-FP8 registers ``weight_scale`` and is left
931 + # as-is; load_weights aliases ``.weight_scale`` -> ``.weight_scale_inv``
932 + # only when the latter is the registered parameter.
933 #
934 # - DeepSeek native ``.scale``: expert scales -> ``.weight_scale``,
935 # everything else -> ``.weight_scale_inv``.
931 - # - AMD-Quark ``.weight_scale``: linear/attn scales ->
932 - # ``.weight_scale_inv``. Expert and shared-expert
933 - # ``w{1,2,3}.weight_scale`` are left untouched (consumed as-is by
934 - # the MXFP4 expert loader, which produces ``w{13,2}_weight_scale``);
936 scale_regex = {
937 re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
938 re.compile(r"\.scale$"): ".weight_scale_inv",
938 - re.compile(r"(?<!\.w[123])\.weight_scale$"): ".weight_scale_inv",
939 }
940 else:
941 # FP8 experts use Fp8MoEMethod (block_quant=True), which registers
vllm/models/deepseek_v4/amd/mtp.py
+4 -5
@@ -335,11 +335,10 @@ class DeepSeekV4MTP(nn.Module):
335 loaded_params: set[str] = set()
336
337 def _resolve_scale_name(name: str) -> str:
338 - # Quark checkpoints name FP8 block scales ``.weight_scale``,
339 - # but block-FP8 layers register them as ``.weight_scale_inv``
340 - # while MXFP4 experts register ``.weight_scale``. Auto-detect:
341 - # rename to ``_inv`` only when that variant exists and the plain
342 - # one does not.
338 + # Quark checkpoints and QuarkW8A8Fp8PerBlock use ``.weight_scale``.
339 + # Native block-FP8 layers register ``.weight_scale_inv``. Rename
340 + # to ``_inv`` only when that variant exists and the plain one
341 + # does not.
342 if name.endswith(".weight_scale") and name not in params_dict:
343 inv = name.removesuffix(".weight_scale") + ".weight_scale_inv"
344 if inv in params_dict:
vllm/models/deepseek_v4/amd/rocm.py
+2 -1
@@ -535,6 +535,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
535 return
536 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
537 _upcast_e8m0_to_fp32,
538 + get_fp8_block_weight_scale,
539 )
540 from vllm.model_executor.utils import replace_parameter
541
@@ -545,7 +546,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
546 # K % 128 (group-128 quant) and N % 16 (shuffle_weight) must hold.
547 if w.shape[-1] % 128 != 0 or w.shape[0] % 16 != 0:
548 return None
548 - ws = getattr(linear, "weight_scale_inv", None) # per-block scale
549 + ws = get_fp8_block_weight_scale(linear)
550 if ws is None:
551 return None
552 if ws.dtype == torch.float8_e8m0fnu:
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+7 -4
@@ -1202,14 +1202,17 @@ def _get_cached_wo_a_bf16(
1202 cached = getattr(wo_a, "_dsv4_wo_a_bf16", None)
1203 if cached is not None:
1204 return cached
1205 - if hasattr(wo_a, "weight_scale_inv"):
1205 + from vllm.model_executor.layers.quantization.utils.fp8_utils import (
1206 + get_fp8_block_weight_scale,
1207 + )
1208 +
1209 + wo_a_scale_param = get_fp8_block_weight_scale(wo_a)
1210 + if wo_a_scale_param is not None:
1211 wo_a_weight = wo_a.weight.view(n_local_groups, o_lora_rank, hidden_dim).to(
1212 torch.float32
1213 )
1214 wo_a_scale = _expand_2d_block_scales(
1210 - wo_a.weight_scale_inv.view(
1211 - n_local_groups, -1, wo_a.weight_scale_inv.shape[-1]
1212 - ),
1215 + wo_a_scale_param.view(n_local_groups, -1, wo_a_scale_param.shape[-1]),
1216 o_lora_rank,
1217 hidden_dim,
1218 )