Summary
On ds4-sm120-preview (and also ds4-sm120, ds4-sm120-full, dsv4, ds4-sm120-next, codex/ds4-sm120-min-enable), DeepseekV4FP8Config.get_quant_method returns Mxfp4MoEMethod for any expert_dtype == "fp4". That only handles DeepSeek's native MXFP4 experts (group-32, e8m0 scales). NVIDIA's nvidia/DeepSeek-V4-Flash-NVFP4 repackages the experts as ModelOpt NVFP4 (group-16, e4m3 block scales + per-tensor weight_scale_2 / input_scale; config.json: producer.name=modelopt, moe_quant_algo=NVFP4, group_size=16). Loading that checkpoint through the MXFP4 path fails:
KeyError: 'layers.0.ffn.experts.w13_input_scale', then
- after guarding that, a
128 vs 256 shape mismatch in FusedMoE._load_w13 (the group-16 vs group-32 ratio).
This is a backport, not a new design (prior art / not-a-duplicate)
I want to be upfront that this duplicates work that already exists elsewhere:
So this is really a backport request. Filing as an issue (rather than a PR onto a frozen branch) so you can decide whether to take a minimal backport on the preview/active SM120 branches or just rebase them onto current upstream main (which subsumes it).
Minimal fix that works on pure SM120 (2× RTX PRO 6000 Blackwell)
Two changes in vllm/model_executor/models/deepseek_v4.py:
- In
get_quant_method, when the checkpoint is ModelOpt-NVFP4 (producer==modelopt and moe_quant_algo==NVFP4), build a ModelOptNvFp4Config and return ModelOptNvFp4FusedMoE instead of Mxfp4MoEMethod. That method registers the correct two-level NVFP4 tensors and auto-selects the Marlin NVFP4 MoE backend, which runs on SM120 with no SM100 CUTLASS / FlashInfer dependency.
- A guard in the expert-loading loop that
continues when name_mapped not in params_dict, so the checkpoint's per-tensor *_input_scale tensors (unused by this kernel path) are skipped instead of raising KeyError.
Applies cleanly to ds4-sm120-preview (git apply --check OK), 2 hunks, +51 lines, 0 deletions:
diff
diff --git a/vllm/model_executor/models/deepseek_v4.py b/vllm/model_executor/models/deepseek_v4.py
--- a/vllm/model_executor/models/deepseek_v4.py
+++ b/vllm/model_executor/models/deepseek_v4.py
@@ -203,11 +203,55 @@ class DeepseekV4FP8Config(Fp8Config):
):
return UnquantizedFusedMoEMethod(layer.moe_config)
if self.expert_dtype == "fp4":
+ # NVIDIA's DeepSeek-V4-Flash-NVFP4 repackages the experts as
+ # ModelOpt NVFP4 (group_size=16, e4m3 block scales + per-tensor
+ # weight_scale_2/input_scale) instead of the native MXFP4
+ # (group 32, e8m0) layout Mxfp4MoEMethod expects. Route those
+ # experts to vLLM's ModelOpt NVFP4 FusedMoE method, which
+ # selects a Marlin (SM120-capable) backend.
+ nvfp4_method = self._maybe_modelopt_nvfp4_moe_method(layer)
+ if nvfp4_method is not None:
+ return nvfp4_method
return Mxfp4MoEMethod(layer.moe_config)
# expert_dtype == "fp8": fall through to Fp8Config which
# returns Fp8MoEMethod with block-wise float32 scales.
return super().get_quant_method(layer, prefix)
+ def _modelopt_nvfp4_quant_config(self):
+ """Return the raw checkpoint quantization_config dict, or {}."""
+ try:
+ hf_cfg = get_current_vllm_config().model_config.hf_config
+ except Exception:
+ return {}
+ qc = getattr(hf_cfg, "quantization_config", None)
+ if qc is None:
+ return {}
+ if hasattr(qc, "to_dict"):
+ try:
+ qc = qc.to_dict()
+ except Exception:
+ pass
+ return qc if isinstance(qc, dict) else {}
+
+ def _maybe_modelopt_nvfp4_moe_method(self, layer):
+ qc = self._modelopt_nvfp4_quant_config()
+ producer = (qc.get("producer") or {}).get("name", "")
+ moe_algo = qc.get("moe_quant_algo")
+ if not (producer == "modelopt" and moe_algo == "NVFP4"):
+ return None
+ from vllm.model_executor.layers.quantization.modelopt import (
+ ModelOptNvFp4Config,
+ ModelOptNvFp4FusedMoE,
+ )
+ if getattr(self, "_nvfp4_moe_cfg", None) is None:
+ self._nvfp4_moe_cfg = ModelOptNvFp4Config(
+ is_checkpoint_nvfp4_serialized=True,
+ kv_cache_quant_algo=None,
+ exclude_modules=list(getattr(self, "ignored_layers", []) or []),
+ group_size=int(qc.get("group_size", 16) or 16),
+ )
+ return ModelOptNvFp4FusedMoE(self._nvfp4_moe_cfg, layer.moe_config)
+
def is_mxfp4_quant(self, prefix, layer):
return isinstance(layer, FusedMoE) and self.expert_dtype == "fp4"
@@ -1452,6 +1496,13 @@ class DeepseekV4Model(nn.Module):
name_mapped = name.replace(weight_name, param_name)
if is_pp_missing_parameter(name_mapped, self):
continue
+ if name_mapped not in params_dict:
+ # The selected MoE backend (e.g. MXFP4/Marlin block
+ # microscaling on SM120) does not register this scale.
+ # NVIDIA's NVFP4 checkpoint ships per-tensor
+ # *_input_scale tensors that this kernel path does not
+ # use; skip them instead of raising KeyError.
+ continue
param = params_dict[name_mapped]
# We should ask the weight loader to return success or not
# here since otherwise we may skip experts with other
Why Marlin on SM120
ModelOptNvFp4FusedMoE's backend selector picks the Marlin NVFP4 MoE kernel on SM120 (compute 12.0), avoiding the SM100-only TMEM/tcgen05 CUTLASS and FlashInfer-TRTLLM paths that aren't available on RTX PRO 6000 / 5090-class parts. This matches the resolution merged upstream in vllm-project#45836.
Test evidence
- HW: 2× NVIDIA RTX PRO 6000 Blackwell (SM120 / compute 12.0), tensor-parallel 2.
- Model:
nvidia/DeepSeek-V4-Flash-NVFP4, --kv-cache-dtype fp8.
- Result: model loads and serves; ~59 tok/s single-stream decode; correct outputs on math and text prompts; tool-calling works via the Anthropic
/v1/messages endpoint.
- Backend selected at runtime: Marlin NVFP4 MoE (no SM100 CUTLASS / FlashInfer required).
AI-assistance disclosure
This change and the investigation behind it were produced with AI assistance (Claude Code). Validation was end-to-end serving on the 2× RTX PRO 6000 host (model load + generation + tool-call), as above; the full pytest kernel/unit suite was not run on this SM120 host. Happy to open a PR against whichever branch you prefer, or to adjust the approach.
Summary
On
ds4-sm120-preview(and alsods4-sm120,ds4-sm120-full,dsv4,ds4-sm120-next,codex/ds4-sm120-min-enable),DeepseekV4FP8Config.get_quant_methodreturnsMxfp4MoEMethodfor anyexpert_dtype == "fp4". That only handles DeepSeek's native MXFP4 experts (group-32, e8m0 scales). NVIDIA'snvidia/DeepSeek-V4-Flash-NVFP4repackages the experts as ModelOpt NVFP4 (group-16, e4m3 block scales + per-tensorweight_scale_2/input_scale;config.json:producer.name=modelopt,moe_quant_algo=NVFP4,group_size=16). Loading that checkpoint through the MXFP4 path fails:KeyError: 'layers.0.ffn.experts.w13_input_scale', then128 vs 256shape mismatch inFusedMoE._load_w13(the group-16 vs group-32 ratio).This is a backport, not a new design (prior art / not-a-duplicate)
I want to be upfront that this duplicates work that already exists elsewhere:
ModelOptNvFp4FusedMoE), but the landed commit took the FlashInfer CUTLASS / B12x path and was validated on GB10 (sm_121a). It did not leave adeepseek_v4.py-level route in any current branch, so the gap is still open on these branches.vllm-project/vllmhas since merged equivalent support: Add NVFP4 MOE support for Deepseek V4. vllm-project/vllm#42209 ("Add NVFP4 MOE support for Deepseek V4") and [NVFP4 MoE/Deepseek V4] Marlin: wire SwiGLU clamp + allow it for clamped models on non-Blackwell vllm-project/vllm#45836 ("[NVFP4 MoE/Deepseek V4] Marlin: wire SwiGLU clamp + allow it for clamped models on non-Blackwell"), which fixes the exactNo NvFp4 MoE backend supports the deployment configurationerror on non-SM100 cards. These fork branches predate that work and haven't inherited it (your SM12x enablement is still tracked in the open upstream PR [New Model][Nvidia] Add SM12x support for DeepSeek V4 Flash with essential fixes vllm-project/vllm#41834).So this is really a backport request. Filing as an issue (rather than a PR onto a frozen branch) so you can decide whether to take a minimal backport on the preview/active SM120 branches or just rebase them onto current upstream main (which subsumes it).
Minimal fix that works on pure SM120 (2× RTX PRO 6000 Blackwell)
Two changes in
vllm/model_executor/models/deepseek_v4.py:get_quant_method, when the checkpoint is ModelOpt-NVFP4 (producer==modeloptandmoe_quant_algo==NVFP4), build aModelOptNvFp4Configand returnModelOptNvFp4FusedMoEinstead ofMxfp4MoEMethod. That method registers the correct two-level NVFP4 tensors and auto-selects the Marlin NVFP4 MoE backend, which runs on SM120 with no SM100 CUTLASS / FlashInfer dependency.continues whenname_mapped not in params_dict, so the checkpoint's per-tensor*_input_scaletensors (unused by this kernel path) are skipped instead of raisingKeyError.Applies cleanly to
ds4-sm120-preview(git apply --checkOK), 2 hunks, +51 lines, 0 deletions:diff
Why Marlin on SM120
ModelOptNvFp4FusedMoE's backend selector picks the Marlin NVFP4 MoE kernel on SM120 (compute 12.0), avoiding the SM100-only TMEM/tcgen05CUTLASS and FlashInfer-TRTLLM paths that aren't available on RTX PRO 6000 / 5090-class parts. This matches the resolution merged upstream in vllm-project#45836.Test evidence
nvidia/DeepSeek-V4-Flash-NVFP4,--kv-cache-dtype fp8./v1/messagesendpoint.AI-assistance disclosure
This change and the investigation behind it were produced with AI assistance (Claude Code). Validation was end-to-end serving on the 2× RTX PRO 6000 host (model load + generation + tool-call), as above; the full pytest kernel/unit suite was not run on this SM120 host. Happy to open a PR against whichever branch you prefer, or to adjust the approach.