@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3f8a874065
Fork 0

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 13, 04:30 UTC

[Kernels] Enable FlashInfer FP8 Blockscale on SM90 (for TEP DSR1) (#27134)

Signed-off-by: Duncan Moss <djm.moss@gmail.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com> Co-authored-by: Wentao Ye <44945378+yewentao256@users.noreply.github.com>

Duncan Moss committed Nov 14, 2025 at 08:02 UTC 3f8a8740656f2c0079b9e2b1623a0758a61104af
4 files changed +179 -68
vllm/model_executor/layers/fused_moe/flashinfer_cutlass_moe.py
+22 -1
@@ -57,6 +57,7 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
57 tp_rank: int = 0,
58 tp_size: int = 1,
59 use_dp: bool = False,
60 + use_deepseek_fp8_block_scale: bool = False,
61 ):
62 super().__init__(quant_config)
63 assert quant_config.quant_dtype in ("nvfp4", torch.float8_e4m3fn, None), (
@@ -69,6 +70,10 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
70 self.tp_size = tp_size
71 self.out_dtype = out_dtype
72 self.use_dp = use_dp
73 + # Enables DeepSeek-style FP8 block-scale path:
74 + # - pass per-block weight scales to the kernel
75 + # - skip input activation quantization (kernel applies scaling)
76 + self.use_deepseek_fp8_block_scale = use_deepseek_fp8_block_scale
77
78 @property
79 def activation_formats(
@@ -147,7 +152,12 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
152 "Only activation silu is supported in FlashInferExperts"
153 )
154
150 - if self.quant_dtype == torch.float8_e4m3fn:
155 + # Select quantization metadata based on FP8 format/path
156 + if (
157 + self.quant_dtype == torch.float8_e4m3fn
158 + and not self.use_deepseek_fp8_block_scale
159 + ):
160 + # FP8 per-tensor path: use global alphas/scales; do not pass input_sf
161 quant_scales = [
162 self.g1_alphas,
163 self.a2_gscale,
@@ -176,6 +186,15 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
186 # FlashInfer API requires weight to be long for nvfp4
187 fc1_expert_weights = w1.view(torch.long)
188 fc2_expert_weights = w2.view(torch.long)
189 + elif self.use_deepseek_fp8_block_scale:
190 + # FP8 block-scale path: provide block-scale weights, omit a1q_scale
191 + quant_scales = [
192 + self.w1_scale,
193 + self.w2_scale,
194 + ]
195 + a1q_scale = None
196 + fc1_expert_weights = w1
197 + fc2_expert_weights = w2
198 else:
199 quant_scales = None
200 a1q_scale = None
@@ -196,6 +215,8 @@ class FlashInferExperts(mk.FusedMoEPermuteExpertsUnpermute):
215 ep_size=self.ep_size,
216 ep_rank=self.ep_rank,
217 output=output,
218 + # Informs FlashInfer to use the block-scale decoding path when True
219 + use_deepseek_fp8_block_scale=self.use_deepseek_fp8_block_scale,
220 )
221
222
vllm/model_executor/layers/fused_moe/flashinfer_cutlass_prepare_finalize.py
+97 -50
@@ -28,11 +28,15 @@ class FlashInferCutlassMoEPrepareAndFinalize(mk.FusedMoEPrepareAndFinalize):
28 self,
29 use_dp: bool,
30 num_dispatchers: int = 1,
31 + use_deepseek_fp8_block_scale: bool = False,
32 ):
33 super().__init__()
34 self.num_dispatchers_ = num_dispatchers
35 self.use_dp = use_dp
36 self.local_tokens = None
37 + # Toggle for DeepSeek-style FP8 block-scale path where activations are
38 + # not quantized here and weight block scales are consumed by the kernel.
39 + self.use_deepseek_fp8_block_scale = use_deepseek_fp8_block_scale
40
41 @property
42 def activation_format(self) -> mk.FusedMoEActivationFormat:
@@ -73,8 +77,9 @@ class FlashInferAllToAllMoEPrepareAndFinalize(FlashInferCutlassMoEPrepareAndFina
77 self,
78 use_dp: bool,
79 num_dispatchers: int = 1,
80 + use_deepseek_fp8_block_scale: bool = False,
81 ):
77 - super().__init__(use_dp, num_dispatchers)
82 + super().__init__(use_dp, num_dispatchers, use_deepseek_fp8_block_scale)
83 self.alltoall_info = None
84
85 # Initialize all2all_manager only for DP case
@@ -97,15 +102,19 @@ class FlashInferAllToAllMoEPrepareAndFinalize(FlashInferCutlassMoEPrepareAndFina
102 )
103
104 if not self.use_dp:
100 - # Non-DP case: standard quantization
101 - a1q, a1q_scale = moe_kernel_quantize_input(
102 - a1,
103 - quant_config.a1_gscale,
104 - quant_config.quant_dtype,
105 - quant_config.per_act_token_quant,
106 - quant_config.block_shape,
107 - is_fp4_scale_swizzled=not self.use_dp,
108 - )
105 + # Non-DP case: quantize activations unless using block-scale path
106 + if not self.use_deepseek_fp8_block_scale:
107 + a1q, a1q_scale = moe_kernel_quantize_input(
108 + a1,
109 + quant_config.a1_gscale,
110 + quant_config.quant_dtype,
111 + quant_config.per_act_token_quant,
112 + quant_config.block_shape,
113 + is_fp4_scale_swizzled=not self.use_dp,
114 + )
115 + else:
116 + a1q = a1
117 + a1q_scale = None
118 else:
119 # DP case: use FlashInfer AllToAll
120 global_num_tokens_cpu = get_local_sizes()
@@ -122,6 +131,7 @@ class FlashInferAllToAllMoEPrepareAndFinalize(FlashInferCutlassMoEPrepareAndFina
131 top_k,
132 num_experts,
133 quant_config,
134 + use_deepseek_fp8_block_scale=self.use_deepseek_fp8_block_scale,
135 )
136 )
137
@@ -154,8 +164,9 @@ class FlashInferAllGatherMoEPrepareAndFinalize(FlashInferCutlassMoEPrepareAndFin
164 self,
165 use_dp: bool,
166 num_dispatchers: int = 1,
167 + use_deepseek_fp8_block_scale: bool = False,
168 ):
158 - super().__init__(use_dp, num_dispatchers)
169 + super().__init__(use_dp, num_dispatchers, use_deepseek_fp8_block_scale)
170
171 def prepare(
172 self,
@@ -173,22 +184,42 @@ class FlashInferAllGatherMoEPrepareAndFinalize(FlashInferCutlassMoEPrepareAndFin
184 if not self.use_dp and quant_config.quant_dtype == "nvfp4":
185 return a1, None, None, topk_ids, topk_weights
186
176 - a1q, a1q_scale = moe_kernel_quantize_input(
177 - a1,
178 - quant_config.a1_gscale,
179 - quant_config.quant_dtype,
180 - quant_config.per_act_token_quant,
181 - quant_config.block_shape,
182 - is_fp4_scale_swizzled=not self.use_dp,
183 - )
187 + if not self.use_deepseek_fp8_block_scale:
188 + a1q, a1q_scale = moe_kernel_quantize_input(
189 + a1,
190 + quant_config.a1_gscale,
191 + quant_config.quant_dtype,
192 + quant_config.per_act_token_quant,
193 + quant_config.block_shape,
194 + is_fp4_scale_swizzled=not self.use_dp,
195 + )
196 + else:
197 + # Block-scale path: pass activations through, omit per-token scales
198 + a1q = a1
199 + a1q_scale = None
200
201 if self.use_dp:
186 - topk_weights, topk_ids, a1q, a1q_scale = get_dp_group().all_gatherv(
187 - [topk_weights, topk_ids, a1q, a1q_scale],
188 - dim=0,
189 - sizes=get_local_sizes(),
190 - )
191 - if quant_config.quant_dtype == "nvfp4":
202 + # Build gather list conditionally - omit a1q_scale if None
203 + # (block-scale path)
204 + gather_list = [topk_weights, topk_ids, a1q]
205 + if a1q_scale is not None:
206 + gather_list.append(a1q_scale)
207 + gathered = get_dp_group().all_gatherv(
208 + gather_list,
209 + dim=0,
210 + sizes=get_local_sizes(),
211 + )
212 + topk_weights, topk_ids, a1q, a1q_scale = gathered
213 + else:
214 + gathered = get_dp_group().all_gatherv(
215 + gather_list,
216 + dim=0,
217 + sizes=get_local_sizes(),
218 + )
219 + topk_weights, topk_ids, a1q = gathered
220 + a1q_scale = None
221 +
222 + if quant_config.quant_dtype == "nvfp4" and a1q_scale is not None:
223 a1q_scale = nvfp4_block_scale_interleave(a1q_scale)
224
225 return a1q, a1q_scale, None, topk_ids, topk_weights
@@ -221,6 +252,7 @@ def flashinfer_alltoall_dispatch(
252 top_k: int,
253 num_experts: int,
254 quant_config: FusedMoEQuantConfig,
255 + use_deepseek_fp8_block_scale: bool = False,
256 ):
257 from flashinfer.comm.trtllm_alltoall import MnnvlMoe
258
@@ -250,30 +282,42 @@ def flashinfer_alltoall_dispatch(
282 )
283 topk_weights = topk_weights.view(dtype=orig_topk_weights_dtype)
284
253 - x, x_sf = moe_kernel_quantize_input(
254 - x,
255 - gs,
256 - quant_config.quant_dtype,
257 - quant_config.per_act_token_quant,
258 - quant_config.block_shape,
259 - is_fp4_scale_swizzled=False, # delay swizzle to after comm
260 - )
261 - x = MnnvlMoe.mnnvl_moe_alltoallv(
262 - x,
263 - alltoall_info,
264 - all2all_manager.workspace_tensor,
265 - ep_rank,
266 - ep_size,
267 - )
285 + if not use_deepseek_fp8_block_scale:
286 + x, x_sf = moe_kernel_quantize_input(
287 + x,
288 + gs,
289 + quant_config.quant_dtype,
290 + quant_config.per_act_token_quant,
291 + quant_config.block_shape,
292 + is_fp4_scale_swizzled=False, # delay swizzle to after comm
293 + )
294 + x = MnnvlMoe.mnnvl_moe_alltoallv(
295 + x,
296 + alltoall_info,
297 + all2all_manager.workspace_tensor,
298 + ep_rank,
299 + ep_size,
300 + )
301
269 - x_sf = MnnvlMoe.mnnvl_moe_alltoallv(
270 - x_sf,
271 - alltoall_info,
272 - all2all_manager.workspace_tensor,
273 - ep_rank,
274 - ep_size,
275 - )
276 - x_sf = nvfp4_block_scale_interleave(x_sf)
302 + x_sf = MnnvlMoe.mnnvl_moe_alltoallv(
303 + x_sf,
304 + alltoall_info,
305 + all2all_manager.workspace_tensor,
306 + ep_rank,
307 + ep_size,
308 + )
309 + if quant_config.quant_dtype == "nvfp4":
310 + x_sf = nvfp4_block_scale_interleave(x_sf)
311 + else:
312 + # Block-scale path: pass activations through without quantization
313 + x_sf = None
314 + x = MnnvlMoe.mnnvl_moe_alltoallv(
315 + x,
316 + alltoall_info,
317 + all2all_manager.workspace_tensor,
318 + ep_rank,
319 + ep_size,
320 + )
321 return alltoall_info, topk_ids, topk_weights, x, x_sf
322
323
@@ -304,6 +348,7 @@ def create_flashinfer_prepare_finalize(
348 use_dp: bool,
349 use_nvfp4: bool = False,
350 enable_alltoallv: bool = False,
351 + use_deepseek_fp8_block_scale: bool = False,
352 ) -> FlashInferCutlassMoEPrepareAndFinalize:
353 """Factory function to create the appropriate FlashInfer implementation."""
354 if use_nvfp4:
@@ -311,5 +356,7 @@ def create_flashinfer_prepare_finalize(
356 return FlashInferAllToAllMoEPrepareAndFinalize(use_dp)
357 else:
358 return FlashInferAllGatherMoEPrepareAndFinalize(use_dp)
314 - # Fp8 only supports AllGather
315 - return FlashInferAllGatherMoEPrepareAndFinalize(use_dp)
359 + # FP8 path currently supported via AllGather; optionally enable block-scale
360 + return FlashInferAllGatherMoEPrepareAndFinalize(
361 + use_dp=use_dp, use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale
362 + )
vllm/model_executor/layers/quantization/fp8.py
+36 -12
@@ -3,6 +3,7 @@
3
4 from collections.abc import Callable
5 from enum import Enum
6 +from functools import partial
7 from typing import TYPE_CHECKING, Any, Optional
8
9 import torch
@@ -122,10 +123,13 @@ def get_fp8_moe_backend(block_quant: bool) -> Fp8MoeBackend:
123 Select the primary FP8 MoE backend
124 Note: Shape-specific fallbacks may still occur at runtime.
125 """
125 - # prefer FlashInfer backends when available and enabled on supported GPUs
126 + # Prefer FlashInfer backends on supported GPUs; allow SM90 and SM100.
127 if (
128 current_platform.is_cuda()
128 - and current_platform.is_device_capability(100)
129 + and (
130 + current_platform.is_device_capability(100)
131 + or current_platform.is_device_capability(90)
132 + )
133 and envs.VLLM_USE_FLASHINFER_MOE_FP8
134 and has_flashinfer_moe()
135 ):
@@ -134,14 +138,14 @@ def get_fp8_moe_backend(block_quant: bool) -> Fp8MoeBackend:
138 logger.info_once("Using FlashInfer FP8 MoE TRTLLM backend for SM100")
139 return Fp8MoeBackend.FLASHINFER_TRTLLM
140 else:
137 - if block_quant:
141 + if block_quant and current_platform.is_device_capability(100):
142 raise ValueError(
143 "FlashInfer FP8 MoE throughput backend does not "
144 "support block quantization. Please use "
145 "VLLM_FLASHINFER_MOE_BACKEND=latency "
146 "instead."
147 )
144 - logger.info_once("Using FlashInfer FP8 MoE CUTLASS backend for SM100")
148 + logger.info_once("Using FlashInfer FP8 MoE CUTLASS backend for SM90/SM100")
149 return Fp8MoeBackend.FLASHINFER_CUTLASS
150
151 # weight-only path for older GPUs without native FP8
@@ -641,6 +645,16 @@ class Fp8MoEMethod(FusedMoEMethodBase):
645 self.flashinfer_moe_backend = FlashinferMoeBackend.TENSORRT_LLM
646 elif self.fp8_backend == Fp8MoeBackend.FLASHINFER_CUTLASS:
647 self.flashinfer_moe_backend = FlashinferMoeBackend.CUTLASS
648 + if self.block_quant:
649 + assert self.weight_block_size == [128, 128], (
650 + f"Only support weight_block_size == [128, 128], "
651 + f"got {self.weight_block_size}"
652 + )
653 + self.flashinfer_moe_fn = partial(
654 + flashinfer_cutlass_moe_fp8,
655 + moe=self.moe,
656 + use_deepseek_fp8_block_scale=self.block_quant,
657 + )
658
659 self.allow_deep_gemm = self.fp8_backend == Fp8MoeBackend.DEEPGEMM
660 self.allow_cutlass_block_scaled_grouped_gemm = (
@@ -1012,8 +1026,15 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1026 ):
1027 return None
1028 elif self.flashinfer_moe_backend == FlashinferMoeBackend.CUTLASS:
1029 + if self.block_quant:
1030 + assert self.weight_block_size == [128, 128], (
1031 + f"Only support weight_block_size == [128, 128], "
1032 + f"got {self.weight_block_size}"
1033 + )
1034 + # Wire block-scale flag through prepare/finalize when using CUTLASS
1035 prepare_finalize = build_flashinfer_fp8_cutlass_moe_prepare_finalize(
1016 - self.moe
1036 + self.moe,
1037 + use_deepseek_fp8_block_scale=self.block_quant,
1038 )
1039 logger.debug_once("%s", prepare_finalize.__class__.__name__)
1040 return prepare_finalize
@@ -1062,9 +1083,11 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1083 )
1084
1085 elif self.flashinfer_moe_backend == FlashinferMoeBackend.CUTLASS:
1086 + # Select GEMM experts with block-scale when weights are block-quantized
1087 experts = select_cutlass_fp8_gemm_impl(
1088 self.moe,
1089 self.moe_quant_config,
1090 + use_deepseek_fp8_block_scale=self.block_quant,
1091 )
1092 logger.debug_once("Using %s", experts.__class__.__name__)
1093 return experts
@@ -1251,16 +1274,17 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1274 workspace=layer.workspace,
1275 )
1276 elif self.flashinfer_moe_backend == FlashinferMoeBackend.CUTLASS:
1254 - assert not self.block_quant
1255 - assert not renormalize and custom_routing_function is not None
1277 assert activation == "silu", (
1278 f"Expected 'silu' activation but got {activation}"
1279 )
1259 - assert scoring_func == "sigmoid", (
1260 - f"Expected 'sigmoid' scoring func but got {scoring_func}"
1261 - )
1262 -
1263 - result = flashinfer_cutlass_moe_fp8(
1280 + if not self.block_quant:
1281 + assert not renormalize and custom_routing_function is not None
1282 + assert scoring_func == "sigmoid", (
1283 + f"Expected 'sigmoid' scoring func but got {scoring_func}"
1284 + )
1285 + # Delegate to CUTLASS FlashInfer path; function already bound with
1286 + # use_deepseek_fp8_block_scale for block-quant when applicable
1287 + result = self.flashinfer_moe_fn(
1288 x,
1289 layer,
1290 topk_weights,
vllm/model_executor/layers/quantization/utils/flashinfer_utils.py
+24 -5
@@ -17,6 +17,7 @@ from vllm.model_executor.layers.fused_moe.flashinfer_cutlass_moe import (
17 from vllm.model_executor.layers.fused_moe.flashinfer_cutlass_prepare_finalize import ( # noqa: E501
18 create_flashinfer_prepare_finalize,
19 )
20 +from vllm.platforms import current_platform
21
22 logger = init_logger(__name__)
23
@@ -190,17 +191,22 @@ def register_moe_scaling_factors(layer: torch.nn.Module) -> None:
191
192
193 def build_flashinfer_fp8_cutlass_moe_prepare_finalize(
193 - moe: FusedMoEConfig | None,
194 + moe: FusedMoEConfig | None, use_deepseek_fp8_block_scale: bool = False
195 ) -> mk.FusedMoEPrepareAndFinalize:
196 """Create a FlashInfer CUTLASS fused-MoE prepare finalize kernel"""
197 use_dp = moe.moe_parallel_config.dp_size > 1 if moe is not None else False
197 - return create_flashinfer_prepare_finalize(use_dp)
198 + # Propagate block-scale flag so prepare/finalize can skip act quantization
199 + # and inform the kernel to consume per-block weight scales.
200 + return create_flashinfer_prepare_finalize(
201 + use_dp, use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale
202 + )
203
204
205 def select_cutlass_fp8_gemm_impl(
206 moe: FusedMoEConfig | None,
207 quant_config: FusedMoEQuantConfig,
208 out_dtype: torch.dtype | None = None,
209 + use_deepseek_fp8_block_scale: bool = False,
210 ) -> mk.FusedMoEPermuteExpertsUnpermute:
211 """Return a GEMM *experts* implementation for fused-MoE layers"""
212
@@ -212,12 +218,14 @@ def select_cutlass_fp8_gemm_impl(
218 ep_size=moe.moe_parallel_config.ep_size,
219 tp_rank=moe.moe_parallel_config.tp_rank,
220 tp_size=moe.moe_parallel_config.tp_size,
221 + use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale,
222 )
223
224 assert out_dtype is not None, "If moe config is None, out_dtype must be passed"
225 return FlashInferExperts(
226 out_dtype=out_dtype,
227 quant_config=quant_config,
228 + use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale,
229 )
230
231
@@ -231,14 +239,22 @@ def flashinfer_cutlass_moe_fp8(
239 global_num_experts: int = -1,
240 expert_map: torch.Tensor | None = None,
241 apply_router_weight_on_input: bool = False,
242 + use_deepseek_fp8_block_scale: bool = False,
243 + moe: FusedMoEConfig | None = None,
244 ) -> torch.Tensor:
245 quant_config = layer.quant_method.get_fused_moe_quant_config(layer)
246 assert quant_config is not None
247
248 + # Construct modular kernel with block-scale support when requested.
249 fused_experts = mk.FusedMoEModularKernel(
239 - build_flashinfer_fp8_cutlass_moe_prepare_finalize(moe=None),
250 + build_flashinfer_fp8_cutlass_moe_prepare_finalize(
251 + moe=moe, use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale
252 + ),
253 select_cutlass_fp8_gemm_impl(
241 - moe=None, quant_config=quant_config, out_dtype=hidden_states.dtype
254 + moe=moe,
255 + quant_config=quant_config,
256 + out_dtype=hidden_states.dtype,
257 + use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale,
258 ),
259 )
260
@@ -258,7 +274,10 @@ def flashinfer_cutlass_moe_fp8(
274
275 def get_flashinfer_moe_backend() -> FlashinferMoeBackend:
276 flashinfer_moe_backend = envs.VLLM_FLASHINFER_MOE_BACKEND
261 - if flashinfer_moe_backend == "throughput":
277 + # Prefer CUTLASS on SM90 to cover both SM90/SM100 generations
278 + if flashinfer_moe_backend == "throughput" or current_platform.is_device_capability(
279 + 90
280 + ):
281 return FlashinferMoeBackend.CUTLASS
282 elif flashinfer_moe_backend == "latency":
283 return FlashinferMoeBackend.TENSORRT_LLM