[Perf] Improve BF16x3 router GEMM accuracy and make it default on sm100 (#55899)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
Thien Tran committed
Sep 9, 2026 at 22:33 UTC
cc4210f6719e3285fdfe4da862ae04c8dbb77ab1
6 files changed
+314
-88
tests/kernels/test_bf16x3_router_gemm_cutedsl.py
+14
-4
@@ -1,6 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Tests for the experimental SM100 BF16x3 router GEMM."""
3
+"""Tests for the SM100 BF16x3 router GEMM."""
4
5
import pytest
6
import torch
@@ -20,7 +20,16 @@ def _requires_sm100_cutedsl():
20
21
@pytest.mark.parametrize(
22
("num_tokens", "hidden_dim", "num_experts"),
23
- [(48, 6144, 128), (96, 3072, 256), (129, 3072, 17)],
23
+ [
24
+ (48, 6144, 128),
25
+ (96, 3072, 256),
26
+ (129, 3072, 17),
27
+ # long-K cases exercise the multi-accumulation path (the split-K
28
+ # heuristic leaves chains of 15 and 32 K-tiles here, above the
29
+ # kernel's num_tmem_acc bound)
30
+ (1024, 8192, 256),
31
+ (2048, 8192, 256),
32
+ ],
33
)
34
def test_bf16x3_router_gemm_matches_reference(
35
num_tokens: int, hidden_dim: int, num_experts: int
@@ -36,8 +45,9 @@ def test_bf16x3_router_gemm_matches_reference(
45
# Match the observed router weight scale
46
w *= 0.053
47
out = bf16x3_router_gemm(x, w)
39
- ref = torch.nn.functional.linear(x.float(), w)
48
+ # FP64 reference: the FP32 reference itself drifts by ~5e-6 at N=2048
49
+ ref = torch.nn.functional.linear(x.double(), w.double())
50
51
assert out.shape == (num_tokens, num_experts)
52
assert out.dtype == torch.float32
43
- assert torch.mean(torch.abs(out - ref)).item() < 5e-6
53
+ assert torch.mean(torch.abs(out.double() - ref)).item() < 5e-6
vllm/config/kernel.py
-3
@@ -230,9 +230,6 @@ class KernelConfig:
230
enable_jit_warmup: bool = True
231
"""If True, run JIT compile warmup during kernel warmup."""
232
233
- enable_bf16x3_router_gemm: bool = False
234
- """If True, use the experimental SM100 BF16x3 CuteDSL router GEMM."""
235
-
233
moe_backend: MoEBackend = "auto"
234
"""Backend for MoE expert computation kernels. Available options:
235
vllm/engine/arg_utils.py
-7
@@ -695,7 +695,6 @@ class EngineArgs:
695
enable_flashinfer_autotune: bool = get_field(
696
KernelConfig, "enable_flashinfer_autotune"
697
)
698
- enable_bf16x3_router_gemm: bool | None = None
698
worker_cls: str = ParallelConfig.worker_cls
699
worker_extension_cls: str = ParallelConfig.worker_extension_cls
700
@@ -1655,10 +1654,6 @@ class EngineArgs:
1654
"--enable-flashinfer-autotune",
1655
**kernel_kwargs["enable_flashinfer_autotune"],
1656
)
1658
- kernel_group.add_argument(
1659
- "--enable-bf16x3-router-gemm",
1660
- **kernel_kwargs["enable_bf16x3_router_gemm"],
1661
- )
1657
moe_backend_kwargs = kernel_kwargs["moe_backend"]
1658
moe_backend_kwargs["type"] = lambda s: s.lower().replace("-", "_")
1659
kernel_group.add_argument("--moe-backend", **moe_backend_kwargs)
@@ -2519,8 +2514,6 @@ class EngineArgs:
2514
"are mutually exclusive"
2515
)
2516
kernel_config.enable_flashinfer_autotune = self.enable_flashinfer_autotune
2522
- if self.enable_bf16x3_router_gemm is not None:
2523
- kernel_config.enable_bf16x3_router_gemm = self.enable_bf16x3_router_gemm
2517
if self.moe_backend != "auto":
2518
kernel_config.moe_backend = self.moe_backend
2519
if self.linear_backend != "auto":
vllm/model_executor/layers/fused_moe/router/bf16x3_router_gemm_cutedsl.py
+249
-56
@@ -6,6 +6,10 @@ Computes ``X @ W.T`` for BF16 ``X`` with shape ``[N, K]`` and FP32 router
6
weights ``W`` with shape ``[M, K]`` by decomposing each FP32 weight value into
7
three BF16 residual terms inside the kernel, then accumulating the three BF16
8
MMA results into FP32 TMEM output.
9
+
10
+The TMEM accumulation chain is bounded to ``num_tmem_acc`` K-tiles: at chunk
11
+boundaries the epilogue warps drain the main-term accumulator into a register
12
+master accumulator and the MMA warp resets it.
13
"""
14
15
from functools import cache
@@ -22,6 +26,7 @@ from quack.compile_utils import make_fake_tensor
26
from vllm.cute_utils import _tcgen05, simple_tma_copy
27
from vllm.triton_utils import tl, triton
28
from vllm.utils import math_utils
29
+from vllm.utils.torch_utils import direct_register_custom_op
30
31
__all__ = ["bf16x3_router_gemm"]
32
@@ -82,6 +87,7 @@ class Sm100BF16x3RouterGemm:
87
self.cta_tile = (BN, 128, 64)
88
self.num_stages = 2
89
self.num_warps = 10
90
+ self.num_tmem_acc = 8
91
92
@cute.jit
93
def _make_tma(self, tensor: cute.Tensor, BM: int, BK: int):
@@ -129,6 +135,7 @@ class Sm100BF16x3RouterGemm:
135
136
BN, BM, BK = self.cta_tile
137
num_stages = self.num_stages
138
+ num_tmem_acc = self.num_tmem_acc
139
140
N, K = X_tma.tma_tensor.shape
141
M, _ = W_tma.tma_tensor.shape
@@ -151,7 +158,8 @@ class Sm100BF16x3RouterGemm:
158
tma_full_mbar = smem.allocate_array(Int64, num_stages)
159
tma_empty_mbar = smem.allocate_array(Int64, num_stages)
160
w_full_mbar = smem.allocate_array(Int64, num_stages)
154
- mma_mbar = smem.allocate_array(Int64, 1)
161
+ acc_full_mbar = smem.allocate_array(Int64, 1)
162
+ acc_empty_mbar = smem.allocate_array(Int64, 1)
163
taddr = smem.allocate(Int32, 4)
164
165
BAR_TMEM_ALLOC = 1
@@ -171,7 +179,8 @@ class Sm100BF16x3RouterGemm:
179
cute.arch.mbarrier_init(tma_full_mbar + i, 1)
180
cute.arch.mbarrier_init(tma_empty_mbar + i, 1)
181
cute.arch.mbarrier_init(w_full_mbar + i, 128)
174
- cute.arch.mbarrier_init(mma_mbar, 1)
182
+ cute.arch.mbarrier_init(acc_full_mbar, 1)
183
+ cute.arch.mbarrier_init(acc_empty_mbar, 128)
184
cute.arch.mbarrier_init_fence()
185
elif warp_id == 1:
186
cpasync.prefetch_descriptor(X_tma.atom)
@@ -213,40 +222,52 @@ class Sm100BF16x3RouterGemm:
222
223
elif warp_id == 8:
224
# MMA warp
216
- stage_id = 0
217
- parity = 0
225
+ tma_stage_id = 0
226
+ tma_parity = 0
227
+
228
+ tmem_acc_count = 0
229
+ tmem_parity = 1
230
231
idesc = _tcgen05.make_bf16_idesc(BM, BN)
232
sdesc = _tcgen05.make_sdesc_128B_swizzle(0)
233
234
for tile_k in cutlass.range(bid_k, k_tiles, split_k, unroll=1):
223
- cute.arch.mbarrier_wait(tma_full_mbar + stage_id, parity)
224
- cute.arch.mbarrier_wait(w_full_mbar + stage_id, parity)
235
+ if tmem_acc_count == 0:
236
+ cute.arch.mbarrier_wait(acc_empty_mbar, tmem_parity)
237
+ cute.arch.mbarrier_wait(tma_full_mbar + tma_stage_id, tma_parity)
238
+ cute.arch.mbarrier_wait(w_full_mbar + tma_stage_id, tma_parity)
239
_tcgen05.fence_after_thread_sync()
240
227
- w_tmem = w_tmem_base + stage_id * (BK // 2 * 3)
228
- x_desc = sdesc | (sX[None, None, stage_id].iterator.toint() >> 4)
241
+ w_tmem = w_tmem_base + tma_stage_id * (BK // 2 * 3)
242
+ x_desc = sdesc | (sX[None, None, tma_stage_id].iterator.toint() >> 4)
243
244
for k in cutlass.range_constexpr(BK // 16):
231
- enable_d = (tile_k > bid_k) or (k > 0)
245
+ enable_d_main = (tmem_acc_count > 0) or (k > 0)
246
+ enable_d_res = (tile_k > bid_k) or (k > 0)
247
_tcgen05.mma_ts_f16(
233
- acc_main, w_tmem + k * 8, x_desc, idesc, enable_d
248
+ acc_main, w_tmem + k * 8, x_desc, idesc, enable_d_main
249
)
250
_tcgen05.mma_ts_f16(
236
- acc_res, w_tmem + 32 + k * 8, x_desc, idesc, enable_d
251
+ acc_res, w_tmem + 32 + k * 8, x_desc, idesc, enable_d_res
252
)
253
_tcgen05.mma_ts_f16(
254
acc_res, w_tmem + 64 + k * 8, x_desc, idesc, True
255
)
256
x_desc += 32 >> 4
257
243
- _tcgen05.commit(tma_empty_mbar + stage_id)
258
+ _tcgen05.commit(tma_empty_mbar + tma_stage_id)
259
245
- stage_id = (stage_id + 1) % self.num_stages
246
- if stage_id == 0:
247
- parity ^= 1
260
+ tma_stage_id = (tma_stage_id + 1) % num_stages
261
+ if tma_stage_id == 0:
262
+ tma_parity ^= 1
263
+
264
+ tmem_acc_count = (tmem_acc_count + 1) % num_tmem_acc
265
+ if tmem_acc_count == 0:
266
+ _tcgen05.commit(acc_full_mbar)
267
+ tmem_parity ^= 1
268
249
- _tcgen05.commit(mma_mbar)
269
+ if tmem_acc_count != 0:
270
+ _tcgen05.commit(acc_full_mbar)
271
272
elif warp_id >= 4:
273
# prep warps: decompose FP32 W into 3xBF16
@@ -303,39 +324,93 @@ class Sm100BF16x3RouterGemm:
324
_tcgen05.alloc(taddr)
325
cute.arch.barrier(barrier_id=BAR_TMEM_ALLOC, number_of_threads=128)
326
306
- if warp_id == 0:
307
- cute.arch.mbarrier_wait(mma_mbar, 0)
308
- cute.arch.barrier(barrier_id=BAR_EPI, number_of_threads=128)
309
- _tcgen05.fence_after_thread_sync()
310
-
311
- cute.arch.griddepcontrol_launch_dependents()
327
+ tiles_local = cute.ceil_div(k_tiles - bid_k, split_k)
328
+ num_chunks = cute.ceil_div(tiles_local, num_tmem_acc)
329
330
WIDTH = 8
314
- for i in cutlass.range_constexpr(BN // WIDTH):
315
- tcol = i * WIDTH
316
- main_regs = cute.make_rmem_tensor(WIDTH, Float32)
317
- res_regs = cute.make_rmem_tensor(WIDTH, Float32)
318
- main_regs.store(_tcgen05.ld(warp_id * 32, tcol, "32x32b", WIDTH))
319
- res_regs.store(_tcgen05.ld(warp_id * 32, BN + tcol, "32x32b", WIDTH))
320
- _tcgen05.wait_ld()
321
-
322
- # CuteDSL will codegen add.f32x2
323
- for j in cutlass.range(WIDTH, vectorize=True):
324
- main_regs[j] += res_regs[j]
331
+ main_regs = cute.make_rmem_tensor(WIDTH, Float32)
332
+ res_regs = cute.make_rmem_tensor(WIDTH, Float32)
333
334
+ if num_chunks == 1:
335
+ # single chunk
336
+ cute.arch.mbarrier_wait(acc_full_mbar, 0)
337
+ _tcgen05.fence_after_thread_sync()
338
w_row_idx = bid_m * BM + tid
327
- for j in cutlass.range_constexpr(WIDTH):
328
- x_row_idx = bid_n * BN + i * WIDTH + j
329
- if x_row_idx < N and w_row_idx < M:
330
- out[bid_k, x_row_idx, w_row_idx] = main_regs[j]
339
+ for i in cutlass.range_constexpr(BN // WIDTH):
340
+ tcol = i * WIDTH
341
+ main_regs.store(_tcgen05.ld(warp_id * 32, tcol, "32x32b", WIDTH))
342
+ res_regs.store(
343
+ _tcgen05.ld(warp_id * 32, BN + tcol, "32x32b", WIDTH)
344
+ )
345
+ _tcgen05.wait_ld()
346
+
347
+ # CuteDSL will codegen add.f32x2
348
+ for j in cutlass.range(WIDTH, vectorize=True):
349
+ main_regs[j] += res_regs[j]
350
+
351
+ for j in cutlass.range_constexpr(WIDTH):
352
+ x_row_idx = bid_n * BN + i * WIDTH + j
353
+ if x_row_idx < N and w_row_idx < M:
354
+ out[bid_k, x_row_idx, w_row_idx] = main_regs[j]
355
+
356
+ else:
357
+ # multiple chunks
358
+ master_acc = cute.make_rmem_tensor(BN, Float32)
359
+
360
+ # chunk 0: pure TMEM load, no accumulation.
361
+ cute.arch.mbarrier_wait(acc_full_mbar, 0)
362
+ _tcgen05.fence_after_thread_sync()
363
+ master_acc.store(_tcgen05.ld(warp_id * 32, 0, "32x32b", BN))
364
+ _tcgen05.wait_ld()
365
+ _tcgen05.fence_before_thread_sync()
366
+ cute.arch.mbarrier_arrive(acc_empty_mbar)
367
+
368
+ # accumulate main tmem acc
369
+ for chunk in cutlass.range(1, num_chunks, unroll=1):
370
+ cute.arch.mbarrier_wait(acc_full_mbar, chunk & 1)
371
+ _tcgen05.fence_after_thread_sync()
372
+
373
+ for i in cutlass.range_constexpr(BN // WIDTH):
374
+ tcol = i * WIDTH
375
+ main_regs.store(
376
+ _tcgen05.ld(warp_id * 32, tcol, "32x32b", WIDTH)
377
+ )
378
+ _tcgen05.wait_ld()
379
+ # CuteDSL refuses to compile
380
+ # master_acc[i * WIDTH + j] += main_regs[j]
381
+ # when vectorize=True
382
+ master_view = cute.local_tile(master_acc, (WIDTH,), (i,))
383
+ for j in cutlass.range(WIDTH, vectorize=True):
384
+ master_view[j] += main_regs[j]
385
+
386
+ _tcgen05.fence_before_thread_sync()
387
+ cute.arch.mbarrier_arrive(acc_empty_mbar)
388
+
389
+ # fold in the residual accumulator
390
+ for i in cutlass.range_constexpr(BN // WIDTH):
391
+ tcol = i * WIDTH
392
+ res_regs.store(
393
+ _tcgen05.ld(warp_id * 32, BN + tcol, "32x32b", WIDTH)
394
+ )
395
+ _tcgen05.wait_ld()
396
+ master_view = cute.local_tile(master_acc, (WIDTH,), (i,))
397
+ for j in cutlass.range(WIDTH, vectorize=True):
398
+ master_view[j] += res_regs[j]
399
400
+ w_row_idx = bid_m * BM + tid
401
+ for j in cutlass.range_constexpr(WIDTH):
402
+ x_row_idx = bid_n * BN + i * WIDTH + j
403
+ if x_row_idx < N and w_row_idx < M:
404
+ out[bid_k, x_row_idx, w_row_idx] = master_acc[i * WIDTH + j]
405
+
406
+ cute.arch.griddepcontrol_launch_dependents()
407
cute.arch.barrier(barrier_id=BAR_EPI, number_of_threads=128)
408
if warp_id == 0:
409
_tcgen05.dealloc()
410
411
@cache
412
@staticmethod
338
- def compile(BN: int = 128, K: int = 6144):
413
+ def compile(K: int, BN: int = 128):
414
N = cute.sym_int()
415
M = cute.sym_int()
416
SPLIT_K = cute.sym_int()
@@ -392,16 +467,19 @@ def _splitk_reduce_kernel(
467
)
468
469
470
+def _splitk_reduce_config(split_k: int) -> tuple[int, int, int]:
471
+ block_s = triton.next_power_of_2(split_k)
472
+ if block_s >= 64:
473
+ return 1, 32, block_s
474
+ if block_s >= 8:
475
+ return 1, 256, block_s
476
+ return min(16, 32 // block_s), 32, block_s
477
+
478
+
479
def splitk_reduce_triton(partials: torch.Tensor, out: torch.Tensor):
480
split_k, N, M = partials.shape
397
- block_s = 1 << (split_k - 1).bit_length()
481
split_stride = partials.stride(0)
399
- if block_s >= 64:
400
- BN, BM = 1, 32
401
- elif block_s >= 8:
402
- BN, BM = 1, 256
403
- else:
404
- BN, BM = min(16, 32 // block_s), 32
482
+ BN, BM, block_s = _splitk_reduce_config(split_k)
483
grid = (triton.cdiv(N, BN), triton.cdiv(M, BM))
484
_splitk_reduce_kernel[grid](
485
partials,
@@ -419,27 +497,124 @@ def splitk_reduce_triton(partials: torch.Tensor, out: torch.Tensor):
497
)
498
499
422
-def bf16x3_router_gemm(X: torch.Tensor, W: torch.Tensor) -> torch.Tensor:
423
- """Return ``X @ W.T`` using the SM100 BF16x3 router GEMM kernel."""
424
- N, K = X.shape
425
- M, _ = W.shape
426
- num_sms = torch.cuda.get_device_properties(X.device).multi_processor_count
500
+# Accuracy-constrained (BN, split_k) configs from GB300 sweeps.
501
+_TILE_CONFIG_OVERRIDES = {
502
+ (2816, 256): { # Hunyuan-V4
503
+ 64: (16, 16),
504
+ 128: (32, 16),
505
+ 256: (64, 16),
506
+ 512: (128, 16),
507
+ },
508
+ (3072, 256): { # MiniMax-M2
509
+ 64: (16, 16),
510
+ 128: (32, 16),
511
+ 256: (64, 16),
512
+ 512: (128, 16),
513
+ },
514
+ (4096, 192): { # Hunyuan-V3
515
+ 64: (32, 32),
516
+ 128: (64, 32),
517
+ 256: (128, 32),
518
+ 512: (128, 19),
519
+ },
520
+ (6144, 128): { # MiniMax-M3
521
+ 64: (32, 64),
522
+ 128: (64, 64),
523
+ 256: (64, 32),
524
+ 512: (128, 32),
525
+ },
526
+}
527
+
528
+
529
+def _pick_tile_config(N: int, K: int, M: int, num_sms: int) -> tuple[int, int]:
530
+ """Return (BN, split_k): tuned table mid-range, generic rule otherwise."""
531
+ k_tiles = math_utils.cdiv(K, 64)
532
+
533
+ if 32 < N <= 512:
534
+ token_bucket = triton.next_power_of_2(N)
535
+ overrides = _TILE_CONFIG_OVERRIDES.get((K, M))
536
+ if overrides is not None:
537
+ BN, split_k = overrides[token_bucket]
538
+ return BN, min(split_k, k_tiles)
539
540
# next power of 2 within 8 and 128
541
BN = triton.next_power_of_2(N)
542
BN = min(max(BN, 8), 128)
543
432
- BM = 128
433
- BK = 64
434
- k_tiles = math_utils.cdiv(K, BK)
435
- grid_m = math_utils.cdiv(M, BM)
544
+ grid_m = math_utils.cdiv(M, 128)
545
grid_n = math_utils.cdiv(N, BN)
437
-
546
base_ctas = grid_m * grid_n
547
split_k = min(k_tiles, max(1, num_sms // base_ctas))
548
+ return BN, split_k
549
+
550
+
551
+def warmup_bf16x3_router_gemm(
552
+ K: int,
553
+ M: int,
554
+ min_num_tokens: int,
555
+ max_num_tokens: int,
556
+) -> tuple[int, ...]:
557
+ """Compile every BF16x3 router GEMM configuration reachable at runtime."""
558
+ from vllm.model_executor.warmup.jit_warmup_triton_helper import (
559
+ TritonWarmupTensor,
560
+ triton_scalar_specialization_rep,
561
+ )
562
+
563
+ device = torch.accelerator.current_device_index()
564
+ num_sms = torch.cuda.get_device_properties(device).multi_processor_count
565
+ gemm_configs: set[int] = set()
566
+ reduce_configs: set[tuple[int, int, int, int, int, int]] = set()
567
+ for N in range(min_num_tokens, max_num_tokens + 1):
568
+ BN, split_k = _pick_tile_config(N, K, M, num_sms)
569
+ gemm_configs.add(BN)
570
+ if split_k == 1:
571
+ continue
572
+ reduce_BN, reduce_BM, block_s = _splitk_reduce_config(split_k)
573
+ reduce_configs.add(
574
+ (
575
+ triton_scalar_specialization_rep(N),
576
+ triton_scalar_specialization_rep(N * M),
577
+ triton_scalar_specialization_rep(split_k),
578
+ reduce_BN,
579
+ reduce_BM,
580
+ block_s,
581
+ )
582
+ )
583
+
584
+ configs = tuple(sorted(gemm_configs))
585
+ for BN in configs:
586
+ Sm100BF16x3RouterGemm.compile(K, BN)
587
+ for N, split_stride, split_k, BN, BM, block_s in sorted(reduce_configs):
588
+ partials = TritonWarmupTensor(torch.float32)
589
+ out = TritonWarmupTensor(torch.float32)
590
+ _splitk_reduce_kernel.warmup(
591
+ partials,
592
+ out,
593
+ N,
594
+ M,
595
+ split_stride,
596
+ split_k,
597
+ BN=BN,
598
+ BM=BM,
599
+ BS=block_s,
600
+ USE_PDL=True,
601
+ num_warps=4,
602
+ launch_pdl=True,
603
+ grid=(1, 1),
604
+ )
605
+ return configs
606
+
607
+
608
+def _bf16x3_router_gemm(X: torch.Tensor, W: torch.Tensor) -> torch.Tensor:
609
+ """Return ``X @ W.T`` using the SM100 BF16x3 router GEMM kernel."""
610
+ N, K = X.shape
611
+ M, _ = W.shape
612
+ num_sms = torch.cuda.get_device_properties(X.device).multi_processor_count
613
+
614
+ BN, split_k = _pick_tile_config(N, K, M, num_sms)
615
616
partials = X.new_empty(split_k, N, M, dtype=torch.float32)
442
- Sm100BF16x3RouterGemm.compile(BN, K)(X, W, partials, split_k)
617
+ Sm100BF16x3RouterGemm.compile(K, BN)(X, W, partials, split_k)
618
619
if split_k == 1:
620
return partials.squeeze(0)
@@ -447,3 +622,21 @@ def bf16x3_router_gemm(X: torch.Tensor, W: torch.Tensor) -> torch.Tensor:
622
out = X.new_empty(N, M, dtype=torch.float32)
623
splitk_reduce_triton(partials, out)
624
return out
625
+
626
+
627
+def _bf16x3_router_gemm_fake(
628
+ X: torch.Tensor,
629
+ W: torch.Tensor,
630
+) -> torch.Tensor:
631
+ return X.new_empty((X.shape[0], W.shape[0]), dtype=torch.float32)
632
+
633
+
634
+direct_register_custom_op(
635
+ op_name="bf16x3_router_gemm",
636
+ op_func=_bf16x3_router_gemm,
637
+ fake_impl=_bf16x3_router_gemm_fake,
638
+)
639
+
640
+
641
+def bf16x3_router_gemm(X: torch.Tensor, W: torch.Tensor) -> torch.Tensor:
642
+ return torch.ops.vllm.bf16x3_router_gemm(X, W)
vllm/model_executor/layers/fused_moe/router/gate_linear.py
+4
-16
@@ -4,15 +4,11 @@ import torch
4
from torch.nn.parameter import Parameter
5
6
import vllm._custom_ops as ops
7
-from vllm.config import get_current_vllm_config_or_none
8
-from vllm.logger import init_logger
7
from vllm.model_executor.custom_op import PluggableLayer
8
from vllm.model_executor.layers.linear import ReplicatedLinear
9
from vllm.platforms import current_platform
10
from vllm.utils.torch_utils import direct_register_custom_op
11
14
-logger = init_logger(__name__)
15
-
12
13
@PluggableLayer.register("gate_linear")
14
class GateLinear(ReplicatedLinear):
@@ -22,7 +18,7 @@ class GateLinear(ReplicatedLinear):
18
K divisible by 8)
19
2. fp32 specialized kernel (SM90+ or gfx950, bf16/fp32 in, fp32 out,
20
M<=32, model-specific shapes)
25
- 3. experimental bf16x3 CuteDSL kernel (opt-in, SM100, bf16 in, fp32 weight)
21
+ 3. bf16x3 CuteDSL kernel (SM100, bf16 in, fp32 weight)
22
4. cuBLAS bf16×bf16→fp32 (SM90+ + bf16 weight + fp32 out_dtype)
23
5. F.linear via ReplicatedLinear (ultimate fallback)
24
@@ -85,11 +81,6 @@ class GateLinear(ReplicatedLinear):
81
self.allow_specialized_router_gemm = can_use_specialized_kernels
82
83
# fp32 specialized kernel eligibility (exact dims, fp32 weight)
88
- vllm_config = get_current_vllm_config_or_none()
89
- enable_bf16x3_router_gemm = (
90
- vllm_config is not None
91
- and vllm_config.kernel_config.enable_bf16x3_router_gemm
92
- )
84
self.allow_fp32_router_gemm = (
85
not bias
86
and self.weight.dtype == torch.float32
@@ -108,10 +99,7 @@ class GateLinear(ReplicatedLinear):
99
and current_platform.is_cuda()
100
and is_blackwell
101
and input_size % 8 == 0
111
- and enable_bf16x3_router_gemm
102
)
113
- if self.allow_bf16x3_router_gemm:
114
- logger.info_once("Enabled experimental SM100 BF16x3 router GEMM.")
103
104
# Fused bf16 x bf16 -> fp32 GEMM eligibility. torch.mm's out_dtype
105
# epilogue folds the fp32 cast into the GEMM, removing the standalone
@@ -198,7 +186,7 @@ class GateLinear(ReplicatedLinear):
186
)
187
return output, None
188
201
- # Tier 3: experimental bf16x3 CuteDSL kernel for fp32 router weights
189
+ # Tier 3: bf16x3 CuteDSL kernel for fp32 router weights
190
if self.allow_bf16x3_router_gemm and x.dtype == torch.bfloat16:
191
from vllm.model_executor.layers.fused_moe.router.bf16x3_router_gemm_cutedsl import ( # noqa: E501
192
bf16x3_router_gemm,
@@ -251,10 +239,10 @@ def fp32_router_gemm_dispatch_impl(
239
240
if allow_bf16x3_router_gemm and x.dtype == torch.bfloat16:
241
from vllm.model_executor.layers.fused_moe.router.bf16x3_router_gemm_cutedsl import ( # noqa: E501
254
- bf16x3_router_gemm,
242
+ _bf16x3_router_gemm,
243
)
244
257
- return bf16x3_router_gemm(x, weight)
245
+ return _bf16x3_router_gemm(x, weight)
246
247
return torch.nn.functional.linear(x.float(), weight)
248
vllm/model_executor/warmup/kernel_warmup.py
+47
-2
@@ -101,6 +101,44 @@ def _warmup_ll_bf16_router_gemm(model: torch.nn.Module) -> None:
101
)
102
103
104
+def _warmup_bf16x3_router_gemm(
105
+ model: torch.nn.Module,
106
+ max_num_tokens: int,
107
+) -> None:
108
+ from vllm.model_executor.layers.fused_moe.router.bf16x3_router_gemm_cutedsl import ( # noqa: E501
109
+ warmup_bf16x3_router_gemm,
110
+ )
111
+ from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear
112
+
113
+ gate = next(
114
+ (
115
+ module
116
+ for module in model.modules()
117
+ if isinstance(module, GateLinear) and module.allow_bf16x3_router_gemm
118
+ ),
119
+ None,
120
+ )
121
+ if gate is None:
122
+ logger.debug_once(
123
+ "Skipping BF16x3 router GEMM warmup: no eligible GateLinear found."
124
+ )
125
+ return
126
+
127
+ min_num_tokens = gate.FP32_MAX_TOKENS + 1 if gate.allow_fp32_router_gemm else 1
128
+ logger.info_once(
129
+ "Warming up BF16x3 router GEMM for K=%d, M=%d.",
130
+ gate.input_size,
131
+ gate.output_size,
132
+ )
133
+ configs = warmup_bf16x3_router_gemm(
134
+ gate.input_size,
135
+ gate.output_size,
136
+ min_num_tokens,
137
+ max_num_tokens,
138
+ )
139
+ logger.info_once("Warmed up BF16x3 router GEMM configs: %s.", configs)
140
+
141
+
142
def _warmup_kimi_k3_gemm_rs_ar() -> None:
143
# Kimi-K3 model construction imports this module only when GEMM-RS/AR is
144
# enabled and initializes its singleton before kernel_warmup runs. Avoid
@@ -131,7 +169,8 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
169
if zeroer is not None:
170
zeroer.warmup(worker.model_runner.kv_cache_config.num_blocks)
171
134
- if worker.vllm_config.kernel_config.enable_jit_warmup:
172
+ enable_jit_warmup = worker.vllm_config.kernel_config.enable_jit_warmup
173
+ if enable_jit_warmup:
174
logger.info("JIT kernel warmup starting.")
175
jit_warmup_start = time.perf_counter()
176
try:
@@ -164,11 +203,17 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
203
)
204
205
# Run next so input-prep kernels JIT against pristine runner state.
167
- if worker.vllm_config.kernel_config.enable_jit_warmup:
206
+ if enable_jit_warmup:
207
kimi_k3_triton_warmup(worker)
208
spec_decode_rejection_warmup(worker)
209
qwen4_exp_qsa_triton_warmup(worker)
210
211
+ if enable_jit_warmup and current_platform.is_device_capability_family(100):
212
+ _warmup_bf16x3_router_gemm(
213
+ worker.get_model(),
214
+ worker.scheduler_config.max_num_batched_tokens,
215
+ )
216
+
217
if current_platform.has_device_capability(90):
218
_warmup_ll_bf16_router_gemm(worker.get_model())
219