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
feat: BF16 FlashInfer Fused Cutlass MOE for Hopper and Blackwell Expert Parallel (#25503)
Signed-off-by: Duncan Moss <djm.moss@gmail.com>
Duncan Moss committed
Sep 24, 2025 at 15:50 UTC
6160ba4151084c78164a0f472ce4da04067f9705
5 files changed
+121
-6
vllm/envs.py
+6
@@ -144,6 +144,7 @@ if TYPE_CHECKING:
144
VLLM_USE_DEEP_GEMM_E8M0_HOPPER: bool = False
145
VLLM_SKIP_DEEP_GEMM_WARMUP: bool = False
146
VLLM_USE_FUSED_MOE_GROUPED_TOPK: bool = True
147
+ VLLM_USE_FLASHINFER_MOE_FP16: bool = False
148
VLLM_USE_FLASHINFER_MOE_FP8: bool = False
149
VLLM_USE_FLASHINFER_MOE_FP4: bool = False
150
VLLM_FLASHINFER_MOE_BACKEND: Literal["throughput",
@@ -1145,6 +1146,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
1146
"VLLM_USE_FUSED_MOE_GROUPED_TOPK":
1147
lambda: bool(int(os.getenv("VLLM_USE_FUSED_MOE_GROUPED_TOPK", "1"))),
1148
1149
+ # Allow use of FlashInfer MoE kernels for fused moe ops.
1150
+ "VLLM_USE_FLASHINFER_MOE_FP16":
1151
+ lambda: bool(int(os.getenv("VLLM_USE_FLASHINFER_MOE_FP16", "0"))),
1152
+
1153
# Allow use of FlashInfer MoE kernels for fused moe ops.
1154
"VLLM_USE_FLASHINFER_MOE_FP8":
1155
lambda: bool(int(os.getenv("VLLM_USE_FLASHINFER_MOE_FP8", "0"))),
@@ -1516,6 +1521,7 @@ def compute_hash() -> str:
1521
"VLLM_USE_DEEP_GEMM_E8M0_HOPPER",
1522
"VLLM_USE_TRTLLM_FP4_GEMM",
1523
"VLLM_USE_FUSED_MOE_GROUPED_TOPK",
1524
+ "VLLM_USE_FLASHINFER_MOE_FP16",
1525
"VLLM_USE_FLASHINFER_MOE_FP8",
1526
"VLLM_USE_FLASHINFER_MOE_FP4",
1527
"VLLM_USE_FLASHINFER_MOE_MXFP4_MXFP8",
vllm/model_executor/layers/fused_moe/flashinfer_cutlass_moe.py
+60
-5
@@ -52,8 +52,10 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
52
tp_size: int = 1,
53
):
54
super().__init__(quant_config)
55
- assert quant_config.quant_dtype in ("nvfp4", torch.float8_e4m3fn), (
56
- "Only nvfp4,fp8 quantization are currently supported.")
55
+ assert quant_config.quant_dtype in (
56
+ "nvfp4", torch.float8_e4m3fn,
57
+ None), ("Only nvfp4, fp8, bfloat16 and"
58
+ " float16 quantization are currently supported.")
59
self.ep_rank = ep_rank
60
self.ep_size = ep_size
61
self.tp_rank = tp_rank
@@ -109,8 +111,9 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
111
"""
112
aq_m, aq_n = aq.shape
113
workspace2 = (0, )
112
- output_shape = (aq_m, aq_n * 2) if self.quant_dtype != \
113
- torch.float8_e4m3fn else (aq_m, aq_n)
114
+ output_shape = (aq_m,
115
+ aq_n * 2) if self.quant_dtype == "nvfp4" else (aq_m,
116
+ aq_n)
117
workspace_dtype = a.dtype
118
workspace1 = output_shape
119
# The workspace is determined by `aq`, since it comes after any
@@ -135,6 +138,10 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
138
expert_tokens_meta: Optional[mk.ExpertTokensMetadata],
139
apply_router_weight_on_input: Optional[bool],
140
):
141
+
142
+ assert activation == "silu", ("Only activation silu is supported in "
143
+ "FlashInferExperts")
144
+
145
if self.quant_dtype == torch.float8_e4m3fn:
146
quant_scales = [
147
self.g1_alphas, self.a2_gscale, self.g2_alphas, self.a1_gscale
@@ -143,7 +150,7 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
150
a1q_scale = None # not passing input_sf in fp8
151
fc1_expert_weights = w1
152
fc2_expert_weights = w2
146
- else:
153
+ elif self.quant_dtype == "nvfp4":
154
# Ensure w1_scale and w2_scale are not None before calling view
155
assert self.w1_scale is not None and self.w2_scale is not None, (
156
"w1_scale and w2_scale must not "
@@ -161,6 +168,11 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
168
# FlashInfer API requires weight to be long for nvfp4
169
fc1_expert_weights = w1.view(torch.long)
170
fc2_expert_weights = w2.view(torch.long)
171
+ else:
172
+ quant_scales = None
173
+ a1q_scale = None
174
+ fc1_expert_weights = w1
175
+ fc2_expert_weights = w2
176
177
_ = flashinfer_cutlass_fused_moe(
178
input=hidden_states,
@@ -211,3 +223,46 @@ def flashinfer_cutlass_moe_fp4(
223
expert_map=expert_map,
224
apply_router_weight_on_input=apply_router_weight_on_input,
225
)
226
+
227
+
228
+def flashinfer_cutlass_moe(
229
+ hidden_states: torch.Tensor,
230
+ w1: torch.Tensor,
231
+ w2: torch.Tensor,
232
+ topk_weights: torch.Tensor,
233
+ topk_ids: torch.Tensor,
234
+ quant_config: FusedMoEQuantConfig,
235
+ inplace: bool = False,
236
+ activation: str = "silu",
237
+ global_num_experts: int = -1,
238
+ expert_map: Optional[torch.Tensor] = None,
239
+ apply_router_weight_on_input: bool = False,
240
+ tp_rank: int = 0,
241
+ tp_size: int = 1,
242
+ ep_rank: int = 0,
243
+ ep_size: int = 1,
244
+ use_dp: bool = False,
245
+) -> torch.Tensor:
246
+ fused_experts = mk.FusedMoEModularKernel(
247
+ create_flashinfer_prepare_finalize(use_dp=use_dp),
248
+ FlashInferExperts(
249
+ out_dtype=hidden_states.dtype,
250
+ quant_config=quant_config,
251
+ tp_rank=tp_rank,
252
+ tp_size=tp_size,
253
+ ep_rank=ep_rank,
254
+ ep_size=ep_size,
255
+ ))
256
+
257
+ return fused_experts(
258
+ hidden_states=hidden_states,
259
+ w1=w1,
260
+ w2=w2,
261
+ topk_weights=topk_weights,
262
+ topk_ids=topk_ids,
263
+ inplace=inplace,
264
+ activation=activation,
265
+ global_num_experts=global_num_experts,
266
+ expert_map=expert_map,
267
+ apply_router_weight_on_input=apply_router_weight_on_input,
268
+ )
vllm/model_executor/layers/fused_moe/flashinfer_cutlass_prepare_finalize.py
+2
-1
@@ -183,7 +183,8 @@ class FlashInferAllGatherMoEPrepareAndFinalize(
183
dim=0,
184
sizes=get_local_sizes(),
185
)
186
- a1q_scale = nvfp4_block_scale_interleave(a1q_scale)
186
+ if quant_config.quant_dtype == "nvfp4":
187
+ a1q_scale = nvfp4_block_scale_interleave(a1q_scale)
188
189
return a1q, a1q_scale, None, topk_ids, topk_weights
190
vllm/model_executor/layers/fused_moe/layer.py
+51
@@ -39,6 +39,7 @@ from vllm.platforms import current_platform
39
from vllm.platforms.interface import CpuArchEnum
40
from vllm.utils import (cdiv, direct_register_custom_op, has_deep_ep, has_pplx,
41
round_up)
42
+from vllm.utils.flashinfer import has_flashinfer_cutlass_fused_moe
43
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
44
45
if current_platform.is_cuda_alike():
@@ -296,6 +297,40 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
297
else:
298
self.rocm_aiter_fused_experts = None # type: ignore
299
300
+ # FlashInfer CUTLASS MoE is only supported on Hopper and later GPUS
301
+ self.flashinfer_cutlass_moe_enabled = (
302
+ has_flashinfer_cutlass_fused_moe()
303
+ and envs.VLLM_USE_FLASHINFER_MOE_FP16
304
+ and self.moe.moe_parallel_config.use_ep
305
+ and self.moe.moe_parallel_config.dp_size == 1
306
+ and current_platform.get_device_capability()[0] >= 9)
307
+ if self.flashinfer_cutlass_moe_enabled:
308
+ logger.info_once(
309
+ "Enabling FlashInfer CUTLASS MoE for UnquantizedFusedMoEMethod"
310
+ )
311
+ from functools import partial
312
+
313
+ from .flashinfer_cutlass_moe import flashinfer_cutlass_moe
314
+ self.flashinfer_cutlass_moe = partial(
315
+ flashinfer_cutlass_moe,
316
+ quant_config=FUSED_MOE_UNQUANTIZED_CONFIG,
317
+ tp_rank=self.moe.moe_parallel_config.tp_rank,
318
+ tp_size=self.moe.moe_parallel_config.tp_size,
319
+ ep_rank=self.moe.moe_parallel_config.ep_rank,
320
+ ep_size=self.moe.moe_parallel_config.ep_size)
321
+ else:
322
+ if (self.moe.moe_parallel_config.use_ep
323
+ and self.moe.moe_parallel_config.dp_size == 1):
324
+ logger.info_once(
325
+ "FlashInfer CUTLASS MoE is available for EP"
326
+ " but not enabled, consider setting"
327
+ " VLLM_USE_FLASHINFER_MOE_FP16=1 to enable it.")
328
+ elif self.moe.moe_parallel_config.dp_size > 1:
329
+ logger.info_once(
330
+ "FlashInfer CUTLASS MoE is currently not available for DP."
331
+ )
332
+ self.flashinfer_cutlass_moe = None # type: ignore
333
+
334
def maybe_make_prepare_finalize(
335
self) -> Optional[FusedMoEPrepareAndFinalize]:
336
if self.rocm_aiter_moe_enabled:
@@ -367,6 +402,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
402
num_pad = 256 // weight.element_size()
403
weight = F.pad(weight, (0, num_pad), "constant", 0)[..., :-num_pad]
404
torch.cuda.empty_cache()
405
+
406
return weight
407
408
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
@@ -386,6 +422,12 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
422
layer.w13_weight.data = shuffled_w13
423
layer.w2_weight.data = shuffled_w2
424
425
+ if self.flashinfer_cutlass_moe_enabled:
426
+ # Swap halves to arrange as [w3; w1] (kernel expectation)
427
+ w1_w, w3_w = torch.chunk(layer.w13_weight.data, 2, dim=1)
428
+ w13_weight_swapped = torch.cat([w3_w, w1_w], dim=1)
429
+ layer.w13_weight.data = w13_weight_swapped.contiguous()
430
+
431
if current_platform.is_xpu():
432
import intel_extension_for_pytorch as ipex
433
layer.ipex_fusion = ipex.llm.modules.GatedMLPMOE(
@@ -536,6 +578,15 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
578
expert_map=expert_map,
579
activation=activation,
580
apply_router_weight_on_input=apply_router_weight_on_input)
581
+ elif self.flashinfer_cutlass_moe_enabled:
582
+ return self.flashinfer_cutlass_moe(
583
+ hidden_states=x,
584
+ w1=layer.w13_weight,
585
+ w2=layer.w2_weight,
586
+ topk_weights=topk_weights,
587
+ topk_ids=topk_ids,
588
+ activation=activation,
589
+ apply_router_weight_on_input=apply_router_weight_on_input)
590
elif self.fused_experts is not None:
591
if self.moe.has_bias:
592
raise ValueError(
vllm/model_executor/layers/fused_moe/modular_kernel.py
+2
@@ -598,6 +598,8 @@ class SharedResizableBuffer:
598
599
def get(self, shape: tuple[int, ...], device: torch.device,
600
dtype: torch.dtype):
601
+ if shape == () or shape is None:
602
+ return None
603
shape_numel = prod(shape)
604
if (self.buffer is None or self.buffer.numel() < shape_numel
605
or self.buffer.device != device or self.buffer.dtype != dtype):