Fused Shared Expert Support for AMD Quark DeepSeek-V4 Model Checkpoints (#48044)

Signed-off-by: Colin Zeng <Colin.Zeng@amd.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Colin Z committed Jul 22, 2026 at 17:34 UTC 27ffbfde8decd340fa0144aea06061374c53e456
3 files changed +175 -13
vllm/models/deepseek_v4/amd/model.py
+121 -11
@@ -8,7 +8,8 @@ import regex as re
8 import torch
9 import torch.nn as nn
10
11 -from vllm.config import VllmConfig
11 +import vllm.envs as envs
12 +from vllm.config import VllmConfig, get_current_vllm_config
13 from vllm.distributed import (
14 get_pp_group,
15 get_tensor_model_parallel_rank,
@@ -110,6 +111,51 @@ class DeepseekV4MLP(nn.Module):
111 return x
112
113
114 +def _shared_experts_are_fp4(config, layer_idx: int | None = None) -> bool:
115 + """Whether the shared experts are MXFP4 and thus fusable.
116 +
117 + ``layer_idx=None`` resolves the model-wide default (global scheme), used by
118 + the main-model weight loader / mapper callers that operate per-model.
119 + """
120 + quant_cfg = getattr(config, "quantization_config", None)
121 + if quant_cfg is None:
122 + return False
123 + if layer_idx is None:
124 + base = None
125 + elif layer_idx >= config.num_hidden_layers:
126 + base = f"mtp.{layer_idx - config.num_hidden_layers}.ffn.shared_experts"
127 + else:
128 + base = f"layers.{layer_idx}.ffn.shared_experts"
129 + if base and any(e.startswith(base) for e in (quant_cfg.get("exclude") or [])):
130 + return False
131 + entry = (
132 + (quant_cfg.get("layer_quant_config") or {}).get(f"{base}.w1") if base else None
133 + )
134 + if entry is None:
135 + entry = quant_cfg.get("global_quant_config")
136 + return ((entry or {}).get("weight") or {}).get("dtype") == "fp4"
137 +
138 +
139 +def _fuse_shared_experts_enabled(config, prefix: str = "") -> bool:
140 + """Whether to fuse the shared expert into the routed MXFP4 grouped GEMM.
141 +
142 + Fusion fuses the shared expert into the routed experts' MXFP4 grouped GEMM,
143 + so it only applies where the shared expert is the same precision as the
144 + routed experts. Some layers may carry a shared expert in a different quantization
145 + than the routed experts; when so, it runs as its own linear and must not be fused.
146 + """
147 + if not (
148 + current_platform.is_rocm()
149 + and getattr(config, "n_shared_experts", None)
150 + and envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS
151 + and not get_current_vllm_config().parallel_config.enable_expert_parallel
152 + ):
153 + return False
154 + return _shared_experts_are_fp4(
155 + config, extract_layer_index(prefix) if prefix else None
156 + )
157 +
158 +
159 class DeepseekV4MoE(nn.Module):
160 def __init__(
161 self,
@@ -164,7 +210,11 @@ class DeepseekV4MoE(nn.Module):
210 requires_grad=False,
211 )
212
167 - if config.n_shared_experts is None:
213 + self.n_shared_experts = config.n_shared_experts
214 +
215 + self.fuse_shared_experts = _fuse_shared_experts_enabled(config, prefix)
216 +
217 + if config.n_shared_experts is None or self.fuse_shared_experts:
218 self.shared_experts = None
219 else:
220 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -188,6 +238,9 @@ class DeepseekV4MoE(nn.Module):
238
239 self.experts = FusedMoE(
240 shared_experts=self.shared_experts,
241 + n_shared_experts=(
242 + config.n_shared_experts if self.fuse_shared_experts else None
243 + ),
244 gate=self.gate,
245 num_experts=config.n_routed_experts,
246 top_k=config.num_experts_per_tok,
@@ -667,7 +720,38 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
720 # Pre-compute expert mapping ONCE.
721 expert_mapping = self.get_expert_mapping()
722
723 + # Use each MoE's own per-layer fusion decision (computed with its prefix
724 + # at init) as the single source of truth, so the redirect below cannot
725 + # diverge from how the module was built if per-layer quantization ever
726 + # mixes fused and non-fused layers.
727 + fuse_by_layer = {
728 + extract_layer_index(mod_name): mod.fuse_shared_experts
729 + for mod_name, mod in self.named_modules()
730 + if isinstance(mod, DeepseekV4MoE)
731 + }
732 + n_routed = self.config.n_routed_experts
733 + # The redirect below maps the single shared-expert tensor group to one
734 + # appended slot; multiple shared experts would need per-expert slicing
735 + # (see deepseek_v2.py). DeepSeek-V4 has n_shared_experts == 1.
736 + if any(fuse_by_layer.values()) and self.config.n_shared_experts != 1:
737 + raise NotImplementedError(
738 + "deepseek-v4 fused shared-expert loading supports only "
739 + f"n_shared_experts == 1, got {self.config.n_shared_experts}"
740 + )
741 +
742 for name, loaded_weight in weights:
743 + # Shared-expert fusion: redirect ``.ffn.shared_experts.w{1,2,3}``
744 + # into appended routed-expert slot ``.ffn.experts.{n_routed}``
745 + # so the MXFP4-quantized shared expert loads through the routed
746 + # expert loader (grouped GEMM). Single shared expert only.
747 + if ".ffn.shared_experts.w" in name and fuse_by_layer.get(
748 + extract_layer_index(name), False
749 + ):
750 + name = name.replace(
751 + ".ffn.shared_experts.w",
752 + f".ffn.experts.{n_routed}.w",
753 + )
754 +
755 for param_name, weight_name, shard_id in stacked_params_mapping:
756 # Skip non-stacked layers and experts (experts handled below).
757 if ".experts." in name:
@@ -745,24 +829,41 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
829 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
830 # Params for weights, fp8 weight scales, fp8 activation scales
831 # (param_name, weight_name, expert_id, shard_id)
832 + # When fusing shared experts, include the appended slots
833 + # (ids n_routed_experts .. n_routed_experts + n_shared - 1) so the
834 + # redirected shared-expert weights route through the expert loader.
835 + n_shared = getattr(self.config, "n_shared_experts", 0) or 0
836 + num_experts = self.config.n_routed_experts + (
837 + n_shared if _fuse_shared_experts_enabled(self.config) else 0
838 + )
839 return fused_moe_make_expert_params_mapping(
840 self,
841 ckpt_gate_proj_name="w1",
842 ckpt_down_proj_name="w2",
843 ckpt_up_proj_name="w3",
753 - num_experts=self.config.n_routed_experts,
844 + num_experts=num_experts,
845 )
846
847
757 -def _make_deepseek_v4_weights_mapper(expert_dtype: str) -> WeightsMapper:
848 +def _make_deepseek_v4_weights_mapper(
849 + expert_dtype: str, fuse_shared_experts: bool = False
850 +) -> WeightsMapper:
851 if expert_dtype == "fp4":
852 # MXFP4 experts use Mxfp4MoEMethod, which registers scales as
853 # ``w{1,2,3}_weight_scale`` (no _inv suffix). FP8 linear and
761 - # shared experts use Fp8LinearMethod's block scales, which
762 - # register as ``weight_scale_inv``.
854 + # (non-fused) shared experts use Fp8LinearMethod's block scales,
855 + # which register as ``weight_scale_inv``.
856 + #
857 + # - DeepSeek native ``.scale``: expert scales -> ``.weight_scale``,
858 + # everything else -> ``.weight_scale_inv``.
859 + # - AMD-Quark ``.weight_scale``: linear/attn scales ->
860 + # ``.weight_scale_inv``. Expert and shared-expert
861 + # ``w{1,2,3}.weight_scale`` are left untouched (consumed as-is by
862 + # the MXFP4 expert loader, which produces ``w{13,2}_weight_scale``);
863 scale_regex = {
864 re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
865 re.compile(r"\.scale$"): ".weight_scale_inv",
866 + re.compile(r"(?<!\.w[123])\.weight_scale$"): ".weight_scale_inv",
867 }
868 else:
869 # FP8 experts use Fp8MoEMethod (block_quant=True), which registers
@@ -771,6 +872,14 @@ def _make_deepseek_v4_weights_mapper(expert_dtype: str) -> WeightsMapper:
872 scale_regex = {
873 re.compile(r"\.scale$"): ".weight_scale_inv",
874 }
875 + # When shared experts are fused into the routed MXFP4 grouped GEMM, the
876 + # shared_experts tensors are redirected to routed expert slots ; leave
877 + # their names untouched here.
878 + substr_map = (
879 + {}
880 + if fuse_shared_experts
881 + else {".shared_experts.w2": ".shared_experts.down_proj"}
882 + )
883 return WeightsMapper(
884 orig_to_new_prefix={
885 "layers.": "model.layers.",
@@ -785,9 +894,7 @@ def _make_deepseek_v4_weights_mapper(expert_dtype: str) -> WeightsMapper:
894 "embed.weight": "embed_tokens.weight",
895 ".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
896 },
788 - orig_to_new_substr={
789 - ".shared_experts.w2": ".shared_experts.down_proj",
790 - },
897 + orig_to_new_substr=substr_map,
898 )
899
900
@@ -804,8 +911,11 @@ class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
911 config = vllm_config.model_config.hf_config
912 self.config = config
913 expert_dtype = getattr(config, "expert_dtype", "fp4")
807 - if expert_dtype != "fp4":
808 - self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(expert_dtype)
914 + fuse_shared_experts = _fuse_shared_experts_enabled(config)
915 + if expert_dtype != "fp4" or fuse_shared_experts:
916 + self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
917 + expert_dtype, fuse_shared_experts=fuse_shared_experts
918 + )
919
920 self.model = self.model_cls(
921 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
vllm/models/deepseek_v4/amd/mtp.py
+17
@@ -334,6 +334,21 @@ class DeepSeekV4MTP(nn.Module):
334 params_dict = dict(self.named_parameters())
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.
343 + if name.endswith(".weight_scale") and name not in params_dict:
344 + inv = name.removesuffix(".weight_scale") + ".weight_scale_inv"
345 + if inv in params_dict:
346 + return inv
347 + # Otherwise leave the name unchanged: either it already matches a
348 + # param, or it is genuinely unknown and should surface the normal
349 + # KeyError downstream rather than be silently rewritten.
350 + return name
351 +
352 # TP for attention
353 tp_size = get_tensor_model_parallel_world_size()
354 tp_rank = get_tensor_model_parallel_rank()
@@ -393,6 +408,7 @@ class DeepSeekV4MTP(nn.Module):
408 if weight_name not in name:
409 continue
410 name = name.replace(weight_name, param_name)
411 + name = _resolve_scale_name(name)
412
413 param = params_dict[name]
414 weight_loader = param.weight_loader
@@ -447,6 +463,7 @@ class DeepSeekV4MTP(nn.Module):
463 )
464 if name.endswith(".ffn.gate.bias"):
465 name = name.replace(".bias", ".e_score_correction_bias")
466 + name = _resolve_scale_name(name)
467 param = params_dict[name]
468 weight_loader = getattr(
469 param, "weight_loader", default_weight_loader
vllm/models/deepseek_v4/quant_config.py
+37 -2
@@ -4,7 +4,7 @@
4
5 from __future__ import annotations
6
7 -from typing import TYPE_CHECKING
7 +from typing import TYPE_CHECKING, cast
8
9 from vllm.config import get_current_vllm_config
10 from vllm.model_executor.layers.fused_moe import (
@@ -117,13 +117,29 @@ class DeepseekV4FP8Config(Fp8Config):
117 def get_name(cls) -> QuantizationMethods:
118 return "deepseek_v4_fp8"
119
120 + @staticmethod
121 + def _is_quark_mxfp4_ocp(hf_quant_cfg: dict) -> bool:
122 + """True for AMD-Quark exports whose global scheme is MXFP4."""
123 + weight = (hf_quant_cfg.get("global_quant_config") or {}).get("weight") or {}
124 + return (
125 + weight.get("dtype") == "fp4"
126 + and weight.get("qscheme") == "per_group"
127 + and weight.get("group_size") == 32
128 + )
129 +
130 @classmethod
131 def override_quantization_method(
132 cls, hf_quant_cfg, user_quant, hf_config=None
133 ) -> QuantizationMethods | None:
134 if not (
135 isinstance(hf_quant_cfg, dict)
126 - and hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
136 + and (
137 + hf_quant_cfg.get("quant_method") in ("fp8", "deepseek_v4_fp8")
138 + or (
139 + hf_quant_cfg.get("quant_method") == "quark"
140 + and cls._is_quark_mxfp4_ocp(hf_quant_cfg)
141 + )
142 + )
143 ):
144 return None
145 model_type = getattr(hf_config, "model_type", None)
@@ -131,6 +147,25 @@ class DeepseekV4FP8Config(Fp8Config):
147 return "deepseek_v4_fp8"
148 return None
149
150 + @classmethod
151 + def from_config(cls, config: dict) -> DeepseekV4FP8Config:
152 + # Reroute AMD-Quark fused shared expert MXFP4 checkpoints onto the fp8
153 + # path: the runtime layout matches the DeepSeek-native fp8 checkpoint,
154 + # so translate the schema into format Fp8Config.from_config expects.
155 + if config.get("quant_method") == "quark":
156 + quark_exclude = config.get("exclude") or []
157 + config = {
158 + "quant_method": "fp8",
159 + "activation_scheme": "dynamic",
160 + "fmt": "e4m3",
161 + "scale_fmt": "ue8m0",
162 + "weight_block_size": [128, 128],
163 + "ignored_layers": [
164 + name for name in quark_exclude if isinstance(name, str)
165 + ],
166 + }
167 + return cast("DeepseekV4FP8Config", super().from_config(config))
168 +
169 def get_quant_method(self, layer, prefix):
170 if isinstance(layer, RoutedExperts):
171 if is_layer_skipped(