main
py 549 lines 20.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from typing import TYPE_CHECKING
4
5 import torch
6
7 from vllm.logger import init_logger
8 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
9 from vllm.utils.math_utils import round_up
10
11 if TYPE_CHECKING:
12 from flashinfer.fused_moe.core import ActivationType
13
14 logger = init_logger(__name__)
15
16 NVFP4_PER_TOKEN_BASE_GLOBAL_SCALE = 1.0 / (448.0 * 6.0)
17
18
19 def activation_to_flashinfer_int(activation: MoEActivation) -> int:
20 return activation_to_flashinfer_type(activation).value
21
22
23 def has_flashinfer_situ_activation() -> bool:
24 try:
25 from flashinfer.fused_moe.core import ActivationType
26 except ImportError:
27 return False
28 return hasattr(ActivationType, "Situ")
29
30
31 def activation_to_flashinfer_type(activation: MoEActivation) -> "ActivationType":
32 from flashinfer.fused_moe.core import ActivationType
33
34 if activation == MoEActivation.SITU:
35 situ = getattr(ActivationType, "Situ", None)
36 if situ is None:
37 raise ValueError("The installed FlashInfer does not support SITU")
38 return situ
39
40 # silu and gelu are mapped to their gated versions SwiGLU and GeGLU respectively
41 ACTIVATION_TO_FI_ACTIVATION = {
42 MoEActivation.SILU_NO_MUL: ActivationType.Silu,
43 MoEActivation.GELU_NO_MUL: ActivationType.Gelu,
44 MoEActivation.SILU: ActivationType.Swiglu,
45 # SwiGLU-OAI uses Swiglu; the OAI alpha/beta/clamp come from gemm1_* args.
46 MoEActivation.SWIGLUOAI: ActivationType.Swiglu,
47 MoEActivation.SWIGLUOAI_UNINTERLEAVE: ActivationType.Swiglu,
48 MoEActivation.GELU: ActivationType.Geglu,
49 MoEActivation.GELU_TANH: ActivationType.Geglu,
50 MoEActivation.RELU2_NO_MUL: ActivationType.Relu2,
51 }
52 return ACTIVATION_TO_FI_ACTIVATION[activation]
53
54
55 def quantize_nvfp4_per_token_input(
56 hidden_states: torch.Tensor,
57 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
58 """Quantize NVFP4 activations with one FP32 decode scale per token."""
59 from flashinfer import SfLayout, nvfp4_quantize
60
61 return nvfp4_quantize(
62 hidden_states,
63 NVFP4_PER_TOKEN_BASE_GLOBAL_SCALE,
64 sfLayout=SfLayout.layout_linear,
65 per_token_activation=True,
66 )
67
68
69 def swap_w13_to_w31(x: torch.Tensor) -> torch.Tensor:
70 return (
71 x.reshape(-1, 2, x.shape[-2] // 2, x.shape[-1]).flip(dims=[1]).reshape(x.shape)
72 )
73
74
75 def rotate_weights_for_fi_trtllm_fp8_per_tensor_moe(
76 gemm1_weights: torch.Tensor, gemm2_weights: torch.Tensor, is_gated_activation: bool
77 ):
78 """Shuffle weights for FI TRT-LLM Format."""
79 from flashinfer import reorder_rows_for_gated_act_gemm, shuffle_matrix_a
80
81 epilogue_tile_m = 128
82 num_experts = gemm1_weights.shape[0]
83 hidden_size = gemm1_weights.shape[-1]
84 intermediate_size = gemm1_weights.shape[1] // 2
85
86 # Reorder rows of W1 for fused gated activation
87 gemm1_weights_fp8_interleaved = []
88 for i in range(num_experts):
89 gemm1_weights_fp8_interleaved.append(
90 reorder_rows_for_gated_act_gemm(gemm1_weights[i])
91 if is_gated_activation
92 else gemm1_weights[i]
93 )
94
95 # Stack weights and scales for all experts
96 gemm1_weights_fp8_interleaved = torch.stack(gemm1_weights_fp8_interleaved).reshape(
97 num_experts, 2 * intermediate_size, hidden_size
98 )
99
100 # Shuffle weights and scaling factors for transposed mma output
101 gemm1_weights_fp8_shuffled = []
102 gemm2_weights_fp8_shuffled = []
103 for i in range(num_experts):
104 gemm1_weights_fp8_shuffled.append(
105 shuffle_matrix_a(
106 gemm1_weights_fp8_interleaved[i].view(torch.uint8), epilogue_tile_m
107 )
108 )
109
110 gemm2_weights_fp8_shuffled.append(
111 shuffle_matrix_a(gemm2_weights[i].view(torch.uint8), epilogue_tile_m)
112 )
113
114 # Stack weights for all experts
115 gemm1_weights.data = torch.stack(gemm1_weights_fp8_shuffled).view(
116 torch.float8_e4m3fn
117 )
118 gemm2_weights.data = torch.stack(gemm2_weights_fp8_shuffled).view(
119 torch.float8_e4m3fn
120 )
121
122
123 def convert_moe_weights_to_flashinfer_trtllm_block_layout(
124 cache_permute_indices: dict[torch.Size, torch.Tensor],
125 w13_weight: torch.Tensor,
126 w2_weight: torch.Tensor,
127 is_gated_act_gemm: bool = True,
128 ) -> tuple[torch.Tensor, torch.Tensor]:
129 """Convert expert weights to FlashInfer's block layout.
130
131 This reorders W13 and W2 in place into the expected epilogue-tiled block
132 layout and returns views of the shuffled weight tensors. Using one expert
133 as scratch space avoids allocating another full copy of both weights.
134 """
135 if w13_weight.dtype != torch.bfloat16 or w2_weight.dtype != torch.bfloat16:
136 raise ValueError(
137 "Unquantized Moe Backend FlashInfer TRTLLM requires bfloat16 weights"
138 )
139
140 from flashinfer.fused_moe.core import (
141 _maybe_get_cached_w3_w1_permute_indices,
142 get_w2_permute_indices_with_cache,
143 )
144
145 epilogue_tile_m = 128
146 block_k = 128
147
148 # Reorder rows of W13 and W2 for fused gated activation and convert to the
149 # block layout expected by the FlashInfer kernel.
150 num_experts = w13_weight.shape[0]
151
152 def _copy_permuted_expert_to_block_layout(
153 out: torch.Tensor,
154 expert_uint8: torch.Tensor,
155 source_indices: torch.Tensor,
156 ) -> None:
157 expert_blocks = expert_uint8.view(
158 expert_uint8.shape[0], out.shape[0], block_k
159 ).permute(1, 0, 2)
160 torch.index_select(
161 expert_blocks,
162 1,
163 source_indices.to(expert_uint8.device),
164 out=out,
165 )
166
167 def _convert_weight_in_place(
168 weight: torch.Tensor,
169 is_w13: bool,
170 ) -> torch.Tensor:
171 rows, cols = weight[0].view(torch.uint8).shape
172 block_layout_shape = (num_experts, cols // block_k, rows, block_k)
173 expert_scratch = torch.empty(
174 block_layout_shape[1:],
175 dtype=torch.uint8,
176 device=weight.device,
177 )
178
179 for i in range(num_experts):
180 expert_uint8 = weight[i].view(torch.uint8)
181 if is_w13:
182 permute_indices = _maybe_get_cached_w3_w1_permute_indices(
183 cache_permute_indices,
184 expert_uint8,
185 epilogue_tile_m,
186 is_gated_act_gemm=is_gated_act_gemm,
187 )
188 if is_gated_act_gemm:
189 permute_indices = (
190 permute_indices + expert_uint8.shape[0] // 2
191 ) % expert_uint8.shape[0]
192 else:
193 permute_indices = get_w2_permute_indices_with_cache(
194 cache_permute_indices,
195 expert_uint8,
196 epilogue_tile_m,
197 )
198
199 _copy_permuted_expert_to_block_layout(
200 expert_scratch,
201 expert_uint8,
202 permute_indices,
203 )
204 expert_uint8.view(-1).copy_(expert_scratch.view(-1))
205
206 return weight.view(torch.uint8).view(block_layout_shape).view(torch.bfloat16)
207
208 return (
209 _convert_weight_in_place(w13_weight, is_w13=True),
210 _convert_weight_in_place(w2_weight, is_w13=False),
211 )
212
213
214 def align_fp4_moe_weights_for_fi(
215 w13: torch.Tensor,
216 w13_scale: torch.Tensor,
217 w2: torch.Tensor,
218 w2_scale: torch.Tensor,
219 is_act_and_mul: bool,
220 min_alignment: int = 16,
221 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int]:
222 """Pad intermediate size so FlashInfer kernels' alignment constraints hold.
223
224 Some FlashInfer FP4 MoE kernels require the intermediate size
225 used for GEMM to be divisible by a small alignment value. When this is
226 not satisfied (e.g. with certain tensor-parallel sizes), we pad the
227 gate/up and down projection weights along the intermediate dim.
228 """
229 # Current local intermediate size (per partition) is the K dimension of
230 # the down projection.
231 num_experts, hidden_size, intermediate = w2.shape
232 intermediate *= 2 # because of packed FP4
233
234 padded_intermediate = round_up(intermediate, min_alignment)
235
236 if padded_intermediate == intermediate:
237 return w13, w13_scale, w2, w2_scale, intermediate
238
239 logger.info_once(
240 "Padding intermediate size from %d to %d for up/down projection weights.",
241 intermediate,
242 padded_intermediate,
243 )
244
245 up_mult = 2 if is_act_and_mul else 1
246 padded_gate_up_dim = up_mult * padded_intermediate
247
248 # Pad w13 and w2 along its intermediate dimension.
249 padded_w13 = w13.new_zeros((num_experts, padded_gate_up_dim, hidden_size // 2))
250 if is_act_and_mul:
251 # Keep the two logical projections independently aligned. Copying the
252 # fused [gate, up] tensor contiguously would move the up projection into
253 # the padded tail of gate when intermediate is rounded up.
254 padded_w13[:, :intermediate, :] = w13[:, :intermediate, :]
255 padded_w13[:, padded_intermediate : padded_intermediate + intermediate, :] = (
256 w13[:, intermediate:, :]
257 )
258 else:
259 padded_w13[:, : w13.shape[1], :] = w13
260
261 padded_w2 = w2.new_zeros((num_experts, hidden_size, padded_intermediate // 2))
262 padded_w2[:, :, : w2.shape[2]] = w2
263
264 padded_w13_scale = w13_scale.new_zeros(
265 (num_experts, padded_gate_up_dim, hidden_size // 16)
266 )
267 if is_act_and_mul:
268 padded_w13_scale[:, :intermediate, :] = w13_scale[:, :intermediate, :]
269 padded_w13_scale[
270 :, padded_intermediate : padded_intermediate + intermediate, :
271 ] = w13_scale[:, intermediate:, :]
272 else:
273 padded_w13_scale[:, : w13_scale.shape[1], :] = w13_scale
274
275 padded_w2_scale = w2_scale.new_zeros(
276 (num_experts, hidden_size, padded_intermediate // 16)
277 )
278 padded_w2_scale[:, :, : w2_scale.shape[2]] = w2_scale
279
280 return padded_w13, padded_w13_scale, padded_w2, padded_w2_scale, padded_intermediate
281
282
283 def align_fp4_moe_hidden_dim_for_fi(
284 w13: torch.Tensor,
285 w13_scale: torch.Tensor,
286 w2: torch.Tensor,
287 w2_scale: torch.Tensor,
288 min_alignment: int = 256,
289 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int]:
290 num_experts, gate_up_dim, packed_hidden_size = w13.shape
291 hidden_size = packed_hidden_size * 2
292 padded_hidden_size = round_up(hidden_size, min_alignment)
293
294 if padded_hidden_size == hidden_size:
295 return w13, w13_scale, w2, w2_scale, hidden_size
296
297 logger.warning_once(
298 "Padding hidden size from %d to %d for FlashInfer NVFP4 MoE weights. "
299 "This requires activation slicing at runtime and may cause "
300 "performance degradation.",
301 hidden_size,
302 padded_hidden_size,
303 )
304
305 padded_w13 = w13.new_zeros((num_experts, gate_up_dim, padded_hidden_size // 2))
306 padded_w13[:, :, :packed_hidden_size] = w13
307
308 padded_w13_scale = w13_scale.new_zeros(
309 (num_experts, gate_up_dim, padded_hidden_size // 16)
310 )
311 padded_w13_scale[:, :, : w13_scale.shape[2]] = w13_scale
312
313 padded_w2 = w2.new_zeros((num_experts, padded_hidden_size, w2.shape[2]))
314 padded_w2[:, : w2.shape[1], :] = w2
315
316 padded_w2_scale = w2_scale.new_zeros(
317 (num_experts, padded_hidden_size, w2_scale.shape[2])
318 )
319 padded_w2_scale[:, : w2_scale.shape[1], :] = w2_scale
320
321 return padded_w13, padded_w13_scale, padded_w2, padded_w2_scale, padded_hidden_size
322
323
324 def align_moe_weights_for_fi(
325 w13: torch.Tensor, w2: torch.Tensor, is_act_and_mul: bool, min_alignment: int = 16
326 ) -> tuple[torch.Tensor, torch.Tensor, int]:
327 """Pad intermediate size so FlashInfer kernels' alignment constraints hold.
328
329 Some FlashInfer MoE kernels require the (gated) intermediate size
330 used for GEMM to be divisible by a small alignment value. When this is
331 not satisfied (e.g. with certain tensor-parallel sizes), we pad the
332 gate/up and down projection weights along the intermediate dim.
333 """
334 # Current local intermediate size (per partition) is the K dimension of
335 # the down projection.
336 num_experts, hidden_size, intermediate = w2.shape
337
338 padded_intermediate = round_up(intermediate, min_alignment)
339
340 if padded_intermediate == intermediate:
341 return w13, w2, intermediate
342
343 logger.info_once(
344 "Padding intermediate size from %d to %d for up/down projection weights.",
345 intermediate,
346 padded_intermediate,
347 )
348
349 up_mult = 2 if is_act_and_mul else 1
350 padded_gate_up_dim = up_mult * padded_intermediate
351
352 # Pad w13 and w2 along its intermediate dimension.
353 padded_w13 = w13.new_zeros((num_experts, padded_gate_up_dim, hidden_size))
354 if is_act_and_mul:
355 padded_w13[:, :intermediate, :] = w13[:, :intermediate, :]
356 padded_w13[:, padded_intermediate : padded_intermediate + intermediate, :] = (
357 w13[:, intermediate:, :]
358 )
359 else:
360 padded_w13[:, :intermediate, :] = w13
361
362 padded_w2 = w2.new_zeros((num_experts, hidden_size, padded_intermediate))
363 padded_w2[:, :, :intermediate] = w2
364
365 return padded_w13, padded_w2, padded_intermediate
366
367
368 def _shuffle_deepseek_fp8_moe_weights(
369 w13: torch.Tensor,
370 w2: torch.Tensor,
371 ) -> tuple[torch.Tensor, torch.Tensor]:
372 """Preprocess DeepSeek FP8 block-scale weights for the FlashInfer TRT-LLM
373 kernel using the shuffle + BlockMajorK layout variant.
374
375 Returns 4D weight tensors in BlockMajorK layout
376 (E, K/block_k, Mn, block_k)
377 """
378 from flashinfer.utils import get_shuffle_matrix_a_row_indices
379
380 epilogue_tile_m = 64
381 block_k = 128
382
383 def shuffle_to_block_major_k(w: torch.Tensor) -> torch.Tensor:
384 # shuffle_matrix_a's row permutation depends only on (M,
385 # epilogue_tile_m), so it is computed once and applied to every expert
386 # in a single gather instead of once per expert. Gathering through the
387 # BlockMajorK-permuted view also folds convert_to_block_layout into
388 # that same kernel. Per-expert loops here cost minutes for a MoE this
389 # wide (~24k tiny launches plus a host round-trip each).
390 num_experts, m, k = w.shape
391 rows = get_shuffle_matrix_a_row_indices(
392 w[0].view(torch.uint8), epilogue_tile_m
393 ).to(w.device)
394 blocked = w.view(torch.uint8).view(num_experts, m, k // block_k, block_k)
395 out = blocked.permute(0, 2, 1, 3)[:, :, rows, :].contiguous()
396 return out.view(torch.float8_e4m3fn)
397
398 return shuffle_to_block_major_k(w13), shuffle_to_block_major_k(w2)
399
400
401 def _shuffle_mxfp8_moe_weights(
402 w13: torch.Tensor,
403 w2: torch.Tensor,
404 w13_scale: torch.Tensor,
405 w2_scale: torch.Tensor,
406 is_gated: bool,
407 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
408 """Preprocess MXFP8 weights and scales for the FlashInfer TRT-LLM kernel.
409
410 All three transforms (gate/up row reorder, ``shuffle_matrix_a`` weight
411 shuffle, ``shuffle_matrix_sf_a`` scale shuffle) are fixed row/index
412 permutations that depend only on the per-expert matrix shape, so the
413 permutation is computed once and applied to every expert in a single
414 gather instead of once per expert. ``block_scale_interleave`` accepts a
415 batched ``(E, M, K)`` scale tensor directly. Output is bit-identical to
416 the per-expert loop but ~20x faster (a 288-expert MoE otherwise costs
417 seconds per layer at load).
418 """
419 from flashinfer.fused_moe.core import (
420 get_reorder_rows_for_gated_act_gemm_row_indices,
421 )
422 from flashinfer.quantization.fp4_quantization import block_scale_interleave
423 from flashinfer.utils import get_shuffle_matrix_a_row_indices
424
425 epilogue_tile_m = 128
426
427 w13_u = w13.view(torch.uint8)
428 w2_u = w2.view(torch.uint8)
429
430 # 1. Interleave gate/up rows (gated activation GEMM layout).
431 if is_gated:
432 gate_idx = get_reorder_rows_for_gated_act_gemm_row_indices(
433 w13_u[0].reshape(w13_u.shape[1], -1)
434 )
435 w13_u = w13_u[:, gate_idx]
436 w13_scale = w13_scale[:, gate_idx]
437
438 def shuffle_weights(t: torch.Tensor) -> torch.Tensor:
439 # Row permutation depends only on (M, epilogue_tile_m).
440 idx = get_shuffle_matrix_a_row_indices(t[0], epilogue_tile_m).to(t.device)
441 return t[:, idx].view(torch.float8_e4m3fn)
442
443 def shuffle_scales(s: torch.Tensor) -> torch.Tensor:
444 # shuffle_matrix_sf_a == row-gather (same indices as the weight shuffle)
445 # followed by the 128x4 block-scale interleave, which is batch-capable.
446 idx = get_shuffle_matrix_a_row_indices(
447 s[0].view(torch.uint8).reshape(s.shape[1], -1), epilogue_tile_m
448 ).to(s.device)
449 interleaved = block_scale_interleave(s[:, idx])
450 return interleaved.reshape(s.shape)
451
452 return (
453 shuffle_weights(w13_u),
454 shuffle_weights(w2_u),
455 shuffle_scales(w13_scale),
456 shuffle_scales(w2_scale),
457 )
458
459
460 def prepare_fp8_moe_layer_for_fi(
461 layer: torch.nn.Module,
462 w13: torch.Tensor,
463 w2: torch.Tensor,
464 w13_scale: torch.Tensor,
465 w13_input_scale: torch.Tensor | None,
466 w2_scale: torch.Tensor,
467 w2_input_scale: torch.Tensor | None,
468 is_trtllm: bool = False,
469 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
470 """Convert Fp8 MoE weights to flashinfer kernel format.
471
472 Note that for trtllm we update the model state dict
473 with the scale format needed for these kernels.
474
475 Note that for per-tensor, we update the layer's
476 intermediate size if the weights needed padding.
477 """
478 assert hasattr(layer.moe_config, "is_act_and_mul")
479 block_quant = (
480 hasattr(layer, "weight_block_size") and layer.weight_block_size is not None
481 )
482 is_mxfp8 = block_quant and w13_scale.dtype == torch.uint8
483 is_deepseek_fp8 = block_quant and not is_mxfp8
484 is_gated = layer.activation.is_gated
485
486 # MXFP8 TRT-LLM requires W31 swap + reorder + shuffle.
487 if is_mxfp8 and is_trtllm:
488 # FlashInfer TRT-LLM SwiGLU expects [up; gate] but vLLM stores
489 # [gate; up]. Swap both weights and scales before interleaving.
490 if layer.moe_config.is_act_and_mul:
491 w13 = swap_w13_to_w31(w13)
492 # Scales may be 2D [E, flat] from _quantize_mxfp8_moe_weight;
493 # reshape to 3D so swap_w13_to_w31 can flip the two halves,
494 # then flatten back.
495 if w13_scale.ndim == 2:
496 num_rows = w13.shape[1] # 2 * intermediate_size
497 w13_scale = w13_scale.reshape(w13_scale.shape[0], num_rows, -1)
498 w13_scale = swap_w13_to_w31(w13_scale)
499 w13_scale = w13_scale.reshape(w13_scale.shape[0], -1)
500 else:
501 w13_scale = swap_w13_to_w31(w13_scale)
502
503 w13, w2, w13_scale, w2_scale = _shuffle_mxfp8_moe_weights(
504 w13, w2, w13_scale, w2_scale, is_gated
505 )
506 return w13, w2, w13_scale, w2_scale
507
508 # Some FI MoE kernels require internal alignment of 16
509 # for the gate-up proj. Pad the weights to respect this.
510 if not block_quant:
511 min_alignment = 16 if is_gated else 128
512 w13, w2, new_intermediate = align_moe_weights_for_fi(
513 w13,
514 w2,
515 layer.moe_config.is_act_and_mul,
516 min_alignment,
517 )
518 layer.moe_config.intermediate_size_per_partition = new_intermediate
519
520 # FI kernels require W31 layout rather than W13.
521 if layer.moe_config.is_act_and_mul:
522 w13 = swap_w13_to_w31(w13)
523 if block_quant:
524 w13_scale = swap_w13_to_w31(w13_scale)
525
526 # DeepSeekFp8 TRT-LLM: shuffle weights into BlockMajorK layout.
527 if is_deepseek_fp8 and is_trtllm:
528 w13, w2 = _shuffle_deepseek_fp8_moe_weights(w13, w2)
529
530 # FI TRT-LLM FP8 per-tensor MoE kernel requires weight shuffle
531 # and registration of alpha scales.
532 if is_trtllm and not block_quant:
533 assert w13_input_scale is not None
534 assert w2_input_scale is not None
535
536 rotate_weights_for_fi_trtllm_fp8_per_tensor_moe(w13, w2, is_gated)
537
538 # Clamp block scales to avoid NaN from the FlashInfer CUTLASS kernel.
539 # Some FP8 models have near-zero block scales (~1e-23) for dead/unused
540 # experts. The CUTLASS kernel doesn't handle these correctly on Hopper
541 # (SM 9.0), producing NaN instead of near-zero output. Clamping to a
542 # small minimum prevents this without affecting model accuracy since
543 # these experts' effective weights are already zero.
544 if block_quant:
545 _FI_CUTLASS_MIN_BLOCK_SCALE = 1e-10
546 w13_scale.clamp_(min=_FI_CUTLASS_MIN_BLOCK_SCALE)
547 w2_scale.clamp_(min=_FI_CUTLASS_MIN_BLOCK_SCALE)
548
549 return w13, w2, w13_scale, w2_scale