A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 2, 23:15 UTC
Add NVFP4 MOE support for Deepseek V4. (#42209)
Signed-off-by: Shiyang Chen <shiychen@nvidia.com>
sychen52 committed
May 22, 2026 at 07:21 UTC
fb21d8b4f9027f4642637c7bb0acc08c29dce387
9 files changed
+217
-17
.buildkite/test_areas/kernels.yaml
+7
-3
@@ -150,9 +150,12 @@ steps:
150
- csrc/quantization/fp4/
151
- csrc/attention/mla/
152
- csrc/quantization/cutlass_w8a8/moe/
153
- - vllm/model_executor/layers/fused_moe/cutlass_moe.py
154
- - vllm/model_executor/layers/fused_moe/flashinfer_cutlass_moe.py
155
- - vllm/model_executor/layers/fused_moe/flashinfer_a2a_prepare_finalize.py
153
+ - vllm/model_executor/layers/fused_moe/experts/cutlass_moe.py
154
+ - vllm/model_executor/layers/fused_moe/experts/flashinfer_cutlass_moe.py
155
+ - vllm/model_executor/layers/fused_moe/experts/trtllm_nvfp4_moe.py
156
+ - vllm/model_executor/layers/fused_moe/oracle/nvfp4.py
157
+ - vllm/model_executor/layers/fused_moe/prepare_finalize/flashinfer_nvlink_one_sided.py
158
+ - vllm/model_executor/layers/fused_moe/prepare_finalize/flashinfer_nvlink_two_sided.py
159
- vllm/model_executor/layers/quantization/utils/flashinfer_utils.py
160
- vllm/v1/attention/backends/flashinfer.py
161
- vllm/v1/attention/backends/mla/cutlass_mla.py
@@ -185,6 +188,7 @@ steps:
188
- pytest -v -s tests/kernels/moe/test_ocp_mx_moe.py
189
- pytest -v -s tests/kernels/moe/test_flashinfer.py
190
- pytest -v -s tests/kernels/moe/test_flashinfer_moe.py
191
+ - pytest -v -s tests/kernels/moe/test_trtllm_nvfp4_moe.py
192
- pytest -v -s tests/kernels/moe/test_cutedsl_moe.py
193
# e2e
194
- pytest -v -s tests/models/quantization/test_nvfp4.py
tests/kernels/moe/test_trtllm_nvfp4_moe.py
+76
-7
@@ -22,6 +22,8 @@ from tests.kernels.quantization.nvfp4_utils import (
22
from tests.kernels.utils import torch_moe
23
from vllm import _custom_ops as ops
24
from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
25
+from vllm.model_executor.custom_op import CustomOp, op_registry
26
+from vllm.model_executor.layers.activation import SiluAndMulWithClamp
27
from vllm.model_executor.layers.fused_moe import fused_topk
28
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
29
from vllm.model_executor.layers.fused_moe.all2all_utils import (
@@ -58,15 +60,45 @@ MNK_FACTORS = [
60
(64, 704, 4096),
61
]
62
63
+_SWIGLU_LIMIT = 0.1
64
+_LARGE_OUTPUT1_SCALE = 32768.0
65
+_CLAMP_OP_NAME = "test_silu_and_mul_with_clamp"
66
+
67
+# Test-only fixed-limit clamp. ``custom_op_name`` makes the class itself
68
+# valid as an ``activation=`` argument to ``torch_moe`` (which only looks
69
+# up ``activation.custom_op_name`` in ``op_registry``), so no
70
+# ``MoEActivation`` enum extension is needed.
71
+if _CLAMP_OP_NAME not in op_registry:
72
+
73
+ @CustomOp.register(_CLAMP_OP_NAME)
74
+ class _SiluAndMulWithClampTest(SiluAndMulWithClamp):
75
+ custom_op_name = _CLAMP_OP_NAME
76
+
77
+ def __init__(self, *, compile_native: bool = True) -> None:
78
+ super().__init__(_SWIGLU_LIMIT, compile_native=compile_native)
79
+
80
+
81
+SILU_WITH_CLAMP = op_registry[_CLAMP_OP_NAME]
82
+
83
+
84
+ACTIVATION_CASES = [
85
+ pytest.param(MoEActivation.SILU, MoEActivation.SILU, None, id="silu"),
86
+ pytest.param(MoEActivation.SILU, SILU_WITH_CLAMP, _SWIGLU_LIMIT, id="silu_clamp"),
87
+ pytest.param(
88
+ MoEActivation.RELU2_NO_MUL,
89
+ MoEActivation.RELU2_NO_MUL,
90
+ None,
91
+ id="relu2_no_mul",
92
+ ),
93
+ pytest.param(MoEActivation.GELU, MoEActivation.GELU, None, id="gelu"),
94
+]
95
+
96
97
@pytest.mark.parametrize("m,n,k", MNK_FACTORS)
98
@pytest.mark.parametrize("e", [128])
99
@pytest.mark.parametrize("topk", [8])
100
@pytest.mark.parametrize("dtype", [torch.bfloat16])
66
-@pytest.mark.parametrize(
67
- "activation",
68
- [MoEActivation.SILU, MoEActivation.RELU2_NO_MUL, MoEActivation.GELU],
69
-)
101
+@pytest.mark.parametrize("activation,torch_activation,swiglu_limit", ACTIVATION_CASES)
102
@torch.inference_mode()
103
def test_trtllm_fp4_moe_no_graph(
104
m: int,
@@ -76,6 +108,8 @@ def test_trtllm_fp4_moe_no_graph(
108
topk: int,
109
dtype: torch.dtype,
110
activation: MoEActivation,
111
+ torch_activation: MoEActivation | type[SiluAndMulWithClamp],
112
+ swiglu_limit: float | None,
113
workspace_init,
114
):
115
# FlashInfer's trtllm_batched_gemm_runner has no precompiled tile
@@ -113,6 +147,15 @@ def test_trtllm_fp4_moe_no_graph(
147
# Match what oracle/nvfp4.py does for this backend.
148
is_scale_swizzled=False,
149
)
150
+ quant_config.gemm1_clamp_limit = swiglu_limit
151
+ if swiglu_limit is not None:
152
+ assert quant_config.g1_alphas is not None
153
+ assert quant_config.a2_gscale is not None
154
+ assert torch.all(quant_config.a2_gscale == 1)
155
+ # With a2_gscale == 1, g1_alphas is the TRTLLM
156
+ # output1_scale_gate_scalar. Make it large enough to catch
157
+ # clamp/output-scale coupling in the FlashInfer kernel wrapper.
158
+ quant_config.g1_alphas.fill_(_LARGE_OUTPUT1_SCALE)
159
160
score = torch.randn((m, e), device="cuda", dtype=dtype)
161
topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
@@ -133,6 +176,23 @@ def test_trtllm_fp4_moe_no_graph(
176
max_num_tokens=next_power_of_2(m),
177
)
178
179
+ trtllm_inner = TrtLlmNvFp4ExpertsModular(
180
+ moe_config=moe_config, quant_config=quant_config
181
+ )
182
+ # Mimic the production weight-loader path so per-expert tensors that
183
+ # are normally precomputed in process_weights_after_loading (g1_scale_c
184
+ # and the rescaled gemm1_clamp_limit) get materialized. The test's
185
+ # synthetic quant_config has g1_alphas/g2_alphas already at their
186
+ # post-fusion values, so we set w13_weight_scale_2 to alias g1_alphas
187
+ # (same tensor) and use input_scale=1 to make the in-place
188
+ # weight_scale_2 *= input_scale step a no-op.
189
+ fake_layer = torch.nn.Module()
190
+ fake_layer.w13_weight_scale_2 = quant_config.g1_alphas
191
+ fake_layer.w2_weight_scale_2 = quant_config.g2_alphas
192
+ fake_layer.w13_input_scale = torch.ones_like(quant_config.g1_alphas)
193
+ fake_layer.w2_input_scale = torch.ones_like(quant_config.g2_alphas)
194
+ trtllm_inner.process_weights_after_loading(fake_layer)
195
+
196
trtllm_experts = mk.FusedMoEKernel(
197
maybe_make_prepare_finalize(
198
moe=moe_config,
@@ -140,7 +200,7 @@ def test_trtllm_fp4_moe_no_graph(
200
allow_new_interface=True,
201
use_monolithic=False,
202
),
143
- TrtLlmNvFp4ExpertsModular(moe_config=moe_config, quant_config=quant_config),
203
+ trtllm_inner,
204
inplace=False,
205
)
206
@@ -195,7 +255,7 @@ def test_trtllm_fp4_moe_no_graph(
255
)
256
257
torch_output = torch_moe(
198
- a_in_dtype, w1_d, w2_d, score, topk, activation=activation
258
+ a_in_dtype, w1_d, w2_d, score, topk, activation=torch_activation
259
)
260
261
torch.testing.assert_close(torch_output, trtllm_output, atol=2e-1, rtol=2e-1)
@@ -203,5 +263,14 @@ def test_trtllm_fp4_moe_no_graph(
263
264
if __name__ == "__main__":
265
test_trtllm_fp4_moe_no_graph(
206
- 64, 704, 4096, 128, 8, torch.bfloat16, MoEActivation.GELU, None
266
+ 64,
267
+ 704,
268
+ 4096,
269
+ 128,
270
+ 8,
271
+ torch.bfloat16,
272
+ MoEActivation.GELU,
273
+ MoEActivation.GELU,
274
+ None,
275
+ None,
276
)
vllm/model_executor/layers/fused_moe/config.py
+7
@@ -805,6 +805,7 @@ def nvfp4_moe_quant_config(
805
w1_bias: torch.Tensor | None = None,
806
w2_bias: torch.Tensor | None = None,
807
is_scale_swizzled: bool = True,
808
+ gemm1_clamp_limit: float | None = None,
809
) -> FusedMoEQuantConfig:
810
"""
811
Construct a quant config for mxfp4 activations and nvp4 weights.
@@ -823,6 +824,7 @@ def nvfp4_moe_quant_config(
824
per_out_ch_quant=False,
825
block_shape=None,
826
is_scale_swizzled=is_scale_swizzled,
827
+ gemm1_clamp_limit=gemm1_clamp_limit,
828
)
829
830
@@ -1271,6 +1273,11 @@ class FusedMoEConfig:
1273
is_act_and_mul: bool = True
1274
is_lora_enabled: bool = False
1275
1276
+ # SwiGLU clamp limit. When set, backends that do not implement the clamp
1277
+ # are filtered out by `FusedMoEExperts.is_supported_config` so the oracle
1278
+ # cannot silently select one and drop the clamp.
1279
+ swiglu_limit: float | None = None
1280
+
1281
# This flag is used to disable the inplace optimization
1282
# in MoE kernels. If this flag is True then the kernel
1283
# should not be using inplace. If the flag is false, the
vllm/model_executor/layers/fused_moe/experts/trtllm_nvfp4_moe.py
+32
-4
@@ -66,6 +66,17 @@ class TrtLlmNvFp4ExpertsBase:
66
else:
67
self.g1_scale_c = self.quant_config.a2_gscale.clone()
68
69
+ if moe_config.is_act_and_mul and quant_config.gemm1_clamp_limit is not None:
70
+ device = torch.accelerator.current_device_index()
71
+ self.gemm1_clamp_limit = torch.full(
72
+ (self.local_num_experts,),
73
+ quant_config.gemm1_clamp_limit,
74
+ dtype=torch.float32,
75
+ device=device,
76
+ )
77
+ else:
78
+ self.gemm1_clamp_limit = None
79
+
80
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
81
layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
82
layer.w2_weight_scale_2.data.mul_(layer.w2_input_scale)
@@ -84,6 +95,20 @@ class TrtLlmNvFp4ExpertsBase:
95
)
96
self.g1_scale_c = layer.g1_scale_c
97
98
+ # Pre-fold the per-expert g1_alphas (= output1_scale_gate_scalar)
99
+ # division so the TRTLLM kernel receives the raw-GEMM-space clamp
100
+ # directly. g1_alphas is set once here in process_weights_after_loading
101
+ # (via the in-place mul above) and never changes again, so this is a
102
+ # static, per-expert constant. Register on the layer so EPLB
103
+ # rearranges it alongside the other expert tensors.
104
+ if self.gemm1_clamp_limit is not None:
105
+ gemm1_clamp_limit = self.gemm1_clamp_limit / self.quant_config.g1_alphas
106
+ layer.register_parameter(
107
+ "gemm1_clamp_limit",
108
+ torch.nn.Parameter(gemm1_clamp_limit, requires_grad=False),
109
+ )
110
+ self.gemm1_clamp_limit = layer.gemm1_clamp_limit
111
+
112
@staticmethod
113
def _supports_current_device() -> bool:
114
"""Supports only Blackwell-family GPUs."""
@@ -201,6 +226,7 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
226
227
# Pack topk ids and weights into format expected by the kernel.
228
packed_tensor = trtllm_moe_pack_topk_ids_weights(topk_ids, topk_weights)
229
+ output1_scale_gate_scalar = self.quant_config.g1_alphas
230
231
# Invoke kernel.
232
flashinfer.fused_moe.trtllm_fp4_block_scale_routed_moe(
@@ -215,12 +241,12 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
241
gemm1_bias=None,
242
gemm1_alpha=None,
243
gemm1_beta=None,
218
- gemm1_clamp_limit=None,
244
+ gemm1_clamp_limit=self.gemm1_clamp_limit,
245
gemm2_weights=w2,
246
gemm2_weights_scale=self.quant_config.w2_scale.view(torch.float8_e4m3fn),
247
gemm2_bias=None,
248
output1_scale_scalar=self.g1_scale_c,
223
- output1_scale_gate_scalar=self.quant_config.g1_alphas,
249
+ output1_scale_gate_scalar=output1_scale_gate_scalar,
250
output2_scale_scalar=self.quant_config.g2_alphas,
251
num_experts=global_num_experts,
252
top_k=self.topk,
@@ -313,6 +339,8 @@ class TrtLlmNvFp4ExpertsMonolithic(
339
if e_score_correction_bias is not None:
340
e_score_correction_bias = e_score_correction_bias.to(torch.bfloat16)
341
342
+ output1_scale_gate_scalar = self.quant_config.g1_alphas
343
+
344
# Invoke kernel.
345
# NOTE: Activation padding and output
346
# truncation are handled by the MoE runner's
@@ -328,12 +356,12 @@ class TrtLlmNvFp4ExpertsMonolithic(
356
gemm1_bias=None,
357
gemm1_alpha=None,
358
gemm1_beta=None,
331
- gemm1_clamp_limit=None,
359
+ gemm1_clamp_limit=self.gemm1_clamp_limit,
360
gemm2_weights=w2,
361
gemm2_weights_scale=self.quant_config.w2_scale.view(torch.float8_e4m3fn),
362
gemm2_bias=None,
363
output1_scale_scalar=self.g1_scale_c,
336
- output1_scale_gate_scalar=self.quant_config.g1_alphas,
364
+ output1_scale_gate_scalar=output1_scale_gate_scalar,
365
output2_scale_scalar=self.quant_config.g2_alphas,
366
num_experts=global_num_experts,
367
top_k=self.topk,
vllm/model_executor/layers/fused_moe/layer.py
+1
@@ -335,6 +335,7 @@ class FusedMoE(PluggableLayer):
335
activation=self.activation,
336
device=vllm_config.device_config.device,
337
routing_method=self.routing_method_type,
338
+ swiglu_limit=swiglu_limit,
339
# TODO: in_dtype == out_dtype?
340
disable_inplace=disable_inplace() or shared_experts is not None,
341
)
vllm/model_executor/layers/fused_moe/oracle/nvfp4.py
+39
-2
@@ -181,6 +181,15 @@ def select_nvfp4_moe_backend(
181
NvFp4MoeBackend.EMULATION,
182
]
183
184
+ NVFP4_BACKENDS_WITH_CLAMP = {
185
+ NvFp4MoeBackend.FLASHINFER_TRTLLM,
186
+ }
187
+
188
+ if config.swiglu_limit is not None:
189
+ AVAILABLE_BACKENDS = [
190
+ b for b in AVAILABLE_BACKENDS if b in NVFP4_BACKENDS_WITH_CLAMP
191
+ ]
192
+
193
use_batched = config.moe_parallel_config.use_batched_activation_format
194
activation_format = (
195
mk.FusedMoEActivationFormat.BatchedExperts
@@ -234,6 +243,16 @@ def select_nvfp4_moe_backend(
243
and requested_backend == NvFp4MoeBackend.FLASHINFER_CUTEDSL
244
):
245
requested_backend = NvFp4MoeBackend.FLASHINFER_CUTEDSL_BATCHED
246
+ if (
247
+ config.swiglu_limit is not None
248
+ and requested_backend not in NVFP4_BACKENDS_WITH_CLAMP
249
+ ):
250
+ raise ValueError(
251
+ f"Model sets swiglu_limit={config.swiglu_limit}, but the "
252
+ f"explicitly requested moe_backend={runner_backend!r} does "
253
+ f"not apply the SwiGLU clamp. Use 'flashinfer_trtllm' or "
254
+ f"'flashinfer_cutlass' instead."
255
+ )
256
return _return_or_raise(
257
requested_backend, config, weight_key, activation_key, activation_format
258
)
@@ -242,17 +261,32 @@ def select_nvfp4_moe_backend(
261
if not envs.VLLM_USE_FLASHINFER_MOE_FP4:
262
# If the user rejects FlashInfer remove those backends.
263
for b in FLASHINFER_NVFP4_MOE_BACKENDS:
245
- AVAILABLE_BACKENDS.remove(b)
264
+ if b in AVAILABLE_BACKENDS:
265
+ AVAILABLE_BACKENDS.remove(b)
266
267
elif envs.is_set("VLLM_FLASHINFER_MOE_BACKEND"):
268
# If user is explicit about backend, validate it.
269
backend = fi_2_vllm_backend_map[get_flashinfer_moe_backend()]
270
+ if (
271
+ config.swiglu_limit is not None
272
+ and backend not in NVFP4_BACKENDS_WITH_CLAMP
273
+ ):
274
+ raise ValueError(
275
+ f"Model sets swiglu_limit={config.swiglu_limit}, but the "
276
+ f"FlashInfer backend selected via VLLM_FLASHINFER_MOE_BACKEND "
277
+ f"({backend.value}) does not apply the SwiGLU clamp."
278
+ )
279
return _return_or_raise(
280
backend, config, weight_key, activation_key, activation_format
281
)
282
else:
283
# If the user is not explicit about the backend, try each.
255
- for backend in FLASHINFER_NVFP4_MOE_BACKENDS:
284
+ fi_backends = [
285
+ b
286
+ for b in FLASHINFER_NVFP4_MOE_BACKENDS
287
+ if config.swiglu_limit is None or b in NVFP4_BACKENDS_WITH_CLAMP
288
+ ]
289
+ for backend in fi_backends:
290
for k_cls in backend_to_kernel_cls(backend):
291
supported, reason = k_cls.is_supported_config(
292
k_cls,
@@ -439,6 +473,7 @@ def make_nvfp4_moe_quant_config(
473
w2_scale_2: torch.Tensor,
474
a13_scale: torch.Tensor,
475
a2_scale: torch.Tensor,
476
+ swiglu_limit: float | None = None,
477
) -> FusedMoEQuantConfig:
478
if backend == NvFp4MoeBackend.MARLIN:
479
return nvfp4_w4a16_moe_quant_config(
@@ -455,6 +490,7 @@ def make_nvfp4_moe_quant_config(
490
a2_gscale=a2_scale,
491
w1_scale=w13_scale,
492
w2_scale=w2_scale,
493
+ gemm1_clamp_limit=swiglu_limit,
494
)
495
496
# Pass w13_scale_2 / w2_scale_2 directly as g1/g2_alphas.
@@ -478,6 +514,7 @@ def make_nvfp4_moe_quant_config(
514
NvFp4MoeBackend.FLASHINFER_CUTEDSL,
515
)
516
),
517
+ gemm1_clamp_limit=swiglu_limit,
518
)
519
520
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w4a4_nvfp4.py
+1
@@ -258,6 +258,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
258
w2_scale_2=layer.w2_weight_scale_2,
259
a13_scale=layer.w13_input_scale,
260
a2_scale=layer.w2_input_scale,
261
+ swiglu_limit=getattr(layer, "swiglu_limit", None),
262
)
263
264
def apply_monolithic(
vllm/model_executor/layers/quantization/modelopt.py
+1
@@ -1601,6 +1601,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1601
w2_scale_2=layer.w2_weight_scale_2,
1602
a13_scale=layer.w13_input_scale,
1603
a2_scale=layer.w2_input_scale,
1604
+ swiglu_limit=getattr(layer, "swiglu_limit", None),
1605
)
1606
1607
@property
vllm/models/deepseek_v4/quant_config.py
+53
-1
@@ -2,6 +2,10 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Quantization config for DeepSeek V4."""
4
5
+from __future__ import annotations
6
+
7
+from typing import TYPE_CHECKING
8
+
9
from vllm.config import get_current_vllm_config
10
from vllm.model_executor.layers.fused_moe import FusedMoE
11
from vllm.model_executor.layers.fused_moe.layer import UnquantizedFusedMoEMethod
@@ -14,6 +18,11 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
18
19
_DEEPSEEK_V4_EXPERT_DTYPES = ("fp4", "fp8")
20
21
+if TYPE_CHECKING:
22
+ from vllm.model_executor.layers.quantization.modelopt import (
23
+ ModelOptNvFp4Config,
24
+ )
25
+
26
27
class DeepseekV4FP8Config(Fp8Config):
28
"""FP8 config for DeepSeek V4 with expert-dtype-aware MoE dispatch.
@@ -38,6 +47,8 @@ class DeepseekV4FP8Config(Fp8Config):
47
def __init__(self, *args, **kwargs):
48
super().__init__(*args, **kwargs)
49
self._resolved_expert_dtype: str | None = None
50
+ self._resolved_moe_quant_algo: str | None = None
51
+ self._nvfp4_config: ModelOptNvFp4Config | None = None
52
# ``is_scale_e8m0`` is a property that resolves on first read,
53
# by which time the current vllm_config has been set.
54
@@ -70,6 +81,36 @@ class DeepseekV4FP8Config(Fp8Config):
81
# checkpoints (Flash-Base) store them as float32.
82
return self.expert_dtype == "fp4"
83
84
+ def _resolve_moe_overrides(self) -> None:
85
+ if self._resolved_moe_quant_algo is not None:
86
+ return
87
+ try:
88
+ hf_config = get_current_vllm_config().model_config.hf_config
89
+ except Exception:
90
+ return
91
+ quant_cfg = getattr(hf_config, "quantization_config", None) or {}
92
+ algo = (quant_cfg.get("moe_quant_algo") or "").upper() or None
93
+ self._resolved_moe_quant_algo = algo or ""
94
+
95
+ @property
96
+ def moe_quant_algo(self) -> str:
97
+ self._resolve_moe_overrides()
98
+ return self._resolved_moe_quant_algo or ""
99
+
100
+ def _get_nvfp4_config(self) -> ModelOptNvFp4Config:
101
+ if self._nvfp4_config is None:
102
+ from vllm.model_executor.layers.quantization.modelopt import (
103
+ ModelOptNvFp4Config,
104
+ )
105
+
106
+ self._nvfp4_config = ModelOptNvFp4Config(
107
+ is_checkpoint_nvfp4_serialized=True,
108
+ kv_cache_quant_algo=None,
109
+ exclude_modules=[],
110
+ group_size=16,
111
+ )
112
+ return self._nvfp4_config
113
+
114
@classmethod
115
def get_name(cls) -> QuantizationMethods:
116
return "deepseek_v4_fp8"
@@ -97,10 +138,21 @@ class DeepseekV4FP8Config(Fp8Config):
138
):
139
return UnquantizedFusedMoEMethod(layer.moe_config)
140
if self.expert_dtype == "fp4":
141
+ if self.moe_quant_algo == "NVFP4":
142
+ from vllm.model_executor.layers.quantization.modelopt import (
143
+ ModelOptNvFp4FusedMoE,
144
+ )
145
+
146
+ return ModelOptNvFp4FusedMoE(
147
+ quant_config=self._get_nvfp4_config(),
148
+ moe_config=layer.moe_config,
149
+ )
150
return Mxfp4MoEMethod(layer.moe_config)
151
# expert_dtype == "fp8": fall through to Fp8Config which
152
# returns Fp8MoEMethod with block-wise float32 scales.
153
return super().get_quant_method(layer, prefix)
154
155
def is_mxfp4_quant(self, prefix, layer):
106
- return isinstance(layer, FusedMoE) and self.expert_dtype == "fp4"
156
+ if not isinstance(layer, FusedMoE) or self.expert_dtype != "fp4":
157
+ return False
158
+ return self.moe_quant_algo != "NVFP4"