Skip to content

NVFP4 checkpoints: DSpark draft experts (MXFP4) routed through NVFP4 path — 0% acceptance; prefix-aware routing fix + measurements #35

Description

@auroter

Defect

NVFP4 conversions of DeepSeek-V4-Flash (NVIDIA's modelopt cast_mxfp4_to_nvfp4 flow) quantize only the main 43-layer stack; the DSpark draft module keeps its original MXFP4 experts. DeepseekV4FP8Config.get_quant_method (vllm/models/deepseek_v4/quant_config.py) routes every RoutedExperts layer through ModelOptNvFp4FusedMoE whenever the checkpoint declares moe_quant_algo=NVFP4 — the prefix argument is unused — so the draft module's MXFP4 weights are decoded as NVFP4 noise. The engine boots and serves normally, but draft acceptance is 0% and speculative decode collapses below the non-speculative rate (42.3 tok/s vs 124.5 without speculation on our hardware). Teacher-forced perplexity is unaffected, which makes the failure easy to miss.

Fix (verified)

Make the routing prefix-aware: draft-module layers (runtime layer index >= num_hidden_layers, per DeepSeekV4DSparkLayer's runtime_layer_idx = config.num_hidden_layers + dspark_layer_idx) load through Mxfp4MoEMethod; is_mxfp4_quant gives the mirrored answer. Python-only, no recompile.

--- a/vllm/models/deepseek_v4/quant_config.py
+++ b/vllm/models/deepseek_v4/quant_config.py
@@ -5,6 +5,7 @@
 from __future__ import annotations
 
+import re
 from typing import TYPE_CHECKING, cast
 
@@ get_quant_method @@
             if self.expert_dtype == "fp4":
-                if self.moe_quant_algo == "NVFP4":
+                if self.moe_quant_algo == "NVFP4" and not self._is_draft_layer(prefix):
                     from vllm.model_executor.layers.quantization.modelopt import (
                         ModelOptNvFp4FusedMoE,
                     )
 
                     return ModelOptNvFp4FusedMoE(
                         quant_config=self._get_nvfp4_config(),
                         moe_config=layer.moe_config,
                     )
                 return Mxfp4MoEMethod(layer.moe_config)
@@ class DeepseekV4FP8Config @@
+    def _is_draft_layer(self, prefix: str) -> bool:
+        # NVFP4 conversions quantize only the main stack; the speculative
+        # module (runtime layer index >= num_hidden_layers) keeps its
+        # original MXFP4 experts and must load through the MXFP4 path.
+        m = re.search(r"layers\.(\d+)\.", prefix)
+        if m is None:
+            return False
+        try:
+            hf_config = get_current_vllm_config().model_config.hf_config
+        except Exception:
+            return False
+        return int(m.group(1)) >= hf_config.num_hidden_layers
+
     def is_mxfp4_quant(self, prefix, layer):
         if not isinstance(layer, RoutedExperts) or self.expert_dtype != "fp4":
             return False
-        return self.moe_quant_algo != "NVFP4"
+        return self.moe_quant_algo != "NVFP4" or self._is_draft_layer(prefix)

Measurements

4x RTX PRO 6000 Blackwell Workstation (SM120), TP4, tag sm120-pr-41834-stable-preview-20260727d, DeepSeek-V4-Flash-0731 NVFP4, --speculative-config '{"method":"dspark","num_speculative_tokens":5}':

draft acceptance single-stream decode
before 0.0% 42.3 tok/s
after 34–39% 168.7 tok/s
after + VLLM_USE_V2_MODEL_RUNNER=1 41.4% 208.4 tok/s
native MXFP4 checkpoint, same flags (reference) 210.5 tok/s

Perplexity pinned at each backend's baseline in all cells (speculation lossless). Artifact, full gate results, and this patch as a file: https://huggingface.co/auroter/DeepSeek-V4-Flash-0731-NVFP4

Happy to turn this into a PR — the patch applies clean at d64074e.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions