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 14, 05:30 UTC
Support Tensorrt-LLM MoE fp4 for low-latency (#21331)
Signed-off-by: Shu Wang <shuw@nvidia.com> Signed-off-by: Po-Han Huang <pohanh@nvidia.com> Signed-off-by: Shu Wang. <shuw@nvidia.com> Signed-off-by: XIn Li <xinli@nvidia.com> Co-authored-by: XIn Li <xinli@nvidia.com>
Shu Wang committed
Aug 7, 2025 at 21:18 UTC
a3b9c17b56d09a091e210222d8e1f75cabe65b84
7 files changed
+288
-43
vllm/envs.py
+15
@@ -129,6 +129,7 @@ if TYPE_CHECKING:
129
VLLM_SKIP_DEEP_GEMM_WARMUP: bool = False
130
VLLM_USE_FLASHINFER_MOE_FP8: bool = False
131
VLLM_USE_FLASHINFER_MOE_FP4: bool = False
132
+ VLLM_FLASHINFER_MOE_BACKEND: str = "throughput"
133
VLLM_XGRAMMAR_CACHE_MB: int = 0
134
VLLM_MSGPACK_ZERO_COPY_THRESHOLD: int = 256
135
VLLM_ALLOW_INSECURE_SERIALIZATION: bool = False
@@ -982,6 +983,20 @@ environment_variables: dict[str, Callable[[], Any]] = {
983
"VLLM_ALL2ALL_BACKEND":
984
lambda: os.getenv("VLLM_ALL2ALL_BACKEND", "naive"),
985
986
+ # Flashinfer MoE backend for vLLM's fused Mixture-of-Experts support. Both
987
+ # require compute capability 10.0 or above.
988
+ # Available options:
989
+ # - "throughput": [default]
990
+ # Uses CUTLASS kernels optimized for high-throughput batch inference.
991
+ # - "latency":
992
+ # Uses TensorRT-LLM kernels optimized for low-latency inference.
993
+ # To set this backend, define the environment variable:
994
+ # export VLLM_FLASHINFER_MOE_BACKEND=latency.
995
+ # If not set, defaults to "throughput".
996
+ "VLLM_FLASHINFER_MOE_BACKEND": lambda: os.getenv(
997
+ "VLLM_FLASHINFER_MOE_BACKEND", "throughput"
998
+ ),
999
+
1000
# Control the maximum number of tokens per expert supported by the
1001
# NVFP4 MoE CUTLASS Kernel. This value is used to create a buffer for
1002
# the blockscale tensor of activations NVFP4 Quantization.
vllm/model_executor/layers/fused_moe/config.py
+2
-1
@@ -192,7 +192,8 @@ class FusedMoEParallelConfig:
192
@property
193
def use_flashinfer_cutlass_kernels(self):
194
return (envs.VLLM_USE_FLASHINFER_MOE_FP4
195
- and has_flashinfer_cutlass_fused_moe())
195
+ and has_flashinfer_cutlass_fused_moe()
196
+ and envs.VLLM_FLASHINFER_MOE_BACKEND == "throughput")
197
198
@staticmethod
199
def make(tp_size_: int, dp_size_: int,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe.py
+4
-5
@@ -105,7 +105,7 @@ class CompressedTensorsW4A4MoeMethod(CompressedTensorsMoEMethod):
105
detect_nvfp4_moe_support)
106
_nvfp4 = detect_nvfp4_moe_support(self.__class__.__name__)
107
self.cutlass_nvfp4_supported = _nvfp4.cutlass_supported
108
- self.allow_flashinfer_cutlass = _nvfp4.allow_flashinfer_cutlass
108
+ self.allow_flashinfer = _nvfp4.allow_flashinfer
109
self.use_marlin = _nvfp4.use_marlin
110
self.group_size = 16
111
self.fused_experts = None # type: ignore[assignment]
@@ -212,7 +212,7 @@ class CompressedTensorsW4A4MoeMethod(CompressedTensorsMoEMethod):
212
requires_grad=False)
213
214
# reorder GEMM1 weights and block scales for FlashInfer CUTLASS kernel.
215
- if self.allow_flashinfer_cutlass:
215
+ if self.allow_flashinfer:
216
w, s = reorder_w1w3_to_w3w1(layer.w13_weight.data,
217
layer.w13_weight_scale.data,
218
dim=-2)
@@ -266,7 +266,7 @@ class CompressedTensorsW4A4MoeMethod(CompressedTensorsMoEMethod):
266
(layer.w2_input_global_scale), requires_grad=False)
267
268
def maybe_swap_experts_impl(self, moe_parallel_config):
269
- if not self.allow_flashinfer_cutlass:
269
+ if not self.allow_flashinfer:
270
return
271
self.fused_experts = build_flashinfer_fp4_cutlass_moe_kernel(
272
moe_parallel_config)
@@ -277,8 +277,7 @@ class CompressedTensorsW4A4MoeMethod(CompressedTensorsMoEMethod):
277
from vllm.model_executor.layers.quantization.utils.flashinfer_fp4_moe import ( # noqa: E501
278
select_nvfp4_gemm_impl)
279
280
- return select_nvfp4_gemm_impl(self.allow_flashinfer_cutlass, moe,
281
- logger)
280
+ return select_nvfp4_gemm_impl(self.allow_flashinfer, moe, logger)
281
282
def apply(
283
self,
vllm/model_executor/layers/quantization/modelopt.py
+252
-32
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from enum import Enum
5
from typing import Any, Callable, Optional, Union
6
7
import torch
@@ -36,6 +37,7 @@ from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
37
from vllm.model_executor.parameter import (ModelWeightParameter,
38
PerTensorScaleParameter)
39
from vllm.scalar_type import scalar_types
40
+from vllm.utils import next_power_of_2
41
from vllm.utils.flashinfer import has_flashinfer_moe
42
43
logger = init_logger(__name__)
@@ -44,6 +46,11 @@ QUANT_ALGOS = ["FP8", "NVFP4"]
46
KV_CACHE_QUANT_ALGOS = ["FP8"]
47
48
49
+class FlashinferMoeBackend(Enum):
50
+ TENSORRT_LLM = "TensorRT-LLM"
51
+ CUTLASS = "CUTLASS"
52
+
53
+
54
class ModelOptFp8Config(QuantizationConfig):
55
"""Config class for ModelOpt FP8."""
56
@@ -185,7 +192,7 @@ class ModelOptFp8LinearMethod(LinearMethodBase):
192
Args: quant_config: The ModelOpt quantization config.
193
"""
194
188
- def __init__(self, quant_config: ModelOptFp8Config):
195
+ def __init__(self, quant_config: ModelOptFp8Config) -> None:
196
self.quant_config = quant_config
197
self.fp8_linear = Fp8LinearOp(
198
act_quant_static=True, act_quant_group_shape=GroupShape.PER_TENSOR)
@@ -265,7 +272,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
272
quant_config: The ModelOpt quantization config.
273
"""
274
268
- def __init__(self, quant_config: ModelOptFp8Config):
275
+ def __init__(self, quant_config: ModelOptFp8Config) -> None:
276
self.quant_config = quant_config
277
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
278
cutlass_fp8_supported)
@@ -670,7 +677,8 @@ class ModelOptNvFp4Config(QuantizationConfig):
677
return cls(is_checkpoint_nvfp4_serialized, kv_cache_quant_algo,
678
exclude_modules, group_size)
679
673
- def is_layer_excluded(self, prefix: str, exclude_modules: list):
680
+ def is_layer_excluded(self, prefix: str,
681
+ exclude_modules: list[str]) -> bool:
682
import regex as re
683
for pattern in exclude_modules:
684
regex_str = pattern.replace('.', r'\.').replace('*', r'.*')
@@ -714,7 +722,7 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
722
Args: quant_config: The ModelOpt quantization config.
723
"""
724
717
- def __init__(self, quant_config: ModelOptNvFp4Config):
725
+ def __init__(self, quant_config: ModelOptNvFp4Config) -> None:
726
self.quant_config = quant_config
727
self.cutlass_nvfp4_supported = cutlass_fp4_supported()
728
self.use_marlin = False
@@ -859,6 +867,16 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
867
return out.view(*output_shape)
868
869
870
+def _get_tile_tokens_dim(num_tokens: int, top_k: int, num_experts: int) -> int:
871
+ # Guess tokens per expert assuming perfect expert distribution first.
872
+ num_tokens_per_expert = (num_tokens * top_k) // num_experts
873
+ # And pad the number to the next power of 2.
874
+ tile_tokens_dim = next_power_of_2(num_tokens_per_expert)
875
+ # Cap to 8-64 tokens per CTA tile as it's the range supported by the kernel.
876
+ tile_tokens_dim = min(max(tile_tokens_dim, 8), 64)
877
+ return tile_tokens_dim
878
+
879
+
880
class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
881
"""
882
MoE Method for FP4 Quantization.
@@ -866,22 +884,40 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
884
quant_config: NVFP4 Quant Config
885
"""
886
869
- def __init__(self, quant_config: ModelOptNvFp4Config):
887
+ def __init__(self, quant_config: ModelOptNvFp4Config) -> None:
888
self.quant_config = quant_config
889
from vllm.model_executor.layers.quantization.utils.nvfp4_moe_support import ( # noqa: E501
890
detect_nvfp4_moe_support)
891
_nvfp4 = detect_nvfp4_moe_support(self.__class__.__name__)
892
self.cutlass_nvfp4_supported = _nvfp4.cutlass_supported
875
- self.allow_flashinfer_cutlass = _nvfp4.allow_flashinfer_cutlass
893
+ self.allow_flashinfer = _nvfp4.allow_flashinfer
894
self.use_marlin = _nvfp4.use_marlin
895
+ self.flashinfer_moe_backend = None
896
+
897
+ if self.allow_flashinfer:
898
+ flashinfer_moe_backend = envs.VLLM_FLASHINFER_MOE_BACKEND
899
+ if flashinfer_moe_backend == "throughput":
900
+ self.flashinfer_moe_backend = FlashinferMoeBackend.CUTLASS
901
+ logger.info_once("Using FlashInfer CUTLASS kernels for "
902
+ "ModelOptNvFp4FusedMoE.")
903
+ elif flashinfer_moe_backend == "latency":
904
+ self.flashinfer_moe_backend = FlashinferMoeBackend.TENSORRT_LLM
905
+ logger.info_once("Using FlashInfer TensorRT-LLM kernels for "
906
+ "ModelOptNvFp4FusedMoE.")
907
+ else:
908
+ allowed_backends = ["throughput", "latency"]
909
+ raise ValueError(
910
+ f"Unknown flashinfer moe backend: {flashinfer_moe_backend}"
911
+ f" expected one of {allowed_backends}")
912
878
- self.fused_experts = None # type: ignore
913
+ self.fused_experts: Optional[
914
+ mk.FusedMoEModularKernel] = None # type: ignore[assignment]
915
916
def maybe_swap_experts_impl(
917
self,
918
moe_parallel_config: FusedMoEParallelConfig,
919
):
884
- if not self.allow_flashinfer_cutlass:
920
+ if not self.allow_flashinfer:
921
return
922
self.fused_experts = build_flashinfer_fp4_cutlass_moe_kernel(
923
moe_parallel_config)
@@ -897,8 +933,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
933
from vllm.model_executor.layers.quantization.utils.flashinfer_fp4_moe import ( # noqa: E501
934
select_nvfp4_gemm_impl)
935
900
- return select_nvfp4_gemm_impl(self.allow_flashinfer_cutlass, moe,
901
- logger)
936
+ return select_nvfp4_gemm_impl(self.allow_flashinfer, moe, logger)
937
938
def uses_weight_scale_2_pattern(self) -> bool:
939
"""
@@ -996,14 +1031,101 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1031
weight_loader=weight_loader)
1032
layer.register_parameter("w2_input_scale", w2_input_scale)
1033
1034
+ def prepare_static_weight_layouts_for_trtllm_moe(
1035
+ self,
1036
+ gemm1_weights: torch.Tensor,
1037
+ gemm2_weights: torch.Tensor,
1038
+ gemm1_scales_linear_fp4_bytes: torch.Tensor,
1039
+ gemm2_scales_linear_fp4_bytes: torch.Tensor,
1040
+ hidden_size: int,
1041
+ intermediate_size: int,
1042
+ num_experts: int,
1043
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
1044
+ """Prepare quantized weights for kernel (done offline with weights)."""
1045
+ from flashinfer import (reorder_rows_for_gated_act_gemm,
1046
+ shuffle_matrix_a, shuffle_matrix_sf_a)
1047
+ epilogue_tile_m = 128 # FIXME: this depends on the kernel internals
1048
+
1049
+ # Convert quantized weights to proper formats
1050
+ gemm1_weights_fp4 = gemm1_weights.view(torch.float8_e4m3fn).reshape(
1051
+ num_experts, 2 * intermediate_size, hidden_size // 2) # packed fp4
1052
+ gemm1_scales_linear_fp4 = gemm1_scales_linear_fp4_bytes.view(
1053
+ torch.float8_e4m3fn).reshape(num_experts, 2 * intermediate_size,
1054
+ hidden_size //
1055
+ 16) # fp8 scaling factors
1056
+
1057
+ gemm2_weights_fp4 = gemm2_weights.view(torch.float8_e4m3fn).reshape(
1058
+ num_experts, hidden_size, intermediate_size // 2) # packed fp4
1059
+ gemm2_scales_linear_fp4 = gemm2_scales_linear_fp4_bytes.view(
1060
+ torch.float8_e4m3fn).reshape(num_experts, hidden_size,
1061
+ intermediate_size //
1062
+ 16) # fp8 scaling factors
1063
+
1064
+ # Reorder rows of W1 and scales for fused gated activation
1065
+ gemm1_weights_fp4_interleaved = []
1066
+ gemm1_scales_fp4_interleaved = []
1067
+ for i in range(num_experts):
1068
+ gemm1_weights_fp4_interleaved.append(
1069
+ reorder_rows_for_gated_act_gemm(gemm1_weights_fp4[i].clone()))
1070
+ gemm1_scales_fp4_interleaved.append(
1071
+ reorder_rows_for_gated_act_gemm(
1072
+ gemm1_scales_linear_fp4[i].clone()))
1073
+
1074
+ # Stack weights and scales for all experts
1075
+ gemm1_weights_fp4_interleaved = torch.stack(
1076
+ gemm1_weights_fp4_interleaved).reshape(num_experts,
1077
+ 2 * intermediate_size,
1078
+ hidden_size // 2)
1079
+ gemm1_scales_fp4_interleaved = torch.stack(
1080
+ gemm1_scales_fp4_interleaved).reshape(num_experts,
1081
+ 2 * intermediate_size,
1082
+ hidden_size // 16)
1083
+
1084
+ # Shuffle weights and scaling factors for transposed mma output
1085
+ gemm1_weights_fp4_shuffled = []
1086
+ gemm1_scales_fp4_shuffled = []
1087
+ gemm2_weights_fp4_shuffled = []
1088
+ gemm2_scales_fp4_shuffled = []
1089
+ for i in range(num_experts):
1090
+ gemm1_weights_fp4_shuffled.append(
1091
+ shuffle_matrix_a(
1092
+ gemm1_weights_fp4_interleaved[i].view(torch.uint8),
1093
+ epilogue_tile_m))
1094
+ gemm1_scales_fp4_shuffled.append(
1095
+ shuffle_matrix_sf_a(
1096
+ gemm1_scales_fp4_interleaved[i].view(torch.uint8),
1097
+ epilogue_tile_m))
1098
+
1099
+ gemm2_weights_fp4_shuffled.append(
1100
+ shuffle_matrix_a(gemm2_weights_fp4[i].view(torch.uint8),
1101
+ epilogue_tile_m))
1102
+ gemm2_scales_fp4_shuffled.append(
1103
+ shuffle_matrix_sf_a(
1104
+ gemm2_scales_linear_fp4[i].view(torch.uint8),
1105
+ epilogue_tile_m))
1106
+
1107
+ # Stack weights for all experts
1108
+ gemm1_weights_fp4_shuffled = torch.stack(gemm1_weights_fp4_shuffled)
1109
+ gemm1_scales_fp4_shuffled = (
1110
+ torch.stack(gemm1_scales_fp4_shuffled).view(
1111
+ torch.float8_e4m3fn).reshape(num_experts,
1112
+ 2 * intermediate_size,
1113
+ hidden_size // 16))
1114
+
1115
+ gemm2_weights_fp4_shuffled = torch.stack(gemm2_weights_fp4_shuffled)
1116
+ gemm2_scales_fp4_shuffled = (
1117
+ torch.stack(gemm2_scales_fp4_shuffled).view(
1118
+ torch.float8_e4m3fn).reshape(num_experts, hidden_size,
1119
+ intermediate_size // 16))
1120
+ return (gemm1_weights_fp4_shuffled, gemm1_scales_fp4_shuffled,
1121
+ gemm2_weights_fp4_shuffled, gemm2_scales_fp4_shuffled)
1122
+
1123
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1000
- # GEMM 1
1001
- # The FlashInfer Cutlass fused MoE kernel expects the combined weights
1002
- # to be ordered as [w3, w1], unlike the standard [w1, w3] layout.
1124
+ # GEMM 1 processing
1125
gemm1_weight = layer.w13_weight.data
1126
gemm1_weight_scale = layer.w13_weight_scale.data
1127
1006
- if self.allow_flashinfer_cutlass:
1128
+ if self.allow_flashinfer:
1129
gemm1_weight, gemm1_weight_scale = reorder_w1w3_to_w3w1(
1130
gemm1_weight, gemm1_weight_scale, dim=-2)
1131
@@ -1011,6 +1133,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1133
layer.w13_weight_scale = Parameter(gemm1_weight_scale,
1134
requires_grad=False)
1135
1136
+ # Common processing for w13_weight_scale_2
1137
if not torch.allclose(layer.w13_weight_scale_2[:, 0],
1138
layer.w13_weight_scale_2[:, 1]):
1139
logger.warning_once(
@@ -1021,26 +1144,18 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1144
layer.w13_weight_scale_2 = Parameter(w13_weight_scale_2,
1145
requires_grad=False)
1146
1147
+ # Common processing for input scales and alphas
1148
w13_input_scale = layer.w13_input_scale.max(dim=1).values.to(
1149
torch.float32)
1150
layer.g1_alphas = Parameter(
1151
(w13_input_scale * w13_weight_scale_2).to(torch.float32),
1152
requires_grad=False)
1153
1030
- assert (layer.w13_weight_scale.shape[2] % 16 == 0), (
1031
- "Expected weight_scale.dim(1) to be divisible by 16")
1032
- assert (layer.w13_weight_scale.dtype == torch.float8_e4m3fn), (
1033
- "Weight Blockscale must be represented as FP8-E4M3")
1034
- w13_blockscale_swizzled = swizzle_blockscale(layer.w13_weight_scale)
1035
-
1036
- layer.w13_blockscale_swizzled = Parameter(w13_blockscale_swizzled,
1037
- requires_grad=False)
1038
-
1154
# This is for quantization, so we need to invert it.
1155
layer.w13_input_scale_quant = Parameter(
1156
(1 / w13_input_scale).to(torch.float32), requires_grad=False)
1157
1043
- # GEMM 2
1158
+ # GEMM 2 processing
1159
layer.g2_alphas = Parameter(
1160
(layer.w2_input_scale * layer.w2_weight_scale_2).to(torch.float32),
1161
requires_grad=False)
@@ -1049,15 +1164,63 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1164
layer.w2_input_scale_quant = Parameter(
1165
(1 / layer.w2_input_scale).to(torch.float32), requires_grad=False)
1166
1052
- assert (layer.w2_weight_scale.shape[2] % 16 == 0), (
1053
- "Expected weight_scale.dim(1) to be divisible by 16")
1054
- assert (layer.w2_weight_scale.dtype == torch.float8_e4m3fn), (
1055
- "Weight Blockscale must be represented as FP8-E4M3")
1056
- w2_blockscale_swizzled = swizzle_blockscale(layer.w2_weight_scale)
1167
+ # TensorRT-LLM specific processing
1168
+ if self.allow_flashinfer and \
1169
+ self.flashinfer_moe_backend == FlashinferMoeBackend.TENSORRT_LLM:
1170
+ # Prepare static weights for TRT-LLM kernel
1171
+ (gemm1_weights_fp4_shuffled, gemm1_scales_fp4_shuffled,
1172
+ gemm2_weights_fp4_shuffled, gemm2_scales_fp4_shuffled
1173
+ ) = self.prepare_static_weight_layouts_for_trtllm_moe(
1174
+ layer.w13_weight,
1175
+ layer.w2_weight,
1176
+ layer.w13_weight_scale,
1177
+ layer.w2_weight_scale,
1178
+ layer.w2_weight.size(-2), # hidden_size
1179
+ layer.w13_weight.size(-2) // 2, # intermediate_size
1180
+ layer.w13_weight.size(0), # num_experts
1181
+ )
1182
+
1183
+ layer.gemm1_weights_fp4_shuffled = Parameter(
1184
+ gemm1_weights_fp4_shuffled, requires_grad=False)
1185
+ layer.gemm2_weights_fp4_shuffled = Parameter(
1186
+ gemm2_weights_fp4_shuffled, requires_grad=False)
1187
+ layer.gemm1_scales_fp4_shuffled = Parameter(
1188
+ gemm1_scales_fp4_shuffled, requires_grad=False)
1189
+ layer.gemm2_scales_fp4_shuffled = Parameter(
1190
+ gemm2_scales_fp4_shuffled, requires_grad=False)
1191
+
1192
+ # Additional parameter needed for TRT-LLM
1193
+ layer.g1_scale_c = Parameter(
1194
+ (layer.w2_input_scale_quant * layer.g1_alphas).to(
1195
+ torch.float32),
1196
+ requires_grad=False,
1197
+ )
1198
1058
- layer.w2_blockscale_swizzled = Parameter(w2_blockscale_swizzled,
1059
- requires_grad=False)
1060
- layer.w2_weight = Parameter(layer.w2_weight.data, requires_grad=False)
1199
+ # Clean up weights that won't be used by TRT-LLM
1200
+ del layer.w2_weight
1201
+ del layer.w2_weight_scale
1202
+ del layer.w13_weight
1203
+ del layer.w13_weight_scale
1204
+ else:
1205
+ # Non-TRT-LLM processing (Cutlass or non-flashinfer)
1206
+ assert (layer.w13_weight_scale.shape[2] % 16 == 0), (
1207
+ "Expected weight_scale.dim(1) to be divisible by 16")
1208
+ assert (layer.w13_weight_scale.dtype == torch.float8_e4m3fn), (
1209
+ "Weight Blockscale must be represented as FP8-E4M3")
1210
+ w13_blockscale_swizzled = swizzle_blockscale(
1211
+ layer.w13_weight_scale)
1212
+ layer.w13_blockscale_swizzled = Parameter(w13_blockscale_swizzled,
1213
+ requires_grad=False)
1214
+
1215
+ assert (layer.w2_weight_scale.shape[2] % 16 == 0), (
1216
+ "Expected weight_scale.dim(1) to be divisible by 16")
1217
+ assert (layer.w2_weight_scale.dtype == torch.float8_e4m3fn), (
1218
+ "Weight Blockscale must be represented as FP8-E4M3")
1219
+ w2_blockscale_swizzled = swizzle_blockscale(layer.w2_weight_scale)
1220
+ layer.w2_blockscale_swizzled = Parameter(w2_blockscale_swizzled,
1221
+ requires_grad=False)
1222
+ layer.w2_weight = Parameter(layer.w2_weight.data,
1223
+ requires_grad=False)
1224
1225
if self.use_marlin:
1226
prepare_moe_fp4_layer_for_marlin(layer)
@@ -1095,6 +1258,60 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1258
"EPLB not supported for `ModelOptNvFp4FusedMoE` yet.")
1259
assert activation == "silu", "Only SiLU activation is supported."
1260
1261
+ if self.allow_flashinfer and \
1262
+ self.flashinfer_moe_backend == FlashinferMoeBackend.TENSORRT_LLM:
1263
+ import flashinfer
1264
+
1265
+ from vllm.model_executor.models.llama4 import Llama4MoE
1266
+
1267
+ a1_gscale = layer.w13_input_scale_quant
1268
+ (hidden_states_fp4,
1269
+ hidden_states_scale_linear_fp4) = flashinfer.fp4_quantize(
1270
+ x,
1271
+ a1_gscale,
1272
+ is_sf_swizzled_layout=False,
1273
+ )
1274
+ use_llama4_routing = \
1275
+ custom_routing_function is Llama4MoE.custom_routing_function
1276
+ routing_method_type = flashinfer.RoutingMethodType.DeepSeekV3
1277
+ if use_llama4_routing:
1278
+ routing_method_type = flashinfer.RoutingMethodType.Llama4
1279
+ out = flashinfer.fused_moe.trtllm_fp4_block_scale_moe(
1280
+ routing_logits=router_logits
1281
+ if use_llama4_routing else router_logits.to(torch.float32),
1282
+ routing_bias=e_score_correction_bias,
1283
+ hidden_states=hidden_states_fp4,
1284
+ hidden_states_scale=hidden_states_scale_linear_fp4.view(
1285
+ torch.float8_e4m3fn).flatten(),
1286
+ gemm1_weights=layer.gemm1_weights_fp4_shuffled.data,
1287
+ gemm1_weights_scale=layer.gemm1_scales_fp4_shuffled.data.view(
1288
+ torch.float8_e4m3fn),
1289
+ gemm1_bias=None,
1290
+ gemm1_alpha=None,
1291
+ gemm1_beta=None,
1292
+ gemm1_clamp_limit=None,
1293
+ gemm2_weights=layer.gemm2_weights_fp4_shuffled.data,
1294
+ gemm2_weights_scale=layer.gemm2_scales_fp4_shuffled.data.view(
1295
+ torch.float8_e4m3fn),
1296
+ gemm2_bias=None,
1297
+ output1_scale_scalar=layer.g1_scale_c.data,
1298
+ output1_scale_gate_scalar=layer.g1_alphas.data,
1299
+ output2_scale_scalar=layer.g2_alphas.data,
1300
+ num_experts=global_num_experts,
1301
+ top_k=top_k,
1302
+ n_group=num_expert_group,
1303
+ topk_group=topk_group,
1304
+ intermediate_size=layer.intermediate_size_per_partition,
1305
+ local_expert_offset=layer.ep_rank * layer.local_num_experts,
1306
+ local_num_experts=layer.local_num_experts,
1307
+ routed_scaling_factor=None,
1308
+ tile_tokens_dim=_get_tile_tokens_dim(x.shape[0], top_k,
1309
+ layer.local_num_experts),
1310
+ routing_method_type=routing_method_type,
1311
+ do_finalize=True,
1312
+ )[0]
1313
+ return out
1314
+
1315
topk_weights, topk_ids = FusedMoE.select_experts(
1316
hidden_states=x,
1317
router_logits=router_logits,
@@ -1149,6 +1366,8 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1366
expert_map=expert_map,
1367
apply_router_weight_on_input=apply_router_weight_on_input)
1368
else:
1369
+ assert self.allow_flashinfer and \
1370
+ self.flashinfer_moe_backend == FlashinferMoeBackend.CUTLASS
1371
out = flashinfer_fp4_cutlass_moe_forward(
1372
self.fused_experts,
1373
layer,
@@ -1160,4 +1379,5 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1379
expert_map=expert_map,
1380
apply_router_weight_on_input=apply_router_weight_on_input,
1381
)
1382
+
1383
return out
vllm/model_executor/layers/quantization/utils/flashinfer_fp4_moe.py
+9
-3
@@ -126,7 +126,7 @@ def flashinfer_fp4_cutlass_moe_forward(
126
127
128
def select_nvfp4_gemm_impl(
129
- allow_flashinfer_cutlass: bool,
129
+ allow_flashinfer: bool,
130
moe, # FusedMoEConfig
131
logger):
132
"""Return a GEMM *experts* implementation for NV-FP4 fused-MoE layers"""
@@ -137,8 +137,14 @@ def select_nvfp4_gemm_impl(
137
all2all_manager = get_ep_group().device_communicator.all2all_manager
138
assert all2all_manager is not None
139
140
- if allow_flashinfer_cutlass:
141
- logger.debug_once("Using FlashInferExperts")
140
+ if allow_flashinfer:
141
+ flashinfer_backend = envs.VLLM_FLASHINFER_MOE_BACKEND
142
+ if flashinfer_backend != "throughput":
143
+ raise ValueError(
144
+ f"Only throughput backend is supported for FlashInferExperts, "
145
+ f"but got {flashinfer_backend}.")
146
+ logger.debug_once(
147
+ "Initializing FlashInferExperts with throughput backend.")
148
return FlashInferExperts(
149
use_nvfp4_w4a4=True,
150
use_dp=moe.moe_parallel_config.dp_size > 1,
vllm/model_executor/layers/quantization/utils/nvfp4_moe_support.py
+2
-2
@@ -21,7 +21,7 @@ class NvFp4Support:
21
"""Result container for NV-FP4 capability probing."""
22
23
cutlass_supported: bool
24
- allow_flashinfer_cutlass: bool
24
+ allow_flashinfer: bool
25
use_marlin: bool
26
27
@@ -54,6 +54,6 @@ def detect_nvfp4_moe_support(class_name: str = "") -> NvFp4Support:
54
55
return NvFp4Support(
56
cutlass_supported=cutlass_supported,
57
- allow_flashinfer_cutlass=allow_flashinfer,
57
+ allow_flashinfer=allow_flashinfer,
58
use_marlin=use_marlin,
59
)
vllm/utils/flashinfer.py
+4
@@ -86,6 +86,8 @@ flashinfer_cutlass_fused_moe = _lazy_import_wrapper("flashinfer.fused_moe",
86
fp4_quantize = _lazy_import_wrapper("flashinfer", "fp4_quantize")
87
nvfp4_block_scale_interleave = _lazy_import_wrapper(
88
"flashinfer", "nvfp4_block_scale_interleave")
89
+trtllm_fp4_block_scale_moe = _lazy_import_wrapper(
90
+ "flashinfer", "trtllm_fp4_block_scale_moe")
91
92
# Special case for autotune since it returns a context manager
93
autotune = _lazy_import_wrapper(
@@ -112,6 +114,7 @@ def has_flashinfer_cutlass_fused_moe() -> bool:
114
("flashinfer.fused_moe", "cutlass_fused_moe"),
115
("flashinfer", "fp4_quantize"),
116
("flashinfer", "nvfp4_block_scale_interleave"),
117
+ ("flashinfer.fused_moe", "trtllm_fp4_block_scale_moe"),
118
]
119
120
for module_name, attr_name in required_functions:
@@ -188,6 +191,7 @@ __all__ = [
191
"flashinfer_cutlass_fused_moe",
192
"fp4_quantize",
193
"nvfp4_block_scale_interleave",
194
+ "trtllm_fp4_block_scale_moe",
195
"autotune",
196
"has_flashinfer_moe",
197
"has_flashinfer_cutlass_fused_moe",