[Kernel] TD operand loads for batched MoE GEMM (moe_mmk) on XPU (#46340)
Signed-off-by: oonyshch <xonyshch@gmail.com> Co-authored-by: Kunshang Ji <kunshang.ji@intel.com>
Lena Onyshchenko committed
Jul 26, 2026 at 02:50 UTC
0111002323e226844a18e7f3e5834e9d0e1b6dcd
9 files changed
+331
-33
tests/kernels/moe/test_batched_moe.py
+184
@@ -351,3 +351,187 @@ def test_fused_moe_batched_experts(
351
torch.testing.assert_close(batched_output, baseline_output, atol=3e-2, rtol=2e-2)
352
353
torch.testing.assert_close(triton_output, batched_output, atol=2e-2, rtol=2e-2)
354
+
355
+
356
+# USE_TD in moe_mmk
357
+
358
+# K % 8 == 0 (bf16, 16-byte alignment)
359
+_TD_SHAPES = [
360
+ (4, 1, 128, 256),
361
+ (8, 64, 512, 512),
362
+ (4, 256, 1024, 2048),
363
+]
364
+
365
+
366
+def _td_supported() -> bool:
367
+ return current_platform.is_xpu() or (
368
+ current_platform.is_cuda() and current_platform.has_device_capability(90)
369
+ )
370
+
371
+
372
+def _run_td(A, B, num_expert_tokens, use_td: bool):
373
+ out = torch.zeros(
374
+ A.shape[0],
375
+ A.shape[1],
376
+ B.shape[1],
377
+ dtype=torch.bfloat16,
378
+ device=A.device,
379
+ )
380
+ import triton
381
+
382
+ from vllm.model_executor.layers.fused_moe.experts.fused_batched_moe import (
383
+ batched_triton_kernel,
384
+ )
385
+
386
+ if use_td:
387
+ from vllm.triton_utils.allocation import set_triton_allocator
388
+
389
+ set_triton_allocator(A.device)
390
+
391
+ E = A.shape[0]
392
+ max_tokens = A.shape[1]
393
+ K = A.shape[2]
394
+ N = B.shape[1]
395
+ BM = BN = BK = 64
396
+ grid = (E, triton.cdiv(max_tokens, BM) * triton.cdiv(N, BN))
397
+ batched_triton_kernel[grid](
398
+ A,
399
+ B,
400
+ out,
401
+ num_expert_tokens,
402
+ tl.bfloat16,
403
+ max_tokens,
404
+ K,
405
+ N,
406
+ None,
407
+ None,
408
+ None,
409
+ A.stride(0),
410
+ A.stride(1),
411
+ A.stride(2),
412
+ B.stride(0),
413
+ B.stride(2),
414
+ B.stride(1),
415
+ out.stride(0),
416
+ out.stride(1),
417
+ out.stride(2),
418
+ 0,
419
+ 0,
420
+ 0,
421
+ 0,
422
+ 0,
423
+ 0,
424
+ 0,
425
+ 0,
426
+ False,
427
+ False,
428
+ False,
429
+ BLOCK_M=BM,
430
+ BLOCK_N=BN,
431
+ BLOCK_K=BK,
432
+ USE_TD=use_td,
433
+ )
434
+ return out
435
+
436
+
437
+@pytest.mark.parametrize("num_experts,max_tokens_per_expert,K,N", _TD_SHAPES)
438
+def test_batched_mm_td_matches_plain(num_experts, max_tokens_per_expert, K, N):
439
+ if not _td_supported():
440
+ pytest.skip("TD requires XPU or CUDA sm_90+")
441
+ set_random_seed(42)
442
+ device = current_platform.device_type
443
+ A = (
444
+ torch.randn(
445
+ num_experts, max_tokens_per_expert, K, device=device, dtype=torch.bfloat16
446
+ )
447
+ / 10
448
+ )
449
+ B = torch.randn(num_experts, N, K, device=device, dtype=torch.bfloat16)
450
+ num_expert_tokens = torch.randint(
451
+ 1,
452
+ max_tokens_per_expert + 1,
453
+ size=(num_experts,),
454
+ device=device,
455
+ dtype=torch.int32,
456
+ )
457
+
458
+ out_plain = _run_td(A, B, num_expert_tokens, False)
459
+ out_td = _run_td(A, B, num_expert_tokens, True)
460
+
461
+ torch.testing.assert_close(out_td, out_plain, atol=6e-2, rtol=6e-2)
462
+
463
+
464
+def test_batched_mm_td_zero_expert_tokens():
465
+ if not _td_supported():
466
+ pytest.skip("TD requires XPU or CUDA sm_90+")
467
+ set_random_seed(42)
468
+ device = current_platform.device_type
469
+ E, M, K, N = 8, 32, 256, 256
470
+ A = torch.randn(E, M, K, device=device, dtype=torch.bfloat16) / 10
471
+ B = torch.randn(E, N, K, device=device, dtype=torch.bfloat16)
472
+ num_expert_tokens = torch.zeros(E, device=device, dtype=torch.int32)
473
+ num_expert_tokens[::2] = M
474
+
475
+ out_plain = _run_td(A, B, num_expert_tokens, False)
476
+ out_td = _run_td(A, B, num_expert_tokens, True)
477
+
478
+ for e in range(E):
479
+ if num_expert_tokens[e].item() == 0:
480
+ assert out_plain[e].abs().max().item() == 0.0
481
+ assert out_td[e].abs().max().item() == 0.0
482
+
483
+ torch.testing.assert_close(out_td, out_plain, atol=6e-2, rtol=6e-2)
484
+
485
+
486
+# BatchedTritonExperts device enablement (XPU)
487
+def test_batched_triton_experts_supports_current_device():
488
+ from vllm.model_executor.layers.fused_moe.experts.fused_batched_moe import (
489
+ BatchedTritonExperts,
490
+ )
491
+
492
+ if current_platform.is_xpu() or current_platform.is_cuda_alike():
493
+ assert BatchedTritonExperts._supports_current_device()
494
+ else:
495
+ pytest.skip("No GPU device available")
496
+
497
+
498
+@pytest.mark.parametrize("m,n,k,e,topk", [(32, 512, 512, 8, 2), (45, 1024, 128, 8, 1)])
499
+def test_batched_experts_end_to_end(m, n, k, e, topk):
500
+ """End-to-end BatchedTritonExperts via the reference (no-comms)
501
+ BatchedPrepareAndFinalize, validated against a torch reference. Exercises
502
+ the device-enablement path."""
503
+ if not (current_platform.is_xpu() or current_platform.is_cuda_alike()):
504
+ pytest.skip("No GPU device available")
505
+
506
+ from vllm.v1.worker.workspace import init_workspace_manager
507
+
508
+ set_random_seed(7)
509
+ device = current_platform.device_type
510
+ init_workspace_manager(torch.device(f"{device}:0"))
511
+
512
+ a = torch.randn((m, k), device=device, dtype=torch.bfloat16) / 10
513
+ score = torch.randn((m, e), device=device, dtype=torch.bfloat16)
514
+ # w1 is the fused gate+up projection [E, 2N, K]; w2 is [E, K, N].
515
+ w1 = torch.randn((e, 2 * n, k), device=device, dtype=torch.bfloat16) / 15
516
+ w2 = torch.randn((e, k, n), device=device, dtype=torch.bfloat16) / 15
517
+
518
+ with set_current_vllm_config(vllm_config):
519
+ topk_weight, topk_ids, _ = fused_topk(a, score, topk, False)
520
+
521
+ baseline_output = torch_experts(a, w1, w2, topk_weight, topk_ids)
522
+ triton_output = batched_moe(a, w1, w2, topk_weight, topk_ids)
523
+
524
+ torch.testing.assert_close(triton_output, baseline_output, atol=3e-2, rtol=2e-2)
525
+
526
+
527
+def test_batched_triton_backend_mapping():
528
+ from vllm.model_executor.layers.fused_moe.oracle.unquantized import (
529
+ UnquantizedMoeBackend,
530
+ map_unquantized_backend,
531
+ )
532
+
533
+ assert (
534
+ map_unquantized_backend("batched_triton")
535
+ == UnquantizedMoeBackend.BATCHED_TRITON
536
+ )
537
+ assert map_unquantized_backend("triton") == UnquantizedMoeBackend.TRITON
vllm/config/kernel.py
+3
@@ -122,6 +122,7 @@ class IrOpPriorityConfig:
122
MoEBackend = Literal[
123
"auto",
124
"triton",
125
+ "batched_triton",
126
"deep_gemm",
127
"deep_gemm_mega_moe",
128
"cutlass",
@@ -192,6 +193,8 @@ class KernelConfig:
193
194
- "auto": Automatically select the best backend based on model and hardware
195
- "triton": Use Triton-based fused MoE kernels
196
+ - "batched_triton": Use batched Triton experts (moe_mmk) on the batched
197
+ activation format ([E_local, max_num_tokens, K])
198
- "deep_gemm": Use DeepGEMM kernels (FP8 block-quantized only)
199
- "deep_gemm_mega_moe": Use DeepGEMM mega MoE kernels
200
- "cutlass": Use vLLM CUTLASS kernels
vllm/model_executor/layers/fused_moe/all2all_utils.py
+11
@@ -19,6 +19,7 @@ from vllm.model_executor.layers.fused_moe.modular_kernel import (
19
FusedMoEPrepareAndFinalize,
20
)
21
from vllm.model_executor.layers.fused_moe.prepare_finalize import (
22
+ BatchedPrepareAndFinalize,
23
make_moe_prepare_and_finalize_naive_dp_ep,
24
make_moe_prepare_and_finalize_no_dp_ep,
25
)
@@ -140,6 +141,16 @@ def maybe_make_prepare_finalize(
141
if not allow_new_interface:
142
return None
143
144
+ # Opt-in XPU batched path: reorganize tokens into E x T x K locally
145
+ # (no all-to-all) so BatchedTritonExperts (moe_mmk TD) can run.
146
+ if current_platform.is_xpu() and moe.moe_backend == "batched_triton":
147
+ return BatchedPrepareAndFinalize(
148
+ max_num_tokens=moe.max_num_tokens,
149
+ num_local_experts=moe.num_local_experts,
150
+ num_dispatchers=1,
151
+ rank=moe.moe_parallel_config.ep_rank,
152
+ )
153
+
154
# For DP/TP case, fall back to naive P/F.
155
if moe.moe_parallel_config.dp_size > 1:
156
logger.info_once(
vllm/model_executor/layers/fused_moe/experts/fused_batched_moe.py
+69
-13
@@ -32,7 +32,15 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
32
kFp8StaticTensorSym,
33
)
34
from vllm.platforms import current_platform
35
-from vllm.triton_utils import tl, triton
35
+from vllm.triton_utils import tl, triton, use_tensor_descriptor
36
+from vllm.triton_utils.allocation import set_triton_allocator
37
+
38
+
39
+def _is_capturing_or_compiling() -> bool:
40
+ # torch.cuda.is_current_stream_capturing() is unavailable on non-CUDA (XPU) torch.
41
+ return torch.compiler.is_compiling() or (
42
+ current_platform.is_cuda_alike() and torch.cuda.is_current_stream_capturing()
43
+ )
44
45
46
@triton.jit
@@ -71,9 +79,32 @@ def moe_mmk(
79
use_w8a8: tl.constexpr,
80
use_w8a16: tl.constexpr,
81
per_act_token_quant: tl.constexpr,
82
+ # TD: a_base_ptr/b_base_ptr are the expert/CTA-offset bases of A[M,K]/B[N,K].
83
+ a_base_ptr=None,
84
+ b_base_ptr=None,
85
+ M=0,
86
+ N=0,
87
+ stride_am: tl.int64 = 0,
88
+ stride_bn: tl.int64 = 0,
89
+ USE_TD: tl.constexpr = False,
90
):
91
offs_k = tl.arange(0, BLOCK_K)
92
93
+ if USE_TD:
94
+ # make_tensor_descriptor requires the last (K) stride to be a
95
+ # compile-time 1; the launcher only enables USE_TD for K-contiguous A/B.
96
+ a_desc = tl.make_tensor_descriptor(
97
+ a_base_ptr,
98
+ shape=[M, K],
99
+ strides=[stride_am, 1],
100
+ block_shape=[BLOCK_M, BLOCK_K],
101
+ )
102
+ b_desc = tl.make_tensor_descriptor(
103
+ b_base_ptr,
104
+ shape=[N, K],
105
+ strides=[stride_bn, 1],
106
+ block_shape=[BLOCK_N, BLOCK_K],
107
+ )
108
if use_w8a16:
109
b_scale_ptrs = (
110
b_scale_ptr + expert_id * stride_bse + offs_n[None, :] * stride_bsn
@@ -110,12 +141,17 @@ def moe_mmk(
141
for k in range(0, tl.cdiv(K, BLOCK_K)):
142
# Load the next block of A and B, generate a mask by checking the
143
# K dimension.
113
- a = tl.load(
114
- a_ptrs,
115
- mask=mask_m[:, None] & (offs_k[None, :] < K - k * BLOCK_K),
116
- other=0.0,
117
- )
118
- b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_K, other=0.0)
144
+ if USE_TD:
145
+ # B is [N, K]; tile is [BLOCK_N, BLOCK_K], transposed for dot.
146
+ a = a_desc.load([0, k * BLOCK_K])
147
+ b = tl.trans(b_desc.load([0, k * BLOCK_K]))
148
+ else:
149
+ a = tl.load(
150
+ a_ptrs,
151
+ mask=mask_m[:, None] & (offs_k[None, :] < K - k * BLOCK_K),
152
+ other=0.0,
153
+ )
154
+ b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_K, other=0.0)
155
# We accumulate along the K dimension.
156
if use_w8a16:
157
accumulator = tl.dot(a, b.to(compute_type), acc=accumulator)
@@ -193,6 +229,7 @@ def expert_triton_kernel(
229
BLOCK_M: tl.constexpr,
230
BLOCK_N: tl.constexpr,
231
BLOCK_K: tl.constexpr,
232
+ USE_TD: tl.constexpr = False,
233
):
234
offs_m = tl.arange(0, BLOCK_M)
235
offs_n = tl.arange(0, BLOCK_N) % N
@@ -238,6 +275,13 @@ def expert_triton_kernel(
275
use_fp8_w8a8,
276
use_int8_w8a16,
277
per_act_token_quant,
278
+ a_ptr,
279
+ b_ptr,
280
+ M,
281
+ N,
282
+ stride_am,
283
+ stride_bn,
284
+ USE_TD,
285
)
286
287
# store in C
@@ -292,6 +336,7 @@ def batched_triton_kernel(
336
BLOCK_M: tl.constexpr,
337
BLOCK_N: tl.constexpr,
338
BLOCK_K: tl.constexpr,
339
+ USE_TD: tl.constexpr = False,
340
):
341
expert_id = tl.program_id(axis=0)
342
e_num_tokens = tl.load(expert_num_tokens + expert_id)
@@ -372,6 +417,7 @@ def batched_triton_kernel(
417
BLOCK_M,
418
BLOCK_N,
419
BLOCK_K,
420
+ USE_TD,
421
)
422
423
@@ -444,6 +490,18 @@ def invoke_moe_batched_triton_kernel(
490
stride_asm = 0
491
stride_ask = 0
492
493
+ use_td = (
494
+ use_tensor_descriptor()
495
+ and A.stride(2) == 1
496
+ and B.stride(2) == 1
497
+ and (K * A.element_size()) % 16 == 0
498
+ and (BLOCK_M & (BLOCK_M - 1)) == 0
499
+ and (BLOCK_N & (BLOCK_N - 1)) == 0
500
+ and (BLOCK_K & (BLOCK_K - 1)) == 0
501
+ )
502
+ if use_td:
503
+ set_triton_allocator(A.device)
504
+
505
batched_triton_kernel[grid](
506
A,
507
B,
@@ -485,6 +543,7 @@ def invoke_moe_batched_triton_kernel(
543
BLOCK_M=BLOCK_M,
544
BLOCK_N=BLOCK_N,
545
BLOCK_K=BLOCK_K,
546
+ USE_TD=use_td,
547
)
548
549
@@ -616,10 +675,7 @@ class NaiveBatchedExperts(mk.FusedMoEExpertsModular):
675
676
for expert in range(num_local_experts):
677
# Indexing expert_num_tokens doesn't work w/cudagraphs or inductor
619
- if (
620
- torch.compiler.is_compiling()
621
- or torch.cuda.is_current_stream_capturing()
622
- ):
678
+ if _is_capturing_or_compiling():
679
num = hidden_states.shape[1]
680
else:
681
num = int(expert_num_tokens[expert].item())
@@ -659,7 +715,7 @@ def batched_moe_kernel_quantize_input(
715
per_act_token_quant: bool,
716
block_shape: list[int] | None = None,
717
) -> tuple[torch.Tensor, torch.Tensor | None]:
662
- if torch.compiler.is_compiling() or torch.cuda.is_current_stream_capturing():
718
+ if _is_capturing_or_compiling():
719
# Note: this does a bunch of extra work because expert_num_tokens is
720
# ignored but it does support torch.compile + cudagraphs.
721
hidden_dim = A.size(-1)
@@ -745,7 +801,7 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
801
802
@staticmethod
803
def _supports_current_device() -> bool:
748
- return current_platform.is_cuda_alike()
804
+ return current_platform.is_cuda_alike() or current_platform.is_xpu()
805
806
@staticmethod
807
def _supports_no_act_and_mul() -> bool:
vllm/model_executor/layers/fused_moe/oracle/unquantized.py
+2
@@ -151,6 +151,7 @@ def map_unquantized_backend(runner_backend: MoEBackend) -> UnquantizedMoeBackend
151
"""Map user's MoEBackend to UnquantizedMoeBackend."""
152
mapping = {
153
"triton": UnquantizedMoeBackend.TRITON,
154
+ "batched_triton": UnquantizedMoeBackend.BATCHED_TRITON,
155
"flashinfer_trtllm": UnquantizedMoeBackend.FLASHINFER_TRTLLM,
156
"flashinfer_cutlass": UnquantizedMoeBackend.FLASHINFER_CUTLASS,
157
"aiter": UnquantizedMoeBackend.AITER,
@@ -233,6 +234,7 @@ def select_unquantized_moe_backend(
234
activation_format = (
235
mk.FusedMoEActivationFormat.BatchedExperts
236
if moe_config.moe_parallel_config.use_batched_activation_format
237
+ or moe_config.moe_backend == "batched_triton"
238
else mk.FusedMoEActivationFormat.Standard
239
)
240
vllm/model_executor/layers/fused_moe/prepare_finalize/batched.py
+22
-11
@@ -112,15 +112,28 @@ class BatchedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
112
113
a1_scale = normalize_scales_shape(quant_config.a1_scale)
114
115
- for expert_id in range(first_expert, last_expert):
116
- topks = torch.any(topk_ids == expert_id, dim=1).flatten()
117
- rows = torch.count_nonzero(topks.flatten())
118
- if rows == 0:
119
- continue
120
- idx = expert_id - first_expert
121
- tokens_per_expert[idx] = rows
122
- rhs = a1[: topks.numel()][topks]
123
- if quant_config.quant_dtype is not None:
115
+ if quant_config.quant_dtype is None:
116
+ # Vectorized dispatch: compute the [E_local, T] hit mask and
117
+ # per-expert slot offsets in one shot (single nonzero sync instead
118
+ # of a launch-bound per-expert Python loop).
119
+ local_ids = torch.arange(first_expert, last_expert, device=a1.device).view(
120
+ -1, 1, 1
121
+ )
122
+ hits = (topk_ids.unsqueeze(0) == local_ids).any(dim=2) # [E_local, T]
123
+ tokens_per_expert[:num_local_experts] = hits.sum(dim=1).to(torch.int32)
124
+ # slot of each token within its expert batch, preserving token order
125
+ slots = hits.to(torch.int32).cumsum(dim=1) - 1 # [E_local, T]
126
+ e_idx, t_idx = hits.nonzero(as_tuple=True)
127
+ b_a1[e_idx, slots[e_idx, t_idx]] = a1[t_idx].to(b_type)
128
+ else:
129
+ for expert_id in range(first_expert, last_expert):
130
+ topks = torch.any(topk_ids == expert_id, dim=1).flatten()
131
+ rows = torch.count_nonzero(topks.flatten())
132
+ if rows == 0:
133
+ continue
134
+ idx = expert_id - first_expert
135
+ tokens_per_expert[idx] = rows
136
+ rhs = a1[: topks.numel()][topks]
137
if a1_scale is not None:
138
if quant_config.is_per_act_token:
139
rhs_a1_scale = a1_scale[: topks.numel()][topks]
@@ -140,8 +153,6 @@ class BatchedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
153
b_a1_scale[idx, :rows] = b_s[:rows]
154
else:
155
b_a1_scale[idx, : b_s.shape[0]] = b_s
143
- else:
144
- b_a1[idx, :rows, :] = rhs
156
157
assert b_a1_scale is None or b_a1_scale.ndim == 3
158
vllm/model_executor/layers/fused_moe/topk_weight_and_reduce.py
+15
-8
@@ -164,13 +164,20 @@ class TopKWeightAndReduceNaiveBatched(mk.TopKWeightAndReduce):
164
first_expert = num_local_experts * self.rank
165
last_expert = first_expert + num_local_experts
166
167
- for expert_id in range(first_expert, last_expert):
168
- matching_tokens = topk_ids == expert_id
169
- topks = torch.any(matching_tokens, dim=1).flatten()
170
- rows = torch.count_nonzero(topks)
171
- rhs = fused_expert_output[expert_id - first_expert, :rows, :]
172
- if not apply_router_weight_on_input:
173
- rhs.mul_(topk_weights[matching_tokens].view(rhs.size(0), 1))
174
- output[topks] = output[topks] + rhs
167
+ # Vectorized weighted scatter-add (single nonzero sync instead of a
168
+ # per-expert loop; mirrors the dispatch path).
169
+ local_ids = torch.arange(
170
+ first_expert, last_expert, device=topk_ids.device
171
+ ).view(-1, 1, 1)
172
+ matching = topk_ids.unsqueeze(0) == local_ids # [E_local, T, topk]
173
+ hits = matching.any(dim=2) # [E_local, T]
174
+ slots = hits.to(torch.int32).cumsum(dim=1) - 1 # [E_local, T]
175
+ e_idx, t_idx = hits.nonzero(as_tuple=True)
176
+ gathered = fused_expert_output[e_idx, slots[e_idx, t_idx], :]
177
+ if not apply_router_weight_on_input:
178
+ # weight of token t at expert e: the topk weight on the matching slot
179
+ weights = (matching * topk_weights.unsqueeze(0)).sum(dim=2) # [E_local, T]
180
+ gathered = gathered * weights[e_idx, t_idx].unsqueeze(1).to(gathered.dtype)
181
+ output.index_add_(0, t_idx, gathered.to(output.dtype))
182
183
return output
vllm/triton_utils/__init__.py
+11
-1
@@ -17,7 +17,17 @@ else:
17
tl = TritonLanguagePlaceholder()
18
tldevice = TritonLanguagePlaceholder()
19
20
+from vllm.triton_utils.tensor_descriptor import use_tensor_descriptor
21
+
22
LOG2E = 1.4426950408889634
23
LOGE2 = 0.6931471805599453
24
23
-__all__ = ["HAS_TRITON", "triton", "tl", "tldevice", "LOG2E", "LOGE2"]
25
+__all__ = [
26
+ "HAS_TRITON",
27
+ "triton",
28
+ "tl",
29
+ "tldevice",
30
+ "LOG2E",
31
+ "LOGE2",
32
+ "use_tensor_descriptor",
33
+]
vllm/triton_utils/tensor_descriptor.py
new
+14
@@ -0,0 +1,14 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+
5
+def use_tensor_descriptor(override: bool | None = None) -> bool:
6
+ """Tri-state VLLM_TRITON_USE_TD: unset=auto (on for XPU), 1/0=force on/off."""
7
+ from vllm import envs
8
+ from vllm.platforms import current_platform
9
+
10
+ if override is None:
11
+ override = envs.VLLM_TRITON_USE_TD
12
+ if override is not None:
13
+ return override
14
+ return current_platform.is_xpu()