main
py 569 lines 18.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Taken from https://github.com/ModelTC/LightLLM/blob/8ed97c74c18f11505b048b1ba00ba5c0cef8bff6/lightllm/common/fused_moe/deepep_scatter_gather.py
4 and updated to fit vllm needs and terminology.
5 """
6
7 import math
8
9 import torch
10
11 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
12 from vllm.model_executor.layers.fused_moe.utils import count_expert_num_tokens
13 from vllm.triton_utils import tl, triton
14 from vllm.utils.deep_gemm import get_mk_alignment_for_contiguous_layout
15 from vllm.utils.math_utils import round_up
16
17
18 def expert_num_tokens_round_up_and_sum(
19 expert_num_tokens: torch.Tensor, alignment: int
20 ) -> int:
21 # Round up each element in expert_num_tokens to the nearest multiple of
22 # alignment.
23 ent = (expert_num_tokens.to(torch.int64) + (alignment - 1)) // alignment * alignment
24 return torch.sum(ent).item()
25
26
27 def compute_aligned_M_and_alignment(
28 M: int,
29 num_topk: int,
30 local_num_experts: int,
31 alignment: int,
32 expert_tokens_meta: mk.ExpertTokensMetadata | None,
33 ) -> tuple[int, int]:
34 """Return (M_sum, alignment_used).
35
36 `alignment_used` may be smaller than the caller-supplied `alignment` on
37 SM100/SM120 when DeepGEMM can JIT a smaller BLOCK_M for the per-call
38 expected_m. Callers that index by block size (e.g. ``M_sum // block_m``)
39 or assert workspace alignment must use the returned `alignment_used`,
40 not their original `alignment` argument.
41
42 Prefer this over the int-returning :func:`compute_aligned_M` when the
43 GEMM call site needs to wrap itself in ``mk_alignment_scope`` or
44 otherwise reason about the actual per-expert padding.
45 """
46 if (expert_tokens_meta is not None) and (
47 expert_tokens_meta.expert_num_tokens_cpu is not None
48 ):
49 return (
50 expert_num_tokens_round_up_and_sum(
51 expert_tokens_meta.expert_num_tokens_cpu, alignment=alignment
52 ),
53 alignment,
54 )
55
56 # expert_num_tokens not on cpu. Cap padding by min(M*num_topk,
57 # local_num_experts) — at batch=1 decode only `num_topk` experts can be
58 # active, so the worst-case `local_num_experts*(align-1)` is too loose.
59 # Also shrink `alignment` to DeepGEMM's per-call theoretical BLOCK_M on
60 # SM100/SM120 when smaller.
61 expected_m = M * num_topk
62 try:
63 from vllm.utils.deep_gemm import (
64 get_theoretical_mk_alignment_for_contiguous_layout,
65 )
66
67 # num_groups=local_num_experts so the helper recovers per-expert em;
68 # omitting it over-picks BLOCK_M on SM120 (heuristic assumes em is
69 # already per-expert).
70 per_call_align = get_theoretical_mk_alignment_for_contiguous_layout(
71 expected_m=expected_m,
72 num_groups=local_num_experts,
73 )
74 if per_call_align and per_call_align <= alignment:
75 alignment = per_call_align
76 except Exception:
77 pass
78
79 max_active_experts = min(M * num_topk, local_num_experts)
80 M_sum = (M * num_topk) + max_active_experts * (alignment - 1)
81 M_sum = round_up(M_sum, alignment)
82 return M_sum, alignment
83
84
85 def compute_aligned_M(
86 M: int,
87 num_topk: int,
88 local_num_experts: int,
89 alignment: int,
90 expert_tokens_meta: mk.ExpertTokensMetadata | None,
91 ) -> int:
92 """Return ``M_sum`` only (backward-compat wrapper).
93
94 Equivalent to :func:`compute_aligned_M_and_alignment`'s first return
95 value. Existing downstream callers and the warmup path that only size
96 a workspace use this. Call sites that need the actual per-expert
97 alignment (to wrap GEMMs in ``mk_alignment_scope``) should use
98 :func:`compute_aligned_M_and_alignment` instead.
99 """
100 M_sum, _ = compute_aligned_M_and_alignment(
101 M, num_topk, local_num_experts, alignment, expert_tokens_meta
102 )
103 return M_sum
104
105
106 @triton.jit
107 def apply_expert_map(expert_id, expert_map):
108 if expert_id != -1:
109 expert_id = tl.load(expert_map + expert_id).to(expert_id.dtype)
110 return expert_id
111
112
113 @triton.jit
114 def _fwd_kernel_ep_scatter_1(
115 num_recv_tokens_per_expert,
116 expert_start_loc,
117 m_indices,
118 num_experts: tl.constexpr,
119 BLOCK_E: tl.constexpr,
120 BLOCK_EXPERT_NUM: tl.constexpr,
121 ALIGN_M: tl.constexpr,
122 ):
123 cur_expert = tl.program_id(0)
124
125 offset_cumsum = tl.arange(0, BLOCK_EXPERT_NUM)
126 tokens_per_expert = tl.load(
127 num_recv_tokens_per_expert + offset_cumsum,
128 mask=offset_cumsum < num_experts,
129 other=0,
130 )
131 # Round up to ALIGN_M so cumsum matches the workspace's per-expert slices.
132 tokens_per_expert = ((tokens_per_expert + ALIGN_M - 1) // ALIGN_M) * ALIGN_M
133 cumsum = tl.cumsum(tokens_per_expert) - tokens_per_expert
134
135 # Extract this block's offset from the register vector (warp shuffle,
136 # no global memory round-trip) then write it once to expert_start_loc.
137 cur_expert_start = tl.sum(
138 tl.where(offset_cumsum == cur_expert, cumsum, tl.zeros_like(cumsum))
139 )
140 tl.store(expert_start_loc + cur_expert, cur_expert_start)
141 cur_expert_token_num = tl.load(num_recv_tokens_per_expert + cur_expert)
142
143 m_indices_start_ptr = m_indices + cur_expert_start
144 off_expert = tl.arange(0, BLOCK_E)
145
146 # any rows in the per-expert aligned region that do not correspond to
147 # real tokens are left untouched here and should remain initialized to
148 # -1 so DeepGEMM can skip them
149 for start_m in tl.range(0, cur_expert_token_num, BLOCK_E):
150 offs = start_m + off_expert
151 mask = offs < cur_expert_token_num
152 tl.store(
153 m_indices_start_ptr + offs,
154 cur_expert,
155 mask=mask,
156 )
157
158
159 @triton.jit
160 def _fwd_kernel_ep_scatter_2(
161 total_token_num,
162 expert_start_loc,
163 recv_x,
164 recv_x_stride0,
165 recv_x_stride1,
166 recv_x_scale,
167 recv_x_scale_stride0,
168 recv_x_scale_stride1,
169 recv_topk,
170 recv_topk_stride0,
171 recv_topk_stride1,
172 output_tensor,
173 output_tensor_stride0,
174 output_tensor_stride1,
175 output_tensor_scale,
176 output_tensor_scale_stride0,
177 output_tensor_scale_stride1,
178 output_index,
179 output_index_stride0,
180 output_index_stride1,
181 topk_num: tl.constexpr,
182 expert_map,
183 HAS_EXPERT_MAP: tl.constexpr,
184 HIDDEN_SIZE: tl.constexpr,
185 HIDDEN_SIZE_PAD: tl.constexpr,
186 SCALE_HIDDEN_SIZE: tl.constexpr,
187 SCALE_HIDDEN_SIZE_PAD: tl.constexpr,
188 PACK_UE8M0: tl.constexpr,
189 SCALE_PACKED_SIZE: tl.constexpr,
190 SCALE_PACKED_SIZE_PAD: tl.constexpr,
191 ):
192 start_token_id = tl.program_id(0)
193 grid_num = tl.num_programs(0)
194
195 offset_in = tl.arange(0, HIDDEN_SIZE_PAD)
196 mask = offset_in < HIDDEN_SIZE
197
198 output_tensor_stride0 = output_tensor_stride0.to(tl.int64)
199
200 if PACK_UE8M0:
201 # One int32 per 4 consecutive 32-wide UE8M0 groups, stored MN-major.
202 offs_pk = tl.arange(0, SCALE_PACKED_SIZE_PAD)
203 mask_pk = offs_pk < SCALE_PACKED_SIZE
204 else:
205 offset_in_s = tl.arange(0, SCALE_HIDDEN_SIZE_PAD)
206 mask_s = offset_in_s < SCALE_HIDDEN_SIZE
207
208 for token_id in range(start_token_id, total_token_num, grid_num):
209 to_copy = tl.load(recv_x + token_id * recv_x_stride0 + offset_in, mask=mask)
210
211 if PACK_UE8M0:
212 # Pack 4 UE8M0 bytes into one int32 (byte j = group 4*pk+j).
213 base_s = recv_x_scale + token_id * recv_x_scale_stride0
214 g0, g1 = offs_pk * 4, offs_pk * 4 + 1
215 g2, g3 = offs_pk * 4 + 2, offs_pk * 4 + 3
216 b0 = tl.load(
217 base_s + g0 * recv_x_scale_stride1, mask=g0 < SCALE_HIDDEN_SIZE
218 )
219 b1 = tl.load(
220 base_s + g1 * recv_x_scale_stride1, mask=g1 < SCALE_HIDDEN_SIZE
221 )
222 b2 = tl.load(
223 base_s + g2 * recv_x_scale_stride1, mask=g2 < SCALE_HIDDEN_SIZE
224 )
225 b3 = tl.load(
226 base_s + g3 * recv_x_scale_stride1, mask=g3 < SCALE_HIDDEN_SIZE
227 )
228 packed_s = (
229 b0.to(tl.int32)
230 | (b1.to(tl.int32) << 8)
231 | (b2.to(tl.int32) << 16)
232 | (b3.to(tl.int32) << 24)
233 )
234 else:
235 to_copy_s = tl.load(
236 recv_x_scale + token_id * recv_x_scale_stride0 + offset_in_s,
237 mask=mask_s,
238 )
239
240 for topk_index in tl.range(0, topk_num, 1, num_stages=4):
241 expert_id = tl.load(recv_topk + token_id * recv_topk_stride0 + topk_index)
242
243 if HAS_EXPERT_MAP:
244 expert_id = apply_expert_map(expert_id, expert_map)
245
246 if expert_id >= 0:
247 dest_token_index = tl.atomic_add(expert_start_loc + expert_id, 1)
248 dest_token_index_i64 = dest_token_index.to(tl.int64)
249 tl.store(
250 output_index + token_id * output_index_stride0 + topk_index,
251 dest_token_index,
252 )
253 output_tensor_ptr = (
254 output_tensor + dest_token_index_i64 * output_tensor_stride0
255 )
256 tl.store(output_tensor_ptr + offset_in, to_copy, mask=mask)
257
258 output_tensor_scale_ptr = (
259 output_tensor_scale + dest_token_index * output_tensor_scale_stride0
260 )
261 if PACK_UE8M0:
262 tl.store(
263 output_tensor_scale_ptr + offs_pk * output_tensor_scale_stride1,
264 packed_s,
265 mask=mask_pk,
266 )
267 else:
268 tl.store(
269 output_tensor_scale_ptr + offset_in_s, to_copy_s, mask=mask_s
270 )
271
272
273 @torch.no_grad()
274 def ep_scatter(
275 recv_x: torch.Tensor,
276 recv_x_scale: torch.Tensor,
277 recv_topk: torch.Tensor,
278 num_recv_tokens_per_expert: torch.Tensor,
279 expert_map: torch.Tensor | None,
280 expert_start_loc: torch.Tensor,
281 output_tensor: torch.Tensor,
282 output_tensor_scale: torch.Tensor,
283 m_indices: torch.Tensor,
284 output_index: torch.Tensor,
285 align_m: int = 128,
286 block_size: int = 128,
287 pack_ue8m0: bool = False,
288 ):
289 # BLOCK_E is the m_indices fill-loop tile (masked), independent of align_m.
290 BLOCK_E = 128
291 BLOCK_D = block_size # block size of activation-scale quantization
292 num_warps = 8
293 num_experts = num_recv_tokens_per_expert.shape[0]
294 hidden_size = recv_x.shape[1]
295 # grid = (triton.cdiv(hidden_size, BLOCK_D), num_experts)
296 grid = num_experts
297
298 assert m_indices.shape[0] % align_m == 0
299 assert expert_start_loc.shape[0] == num_experts
300
301 # pack_ue8m0: scatter packs 4 UE8M0 bytes per int32; else copies scales as-is.
302 scale_hidden_size = hidden_size // BLOCK_D
303 scale_packed_size = (scale_hidden_size + 3) // 4 if pack_ue8m0 else 1
304
305 _fwd_kernel_ep_scatter_1[(grid,)](
306 num_recv_tokens_per_expert,
307 expert_start_loc,
308 m_indices,
309 num_experts=num_experts,
310 num_warps=num_warps,
311 BLOCK_E=BLOCK_E,
312 BLOCK_EXPERT_NUM=triton.next_power_of_2(num_experts),
313 ALIGN_M=align_m,
314 )
315
316 grid = min(recv_topk.shape[0], 1024 * 8)
317
318 _fwd_kernel_ep_scatter_2[(grid,)](
319 recv_topk.shape[0],
320 expert_start_loc,
321 recv_x,
322 recv_x.stride(0),
323 recv_x.stride(1),
324 recv_x_scale,
325 recv_x_scale.stride(0),
326 recv_x_scale.stride(1),
327 recv_topk,
328 recv_topk.stride(0),
329 recv_topk.stride(1),
330 output_tensor,
331 output_tensor.stride(0),
332 output_tensor.stride(1),
333 output_tensor_scale,
334 output_tensor_scale.stride(0),
335 output_tensor_scale.stride(1),
336 output_index,
337 output_index.stride(0),
338 output_index.stride(1),
339 topk_num=recv_topk.shape[1],
340 expert_map=expert_map,
341 HAS_EXPERT_MAP=expert_map is not None,
342 num_warps=num_warps,
343 HIDDEN_SIZE=hidden_size,
344 HIDDEN_SIZE_PAD=triton.next_power_of_2(hidden_size),
345 SCALE_HIDDEN_SIZE=scale_hidden_size,
346 SCALE_HIDDEN_SIZE_PAD=triton.next_power_of_2(scale_hidden_size),
347 PACK_UE8M0=pack_ue8m0,
348 SCALE_PACKED_SIZE=scale_packed_size,
349 SCALE_PACKED_SIZE_PAD=triton.next_power_of_2(scale_packed_size),
350 )
351 return
352
353
354 @triton.jit
355 def _fwd_kernel_ep_gather(
356 total_token_num,
357 input_tensor,
358 input_tensor_stride0,
359 input_tensor_stride1,
360 recv_topk_ids,
361 recv_topk_ids_stride0,
362 recv_topk_ids_stride1,
363 recv_topk_weight,
364 recv_topk_weight_stride0,
365 recv_topk_weight_stride1,
366 input_index,
367 input_index_stride0,
368 input_index_stride1,
369 output_tensor,
370 output_tensor_stride0,
371 output_tensor_stride1,
372 topk_num: tl.constexpr,
373 expert_map,
374 HAS_EXPERT_MAP: tl.constexpr,
375 BLOCK_D: tl.constexpr,
376 ):
377 cur_block = tl.program_id(0)
378 start_cur_token = tl.program_id(1)
379 grid_num = tl.num_programs(1)
380
381 for cur_token in range(start_cur_token, total_token_num, grid_num):
382 off_d = tl.arange(0, BLOCK_D)
383 accumulator = tl.zeros([BLOCK_D], dtype=tl.float32)
384 for topk_index in range(0, topk_num):
385 expert_id = tl.load(
386 recv_topk_ids + cur_token * recv_topk_ids_stride0 + topk_index
387 )
388
389 if HAS_EXPERT_MAP:
390 expert_id = apply_expert_map(expert_id, expert_map)
391
392 if expert_id >= 0:
393 source_token_index = tl.load(
394 input_index + cur_token * input_index_stride0 + topk_index
395 )
396 source_token_index_i64 = source_token_index.to(tl.int64)
397 acc_weight = tl.load(
398 recv_topk_weight + cur_token * recv_topk_weight_stride0 + topk_index
399 )
400 tmp = tl.load(
401 input_tensor
402 + source_token_index_i64 * input_tensor_stride0
403 + cur_block * BLOCK_D
404 + off_d
405 )
406 accumulator += tmp.to(tl.float32) * acc_weight
407
408 tl.store(
409 output_tensor
410 + cur_token * output_tensor_stride0
411 + cur_block * BLOCK_D
412 + off_d,
413 accumulator.to(output_tensor.dtype.element_ty),
414 )
415
416
417 @torch.no_grad()
418 def ep_gather(
419 input_tensor: torch.Tensor,
420 recv_topk_ids: torch.Tensor,
421 recv_topk_weight: torch.Tensor,
422 input_index: torch.Tensor,
423 expert_map: torch.Tensor | None,
424 output_tensor: torch.Tensor,
425 ):
426 num_warps = 2
427 num_tokens = output_tensor.shape[0]
428 hidden_size = input_tensor.shape[1]
429 BLOCK_D = math.gcd(hidden_size, 1024)
430 assert hidden_size % BLOCK_D == 0
431 grid = (triton.cdiv(hidden_size, BLOCK_D), min(num_tokens, 1024))
432
433 _fwd_kernel_ep_gather[grid](
434 num_tokens,
435 input_tensor,
436 input_tensor.stride(0),
437 input_tensor.stride(1),
438 recv_topk_ids,
439 recv_topk_ids.stride(0),
440 recv_topk_ids.stride(1),
441 recv_topk_weight,
442 recv_topk_weight.stride(0),
443 recv_topk_weight.stride(1),
444 input_index,
445 input_index.stride(0),
446 input_index.stride(1),
447 output_tensor,
448 output_tensor.stride(0),
449 output_tensor.stride(1),
450 topk_num=recv_topk_ids.shape[1],
451 expert_map=expert_map,
452 HAS_EXPERT_MAP=expert_map is not None,
453 num_warps=num_warps,
454 BLOCK_D=BLOCK_D,
455 )
456 return
457
458
459 def deepgemm_moe_permute(
460 aq: torch.Tensor,
461 aq_scale: torch.Tensor,
462 topk_ids: torch.Tensor,
463 local_num_experts: int,
464 expert_map: torch.Tensor | None,
465 expert_tokens_meta: mk.ExpertTokensMetadata | None,
466 aq_out: torch.Tensor | None = None,
467 block_size: int | None = None,
468 ):
469 assert aq.ndim == 2
470 assert topk_ids.dtype.is_signed, "The kernel uses -1 to represent invalid topk_ids"
471 H = aq.size(1)
472 device = aq.device
473
474 block_m, block_k = get_mk_alignment_for_contiguous_layout()
475 # The activation-scale group size may differ from the M/K tile alignment
476 # (e.g. MXFP8 uses a 32-element scale group while block_k stays 128).
477 if block_size is not None:
478 block_k = block_size
479
480 M_sum, align_used = compute_aligned_M_and_alignment(
481 M=topk_ids.size(0),
482 num_topk=topk_ids.size(1),
483 local_num_experts=local_num_experts,
484 alignment=block_m,
485 expert_tokens_meta=expert_tokens_meta,
486 )
487
488 expert_start_loc = torch.empty(
489 (local_num_experts), device=device, dtype=torch.int32
490 )
491
492 assert aq_out is None or aq_out.shape == (M_sum, H)
493 if aq_out is None:
494 aq_out = torch.empty((M_sum, H), device=device, dtype=aq.dtype)
495
496 # uint8 UE8M0 (MXFP8) -> scatter packs into DeepGEMM's int32 MN-major
497 # TMA-aligned layout; float32 (FP8/FP4) scattered row-major as-is.
498 pack_ue8m0 = aq_scale.dtype == torch.uint8
499 sf_k = H // block_k
500 if pack_ue8m0:
501 packed_sf_k = (sf_k + 3) // 4
502 tma_aligned_mn = round_up(M_sum, 4)
503 aq_scale_out = torch.empty_strided(
504 (M_sum, packed_sf_k),
505 (1, tma_aligned_mn),
506 device=device,
507 dtype=torch.int32,
508 )
509 else:
510 aq_scale_out = torch.zeros((M_sum, sf_k), device=device, dtype=torch.float32)
511
512 # DeepGEMM uses negative values in m_indices (here expert_ids) to mark
513 # completely invalid / padded blocks that should be skipped. We always
514 # initialize expert_ids to -1 so any row that is not explicitly written
515 # by the scatter kernel will be treated as invalid and skipped by
516 # DeepGEMM's scheduler.
517 expert_ids = torch.full(
518 (M_sum,),
519 fill_value=-1,
520 device=device,
521 dtype=torch.int32,
522 )
523 inv_perm = torch.empty(topk_ids.shape, device=device, dtype=torch.int32)
524
525 expert_num_tokens = None
526 if expert_tokens_meta is not None:
527 expert_num_tokens = expert_tokens_meta.expert_num_tokens
528 if expert_num_tokens is None:
529 # A cudagraph-decode carrier may hold only psum_recv_per_rank and leave
530 # expert_num_tokens None; count locally as in the meta-absent case.
531 expert_num_tokens = count_expert_num_tokens(
532 topk_ids, local_num_experts, expert_map
533 )
534
535 ep_scatter(
536 recv_x=aq,
537 recv_x_scale=aq_scale,
538 recv_topk=topk_ids,
539 num_recv_tokens_per_expert=expert_num_tokens,
540 expert_start_loc=expert_start_loc,
541 expert_map=expert_map,
542 output_tensor=aq_out,
543 output_tensor_scale=aq_scale_out,
544 m_indices=expert_ids,
545 output_index=inv_perm,
546 align_m=align_used,
547 block_size=block_k,
548 pack_ue8m0=pack_ue8m0,
549 )
550
551 return aq_out, aq_scale_out, expert_ids, inv_perm, align_used
552
553
554 def deepgemm_unpermute_and_reduce(
555 a: torch.Tensor, # Grouped gemm output
556 topk_ids: torch.Tensor,
557 topk_weights: torch.Tensor,
558 inv_perm: torch.Tensor,
559 expert_map: torch.Tensor | None,
560 output: torch.Tensor,
561 ):
562 return ep_gather(
563 input_tensor=a,
564 recv_topk_ids=topk_ids,
565 recv_topk_weight=topk_weights,
566 input_index=inv_perm,
567 expert_map=expert_map,
568 output_tensor=output,
569 )