[NVIDIA] Support NVFP4 MoE for DeepSeek-V4 (#25820)

This commit is contained in:
Trevor Morris
2026-06-21 19:35:14 -07:00
committed by GitHub
parent 5deca2d39f
commit c0bb04b67f
10 changed files with 385 additions and 17 deletions
@@ -59,6 +59,17 @@ def apply_deepseek_v4_defaults(server_args: ServerArgs, model_arch: str) -> None
f"Setting swa_full_tokens_ratio to {server_args.swa_full_tokens_ratio} for {model_arch}."
)
# nvidia/DeepSeek-V4-Pro-NVFP4 uses flashinfer_trtllm_routed MoE runner backend.
if (
server_args.moe_runner_backend == "auto"
and server_args.get_model_config().nvfp4_moe_meta is not None
):
server_args.moe_runner_backend = "flashinfer_trtllm_routed"
logger.info(
"Use flashinfer_trtllm_routed as MoE runner backend for "
f"{model_arch} hybrid FP8+NVFP4 checkpoint."
)
def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
"""Validate DeepSeek V4 context-parallel configuration."""
+22
View File
@@ -302,6 +302,28 @@ class ModelConfig:
if n_group is not None:
self.hf_config.topk_group = n_group
# Handle hybrid NVFP4 moe (nvidia/DeepSeek-V4-Pro-NVFP4)
self.nvfp4_moe_meta: Optional[dict] = None
hybrid_quant_cfg = getattr(self.hf_config, "quantization_config", None)
if hybrid_quant_cfg is not None and not isinstance(hybrid_quant_cfg, dict):
hybrid_quant_cfg = hybrid_quant_cfg.to_dict()
if (
hybrid_quant_cfg is not None
and str(hybrid_quant_cfg.get("quant_algo", "")).upper() == "MIXED_PRECISION"
and str(hybrid_quant_cfg.get("moe_quant_algo", "")).upper() == "NVFP4"
and hybrid_quant_cfg.get("group_size") is not None
):
self.nvfp4_moe_meta = {
"group_size": int(hybrid_quant_cfg["group_size"]),
"exclude_modules": list(hybrid_quant_cfg.get("ignore") or []),
}
logger.info(
"Auto-detected hybrid FP8+NVFP4 checkpoint "
"(NVFP4 MoE group_size=%d, %d exclude_modules)",
self.nvfp4_moe_meta["group_size"],
len(self.nvfp4_moe_meta["exclude_modules"]),
)
# Check model type
self.attention_chunk_size = getattr(
self.hf_text_config, "attention_chunk_size", None
+11 -1
View File
@@ -65,7 +65,14 @@ class HashTopK(nn.Module):
)
self._init_default_tid2eid()
assert not apply_routed_scaling_factor_on_output, "not implemented"
self.apply_routed_scaling_factor_on_output = (
apply_routed_scaling_factor_on_output
)
if apply_routed_scaling_factor_on_output and num_fused_shared_experts > 0:
raise NotImplementedError(
"HashTopK + apply_routed_scaling_factor_on_output is not supported "
"with fused shared experts; pass --disable-shared-experts-fusion."
)
def _init_default_tid2eid(self) -> None:
topk = self.tid2eid.shape[1]
@@ -188,6 +195,9 @@ class HashTopK(nn.Module):
if _is_hip or _is_npu:
topk_weights = topk_weights.to(torch.float32)
if self.apply_routed_scaling_factor_on_output:
topk_weights = topk_weights * self.routed_scaling_factor
log2phy_prob = None
if (
expert_location_dispatch_info is not None
@@ -3,7 +3,7 @@ from __future__ import annotations
import contextvars
from contextlib import contextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING, Generator, cast
from typing import TYPE_CHECKING, Generator, Optional, cast
import torch
from torch.nn import Module
@@ -857,6 +857,8 @@ class FlashInferTrtllmFp4MoeQuantInfo(MoeQuantInfo):
routing_method_type: int
use_per_token_activation: bool = False
gemm1_clamp_limit: Optional[torch.Tensor] = None
def quantize_hidden_states_fp4(
hidden_states: torch.Tensor,
@@ -953,18 +955,6 @@ def fused_experts_none_to_flashinfer_trtllm_fp4(
runner_config.activation, is_gated=runner_config.is_gated
)
# Build per-expert clamp-limit tensor from the per-layer scalar.
_clamp_val = runner_config.gemm1_clamp_limit
if _clamp_val is not None:
gemm1_clamp_limit = torch.full(
(quant_info.local_num_experts,),
_clamp_val,
dtype=torch.float32,
device=hs_fp4.device,
)
else:
gemm1_clamp_limit = None
# Fall back to routed path when topk was already materialized (e.g. sigmoid routing).
if not use_routed_topk and TopKOutputChecker.format_is_standard(topk_output):
use_routed_topk = True
@@ -1020,7 +1010,7 @@ def fused_experts_none_to_flashinfer_trtllm_fp4(
gemm1_bias=None,
gemm1_alpha=None,
gemm1_beta=None,
gemm1_clamp_limit=gemm1_clamp_limit,
gemm1_clamp_limit=quant_info.gemm1_clamp_limit,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale.view(torch.float8_e4m3fn),
gemm2_bias=None,
@@ -1060,7 +1050,7 @@ def fused_experts_none_to_flashinfer_trtllm_fp4(
gemm1_bias=None,
gemm1_alpha=None,
gemm1_beta=None,
gemm1_clamp_limit=gemm1_clamp_limit,
gemm1_clamp_limit=quant_info.gemm1_clamp_limit,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale.view(torch.float8_e4m3fn),
gemm2_bias=None,
@@ -41,6 +41,7 @@ from sglang.srt.layers.quantization.fp4_utils import (
fp4_quantize,
get_fp4_gemm_runner_backend,
)
from sglang.srt.layers.quantization.fp8 import Fp8Config
from sglang.srt.layers.quantization.fp8_kernel import scaled_fp8_quant
from sglang.srt.layers.quantization.fp8_utils import (
apply_fp8_linear,
@@ -1354,6 +1355,45 @@ class ModelOptFp4Config(ModelOptQuantConfig):
)
class HybridFp8NvFp4Config(Fp8Config):
"""FP8 (linear/attention/MTP MoE) + NVFP4 (FusedMoE) hybrid quantization.
For checkpoints like nvidia/DeepSeek-V4-Pro-NVFP4 where
config.json:quantization_config declares quant_method=fp8 and
moe_quant_algo=NVFP4. FusedMoE layers route through
ModelOptNvFp4FusedMoEMethod; linear / attention layers
delegate to the inherited Fp8Config dispatch.
"""
def __init__(self, fp8_config: Fp8Config, nvfp4_config: ModelOptFp4Config):
# Inherit all of fp8_config's state without re-running its
# validation / logging (already happened at fp8_config build time).
self.__dict__.update(fp8_config.__dict__)
self.nvfp4_config = nvfp4_config
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
if isinstance(layer, FusedMoE):
if not self.nvfp4_config.is_layer_excluded(prefix):
return ModelOptNvFp4FusedMoEMethod(self.nvfp4_config)
# Fall back to MXFP4 for MTP MoE layers
if self.is_fp4_experts:
from sglang.srt.layers.quantization.fp8 import Fp8MoEMethod
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
Mxfp4FlashinferTrtllmMoEMethod,
)
return Mxfp4FlashinferTrtllmMoEMethod(Fp8MoEMethod(self), prefix=prefix)
return super().get_quant_method(layer, prefix)
def apply_weight_name_mapper(self, hf_to_sglang_mapper: WeightsMapper):
super().apply_weight_name_mapper(hf_to_sglang_mapper)
self.nvfp4_config.apply_weight_name_mapper(hf_to_sglang_mapper)
class ModelOptFp4LinearMethod(LinearMethodBase):
"""Linear method for NVFP4.
Supports loading NVFP4 checkpoints with the following structure:
@@ -2053,6 +2093,18 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
(1 / w2_input_scale).to(torch.float32),
)
swiglu_limit = layer.moe_runner_config.swiglu_limit
if (
swiglu_limit is not None
and layer.moe_runner_config.is_gated
and self.enable_flashinfer_trtllm_moe
):
copy_or_rebind_param(
layer,
"gemm1_clamp_limit",
(swiglu_limit / layer.g1_alphas).to(torch.float32),
)
# TODO: for flashinfer always do MOE_NVFP4_DISPATCH
layer.dispatcher.set_quant_config(
{
@@ -2338,6 +2390,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
layer, "routing_method_type", RoutingMethodType.Default
)
gemm1_clamp = getattr(layer, "gemm1_clamp_limit", None)
quant_info = FlashInferTrtllmFp4MoeQuantInfo(
w13_weight=layer.w13_weight.data,
w2_weight=layer.w2_weight.data,
@@ -2353,6 +2406,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
intermediate_size_per_partition=layer.intermediate_size_per_partition,
routing_method_type=routing_method_type,
use_per_token_activation=self.quant_config.use_per_token_activation,
gemm1_clamp_limit=gemm1_clamp.data if gemm1_clamp is not None else None,
)
return self.runner.run(dispatch_output, quant_info)
+21
View File
@@ -245,6 +245,27 @@ def _get_quantization_config(
if isinstance(quant_config, Fp8Config):
quant_config.is_fp4_experts = model_config.is_fp4_experts
# Handle hybrid NVFP4 moe (nvidia/DeepSeek-V4-Pro-NVFP4)
nvfp4_meta = model_config.nvfp4_moe_meta
if nvfp4_meta is not None:
from sglang.srt.layers.quantization.modelopt_quant import (
HybridFp8NvFp4Config,
ModelOptFp4Config,
)
# MTP MoE layers (model.decoder.*) are not NVFP4 quantized.
nvfp4_exclude_modules = list(
nvfp4_meta.get("exclude_modules") or []
) + ["model.decoder.*"]
nvfp4_config = ModelOptFp4Config(
is_checkpoint_nvfp4_serialized=True,
group_size=int(nvfp4_meta["group_size"]),
exclude_modules=nvfp4_exclude_modules,
packed_modules_mapping=quant_config.packed_modules_mapping,
)
quant_config = HybridFp8NvFp4Config(
fp8_config=quant_config, nvfp4_config=nvfp4_config
)
if not _is_npu:
major, minor = get_device_capability()
+5 -1
View File
@@ -2394,7 +2394,11 @@ class DeepseekV4ForCausalLM(nn.Module):
assert len(cache_wqkv_a_weight) == 0, cache_wqkv_a_weight.keys()
unloaded_params = params_dict.keys() - loaded_params
skipped_checking_patterns = ["attn_mqa.k_scale", "attn_mqa.v_scale"]
skipped_checking_patterns = [
"attn_mqa.k_scale",
"attn_mqa.v_scale",
"blockscale_swizzled",
]
if not self.pp_group.is_first_rank:
skipped_checking_patterns.append("embed_tokens")
if not self.pp_group.is_last_rank: