main
py 451 lines 15.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Utility helpers for NVFP4 + FlashInfer fused-MoE path."""
4
5 from typing import TYPE_CHECKING
6
7 import torch
8
9 from vllm.logger import init_logger
10 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
11 from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
12 align_fp4_moe_hidden_dim_for_fi,
13 align_fp4_moe_weights_for_fi,
14 )
15 from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
16 swizzle_blockscale,
17 )
18 from vllm.model_executor.layers.quantization.utils.quant_utils import (
19 amax_for_moe_activation_quant,
20 )
21
22 if TYPE_CHECKING:
23 from vllm.model_executor.layers.fused_moe import RoutedExperts
24 from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
25 NvFp4MoeBackend,
26 )
27
28 logger = init_logger(__name__)
29
30 __all__ = [
31 "reorder_w1w3_to_w3w1",
32 ]
33
34
35 def reorder_w1w3_to_w3w1(
36 weight: torch.Tensor, scale: torch.Tensor, dim: int = -2
37 ) -> tuple[torch.Tensor, torch.Tensor]:
38 """Re-order concatenated `[w1, w3]` tensors to `[w3, w1]` in-place.
39
40 `weight` and `scale` must be contiguous; they remain contiguous on return.
41 """
42 assert weight.is_contiguous(), "weight must be contiguous"
43 assert scale.is_contiguous(), "scale must be contiguous"
44 size = weight.size(dim)
45 assert size % 2 == 0, f"Expected even size in dim {dim}, got {size}"
46 half = size // 2
47 d = dim % weight.dim()
48
49 # 64 MB transient cap
50 bytes_per_row = max(
51 weight.numel() // size * weight.element_size(),
52 scale.numel() // size * scale.element_size(),
53 )
54 chunk = max(1, min(half, (64 << 20) // max(bytes_per_row, 1)))
55
56 fa, fb = [slice(None)] * weight.dim(), [slice(None)] * weight.dim()
57 for off in range(0, half, chunk):
58 end = min(off + chunk, half)
59 fa[d], fb[d] = slice(off, end), slice(half + off, half + end)
60 a, b = tuple(fa), tuple(fb)
61 for t in (weight, scale):
62 tmp = t[b].clone()
63 t[b] = t[a]
64 t[a] = tmp
65
66 return weight, scale
67
68
69 def interleave_linear_and_gate(
70 x: torch.Tensor,
71 group_size: int = 64,
72 dim: int = -1,
73 ) -> torch.Tensor:
74 """Interleave gate and linear weight rows for CuteDSL wrapper."""
75 sizes = x.size()
76 dim = dim % x.dim()
77 assert sizes[dim] % (group_size * 2) == 0, (
78 f"dim {dim} size {sizes[dim]} must be divisible by {group_size * 2}"
79 )
80 prev_sizes = sizes[:dim]
81 post_sizes = sizes[dim + 1 :]
82 x = x.view(*prev_sizes, 2, sizes[dim] // (group_size * 2), group_size, *post_sizes)
83 x = x.transpose(dim, dim + 1).contiguous().view(*sizes)
84 return x
85
86
87 def reorder_w13_to_w31_for_flashinfer_cutedsl(
88 activation: MoEActivation,
89 w13: torch.Tensor,
90 w13_scale: torch.Tensor,
91 ) -> tuple[torch.Tensor, torch.Tensor]:
92 """Normalize gated w13 rows to the [up; gate] order used by FlashInfer."""
93 if activation == MoEActivation.SWIGLUOAI:
94 # gpt-oss checkpoints store w13 interleaved as [gate0, up0, gate1, ...].
95 gate, up = w13[:, 0::2], w13[:, 1::2]
96 gate_scale, up_scale = w13_scale[:, 0::2], w13_scale[:, 1::2]
97 return (
98 torch.cat([up, gate], dim=1).contiguous(),
99 torch.cat([up_scale, gate_scale], dim=1).contiguous(),
100 )
101
102 half = w13.shape[1] // 2
103 return (
104 torch.cat([w13[:, half:], w13[:, :half]], dim=1).contiguous(),
105 torch.cat([w13_scale[:, half:], w13_scale[:, :half]], dim=1).contiguous(),
106 )
107
108
109 def prepare_nvfp4_moe_layer_for_flashinfer_cutedsl(
110 layer: "RoutedExperts",
111 w13: torch.Tensor,
112 w13_scale: torch.Tensor,
113 w13_scale_2: torch.Tensor,
114 a13_scale: torch.Tensor,
115 w2: torch.Tensor,
116 w2_scale: torch.Tensor,
117 w2_scale_2: torch.Tensor,
118 a2_scale: torch.Tensor,
119 ) -> tuple[
120 torch.Tensor,
121 torch.Tensor,
122 torch.Tensor,
123 torch.Tensor,
124 torch.Tensor,
125 torch.Tensor,
126 torch.Tensor,
127 torch.Tensor,
128 ]:
129 """Prepare weights for the CuteDSL wrapper-based NvFP4 MoE backend.
130
131 Pads the runtime expert tensors to the kernel's GEMM alignment, converts
132 weight scale factors to the MMA layout expected by CuteDslMoEWrapper, and
133 interleaves w13 gate/linear rows for gated activations.
134 """
135 # Global scaling factors (same as other FlashInfer backends).
136 num_experts = w13.shape[0]
137 enable_eplb = layer.moe_config.moe_parallel_config.enable_eplb
138 a13_scale = amax_for_moe_activation_quant(a13_scale, enable_eplb).repeat(
139 num_experts
140 )
141 a2_scale = amax_for_moe_activation_quant(a2_scale, enable_eplb).repeat(num_experts)
142
143 gated = layer.activation.is_gated
144 if gated:
145 w13, w13_scale = reorder_w13_to_w31_for_flashinfer_cutedsl(
146 layer.activation, w13, w13_scale
147 )
148
149 # GEMM1's output dimension must be a multiple of 128: 2I for gated
150 # activations (also required by interleaving), but only I for non-gated.
151 # Keep the checkpoint tensors unchanged and pad only the kernel's runtime
152 # representation. Zero rows also make the padded GEMM2 contraction a no-op.
153 w13, w13_scale, w2, w2_scale, padded_intermediate = align_fp4_moe_weights_for_fi(
154 w13,
155 w13_scale,
156 w2,
157 w2_scale,
158 is_act_and_mul=gated,
159 min_alignment=64 if gated else 128,
160 )
161 layer.moe_config.intermediate_size_per_partition = padded_intermediate
162
163 # GEMM1 gathers full 256-element K tiles of activations and their scales
164 # without a K-tail predicate. Pad runtime weights to match; the MoE runner
165 # pads activations and trims the output using the original hidden size.
166 w13, w13_scale, w2, w2_scale, padded_hidden = align_fp4_moe_hidden_dim_for_fi(
167 w13, w13_scale, w2, w2_scale
168 )
169 if layer.moe_config.hidden_dim_unpadded is None:
170 layer.moe_config.hidden_dim_unpadded = layer.moe_config.hidden_dim
171 layer.moe_config.hidden_dim = padded_hidden
172
173 if gated:
174 # Interleave up/gate rows for the fused gated activation.
175 w13 = interleave_linear_and_gate(w13, group_size=64, dim=1)
176 w13_scale = interleave_linear_and_gate(w13_scale, group_size=64, dim=1)
177
178 w13_scale = swizzle_blockscale(w13_scale)
179 w2_scale = swizzle_blockscale(w2_scale)
180
181 return (
182 w13,
183 w13_scale,
184 w13_scale_2,
185 a13_scale,
186 w2,
187 w2_scale,
188 w2_scale_2,
189 a2_scale,
190 )
191
192
193 def nvfp4_swizzled_scale_to_cutedsl_mma_view(scale: torch.Tensor) -> torch.Tensor:
194 """View a swizzled (E, M_padded, K_sf_padded) block-scale tensor in the
195 MMA layout expected by the CuteDSL MoE kernel.
196
197 The returned tensor aliases `scale`'s storage, so in-place updates of the
198 registered Parameter (weight reloads, EPLB rearrangement) are visible to
199 the kernel with no extra bookkeeping.
200 """
201 from flashinfer.cute_dsl.utils import convert_sf_to_mma_layout
202
203 num_experts, m_padded, k_sf_padded = scale.shape
204 mma_view = convert_sf_to_mma_layout(
205 scale.reshape(num_experts * m_padded, k_sf_padded),
206 m=m_padded,
207 k=k_sf_padded * 16,
208 num_groups=num_experts,
209 sf_vec_size=16,
210 )
211 assert mma_view.data_ptr() == scale.data_ptr(), (
212 "convert_sf_to_mma_layout no longer returns a view of its input; "
213 "the quant config would go stale after weight updates."
214 )
215 return mma_view
216
217
218 def prepare_static_weights_for_trtllm_fp4_moe(
219 gemm1_weights: torch.Tensor,
220 gemm2_weights: torch.Tensor,
221 gemm1_scales_linear_fp4_bytes: torch.Tensor,
222 gemm2_scales_linear_fp4_bytes: torch.Tensor,
223 hidden_size: int,
224 intermediate_size: int,
225 num_experts: int,
226 is_gated_activation: bool,
227 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
228 """Shuffle NVFP4 weights into the FlashInfer TRT-LLM layout."""
229 from flashinfer import nvfp4_block_scale_interleave
230 from flashinfer.fused_moe.core import (
231 _maybe_get_cached_w3_w1_permute_indices,
232 get_w2_permute_indices_with_cache,
233 )
234
235 permute_indices_cache: dict[tuple[object, ...], torch.Tensor] = {}
236 epilogue_tile_m = 128 # FIXME: this depends on the kernel internals
237 gemm1_intermediate_size = (
238 2 * intermediate_size if is_gated_activation else intermediate_size
239 )
240
241 # Convert quantized weights to proper formats
242 gemm1_weights_fp4 = gemm1_weights.view(torch.float8_e4m3fn).reshape(
243 num_experts, gemm1_intermediate_size, hidden_size // 2
244 ) # packed fp4
245 gemm1_scales_linear_fp4 = gemm1_scales_linear_fp4_bytes.view(
246 torch.float8_e4m3fn
247 ).reshape(
248 num_experts, gemm1_intermediate_size, hidden_size // 16
249 ) # fp8 scaling factors
250
251 gemm2_weights_fp4 = gemm2_weights.view(torch.float8_e4m3fn).reshape(
252 num_experts, hidden_size, intermediate_size // 2
253 ) # packed fp4
254 gemm2_scales_linear_fp4 = gemm2_scales_linear_fp4_bytes.view(
255 torch.float8_e4m3fn
256 ).reshape(num_experts, hidden_size, intermediate_size // 16) # fp8 scaling factors
257
258 gemm1_weight_indices = _maybe_get_cached_w3_w1_permute_indices(
259 permute_indices_cache,
260 gemm1_weights_fp4[0].view(torch.uint8),
261 epilogue_tile_m,
262 is_gated_act_gemm=is_gated_activation,
263 )
264 gemm1_scale_indices = _maybe_get_cached_w3_w1_permute_indices(
265 permute_indices_cache,
266 gemm1_scales_linear_fp4[0].view(torch.uint8),
267 epilogue_tile_m,
268 num_elts_per_sf=16,
269 is_gated_act_gemm=is_gated_activation,
270 )
271 gemm2_weight_indices = get_w2_permute_indices_with_cache(
272 permute_indices_cache,
273 gemm2_weights_fp4[0].view(torch.uint8),
274 epilogue_tile_m,
275 )
276 gemm2_scale_indices = get_w2_permute_indices_with_cache(
277 permute_indices_cache,
278 gemm2_scales_linear_fp4[0].view(torch.uint8),
279 epilogue_tile_m,
280 num_elts_per_sf=16,
281 )
282
283 gemm1_weights_fp4_shuffled = torch.empty_like(gemm1_weights_fp4, dtype=torch.uint8)
284 gemm1_scales_fp4_shuffled = torch.empty_like(gemm1_scales_linear_fp4)
285 gemm2_weights_fp4_shuffled = torch.empty_like(gemm2_weights_fp4, dtype=torch.uint8)
286 gemm2_scales_fp4_shuffled = torch.empty_like(gemm2_scales_linear_fp4)
287 gemm1_scale_scratch = torch.empty_like(
288 gemm1_scales_linear_fp4[0], dtype=torch.uint8
289 )
290 gemm2_scale_scratch = torch.empty_like(
291 gemm2_scales_linear_fp4[0], dtype=torch.uint8
292 )
293
294 for expert_id in range(num_experts):
295 torch.index_select(
296 gemm1_weights_fp4[expert_id].view(torch.uint8),
297 0,
298 gemm1_weight_indices,
299 out=gemm1_weights_fp4_shuffled[expert_id],
300 )
301 torch.index_select(
302 gemm1_scales_linear_fp4[expert_id].view(torch.uint8),
303 0,
304 gemm1_scale_indices,
305 out=gemm1_scale_scratch,
306 )
307 gemm1_scales_fp4_shuffled[expert_id].view(torch.uint8).reshape(-1).copy_(
308 nvfp4_block_scale_interleave(gemm1_scale_scratch)
309 )
310
311 torch.index_select(
312 gemm2_weights_fp4[expert_id].view(torch.uint8),
313 0,
314 gemm2_weight_indices,
315 out=gemm2_weights_fp4_shuffled[expert_id],
316 )
317 torch.index_select(
318 gemm2_scales_linear_fp4[expert_id].view(torch.uint8),
319 0,
320 gemm2_scale_indices,
321 out=gemm2_scale_scratch,
322 )
323 gemm2_scales_fp4_shuffled[expert_id].view(torch.uint8).reshape(-1).copy_(
324 nvfp4_block_scale_interleave(gemm2_scale_scratch)
325 )
326
327 return (
328 gemm1_weights_fp4_shuffled,
329 gemm1_scales_fp4_shuffled,
330 gemm2_weights_fp4_shuffled,
331 gemm2_scales_fp4_shuffled,
332 )
333
334
335 def prepare_nvfp4_moe_layer_for_fi_or_cutlass(
336 backend: "NvFp4MoeBackend",
337 layer: "RoutedExperts",
338 w13: torch.Tensor,
339 w13_scale: torch.Tensor,
340 w13_scale_2: torch.Tensor,
341 a13_scale: torch.Tensor,
342 w2: torch.Tensor,
343 w2_scale: torch.Tensor,
344 w2_scale_2: torch.Tensor,
345 a2_scale: torch.Tensor,
346 is_act_and_mul: bool,
347 ) -> tuple[
348 torch.Tensor,
349 torch.Tensor,
350 torch.Tensor,
351 torch.Tensor,
352 torch.Tensor,
353 torch.Tensor,
354 torch.Tensor,
355 torch.Tensor,
356 ]:
357 # Delayed import for circular dependency avoidance.
358 from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
359 NvFp4MoeBackend,
360 is_global_sf_supported_for_nvfp4_backend,
361 )
362
363 assert backend in [
364 NvFp4MoeBackend.VLLM_CUTLASS,
365 NvFp4MoeBackend.FLASHINFER_CUTLASS,
366 NvFp4MoeBackend.FLASHINFER_TRTLLM,
367 NvFp4MoeBackend.FLASHINFER_CUTEDSL_BATCHED,
368 NvFp4MoeBackend.FLASHINFER_B12X,
369 ]
370
371 # Reorder [w1, w3] to [w3, w1] for FI NVFP4 MoE kernels.
372 is_gated = layer.activation.is_gated
373 if (
374 is_gated
375 and is_act_and_mul
376 and backend
377 in [
378 NvFp4MoeBackend.FLASHINFER_CUTLASS,
379 NvFp4MoeBackend.FLASHINFER_TRTLLM,
380 NvFp4MoeBackend.FLASHINFER_B12X,
381 ]
382 ):
383 w13, w13_scale = reorder_w1w3_to_w3w1(w13, w13_scale)
384
385 # For some FI kernels, the input scales are shared by all experts.
386 if is_global_sf_supported_for_nvfp4_backend(backend):
387 num_experts = w13.shape[0]
388 enable_eplb = layer.moe_config.moe_parallel_config.enable_eplb
389 a13_scale = amax_for_moe_activation_quant(a13_scale, enable_eplb).repeat(
390 num_experts
391 )
392 a2_scale = amax_for_moe_activation_quant(a2_scale, enable_eplb).repeat(
393 num_experts
394 )
395 else:
396 a13_scale = a13_scale.max(dim=1).values.to(torch.float32)
397
398 # Shuffle weights and scales for FI TRTLLM NVFP4 MoE kernels.
399 if backend == NvFp4MoeBackend.FLASHINFER_TRTLLM:
400 w13, w13_scale, w2, w2_scale, padded_hidden = align_fp4_moe_hidden_dim_for_fi(
401 w13, w13_scale, w2, w2_scale
402 )
403 if layer.moe_config.hidden_dim_unpadded is None:
404 layer.moe_config.hidden_dim_unpadded = layer.moe_config.hidden_dim
405 layer.moe_config.hidden_dim = padded_hidden
406
407 # Align weights for FI NVFP4 MoE kernels.
408 # FlashInfer's TRT-LLM block-scale shuffle asserts the gate/up row dim
409 # (= up_mult * padded_intermediate, up_mult=2 when gated) is a multiple of
410 # 128. So gated needs padded_intermediate % 64 (2*64=128); the old value 16
411 # left 2*intermediate a multiple of only 32, so an NVFP4 MoE whose rank-local
412 # intermediate is not 128-aligned at TP>1 (e.g. Gemma-4-26B-A4B at tp4) hit
413 # `assert M % 128 == 0`. Padded rows are zero -> outputs unchanged.
414 min_alignment = 64 if is_gated else 128
415 w13, w13_scale, w2, w2_scale, padded_intermediate = (
416 align_fp4_moe_weights_for_fi(
417 w13, w13_scale, w2, w2_scale, is_act_and_mul, min_alignment
418 )
419 )
420 layer.moe_config.intermediate_size_per_partition = padded_intermediate
421
422 w13, w13_scale, w2, w2_scale = prepare_static_weights_for_trtllm_fp4_moe(
423 w13,
424 w2,
425 w13_scale,
426 w2_scale,
427 hidden_size=w2.size(-2),
428 intermediate_size=w13.size(-2) // 2 if is_gated else w13.size(-2),
429 num_experts=w13.size(0),
430 is_gated_activation=is_gated,
431 )
432 else:
433 # Swizzle the block scales for other FI NVFP4 MoE kernels.
434 w13_scale = swizzle_blockscale(w13_scale)
435
436 # Apply padding if needed.
437 pad_size = w13_scale.size(1) - w13.size(1)
438 if pad_size > 0:
439 if is_act_and_mul:
440 raise NotImplementedError(
441 "Intermediate size padding for w1 and w3, for %s "
442 "NvFp4 backend, but this is not currently supported",
443 backend.value,
444 )
445 w13 = torch.nn.functional.pad(w13, (0, 0, 0, pad_size))
446 w2 = torch.nn.functional.pad(w2, (0, pad_size // 2, 0, 0))
447 w2_scale = torch.nn.functional.pad(w2_scale, (0, pad_size // 16))
448
449 w2_scale = swizzle_blockscale(w2_scale)
450
451 return w13, w13_scale, w13_scale_2, a13_scale, w2, w2_scale, w2_scale_2, a2_scale