main
py 1,855 lines 61.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Fused MoE Triton kernels."""
4
5 import functools
6 import json
7 import os
8 from typing import Any
9
10 import torch
11
12 import vllm.envs as envs
13 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
14 from vllm import _custom_ops as ops
15 from vllm.logger import init_logger
16 from vllm.model_executor.layers.fused_moe.activation import (
17 MoEActivation,
18 apply_moe_activation,
19 )
20 from vllm.model_executor.layers.fused_moe.config import (
21 FUSED_MOE_UNQUANTIZED_CONFIG,
22 FusedMoEQuantConfig,
23 _get_config_dtype_str,
24 )
25 from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
26 moe_align_block_size,
27 )
28 from vllm.model_executor.layers.fused_moe.utils import (
29 enable_swap_ab,
30 moe_kernel_quantize_input,
31 resolve_moe_use_td,
32 warn_if_moe_use_td_ineffective,
33 )
34 from vllm.platforms import current_platform
35 from vllm.triton_utils import tl, triton
36 from vllm.triton_utils.allocation import set_triton_allocator
37 from vllm.utils.math_utils import next_power_of_2
38 from vllm.utils.platform_utils import get_device_name_as_file_name
39 from vllm.utils.torch_utils import direct_register_custom_op
40
41 logger = init_logger(__name__)
42
43
44 @triton.jit
45 def write_zeros_to_output(
46 c_ptr,
47 stride_cm,
48 stride_cn,
49 pid_n,
50 N,
51 offs_token,
52 token_mask,
53 BLOCK_SIZE_M,
54 BLOCK_SIZE_N,
55 compute_type,
56 ):
57 accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=compute_type)
58 offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
59 c_ptrs = c_ptr + stride_cm * offs_token[:, None] + stride_cn * offs_cn[None, :]
60 c_mask = token_mask[:, None] & (offs_cn[None, :] < N)
61 tl.store(c_ptrs, accumulator, mask=c_mask)
62
63
64 @triton.jit
65 def fused_moe_kernel_gptq_awq(
66 # Pointers to matrices
67 a_ptr,
68 b_ptr,
69 c_ptr,
70 b_scale_ptr,
71 b_zp_ptr,
72 topk_weights_ptr,
73 sorted_token_ids_ptr,
74 expert_ids_ptr,
75 num_tokens_post_padded_ptr,
76 # Matrix dimensions
77 N: tl.constexpr,
78 K: tl.constexpr,
79 EM,
80 num_valid_tokens,
81 # The stride variables represent how much to increase the ptr by when
82 # moving by 1 element in a particular dimension. E.g. `stride_am` is
83 # how much to increase `a_ptr` by to get the element one row down
84 # (A has M rows).
85 stride_am,
86 stride_ak,
87 stride_be,
88 stride_bk,
89 stride_bn,
90 stride_cm,
91 stride_cn,
92 stride_bse,
93 stride_bsk,
94 stride_bsn,
95 stride_bze,
96 stride_bzk,
97 stride_bzn,
98 block_k_diviable: tl.constexpr,
99 group_size: tl.constexpr,
100 # Meta-parameters
101 BLOCK_SIZE_M: tl.constexpr,
102 BLOCK_SIZE_N: tl.constexpr,
103 BLOCK_SIZE_K: tl.constexpr,
104 GROUP_SIZE_M: tl.constexpr,
105 SPLIT_K: tl.constexpr,
106 MUL_ROUTED_WEIGHT: tl.constexpr,
107 top_k: tl.constexpr,
108 compute_type: tl.constexpr,
109 has_zp: tl.constexpr,
110 use_int4_w4a16: tl.constexpr,
111 use_int8_w8a16: tl.constexpr,
112 ):
113 """Implements the fused computation for a Mixture of Experts (MOE) using
114 token and expert matrices.
115
116 Key Parameters:
117 - A: The input tensor representing tokens with shape (*, K), where '*' can
118 be any shape representing batches and K is the feature dimension of
119 each token.
120 - B: The stacked MOE weight tensor with shape (E, N, K), where E is
121 the number of experts, K is the input feature dimension, and N is
122 the output feature dimension.
123 - C: The output cache tensor with shape (M, topk, N), where M is the
124 total number of tokens post padding, topk is the number of times
125 each token is repeated, and N is the output feature dimension.
126 - sorted_token_ids: A tensor containing the sorted indices of tokens,
127 repeated topk times and arranged by the expert index they are
128 assigned to.
129 - expert_ids: A tensor containing the indices of the expert for each
130 block. It determines which expert matrix from B should be used for
131 each block in A.
132 This kernel performs the multiplication of a token by its corresponding
133 expert matrix as determined by `expert_ids`. The sorting of
134 `sorted_token_ids` by expert index and padding ensures divisibility by
135 BLOCK_SIZE_M, which is necessary to maintain consistency in block matrix
136 multiplication across different blocks processed by the same expert.
137 """
138 # -----------------------------------------------------------
139 # Map program ids `pid` to the block of C it should compute.
140 # This is done in a grouped ordering to promote L2 data reuse.
141 pid = tl.program_id(axis=0)
142 num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
143 num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
144 num_pid_in_group = GROUP_SIZE_M * num_pid_n
145 group_id = pid // num_pid_in_group
146 first_pid_m = group_id * GROUP_SIZE_M
147 group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
148 pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
149 pid_n = (pid % num_pid_in_group) // group_size_m
150
151 # ----------------------------------------------------------
152 # Create pointers for the first blocks of A and B.
153 # We will advance this pointer as we move in the K direction
154 # and accumulate
155 # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers
156 # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers
157 num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
158 if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
159 return
160 offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
161 # Cast to int64 to prevent overflow in stride*offset products
162 offs_token = tl.load(sorted_token_ids_ptr + offs_token_id).to(tl.int64)
163 token_mask = offs_token < num_valid_tokens
164
165 off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
166 if off_experts == -1:
167 # -----------------------------------------------------------
168 # Write back zeros to the output when the expert is not
169 # in the current expert parallel rank.
170 write_zeros_to_output(
171 c_ptr,
172 stride_cm,
173 stride_cn,
174 pid_n,
175 N,
176 offs_token,
177 token_mask,
178 BLOCK_SIZE_M,
179 BLOCK_SIZE_N,
180 compute_type,
181 )
182 return
183
184 offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)) % N
185 offs_k = tl.arange(0, BLOCK_SIZE_K)
186 a_ptrs = a_ptr + (
187 offs_token[:, None] // top_k * stride_am + offs_k[None, :] * stride_ak
188 )
189
190 if use_int4_w4a16:
191 b_ptrs = (
192 b_ptr
193 + off_experts * stride_be
194 + (offs_k[:, None] // 2) * stride_bk
195 + offs_bn[None, :] * stride_bn
196 )
197 b_shifter = (offs_k[:, None] % 2) * 4
198 elif use_int8_w8a16:
199 b_ptrs = (
200 b_ptr
201 + off_experts * stride_be
202 + offs_k[:, None] * stride_bk
203 + offs_bn[None, :] * stride_bn
204 )
205
206 if not has_zp and use_int4_w4a16:
207 b_zp_num = 8
208 if not has_zp and use_int8_w8a16:
209 b_zp_num = 128
210 elif has_zp and use_int4_w4a16:
211 b_zp_shifter = (offs_bn[None, :] % 2) * 4
212
213 # -----------------------------------------------------------
214 # Iterate to compute a block of the C matrix.
215 # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block
216 # of fp32 values for higher accuracy.
217 # `accumulator` will be converted back to fp16 after the loop.
218 accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
219 for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
220 # Load the next block of A and B, generate a mask by checking the
221 # K dimension.
222
223 if not block_k_diviable:
224 k_mask = offs_k[:, None] < K - k * BLOCK_SIZE_K
225 k_other = 0.0
226 else:
227 k_mask = None
228 k_other = None
229
230 a = tl.load(
231 a_ptrs,
232 mask=token_mask[:, None] & (offs_k[None, :] < K - k * BLOCK_SIZE_K),
233 other=0.0,
234 )
235 b = tl.load(b_ptrs)
236 if use_int4_w4a16:
237 b = (b >> b_shifter) & 0xF
238
239 b_scale_ptrs = (
240 b_scale_ptr
241 + off_experts * stride_bse
242 + offs_bn[None, :] * stride_bsn
243 + ((offs_k[:, None] + BLOCK_SIZE_K * k) // group_size) * stride_bsk
244 )
245 b_scale = tl.load(b_scale_ptrs, mask=k_mask, other=k_other)
246 b_scale = b_scale.to(tl.float32)
247
248 if has_zp and use_int4_w4a16:
249 offs_k_true = (offs_k[:, None] + BLOCK_SIZE_K * k) // group_size
250 b_zp_ptrs = (
251 b_zp_ptr
252 + off_experts * stride_bze
253 + (offs_bn[None, :] // 2) * stride_bzn
254 + offs_k_true * stride_bzk
255 )
256 b_zp = tl.load(b_zp_ptrs, mask=k_mask, other=k_other)
257 b_zp = (b_zp >> b_zp_shifter) & 0xF
258 b_zp = b_zp.to(tl.float32)
259 elif has_zp and use_int8_w8a16:
260 offs_k_true = (offs_k[:, None] + BLOCK_SIZE_K * k) // group_size
261 b_zp_ptrs = (
262 b_zp_ptr
263 + off_experts * stride_bze
264 + offs_bn[None, :] * stride_bzn
265 + offs_k_true * stride_bzk
266 )
267 b_zp = tl.load(b_zp_ptrs, mask=k_mask, other=k_other)
268 b_zp = b_zp.to(tl.float32)
269
270 # We accumulate along the K dimension.
271 if has_zp:
272 b = ((b.to(tl.float32) - b_zp) * b_scale).to(compute_type)
273 else:
274 b = ((b.to(tl.float32) - b_zp_num) * b_scale).to(compute_type)
275 accumulator = tl.dot(a, b, acc=accumulator)
276
277 # Advance the ptrs to the next K block.
278 a_ptrs += BLOCK_SIZE_K * stride_ak
279 if use_int4_w4a16:
280 b_ptrs += (BLOCK_SIZE_K // 2) * stride_bk
281 else:
282 b_ptrs += BLOCK_SIZE_K * stride_bk
283
284 if MUL_ROUTED_WEIGHT:
285 moe_weight = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0)
286 accumulator = accumulator * moe_weight[:, None]
287
288 accumulator = accumulator.to(compute_type)
289 # -----------------------------------------------------------
290 # Write back the block of the output
291 offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
292 c_ptrs = c_ptr + stride_cm * offs_token[:, None] + stride_cn * offs_cn[None, :]
293 c_mask = token_mask[:, None] & (offs_cn[None, :] < N)
294 tl.store(c_ptrs, accumulator, mask=c_mask)
295
296
297 @triton.jit
298 def fused_moe_kernel(
299 # Pointers to matrices
300 a_ptr,
301 b_ptr,
302 c_ptr,
303 b_bias_ptr,
304 a_scale_ptr,
305 b_scale_ptr,
306 topk_weights_ptr,
307 sorted_token_ids_ptr,
308 expert_ids_ptr,
309 num_tokens_post_padded_ptr,
310 # Matrix dimensions
311 N,
312 K,
313 EM,
314 num_valid_tokens,
315 # The stride variables represent how much to increase the ptr by when
316 # moving by 1 element in a particular dimension. E.g. `stride_am` is
317 # how much to increase `a_ptr` by to get the element one row down
318 # (A has M rows).
319 stride_am,
320 stride_ak,
321 stride_be,
322 stride_bk,
323 stride_bn,
324 stride_cm,
325 stride_cn,
326 stride_asm,
327 stride_ask,
328 stride_bse,
329 stride_bsk,
330 stride_bsn,
331 stride_bbe, # bias expert stride
332 stride_bbn, # bias N stride
333 # Block size for block-wise quantization
334 group_n: tl.constexpr,
335 group_k: tl.constexpr,
336 naive_block_assignment: tl.constexpr,
337 # Meta-parameters
338 BLOCK_SIZE_M: tl.constexpr,
339 BLOCK_SIZE_N: tl.constexpr,
340 BLOCK_SIZE_K: tl.constexpr,
341 GROUP_SIZE_M: tl.constexpr,
342 SPLIT_K: tl.constexpr,
343 MUL_ROUTED_WEIGHT: tl.constexpr,
344 top_k: tl.constexpr,
345 compute_type: tl.constexpr,
346 use_fp8_w8a8: tl.constexpr,
347 use_int8_w8a8: tl.constexpr,
348 use_int8_w8a16: tl.constexpr,
349 per_channel_quant: tl.constexpr,
350 HAS_BIAS: tl.constexpr,
351 SWAP_AB: tl.constexpr,
352 # Tensor-descriptor path for the A gather and B load in the K-loop.
353 USE_TD: tl.constexpr = False,
354 ):
355 """Implements the fused computation for a Mixture of Experts (MOE) using
356 token and expert matrices.
357
358 Key Parameters:
359 - A: The input tensor representing tokens with shape (*, K), where '*' can
360 be any shape representing batches and K is the feature dimension of
361 each token.
362 - B: The stacked MOE weight tensor with shape (E, N, K), where E is
363 the number of experts, K is the input feature dimension, and N is
364 the output feature dimension.
365 - C: The output cache tensor with shape (M, topk, N), where M is the
366 total number of tokens post padding, topk is the number of times
367 each token is repeated, and N is the output feature dimension.
368 - sorted_token_ids: A tensor containing the sorted indices of tokens,
369 repeated topk times and arranged by the expert index they are
370 assigned to.
371 - expert_ids: A tensor containing the indices of the expert for each
372 block. It determines which expert matrix from B should be used for
373 each block in A.
374 - naive_block_assignment: A boolean flag indicating whether to use naive
375 token wise block assignment. If True, each block corresponds to a
376 single token.
377 This kernel performs the multiplication of a token by its corresponding
378 expert matrix as determined by `expert_ids`. The sorting of
379 `sorted_token_ids` by expert index and padding ensures divisibility by
380 BLOCK_SIZE_M, which is necessary to maintain consistency in block matrix
381 multiplication across different blocks processed by the same expert.
382 """
383 # -----------------------------------------------------------
384 # Map program ids `pid` to the block of C it should compute.
385 # This is done in a grouped ordering to promote L2 data reuse.
386 pid = tl.program_id(axis=0)
387 num_pid_m = tl.cdiv(EM, BLOCK_SIZE_M)
388 num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
389 num_pid_in_group = GROUP_SIZE_M * num_pid_n
390 group_id = pid // num_pid_in_group
391 first_pid_m = group_id * GROUP_SIZE_M
392 group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
393 pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
394 pid_n = (pid % num_pid_in_group) // group_size_m
395
396 # ----------------------------------------------------------
397 # Create pointers for the first blocks of A and B.
398 # We will advance this pointer as we move in the K direction
399 # and accumulate
400 # `a_ptrs` is a block of [BLOCK_SIZE_M, BLOCK_SIZE_K] pointers
401 # `b_ptrs` is a block of [BLOCK_SIZE_K, BLOCK_SIZE_N] pointers
402 offs = tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
403 num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr)
404 if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
405 return
406 if not naive_block_assignment:
407 offs_token_id = pid_m * BLOCK_SIZE_M + offs
408 offs_token = tl.load(sorted_token_ids_ptr + offs_token_id)
409 else:
410 offs_token = tl.where(
411 offs == 0,
412 pid_m, # first element = pid_m
413 num_valid_tokens, # remaining elements = constant
414 )
415 # Cast to int64 to prevent overflow in stride*offset products
416 # (e.g. stride_cm * offs_token can exceed int32 for large token counts)
417 offs_token = offs_token.to(tl.int64)
418
419 token_mask = offs_token < num_valid_tokens
420
421 off_experts = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
422 if off_experts == -1:
423 # -----------------------------------------------------------
424 # Write back zeros to the output when the expert is not
425 # in the current expert parallel rank.
426 write_zeros_to_output(
427 c_ptr,
428 stride_cm,
429 stride_cn,
430 pid_n,
431 N,
432 offs_token,
433 token_mask,
434 BLOCK_SIZE_M,
435 BLOCK_SIZE_N,
436 compute_type,
437 )
438 return
439
440 offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)) % N
441 offs_k = tl.arange(0, BLOCK_SIZE_K)
442 # TD gather and the SWAP_AB accumulator layout are mutually exclusive.
443 tl.static_assert(not (USE_TD and SWAP_AB))
444 if USE_TD:
445 # ``tt.descriptor_gather`` requires block_shape[0] == 1 and i32 idx.
446 m_td = num_valid_tokens // top_k
447 a_desc = tl.make_tensor_descriptor(
448 base=a_ptr,
449 shape=(m_td, K),
450 strides=(stride_am, stride_ak),
451 block_shape=(1, BLOCK_SIZE_K),
452 )
453 b_desc = tl.make_tensor_descriptor(
454 base=b_ptr + off_experts * stride_be,
455 shape=(N, K),
456 strides=(stride_bn, stride_bk),
457 block_shape=(BLOCK_SIZE_N, BLOCK_SIZE_K),
458 )
459 gather_idx = (offs_token // top_k).to(tl.int32)
460 elif SWAP_AB:
461 a_ptrs = a_ptr + (
462 offs_k[:, None] * stride_ak + offs_token[None, :] // top_k * stride_am
463 )
464 b_ptrs = (
465 b_ptr
466 + off_experts * stride_be
467 + (offs_bn[:, None] * stride_bn + offs_k[None, :] * stride_bk)
468 )
469 else:
470 a_ptrs = a_ptr + (
471 offs_token[:, None] // top_k * stride_am + offs_k[None, :] * stride_ak
472 )
473 b_ptrs = (
474 b_ptr
475 + off_experts * stride_be
476 + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
477 )
478 if use_int8_w8a16:
479 b_scale_ptrs = (
480 b_scale_ptr + off_experts * stride_bse + offs_bn[None, :] * stride_bsn
481 )
482 b_scale = tl.load(b_scale_ptrs)
483
484 if use_fp8_w8a8 or use_int8_w8a8:
485 # block-wise
486 if group_k > 0 and group_n > 0:
487 a_scale_ptrs = a_scale_ptr + (offs_token // top_k) * stride_asm
488 offs_bsn = offs_bn // group_n
489 b_scale_ptrs = (
490 b_scale_ptr + off_experts * stride_bse + offs_bsn * stride_bsn
491 )
492 # channel-wise
493 elif per_channel_quant:
494 b_scale_ptrs = (
495 b_scale_ptr + off_experts * stride_bse + offs_bn[None, :] * stride_bsn
496 )
497 b_scale = tl.load(b_scale_ptrs)
498 # Load per-token scale for activations
499 a_scale_ptrs = a_scale_ptr + (offs_token // top_k) * stride_asm
500 a_scale = tl.load(a_scale_ptrs, mask=token_mask, other=0.0)[:, None]
501 # tensor-wise
502 else:
503 a_scale = tl.load(a_scale_ptr)
504 b_scale = tl.load(b_scale_ptr + off_experts)
505 if HAS_BIAS:
506 # bias shape: [num_experts, N]
507 bias_ptrs = b_bias_ptr + off_experts * stride_bbe + offs_bn * stride_bbn
508 bias = tl.load(bias_ptrs, mask=(offs_bn < N), other=0.0)
509 # -----------------------------------------------------------
510 # Iterate to compute a block of the C matrix.
511 # We accumulate into a `[BLOCK_SIZE_M, BLOCK_SIZE_N]` block
512 # of fp32 values for higher accuracy.
513 # `accumulator` will be converted back to fp16 after the loop.
514 if SWAP_AB:
515 accumulator = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32)
516 else:
517 accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
518 for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
519 # Load the next block of A and B, generate a mask by checking the
520 # K dimension.
521 if USE_TD:
522 a = a_desc.gather(gather_idx, k * BLOCK_SIZE_K)
523 b = b_desc.load([pid_n * BLOCK_SIZE_N, k * BLOCK_SIZE_K]).T
524 elif SWAP_AB:
525 a_mask = (offs_k[:, None] < K - k * BLOCK_SIZE_K) & token_mask[None, :]
526 b_mask = offs_k[None, :] < K - k * BLOCK_SIZE_K
527 a = tl.load(a_ptrs, mask=a_mask, other=0.0)
528 b = tl.load(b_ptrs, mask=b_mask, other=0.0)
529 else:
530 a = tl.load(
531 a_ptrs,
532 mask=token_mask[:, None] & (offs_k[None, :] < K - k * BLOCK_SIZE_K),
533 other=0.0,
534 )
535 b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_SIZE_K, other=0.0)
536 # We accumulate along the K dimension.
537 if use_int8_w8a16:
538 accumulator = tl.dot(a, b.to(compute_type), acc=accumulator)
539 elif use_fp8_w8a8 or use_int8_w8a8:
540 if group_k > 0 and group_n > 0:
541 k_start = k * BLOCK_SIZE_K
542 offs_ks = k_start // group_k
543 a_scale = tl.load(
544 a_scale_ptrs + offs_ks * stride_ask, mask=token_mask, other=0.0
545 )
546 b_scale = tl.load(b_scale_ptrs + offs_ks * stride_bsk)
547 if SWAP_AB:
548 accumulator += tl.dot(b, a) * b_scale[:, None] * a_scale[None, :]
549 else:
550 accumulator += tl.dot(a, b) * a_scale[:, None] * b_scale[None, :]
551 else:
552 if use_fp8_w8a8:
553 # acc used to enable fp8_fast_accum
554 if SWAP_AB:
555 accumulator = tl.dot(b, a, acc=accumulator)
556 else:
557 accumulator = tl.dot(a, b, acc=accumulator)
558 else:
559 accumulator += tl.dot(a, b)
560 else:
561 accumulator += tl.dot(a, b)
562 if not USE_TD:
563 # Advance the ptrs to the next K block.
564 a_ptrs += BLOCK_SIZE_K * stride_ak
565 b_ptrs += BLOCK_SIZE_K * stride_bk
566
567 if SWAP_AB:
568 accumulator = tl.trans(accumulator, (1, 0))
569
570 # Dequantization for supported quantization schemes:
571 # - int8_w8a16
572 # - fp8_w8a8
573 # - int8_w8a8
574 # Accumulator and scalings are in float32 to preserve numerical accuracy.
575 if use_int8_w8a16:
576 accumulator = accumulator * b_scale
577 elif (use_fp8_w8a8 or use_int8_w8a8) and not (group_k > 0 and group_n > 0):
578 accumulator = accumulator * a_scale * b_scale
579
580 # Bias addition:
581 # Bias must be applied after dequantization:
582 # - Since bias is typically not quantized
583 # - Bias should not be scaled by quantization factors
584 if HAS_BIAS:
585 accumulator += bias[None, :]
586
587 # Router (MoE) weight multiplication:
588 # This multiplication MUST be performed in float32 before any precision
589 # conversion to ensure numerical stability, which is especially critical
590 # on ROCm platforms.
591 if MUL_ROUTED_WEIGHT:
592 moe_weight = tl.load(
593 topk_weights_ptr + offs_token,
594 mask=token_mask,
595 other=0,
596 )
597 accumulator *= moe_weight[:, None]
598
599 # Final precision conversion:
600 # Cast once at the end to the desired compute/output dtype.
601 accumulator = accumulator.to(compute_type)
602
603 # -----------------------------------------------------------
604 # Write back the block of the output
605 offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
606 c_ptrs = c_ptr + stride_cm * offs_token[:, None] + stride_cn * offs_cn[None, :]
607 c_mask = token_mask[:, None] & (offs_cn[None, :] < N)
608 tl.store(c_ptrs, accumulator, mask=c_mask)
609
610
611 # NOTE(zyongye): we can remove all the wna16 kernel
612 # once we drop off sm75 support
613 def invoke_fused_moe_wna16_cuda_kernel(
614 A: torch.Tensor,
615 B: torch.Tensor,
616 C: torch.Tensor,
617 B_scale: torch.Tensor | None,
618 B_zp: torch.Tensor | None,
619 topk_weights: torch.Tensor | None,
620 sorted_token_ids: torch.Tensor | None,
621 expert_ids: torch.Tensor,
622 num_tokens_post_padded: torch.Tensor,
623 mul_routed_weight: bool,
624 top_k: int,
625 config: dict[str, Any],
626 block_shape: list[int],
627 ):
628 assert B_scale is not None and B_scale.ndim == 3
629 assert B_zp is None or B_zp.ndim == 3
630 assert block_shape is None or block_shape[0] == 0
631
632 M = A.size(0)
633 num_tokens = M * top_k
634 bit = 4
635
636 config = config.copy()
637 config.update(
638 get_moe_wna16_block_config(
639 config=config,
640 use_moe_wna16_cuda=True,
641 num_valid_tokens=num_tokens,
642 size_k=A.size(1),
643 size_n=B.size(1),
644 num_experts=B.size(1),
645 group_size=block_shape[1],
646 real_top_k=top_k,
647 block_size_m=config["BLOCK_SIZE_M"],
648 )
649 )
650
651 ops.moe_wna16_gemm(
652 A,
653 C,
654 B,
655 B_scale,
656 B_zp,
657 topk_weights if mul_routed_weight else None,
658 sorted_token_ids,
659 expert_ids,
660 num_tokens_post_padded,
661 top_k,
662 config["BLOCK_SIZE_M"],
663 config["BLOCK_SIZE_N"],
664 config["BLOCK_SIZE_K"],
665 bit,
666 )
667
668
669 # NOTE(zyongye): we can remove all the wna16 kernel
670 # once we drop off sm75 support
671 def invoke_fused_moe_wna16_triton_kernel(
672 A: torch.Tensor,
673 B: torch.Tensor,
674 C: torch.Tensor,
675 B_scale: torch.Tensor | None,
676 B_zp: torch.Tensor | None,
677 topk_weights: torch.Tensor | None,
678 sorted_token_ids: torch.Tensor,
679 expert_ids: torch.Tensor,
680 num_tokens_post_padded: torch.Tensor,
681 mul_routed_weight: bool,
682 top_k: int,
683 config: dict[str, Any],
684 compute_type: tl.dtype,
685 use_int8_w8a16: bool,
686 use_int4_w4a16: bool,
687 block_shape: list[int] | None,
688 ):
689 assert B_scale is not None and B_scale.ndim == 3
690 assert B_zp is None or B_zp.ndim == 3
691 assert block_shape is not None and block_shape[0] == 0
692
693 M = A.size(0)
694 num_tokens = M * top_k
695
696 EM = sorted_token_ids.size(0)
697 if A.size(0) < config["BLOCK_SIZE_M"]:
698 # optimize for small batch_size.
699 # We assume that top_ids of each token is unique,
700 # so num_valid_experts <= batch_size <= BLOCK_SIZE_M,
701 # and we can skip some invalid blocks.
702 EM = min(sorted_token_ids.size(0), A.size(0) * top_k * config["BLOCK_SIZE_M"])
703 grid = lambda META: (
704 triton.cdiv(EM, META["BLOCK_SIZE_M"])
705 * triton.cdiv(B.size(1), META["BLOCK_SIZE_N"]),
706 )
707 config = config.copy()
708 config.update(
709 get_moe_wna16_block_config(
710 config=config,
711 use_moe_wna16_cuda=False,
712 num_valid_tokens=num_tokens,
713 size_k=A.size(1),
714 size_n=B.size(1),
715 num_experts=B.size(1),
716 group_size=block_shape[1],
717 real_top_k=top_k,
718 block_size_m=config["BLOCK_SIZE_M"],
719 )
720 )
721
722 fused_moe_kernel_gptq_awq[grid](
723 A,
724 B,
725 C,
726 B_scale,
727 B_zp,
728 topk_weights,
729 sorted_token_ids,
730 expert_ids,
731 num_tokens_post_padded,
732 B.size(1),
733 A.size(1),
734 EM,
735 num_tokens,
736 A.stride(0),
737 A.stride(1),
738 B.stride(0),
739 B.stride(2),
740 B.stride(1),
741 C.stride(1),
742 C.stride(2),
743 B_scale.stride(0),
744 B_scale.stride(2),
745 B_scale.stride(1),
746 B_zp.stride(0) if B_zp is not None else 0,
747 B_zp.stride(2) if B_zp is not None else 0,
748 B_zp.stride(1) if B_zp is not None else 0,
749 block_k_diviable=A.size(1) % config["BLOCK_SIZE_K"] == 0,
750 group_size=block_shape[1],
751 MUL_ROUTED_WEIGHT=mul_routed_weight,
752 top_k=top_k,
753 compute_type=compute_type,
754 has_zp=B_zp is not None,
755 use_int4_w4a16=use_int4_w4a16,
756 use_int8_w8a16=use_int8_w8a16,
757 **config,
758 )
759
760
761 def invoke_fused_moe_triton_kernel(
762 A: torch.Tensor,
763 B: torch.Tensor,
764 C: torch.Tensor,
765 A_scale: torch.Tensor | None,
766 B_scale: torch.Tensor | None,
767 topk_weights: torch.Tensor | None,
768 sorted_token_ids: torch.Tensor | None,
769 expert_ids: torch.Tensor,
770 num_tokens_post_padded: torch.Tensor,
771 mul_routed_weight: bool,
772 top_k: int,
773 config: dict[str, Any],
774 compute_type: tl.dtype,
775 use_fp8_w8a8: bool,
776 use_int8_w8a8: bool,
777 use_int8_w8a16: bool,
778 use_int4_w4a16: bool,
779 per_channel_quant: bool,
780 block_shape: list[int] | None = None,
781 B_bias: torch.Tensor | None = None,
782 ):
783 assert topk_weights is not None or not mul_routed_weight
784 assert topk_weights is None or topk_weights.stride(1) == 1
785 assert sorted_token_ids is None or sorted_token_ids.stride(0) == 1
786
787 if use_fp8_w8a8:
788 SWAP_AB = enable_swap_ab(config["BLOCK_SIZE_M"], config["BLOCK_SIZE_N"])
789 else:
790 SWAP_AB = False
791
792 # Quantized weights always carry a B_scale (see the asserts below); key off
793 # that rather than enumerating quant flags, which misses w8a16-fp8/nvfp4/etc.
794 is_quantized = B_scale is not None
795 warn_if_moe_use_td_ineffective("TRITON", is_quantized=is_quantized)
796
797 # TD path is unvalidated under quantization; fall back to the pointer path.
798 use_td = resolve_moe_use_td() and not is_quantized
799 if use_td:
800 # The TD path builds a tensor descriptor inside the kernel, which
801 # requires a PyTorch-backed scratch allocator to be registered
802 # (Triton raises "no allocator was set" otherwise on CUDA).
803 set_triton_allocator(A.device)
804
805 if use_fp8_w8a8 or use_int8_w8a8:
806 assert B_scale is not None
807 assert block_shape is None or triton.cdiv(
808 B.size(-2), block_shape[0]
809 ) == B_scale.size(-2)
810 assert block_shape is None or triton.cdiv(
811 B.size(-1), block_shape[1]
812 ) == B_scale.size(-1)
813 elif use_int8_w8a16 or use_int4_w4a16:
814 assert B_scale is not None
815 assert block_shape is None or block_shape[0] == 0
816 else:
817 assert A_scale is None
818 assert B_scale is None
819
820 M = A.size(0)
821 num_tokens = M * top_k
822 if sorted_token_ids is not None:
823 EM = sorted_token_ids.size(0)
824 if A.size(0) < config["BLOCK_SIZE_M"]:
825 # optimize for small batch_size.
826 # We assume that top_ids of each token is unique,
827 # so num_valid_experts <= batch_size <= BLOCK_SIZE_M,
828 # and we can skip some invalid blocks.
829 EM = min(
830 sorted_token_ids.size(0), A.size(0) * top_k * config["BLOCK_SIZE_M"]
831 )
832 else:
833 EM = num_tokens * config["BLOCK_SIZE_M"]
834 grid = lambda META: (
835 triton.cdiv(EM, META["BLOCK_SIZE_M"])
836 * triton.cdiv(B.size(1), META["BLOCK_SIZE_N"]),
837 )
838 HAS_BIAS = B_bias is not None
839
840 config = config.copy()
841 config["SPLIT_K"] = 1
842 BLOCK_SIZE_K = config.pop("BLOCK_SIZE_K")
843 if block_shape is not None:
844 BLOCK_SIZE_K = min(BLOCK_SIZE_K, min(block_shape[0], block_shape[1]))
845 if use_td and A.size(1) % BLOCK_SIZE_K != 0:
846 # TD gather/load feeding tl.dot with a non-block-aligned K
847 # miscompiles (~74% of output elements wrong) on real HW;
848 # this is a compiler-codegen issue, not a Python-maskable
849 # boundary gap. Fall back to the pointer-arith path.
850 logger.warning_once(
851 "Disabling VLLM_TRITON_USE_TD for this MoE launch: K=%d is not "
852 "a multiple of BLOCK_SIZE_K=%d, which triggers a known "
853 "Triton tensor-descriptor + tl.dot miscompilation.",
854 A.size(1),
855 BLOCK_SIZE_K,
856 )
857 use_td = False
858
859 # Triton treats 0-D tensor arguments as scalar values, but the kernel
860 # loads tensor-wise activation scales through a pointer.
861 if A_scale is not None and A_scale.ndim == 0:
862 A_scale = A_scale.reshape(1)
863
864 fused_moe_kernel[grid](
865 A,
866 B,
867 C,
868 B_bias,
869 A_scale,
870 B_scale,
871 topk_weights,
872 sorted_token_ids,
873 expert_ids,
874 num_tokens_post_padded,
875 B.size(1),
876 B.size(2),
877 EM,
878 num_tokens,
879 A.stride(0),
880 A.stride(1),
881 B.stride(0),
882 B.stride(2),
883 B.stride(1),
884 C.stride(1),
885 C.stride(2),
886 A_scale.stride(0) if A_scale is not None and A_scale.ndim == 2 else 0,
887 A_scale.stride(1) if A_scale is not None and A_scale.ndim == 2 else 0,
888 B_scale.stride(0) if B_scale is not None and B_scale.ndim >= 2 else 0,
889 B_scale.stride(2) if B_scale is not None and B_scale.ndim == 3 else 0,
890 B_scale.stride(1) if B_scale is not None and B_scale.ndim >= 2 else 0,
891 B_bias.stride(0) if B_bias is not None else 0,
892 B_bias.stride(1) if B_bias is not None else 0,
893 0 if block_shape is None else block_shape[0],
894 0 if block_shape is None else block_shape[1],
895 MUL_ROUTED_WEIGHT=mul_routed_weight,
896 top_k=top_k,
897 compute_type=compute_type,
898 use_fp8_w8a8=use_fp8_w8a8,
899 use_int8_w8a8=use_int8_w8a8,
900 use_int8_w8a16=use_int8_w8a16,
901 per_channel_quant=per_channel_quant,
902 naive_block_assignment=(sorted_token_ids is None),
903 HAS_BIAS=HAS_BIAS,
904 BLOCK_SIZE_K=BLOCK_SIZE_K,
905 SWAP_AB=SWAP_AB,
906 USE_TD=use_td,
907 **config,
908 )
909
910
911 def dispatch_fused_moe_kernel(
912 A: torch.Tensor,
913 B: torch.Tensor,
914 C: torch.Tensor,
915 A_scale: torch.Tensor | None,
916 B_scale: torch.Tensor | None,
917 B_zp: torch.Tensor | None,
918 topk_weights: torch.Tensor | None,
919 sorted_token_ids: torch.Tensor | None,
920 expert_ids: torch.Tensor,
921 num_tokens_post_padded: torch.Tensor,
922 mul_routed_weight: bool,
923 top_k: int,
924 config: dict[str, Any],
925 compute_type: tl.dtype,
926 use_fp8_w8a8: bool,
927 use_int8_w8a8: bool,
928 use_int8_w8a16: bool,
929 use_int4_w4a16: bool,
930 per_channel_quant: bool,
931 block_shape: list[int] | None = None,
932 B_bias: torch.Tensor | None = None,
933 ) -> None:
934 assert topk_weights is not None or not mul_routed_weight
935 assert topk_weights is None or topk_weights.stride(1) == 1
936 assert sorted_token_ids is None or sorted_token_ids.stride(0) == 1
937
938 M = A.size(0)
939 num_tokens = M * top_k
940
941 if (use_int8_w8a16 or use_int4_w4a16) and (
942 block_shape is not None and block_shape[1] > 0
943 ):
944 assert B_bias is None
945
946 use_moe_wna16_cuda = should_moe_wna16_use_cuda(
947 num_valid_tokens=num_tokens,
948 group_size=block_shape[1],
949 num_experts=B.size(0),
950 bit=4 if use_int4_w4a16 else 8,
951 )
952
953 if use_moe_wna16_cuda:
954 invoke_fused_moe_wna16_cuda_kernel(
955 A,
956 B,
957 C,
958 B_scale,
959 B_zp,
960 topk_weights,
961 sorted_token_ids,
962 expert_ids,
963 num_tokens_post_padded,
964 mul_routed_weight,
965 top_k,
966 config,
967 block_shape,
968 )
969 return
970 invoke_fused_moe_wna16_triton_kernel(
971 A,
972 B,
973 C,
974 B_scale,
975 B_zp,
976 topk_weights,
977 sorted_token_ids,
978 expert_ids,
979 num_tokens_post_padded,
980 mul_routed_weight,
981 top_k,
982 config,
983 compute_type,
984 use_int8_w8a16,
985 use_int4_w4a16,
986 block_shape,
987 )
988
989 else:
990 invoke_fused_moe_triton_kernel(
991 A,
992 B,
993 C,
994 A_scale,
995 B_scale,
996 topk_weights,
997 sorted_token_ids,
998 expert_ids,
999 num_tokens_post_padded,
1000 mul_routed_weight,
1001 top_k,
1002 config,
1003 compute_type,
1004 use_fp8_w8a8,
1005 use_int8_w8a8,
1006 use_int8_w8a16,
1007 use_int4_w4a16,
1008 per_channel_quant,
1009 block_shape,
1010 B_bias,
1011 )
1012
1013
1014 @triton.jit
1015 def compute_identity_kernel(
1016 top_k: int,
1017 hidden_states_ptr: tl.tensor,
1018 expert_scales_ptr: tl.tensor,
1019 num_tokens: int,
1020 output_ptr: tl.tensor,
1021 hidden_dim: int,
1022 scales_stride: int,
1023 BLOCK_SIZE: tl.constexpr,
1024 ) -> None:
1025 pid = tl.program_id(0)
1026
1027 batch_id = pid // (hidden_dim // BLOCK_SIZE)
1028 dim_offset = pid % (hidden_dim // BLOCK_SIZE) * BLOCK_SIZE
1029
1030 if batch_id >= num_tokens or dim_offset >= hidden_dim:
1031 return
1032
1033 h = tl.load(
1034 hidden_states_ptr
1035 + batch_id * hidden_dim
1036 + dim_offset
1037 + tl.arange(0, BLOCK_SIZE),
1038 mask=(dim_offset + tl.arange(0, BLOCK_SIZE)) < hidden_dim,
1039 )
1040
1041 result = tl.zeros([BLOCK_SIZE], dtype=tl.float32)
1042 for i in range(top_k):
1043 scale = tl.load(expert_scales_ptr + batch_id * scales_stride + i)
1044 result += h * scale
1045
1046 tl.store(
1047 output_ptr + batch_id * hidden_dim + dim_offset + tl.arange(0, BLOCK_SIZE),
1048 result,
1049 mask=(dim_offset + tl.arange(0, BLOCK_SIZE)) < hidden_dim,
1050 )
1051
1052
1053 def zero_experts_compute_triton(
1054 expert_indices: torch.Tensor,
1055 expert_scales: torch.Tensor,
1056 num_experts: int,
1057 zero_expert_type: str,
1058 hidden_states: torch.Tensor,
1059 ) -> torch.Tensor:
1060 N = expert_indices.numel()
1061 top_k = expert_indices.size(-1)
1062 grid = lambda meta: (triton.cdiv(N, meta["BLOCK_SIZE"]),)
1063
1064 if zero_expert_type == "identity":
1065 zero_expert_mask = expert_indices < num_experts
1066 zero_expert_scales = expert_scales.clone()
1067 zero_expert_scales[zero_expert_mask] = 0.0
1068
1069 normal_expert_mask = expert_indices >= num_experts
1070 expert_indices[normal_expert_mask] = 0
1071 expert_scales[normal_expert_mask] = 0.0
1072
1073 output = torch.zeros_like(hidden_states).to(hidden_states.device)
1074 hidden_dim = hidden_states.size(-1)
1075 num_tokens = hidden_states.size(0)
1076
1077 grid = lambda meta: (num_tokens * (hidden_dim // meta["BLOCK_SIZE"]),)
1078 compute_identity_kernel[grid](
1079 top_k,
1080 hidden_states,
1081 zero_expert_scales,
1082 num_tokens,
1083 output,
1084 hidden_dim,
1085 zero_expert_scales.stride(0),
1086 BLOCK_SIZE=256,
1087 )
1088
1089 return output
1090
1091
1092 # Adapted from: https://github.com/sgl-project/sglang/pull/2628
1093 def get_config_file_name(
1094 E: int, N: int, dtype: str | None, block_shape: list[int] | None = None
1095 ) -> str:
1096 device_name = get_device_name_as_file_name()
1097 # Set device_name to H200 if a device from the H200 family is detected
1098 if "H200" in device_name.split("_"):
1099 device_name = "NVIDIA_H200"
1100 dtype_selector = "" if not dtype else f",dtype={dtype}"
1101 block_shape_selector = (
1102 "" if not block_shape or not all(block_shape) else f",block_shape={block_shape}"
1103 ).replace(" ", "")
1104 return f"E={E},N={N},device_name={device_name}{dtype_selector}{block_shape_selector}.json" # noqa: E501
1105
1106
1107 # Adapted from: https://github.com/sgl-project/sglang/pull/2628
1108 @functools.lru_cache
1109 def get_moe_configs(
1110 E: int,
1111 N: int,
1112 dtype: str | None,
1113 block_n: int | None = None,
1114 block_k: int | None = None,
1115 ) -> dict[int, Any] | None:
1116 """Return optimized configurations for the fused MoE kernel.
1117
1118 The return value will be a dictionary that maps an irregular grid of
1119 batch sizes to configurations of the fused_moe kernel. To evaluate the
1120 kernel on a given batch size bs, the closest batch size in the grid should
1121 be picked and the associated configuration chosen to invoke the kernel.
1122 """
1123 # Avoid optimizing for the batch invariant case. Use default config
1124 if envs.VLLM_BATCH_INVARIANT:
1125 return None
1126
1127 # First look up if an optimized configuration is available in the configs
1128 # directory
1129 block_shape = [block_n, block_k] if block_n and block_k else None
1130 json_file_name = get_config_file_name(E, N, dtype, block_shape)
1131
1132 config_file_paths = []
1133
1134 # note that we prioritize user defined config
1135 user_defined_config_folder = envs.VLLM_TUNED_CONFIG_FOLDER
1136 if user_defined_config_folder is not None:
1137 user_defined_config_file_path = os.path.join(
1138 user_defined_config_folder, json_file_name
1139 )
1140 config_file_paths.append(user_defined_config_file_path)
1141
1142 default_config_file_path = os.path.join(
1143 os.path.dirname(os.path.realpath(__file__)), "configs", json_file_name
1144 )
1145 config_file_paths.append(default_config_file_path)
1146
1147 for config_file_path in config_file_paths:
1148 if os.path.exists(config_file_path):
1149 with open(config_file_path) as f:
1150 logger.info_once(
1151 "Using configuration from %s for MoE layer.",
1152 config_file_path,
1153 scope="global",
1154 )
1155 # If a configuration has been found, return it
1156 tuned_config = json.load(f)
1157 # Delete triton_version from tuned_config
1158 tuned_config.pop("triton_version", None)
1159 return {int(key): val for key, val in tuned_config.items()}
1160
1161 # If no optimized configuration is available, we will use the default
1162 # configuration
1163 logger.warning_once(
1164 "Using default MoE config. Performance might be sub-optimal! "
1165 "Config file not found at %s",
1166 ", ".join(config_file_paths),
1167 )
1168 return None
1169
1170
1171 def _ensure_block_size_k_divisible(
1172 size_k: int, block_size_k: int, group_size: int
1173 ) -> int:
1174 """Ensure block_size_k is a divisor of size_k and divisible by group_size.
1175
1176 This ensures BLOCK_SIZE_K compatibility with MoeWNA16 CUDA kernel which
1177 requires size_k % BLOCK_SIZE_K == 0 and BLOCK_SIZE_K % group_size == 0.
1178
1179 Args:
1180 size_k: The size_k dimension that must be divisible by result.
1181 block_size_k: Preferred block size (will be adjusted if needed).
1182 group_size: The result must be divisible by this.
1183
1184 Returns:
1185 A valid BLOCK_SIZE_K that divides size_k and is divisible by group_size.
1186
1187 """
1188 # Fast path: already valid
1189 if size_k % block_size_k == 0 and block_size_k % group_size == 0:
1190 return block_size_k
1191
1192 # Find the largest value that:
1193 # 1. Divides size_k (size_k % candidate == 0)
1194 # 2. Is divisible by group_size (candidate % group_size == 0)
1195 # 3. Is <= block_size_k (prefer smaller values close to block_size_k)
1196 #
1197 # Strategy: Search from min(block_size_k, size_k) down to group_size,
1198 # stepping by group_size to ensure divisibility by group_size
1199 max_search = min(block_size_k, size_k)
1200 start = (max_search // group_size) * group_size
1201 for candidate in range(start, group_size - 1, -group_size):
1202 if size_k % candidate == 0:
1203 return candidate
1204
1205 # Fallback: if group_size divides size_k, use it
1206 # This should always be true with correct group_size configuration
1207 if size_k % group_size == 0:
1208 return group_size
1209
1210 # This should not happen with correct group_size, but ensure divisibility
1211 return size_k
1212
1213
1214 def get_moe_wna16_block_config(
1215 config: dict[str, int],
1216 use_moe_wna16_cuda: bool,
1217 num_valid_tokens: int,
1218 size_k: int,
1219 size_n: int,
1220 num_experts: int,
1221 group_size: int,
1222 real_top_k: int,
1223 block_size_m: int,
1224 ):
1225 if "BLOCK_SIZE_N" in config and "BLOCK_SIZE_K" in config:
1226 # optimal block config is set
1227 return {}
1228 if not use_moe_wna16_cuda:
1229 # triton moe wna16 kernel
1230 if num_valid_tokens // real_top_k == 1:
1231 # if bs=1, use a smaller BLOCK_SIZE_N
1232 return {"BLOCK_SIZE_N": 32, "BLOCK_SIZE_K": 64}
1233 else:
1234 return {"BLOCK_SIZE_N": 64, "BLOCK_SIZE_K": 32}
1235 else:
1236 # cuda moe wna16 kernel
1237 # set default block_size 128, and increase them when num_blocks
1238 # is too large.
1239 block_size_n = 128
1240 block_size_k = 128
1241 if block_size_k <= group_size:
1242 block_size_k = group_size
1243
1244 num_n_blocks = size_k // block_size_k
1245 num_k_blocks = size_n // block_size_k
1246 num_m_blocks = (
1247 num_valid_tokens + block_size_m - 1
1248 ) / block_size_m + num_experts
1249 if num_valid_tokens // real_top_k <= block_size_m:
1250 num_m_blocks = min(num_m_blocks, num_valid_tokens)
1251 num_blocks = num_m_blocks * num_n_blocks * num_k_blocks
1252
1253 if size_k % 256 == 0 and num_blocks >= 256 and block_size_k < 256:
1254 block_size_k = 256
1255 num_blocks = num_blocks // (256 // block_size_k)
1256
1257 if (
1258 num_m_blocks <= 16
1259 and size_k % (block_size_k * 2) == 0
1260 and size_k % (block_size_k * 2) == 0
1261 and block_size_k <= 512
1262 and num_blocks >= 512
1263 ):
1264 block_size_k = block_size_k * 2
1265 num_blocks = num_blocks // 2
1266
1267 if num_blocks > 1024:
1268 block_size_n = 256
1269 num_n_blocks = num_n_blocks // 2
1270 num_blocks = num_blocks // 2
1271
1272 if size_n <= 1024 and num_blocks >= 1024:
1273 # The kernel performance got much better with BLOCK_SIZE_N=1024
1274 # when num_blocks is large, event when N is small.
1275 # Not sure why, maybe it force the CUDA SM process only one block
1276 # at the same time.
1277 block_size_n = 1024
1278
1279 # Ensure BLOCK_SIZE_K is a divisor of size_k for CUDA kernel compatibility
1280 block_size_k = _ensure_block_size_k_divisible(size_k, block_size_k, group_size)
1281
1282 return {"BLOCK_SIZE_N": block_size_n, "BLOCK_SIZE_K": block_size_k}
1283
1284
1285 def should_moe_wna16_use_cuda(
1286 num_valid_tokens: int, group_size: int, num_experts: int, bit: int
1287 ):
1288 return (
1289 current_platform.is_cuda()
1290 and bit == 4
1291 and group_size in [32, 64, 128]
1292 and num_valid_tokens / num_experts <= 6
1293 )
1294
1295
1296 def get_default_config(
1297 M: int,
1298 E: int,
1299 N: int,
1300 K: int,
1301 topk: int,
1302 dtype: str | None,
1303 block_shape: list[int] | None = None,
1304 ) -> dict[str, int]:
1305 if envs.VLLM_BATCH_INVARIANT:
1306 return {
1307 "BLOCK_SIZE_M": 64,
1308 "BLOCK_SIZE_N": 64,
1309 "BLOCK_SIZE_K": 32,
1310 "GROUP_SIZE_M": 8,
1311 "SPLIT_K": 1,
1312 }
1313
1314 # num_stages can cause triton.runtime.errors.OutOfResources on ROCm.
1315 num_stages_rocm = 2
1316
1317 if dtype == "fp8_w8a8" and block_shape is not None:
1318 # Block-wise quant. Use a small M tile for decode-like batches where
1319 # tokens are spread thin across experts. Larger batches benefit from
1320 # GROUP_SIZE_M > 1 because the per-block scales add memory traffic
1321 # that benefits from L2 tile reuse.
1322 #
1323 # BLOCK_SIZE_N need not equal block_shape[0]: the kernel indexes block
1324 # scales per N element (offs_bn // group_n), so any N tile dividing the
1325 # quant block is valid. At decode a 128-wide N tile leaves the gate-up
1326 # GEMM SM-bound; a 64-wide tile exposes ~2x the thread blocks, and the
1327 # swap-AB kernel keeps it efficient on Hopper down to the smallest
1328 # batches, so prefer N=64 through low batch sizes. CUDA only (validated
1329 # on NVIDIA); ROCm keeps its prior tile/pipeline sizes.
1330 if current_platform.is_rocm():
1331 block_n = block_shape[0]
1332 num_stages = num_stages_rocm
1333 elif M <= 8 and block_shape[0] % 64 == 0:
1334 block_n = 64
1335 # The smallest batches are memory-latency bound, so a deeper
1336 # pipeline hides the weight loads; by M=8 it turns occupancy/SMEM
1337 # bound and the extra stages hurt.
1338 num_stages = 4 if M <= 4 else 3
1339 else:
1340 block_n = block_shape[0]
1341 num_stages = 3
1342 config = {
1343 "BLOCK_SIZE_M": 16 if M <= 64 else 64,
1344 "BLOCK_SIZE_N": block_n,
1345 "BLOCK_SIZE_K": block_shape[1],
1346 "GROUP_SIZE_M": 1 if M <= 16 else 32,
1347 "SPLIT_K": 1,
1348 "num_warps": 4,
1349 "num_stages": num_stages,
1350 }
1351 elif dtype in ["int4_w4a16", "int8_w8a16"] and block_shape is not None:
1352 # moe wna16 kernels
1353 # only set BLOCK_SIZE_M
1354 # BLOCK_SIZE_N and BLOCK_SIZE_K would be set later
1355 bit = 4 if dtype == "int4_w4a16" else 8
1356 use_moe_wna16_cuda = should_moe_wna16_use_cuda(M * topk, block_shape[1], E, bit)
1357 if use_moe_wna16_cuda:
1358 config = {
1359 "BLOCK_SIZE_M": min(16, next_power_of_2(M)),
1360 "GROUP_SIZE_M": 1,
1361 "SPLIT_K": 1,
1362 }
1363 elif M <= 20:
1364 config = {"BLOCK_SIZE_M": 16, "GROUP_SIZE_M": 1, "SPLIT_K": 1}
1365 elif M <= 40:
1366 config = {"BLOCK_SIZE_M": 32, "GROUP_SIZE_M": 1, "SPLIT_K": 1}
1367 else:
1368 config = {"BLOCK_SIZE_M": 64, "GROUP_SIZE_M": 1, "SPLIT_K": 1}
1369 else:
1370 # General defaults for bf16/fp16 and fp8 per-tensor.
1371 # Tile sizes scale with batch: small batches are memory-bound
1372 # (favor tall-K tiles), large batches are compute-bound (favor
1373 # large M/N tiles with more warps).
1374 if M <= 32:
1375 block_m = 16
1376 elif M <= 96:
1377 block_m = 32
1378 elif M <= 512:
1379 block_m = 64
1380 else:
1381 block_m = 128
1382
1383 block_n = 64 if M <= 64 else 128
1384
1385 # Small batches benefit from longer reduction (larger K tile),
1386 # while large batches prefer more output parallelism.
1387 # FP8 elements are half-width so larger K tiles are always cheap.
1388 block_k = 128 if dtype == "fp8_w8a8" or M <= 64 else 64
1389
1390 # Grouping adjacent M-blocks lets them share weight tiles in L2.
1391 # Only helps when there are enough M-blocks per expert to group;
1392 # with many experts each one sees few tokens so grouping is useless.
1393 tokens_per_expert = M // max(E, 1)
1394 group_m = 16 if tokens_per_expert > 128 else 1
1395
1396 # Large batches have enough blocks to saturate the GPU, so we
1397 # use more warps per block to increase arithmetic intensity.
1398 num_warps = 4 if M <= 128 else 8
1399
1400 if current_platform.is_rocm():
1401 num_stages = num_stages_rocm
1402 elif M <= 32:
1403 num_stages = 4
1404 else:
1405 num_stages = 3
1406
1407 config = {
1408 "BLOCK_SIZE_M": block_m,
1409 "BLOCK_SIZE_N": block_n,
1410 "BLOCK_SIZE_K": block_k,
1411 "GROUP_SIZE_M": group_m,
1412 "SPLIT_K": 1,
1413 "num_warps": num_warps,
1414 "num_stages": num_stages,
1415 }
1416 return config
1417
1418
1419 def try_get_optimal_moe_config(
1420 w1_shape: tuple[int, ...],
1421 w2_shape: tuple[int, ...],
1422 top_k: int,
1423 dtype: str | None,
1424 M: int,
1425 block_shape: list[int] | None = None,
1426 ) -> dict[str, int]:
1427 from vllm.model_executor.layers.fused_moe import get_config
1428
1429 override_config = get_config()
1430 if override_config:
1431 config = override_config
1432 else:
1433 # First try to load optimal config from the file
1434 E, _, N = w2_shape
1435 if dtype == "int4_w4a16":
1436 N = N * 2
1437 block_n = block_shape[0] if block_shape else 0
1438 block_k = block_shape[1] if block_shape else 0
1439 configs = get_moe_configs(E, N, dtype, block_n, block_k)
1440
1441 if configs:
1442 # If an optimal configuration map has been found, look up the
1443 # optimal config
1444 config = configs[min(configs.keys(), key=lambda x: abs(x - M))]
1445 else:
1446 # Else use the default config
1447 config = get_default_config(M, E, N, w1_shape[2], top_k, dtype, block_shape)
1448 return config
1449
1450
1451 def fused_experts_op(
1452 hidden_states: torch.Tensor,
1453 w1: torch.Tensor,
1454 w2: torch.Tensor,
1455 topk_weights: torch.Tensor,
1456 topk_ids: torch.Tensor,
1457 activation: str = "silu",
1458 apply_router_weight_on_input: bool = False,
1459 use_fp8_w8a8: bool = False,
1460 use_int8_w8a8: bool = False,
1461 use_int8_w8a16: bool = False,
1462 use_int4_w4a16: bool = False,
1463 ocp_mx_scheme: str | None = None,
1464 per_channel_quant: bool = False,
1465 global_num_experts: int = -1,
1466 expert_map: torch.Tensor | None = None,
1467 w1_scale: torch.Tensor | None = None,
1468 w2_scale: torch.Tensor | None = None,
1469 w1_zp: torch.Tensor | None = None,
1470 w2_zp: torch.Tensor | None = None,
1471 a1_scale: torch.Tensor | None = None,
1472 a2_scale: torch.Tensor | None = None,
1473 block_shape: list[int] | None = None,
1474 w1_bias: torch.Tensor | None = None,
1475 w2_bias: torch.Tensor | None = None,
1476 ) -> torch.Tensor:
1477 return fused_experts_impl(
1478 hidden_states,
1479 w1,
1480 w2,
1481 topk_weights,
1482 topk_ids,
1483 activation,
1484 apply_router_weight_on_input,
1485 use_fp8_w8a8,
1486 use_int8_w8a8,
1487 use_int8_w8a16,
1488 use_int4_w4a16,
1489 ocp_mx_scheme,
1490 per_channel_quant,
1491 global_num_experts,
1492 expert_map,
1493 w1_scale,
1494 w2_scale,
1495 w1_zp,
1496 w2_zp,
1497 a1_scale,
1498 a2_scale,
1499 block_shape,
1500 w1_bias,
1501 w2_bias,
1502 )
1503
1504
1505 def fused_experts_op_fake(
1506 hidden_states: torch.Tensor,
1507 w1: torch.Tensor,
1508 w2: torch.Tensor,
1509 topk_weights: torch.Tensor,
1510 topk_ids: torch.Tensor,
1511 activation: str = "silu",
1512 apply_router_weight_on_input: bool = False,
1513 use_fp8_w8a8: bool = False,
1514 use_int8_w8a8: bool = False,
1515 use_int8_w8a16: bool = False,
1516 use_int4_w4a16: bool = False,
1517 ocp_mx_scheme: str | None = None,
1518 per_channel_quant: bool = False,
1519 global_num_experts: int = -1,
1520 expert_map: torch.Tensor | None = None,
1521 w1_scale: torch.Tensor | None = None,
1522 w2_scale: torch.Tensor | None = None,
1523 w1_zp: torch.Tensor | None = None,
1524 w2_zp: torch.Tensor | None = None,
1525 a1_scale: torch.Tensor | None = None,
1526 a2_scale: torch.Tensor | None = None,
1527 block_shape: list[int] | None = None,
1528 w1_bias: torch.Tensor | None = None,
1529 w2_bias: torch.Tensor | None = None,
1530 ) -> torch.Tensor:
1531 return torch.empty_like(hidden_states)
1532
1533
1534 direct_register_custom_op(
1535 op_name="fused_experts",
1536 op_func=fused_experts_op,
1537 fake_impl=fused_experts_op_fake,
1538 )
1539
1540
1541 def _prepare_expert_assignment(
1542 topk_ids: torch.Tensor,
1543 config: dict[str, Any],
1544 num_tokens: int,
1545 top_k_num: int,
1546 global_num_experts: int,
1547 expert_map: torch.Tensor | None,
1548 *,
1549 use_int8_w8a16: bool = False,
1550 use_int4_w4a16: bool = False,
1551 block_shape: list[int] | None = None,
1552 ignore_invalid_experts: bool = False,
1553 ) -> tuple[torch.Tensor | None, torch.Tensor, torch.Tensor]:
1554 """Prepare expert assignments for the aligned and low-latency Triton paths."""
1555 # SPARSITY_FACTOR is a heuristic margin ensuring tokens_in_chunk * top_k
1556 # activates only a small fraction of total experts
1557 # Skips moe_align_block_size and activates the `sorted_token_ids is None`
1558 # path of the fused_moe_kernel kernel
1559 naive_block_assignment = (
1560 expert_map is None
1561 and num_tokens * top_k_num * 4 <= global_num_experts
1562 and not (
1563 (use_int8_w8a16 or use_int4_w4a16)
1564 and block_shape is not None
1565 and block_shape[1] > 0
1566 )
1567 )
1568
1569 if naive_block_assignment:
1570 return (
1571 None,
1572 topk_ids.view(-1),
1573 torch.full(
1574 (1,),
1575 topk_ids.numel() * config["BLOCK_SIZE_M"],
1576 dtype=torch.int32,
1577 device=topk_ids.device,
1578 ),
1579 )
1580
1581 return moe_align_block_size(
1582 topk_ids,
1583 config["BLOCK_SIZE_M"],
1584 global_num_experts,
1585 expert_map,
1586 ignore_invalid_experts=ignore_invalid_experts,
1587 )
1588
1589
1590 def fused_experts(
1591 hidden_states: torch.Tensor,
1592 w1: torch.Tensor,
1593 w2: torch.Tensor,
1594 topk_weights: torch.Tensor,
1595 topk_ids: torch.Tensor,
1596 activation: MoEActivation = MoEActivation.SILU,
1597 apply_router_weight_on_input: bool = False,
1598 global_num_experts: int = -1,
1599 expert_map: torch.Tensor | None = None,
1600 quant_config: FusedMoEQuantConfig | None = None,
1601 ) -> torch.Tensor:
1602 """Run fused MoE expert computation using Triton kernels."""
1603 if quant_config is None:
1604 quant_config = FUSED_MOE_UNQUANTIZED_CONFIG
1605
1606 return torch.ops.vllm.fused_experts(
1607 hidden_states=hidden_states,
1608 w1=w1,
1609 w2=w2,
1610 topk_weights=topk_weights,
1611 topk_ids=topk_ids,
1612 activation=activation.value,
1613 apply_router_weight_on_input=apply_router_weight_on_input,
1614 use_fp8_w8a8=quant_config.use_fp8_w8a8,
1615 use_int8_w8a8=quant_config.use_int8_w8a8,
1616 use_int8_w8a16=quant_config.use_int8_w8a16,
1617 use_int4_w4a16=quant_config.use_int4_w4a16,
1618 ocp_mx_scheme=quant_config.ocp_mx_scheme,
1619 per_channel_quant=quant_config.per_act_token_quant,
1620 global_num_experts=global_num_experts,
1621 expert_map=expert_map,
1622 w1_scale=quant_config.w1_scale,
1623 w2_scale=quant_config.w2_scale,
1624 w1_zp=quant_config.w1_zp,
1625 w2_zp=quant_config.w2_zp,
1626 a1_scale=quant_config.a1_scale,
1627 a2_scale=quant_config.a2_scale,
1628 block_shape=quant_config.block_shape,
1629 w1_bias=quant_config.w1_bias,
1630 w2_bias=quant_config.w2_bias,
1631 )
1632
1633
1634 def _get_config_quant_dtype(
1635 use_fp8_w8a8: bool,
1636 use_int8_w8a8: bool,
1637 ) -> None | torch.dtype | str:
1638 """Get the quantization type based on the quantization strategy flags.
1639 We don't have a quant_config at this point so we need to work backwards.
1640 A return type of None means no quantization is required because the
1641 input is unquantized or has been quantized prior to calling
1642 fused_experts_impl.
1643 """
1644 if use_fp8_w8a8:
1645 return current_platform.fp8_dtype()
1646 if use_int8_w8a8:
1647 return torch.int8
1648
1649 return None
1650
1651
1652 def fused_experts_impl(
1653 hidden_states: torch.Tensor,
1654 w1: torch.Tensor,
1655 w2: torch.Tensor,
1656 topk_weights: torch.Tensor,
1657 topk_ids: torch.Tensor,
1658 activation: str = "silu",
1659 apply_router_weight_on_input: bool = False,
1660 use_fp8_w8a8: bool = False,
1661 use_int8_w8a8: bool = False,
1662 use_int8_w8a16: bool = False,
1663 use_int4_w4a16: bool = False,
1664 ocp_mx_scheme: str | None = None,
1665 per_channel_quant: bool = False,
1666 global_num_experts: int = -1,
1667 expert_map: torch.Tensor | None = None,
1668 w1_scale: torch.Tensor | None = None,
1669 w2_scale: torch.Tensor | None = None,
1670 w1_zp: torch.Tensor | None = None,
1671 w2_zp: torch.Tensor | None = None,
1672 a1_scale: torch.Tensor | None = None,
1673 a2_scale: torch.Tensor | None = None,
1674 block_shape: list[int] | None = None,
1675 w1_bias: torch.Tensor | None = None,
1676 w2_bias: torch.Tensor | None = None,
1677 ) -> torch.Tensor:
1678 if ocp_mx_scheme is not None:
1679 raise NotImplementedError(
1680 f"Using ocp_mx_scheme={ocp_mx_scheme} in functional fused_experts call is "
1681 "deprecated. Please use OCP_MXQuantizationEmulationTritonExperts."
1682 )
1683
1684 # Convert string activation to enum for internal use
1685 activation_enum = MoEActivation.from_str(activation)
1686
1687 # Check constraints.
1688 if use_int4_w4a16:
1689 assert hidden_states.size(1) // 2 == w1.size(2), "Hidden size mismatch"
1690 else:
1691 assert hidden_states.size(1) == w1.size(2), (
1692 f"Hidden size mismatch {hidden_states.size(1)} != {w1.size(2)}"
1693 )
1694
1695 assert topk_weights.size() == topk_ids.size(), "topk shape mismatch"
1696 assert hidden_states.is_contiguous(), "Hidden_states must be contiguous"
1697 assert w1.stride(-1) == 1, "Stride of last dimension must be 1"
1698 assert w2.stride(-1) == 1, "Stride of last dimension must be 1"
1699 assert hidden_states.dtype in [torch.float32, torch.float16, torch.bfloat16]
1700
1701 num_tokens = hidden_states.size(0)
1702 E, N, _ = w1.size()
1703 K = w2.size(1)
1704 if global_num_experts == -1:
1705 global_num_experts = E
1706 top_k_num = topk_ids.size(1)
1707
1708 M = num_tokens
1709
1710 config_dtype = _get_config_dtype_str(
1711 use_fp8_w8a8=use_fp8_w8a8,
1712 use_int8_w8a16=use_int8_w8a16,
1713 use_int4_w4a16=use_int4_w4a16,
1714 dtype=hidden_states.dtype,
1715 )
1716
1717 # Note: for use_int8_w8a16 or use_int4_w4a16, the activations are
1718 # quantized prior to calling fused_experts.
1719 quant_dtype = _get_config_quant_dtype(
1720 use_fp8_w8a8=use_fp8_w8a8,
1721 use_int8_w8a8=use_int8_w8a8,
1722 )
1723
1724 get_config_func = functools.partial(
1725 try_get_optimal_moe_config,
1726 w1.size(),
1727 w2.size(),
1728 top_k_num,
1729 config_dtype,
1730 block_shape=block_shape,
1731 )
1732
1733 config = get_config_func(M)
1734
1735 # We can reuse the memory between these because by the time we need
1736 # cache3, we're done with cache1
1737 cache13 = torch.empty(
1738 M * top_k_num * max(N, K),
1739 device=hidden_states.device,
1740 dtype=hidden_states.dtype,
1741 )
1742 intermediate_cache1 = cache13[: M * top_k_num * N].view(M, top_k_num, N)
1743 intermediate_cache3 = cache13[: M * top_k_num * K].view(M, top_k_num, K)
1744
1745 # This needs separate memory since it's used concurrently with cache1
1746 activation_out_dim = mk.FusedMoEExpertsModular.adjust_N_for_activation(
1747 N, activation_enum
1748 )
1749 intermediate_cache2 = torch.empty(
1750 (M * top_k_num, activation_out_dim),
1751 device=hidden_states.device,
1752 dtype=hidden_states.dtype,
1753 )
1754
1755 if hidden_states.dtype == torch.bfloat16:
1756 compute_type = tl.bfloat16
1757 elif hidden_states.dtype == torch.float16:
1758 compute_type = tl.float16
1759 elif hidden_states.dtype == torch.float32:
1760 compute_type = tl.float32
1761 else:
1762 raise ValueError(f"Unsupported compute_type: {hidden_states.dtype}")
1763
1764 out_hidden_states = torch.empty_like(hidden_states)
1765
1766 qhidden_states, a1q_scale = moe_kernel_quantize_input(
1767 A=hidden_states,
1768 A_scale=a1_scale,
1769 quant_dtype=quant_dtype,
1770 per_act_token_quant=per_channel_quant,
1771 block_shape=block_shape,
1772 )
1773
1774 sorted_token_ids, expert_ids, num_tokens_post_padded = _prepare_expert_assignment(
1775 topk_ids,
1776 config,
1777 num_tokens,
1778 top_k_num,
1779 global_num_experts,
1780 expert_map,
1781 use_int8_w8a16=use_int8_w8a16,
1782 use_int4_w4a16=use_int4_w4a16,
1783 block_shape=block_shape,
1784 ignore_invalid_experts=True,
1785 )
1786
1787 dispatch_fused_moe_kernel(
1788 qhidden_states,
1789 w1,
1790 intermediate_cache1,
1791 a1q_scale,
1792 w1_scale,
1793 w1_zp,
1794 topk_weights,
1795 sorted_token_ids,
1796 expert_ids,
1797 num_tokens_post_padded,
1798 apply_router_weight_on_input,
1799 top_k_num,
1800 config,
1801 compute_type=compute_type,
1802 use_fp8_w8a8=use_fp8_w8a8,
1803 use_int8_w8a8=use_int8_w8a8,
1804 use_int8_w8a16=use_int8_w8a16,
1805 use_int4_w4a16=use_int4_w4a16,
1806 per_channel_quant=per_channel_quant,
1807 block_shape=block_shape,
1808 B_bias=w1_bias,
1809 )
1810
1811 apply_moe_activation(
1812 activation_enum, intermediate_cache2, intermediate_cache1.view(-1, N)
1813 )
1814
1815 qintermediate_cache2, a2q_scale = moe_kernel_quantize_input(
1816 A=intermediate_cache2,
1817 A_scale=a2_scale,
1818 quant_dtype=quant_dtype,
1819 per_act_token_quant=per_channel_quant,
1820 block_shape=block_shape,
1821 )
1822
1823 if expert_map is not None:
1824 intermediate_cache3.zero_()
1825
1826 dispatch_fused_moe_kernel(
1827 qintermediate_cache2,
1828 w2,
1829 intermediate_cache3,
1830 a2q_scale,
1831 w2_scale,
1832 w2_zp,
1833 topk_weights,
1834 sorted_token_ids,
1835 expert_ids,
1836 num_tokens_post_padded,
1837 not apply_router_weight_on_input,
1838 1,
1839 config,
1840 compute_type=compute_type,
1841 use_fp8_w8a8=use_fp8_w8a8,
1842 use_int8_w8a8=use_int8_w8a8,
1843 use_int8_w8a16=use_int8_w8a16,
1844 use_int4_w4a16=use_int4_w4a16,
1845 per_channel_quant=per_channel_quant,
1846 block_shape=block_shape,
1847 B_bias=w2_bias,
1848 )
1849
1850 ops.moe_sum(
1851 intermediate_cache3.view(*intermediate_cache3.size()),
1852 out_hidden_states,
1853 )
1854
1855 return out_hidden_states