[Perf][Feat] Add generic cuteDSL LL BF16 router (GEMM) (#42562)
Signed-off-by: LopezCastroRoberto <rocastro@redhat.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Signed-off-by: Roberto L. Castro <38211239+LopezCastroRoberto@users.noreply.github.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com>
Roberto L. Castro committed
Jul 14, 2026 at 08:01 UTC
0762f2afeb74f790e8c5ebe2b95a012eb38d499e
8 files changed
+1812
-8
.buildkite/test_areas/kernels.yaml
+5
@@ -232,6 +232,10 @@ steps:
232
- vllm/v1/attention/backends/mla/flashinfer_mla.py
233
- vllm/v1/attention/selector.py
234
- vllm/platforms/cuda.py
235
+ - vllm/model_executor/kernels/linear/cute_dsl/ll_bf16.py
236
+ - vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_dotprod.py
237
+ - vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_splitk.py
238
+ - tests/kernels/test_ll_bf16_gemm.py
239
- tests/kernels/test_top_k_per_row.py
240
commands:
241
- nvidia-smi
@@ -260,6 +264,7 @@ steps:
264
- pytest -v -s tests/kernels/moe/test_flashinfer_moe.py
265
- pytest -v -s tests/kernels/moe/test_trtllm_nvfp4_moe.py
266
- pytest -v -s tests/kernels/moe/test_cutedsl_moe.py
267
+ - pytest -v -s tests/kernels/test_ll_bf16_gemm.py
268
# e2e
269
- pytest -v -s tests/models/quantization/test_nvfp4.py
270
tests/kernels/test_ll_bf16_gemm.py
new
+553
@@ -0,0 +1,553 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Tests for cuteDSL low-latency router GEMM (dot-product + split-K)."""
4
+
5
+import pytest
6
+import torch
7
+import torch.nn.functional as F
8
+
9
+pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
10
+
11
+
12
+@pytest.fixture(autouse=True, scope="module")
13
+def _require_sm90_and_cutedsl():
14
+ if torch.cuda.get_device_capability()[0] < 9:
15
+ pytest.skip("Requires SM90+ (Hopper/Blackwell)")
16
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
17
+ is_available,
18
+ )
19
+
20
+ if not is_available():
21
+ pytest.skip("cuteDSL (CUTLASS Python) not installed")
22
+
23
+
24
+# ===== Helpers =====
25
+
26
+
27
+def _ref(a, b):
28
+ return torch.mm(a.float(), b.float().T)
29
+
30
+
31
+def _assert_close(out, ref, *, min_cos_sim=0.99, context=""):
32
+ assert out.device.type == "cuda", f"{context}: not on CUDA"
33
+ assert torch.isfinite(out).all(), f"{context}: NaN/Inf"
34
+ cos = F.cosine_similarity(
35
+ out.reshape(-1).float(), ref.reshape(-1).float(), dim=0
36
+ ).item()
37
+ assert cos > min_cos_sim, (
38
+ f"{context}: cos_sim {cos:.6f} < {min_cos_sim} "
39
+ f"(abs_err={(out.float() - ref.float()).abs().max().item():.2e})"
40
+ )
41
+
42
+
43
+def _can_precompile(a, b):
44
+ return (
45
+ a.dim() == 2
46
+ and b.dim() == 2
47
+ and a.dtype == torch.bfloat16
48
+ and b.dtype == torch.bfloat16
49
+ and a.device.type == "cuda"
50
+ and b.device.type == "cuda"
51
+ and a.device == b.device
52
+ and a.shape[1] == b.shape[1]
53
+ and a.is_contiguous()
54
+ and b.is_contiguous()
55
+ )
56
+
57
+
58
+def _gemm(a, b):
59
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
60
+ ll_bf16_gemm,
61
+ ll_bf16_gemm_kernel,
62
+ )
63
+
64
+ if _can_precompile(a, b):
65
+ compile_key = ll_bf16_gemm_kernel.dispatch(
66
+ M=a.shape[0], K=a.shape[1], N=b.shape[0]
67
+ )
68
+ ll_bf16_gemm_kernel.compile(compile_key)
69
+ return ll_bf16_gemm(a, b)
70
+
71
+
72
+# ===== Shapes =====
73
+
74
+SHAPES = [
75
+ (256, 7168, "DSV3"),
76
+ (256, 14400, "DSV4-Flash"),
77
+ (128, 5120, "DeepSeek-V2"),
78
+ (8, 4096, "Mixtral-8x7B"),
79
+ (64, 2880, "non-tile-aligned-K"),
80
+ (256, 2048, "split-K-boundary"),
81
+]
82
+
83
+SHAPES_SPLITK = [(n, k, d) for n, k, d in SHAPES if k >= 2048]
84
+
85
+
86
+# =================================================================
87
+# Dot-product kernel (M<=4 or K<2048)
88
+# =================================================================
89
+
90
+
91
+@pytest.mark.parametrize("M", [1, 2, 3, 4])
92
+@pytest.mark.parametrize("N,K,desc", SHAPES, ids=[s[2] for s in SHAPES])
93
+def test_dotprod(M, N, K, desc):
94
+ torch.manual_seed(42)
95
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
96
+ b = torch.randn(N, K, dtype=torch.bfloat16, device="cuda")
97
+ out = _gemm(a, b)
98
+ assert out.dtype == torch.float32
99
+ assert out.shape == (M, N)
100
+ _assert_close(out, _ref(a, b), context=f"dotprod {M}x{N}x{K}")
101
+
102
+
103
+@pytest.mark.parametrize("M", [1, 4, 8, 16])
104
+def test_dotprod_small_K(M):
105
+ """K<2048 forces dot-product regardless of M."""
106
+ torch.manual_seed(42)
107
+ a = torch.randn(M, 1024, dtype=torch.bfloat16, device="cuda")
108
+ b = torch.randn(64, 1024, dtype=torch.bfloat16, device="cuda")
109
+ _assert_close(_gemm(a, b), _ref(a, b), context=f"small_K M={M}")
110
+
111
+
112
+@pytest.mark.parametrize("K", [16, 32, 64, 128, 256, 512, 1024, 1536])
113
+def test_dotprod_K_sweep(K):
114
+ torch.manual_seed(42)
115
+ a = torch.randn(4, K, dtype=torch.bfloat16, device="cuda")
116
+ b = torch.randn(32, K, dtype=torch.bfloat16, device="cuda")
117
+ _assert_close(_gemm(a, b), _ref(a, b), context=f"K={K}")
118
+
119
+
120
+# =================================================================
121
+# Split-K kernel (M>4 and K>=2048)
122
+# =================================================================
123
+
124
+
125
+@pytest.mark.parametrize("M", [5, 6, 8, 12, 16])
126
+@pytest.mark.parametrize("N,K,desc", SHAPES_SPLITK, ids=[s[2] for s in SHAPES_SPLITK])
127
+def test_splitk(M, N, K, desc):
128
+ torch.manual_seed(42)
129
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
130
+ b = torch.randn(N, K, dtype=torch.bfloat16, device="cuda")
131
+ out = _gemm(a, b)
132
+ assert out.dtype == torch.float32
133
+ assert out.shape == (M, N)
134
+ _assert_close(out, _ref(a, b), context=f"splitk {M}x{N}x{K}")
135
+
136
+
137
+@pytest.mark.parametrize("K", [2048, 2304, 2880, 3072, 4096, 5120, 7168, 14400])
138
+def test_splitk_K_sweep(K):
139
+ """Includes non-tile-aligned K (2880) and uneven split (2304)."""
140
+ torch.manual_seed(42)
141
+ a = torch.randn(8, K, dtype=torch.bfloat16, device="cuda")
142
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda")
143
+ _assert_close(_gemm(a, b), _ref(a, b), context=f"splitk K={K}")
144
+
145
+
146
+# =================================================================
147
+# Dispatch boundary (M=4/5, K=2032/2048)
148
+# =================================================================
149
+
150
+
151
+@pytest.mark.parametrize("M", [4, 5])
152
+@pytest.mark.parametrize("K", [2032, 2048])
153
+def test_dispatch_boundary(M, K):
154
+ torch.manual_seed(42)
155
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
156
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda")
157
+ path = "splitk" if M > 4 and K >= 2048 else "dotprod"
158
+ _assert_close(_gemm(a, b), _ref(a, b), context=f"M={M} K={K} ({path})")
159
+
160
+
161
+# =================================================================
162
+# Arbitrary N
163
+# =================================================================
164
+
165
+
166
+@pytest.mark.parametrize("N", [1, 3, 7, 16, 17, 64, 128, 256, 384])
167
+def test_arbitrary_N_dotprod(N):
168
+ torch.manual_seed(42)
169
+ a = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
170
+ b = torch.randn(N, 2048, dtype=torch.bfloat16, device="cuda")
171
+ out = _gemm(a, b)
172
+ assert out.shape == (4, N)
173
+ _assert_close(out, _ref(a, b), context=f"dotprod N={N}")
174
+
175
+
176
+@pytest.mark.parametrize("N", [1, 8, 16, 17, 64, 128, 256])
177
+def test_arbitrary_N_splitk(N):
178
+ torch.manual_seed(42)
179
+ a = torch.randn(8, 4096, dtype=torch.bfloat16, device="cuda")
180
+ b = torch.randn(N, 4096, dtype=torch.bfloat16, device="cuda")
181
+ out = _gemm(a, b)
182
+ assert out.shape == (8, N)
183
+ _assert_close(out, _ref(a, b), context=f"splitk N={N}")
184
+
185
+
186
+# =================================================================
187
+# Single token (M=1, decode path)
188
+# =================================================================
189
+
190
+
191
+@pytest.mark.parametrize(
192
+ "N,K",
193
+ [(256, 7168), (256, 14400), (8, 4096), (384, 7168)],
194
+ ids=["DSV3", "DSV4-Flash", "Mixtral", "DSV4-Pro"],
195
+)
196
+def test_single_token(N, K):
197
+ torch.manual_seed(42)
198
+ a = torch.randn(1, K, dtype=torch.bfloat16, device="cuda")
199
+ b = torch.randn(N, K, dtype=torch.bfloat16, device="cuda")
200
+ out = _gemm(a, b)
201
+ assert out.shape == (1, N)
202
+ _assert_close(out, _ref(a, b), context=f"M=1 {N}x{K}")
203
+
204
+
205
+# =================================================================
206
+# Numerical robustness
207
+# =================================================================
208
+
209
+
210
+@pytest.mark.parametrize("M,K", [(4, 2048), (8, 4096)], ids=["dotprod", "splitk"])
211
+def test_large_values(M, K):
212
+ torch.manual_seed(42)
213
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda") * 100
214
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda") * 100
215
+ out = _gemm(a, b)
216
+ assert torch.isfinite(out).all()
217
+ _assert_close(out, _ref(a, b), context=f"large M={M}")
218
+
219
+
220
+def test_near_zero():
221
+ torch.manual_seed(42)
222
+ a = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda") * 1e-4
223
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda") * 1e-4
224
+ out = _gemm(a, b)
225
+ assert torch.isfinite(out).all()
226
+ assert out.abs().max() < 1.0
227
+
228
+
229
+def test_zeros():
230
+ a = torch.zeros(4, 2048, dtype=torch.bfloat16, device="cuda")
231
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda")
232
+ assert (_gemm(a, b) == 0).all()
233
+
234
+
235
+def test_ones():
236
+ a = torch.ones(1, 2048, dtype=torch.bfloat16, device="cuda")
237
+ b = torch.randn(32, 2048, dtype=torch.bfloat16, device="cuda")
238
+ _assert_close(_gemm(a, b), _ref(a, b), context="ones")
239
+
240
+
241
+# =================================================================
242
+# Output dtype
243
+# =================================================================
244
+
245
+
246
+@pytest.mark.parametrize("M,K", [(4, 2048), (8, 4096)], ids=["dotprod", "splitk"])
247
+def test_output_fp32(M, K):
248
+ torch.manual_seed(42)
249
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
250
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda")
251
+ assert _gemm(a, b).dtype == torch.float32
252
+
253
+
254
+# =================================================================
255
+# Determinism
256
+# =================================================================
257
+
258
+
259
+@pytest.mark.parametrize(
260
+ "M,K", [(1, 4096), (4, 4096), (5, 4096), (8, 4096), (16, 4096)]
261
+)
262
+def test_deterministic(M, K):
263
+ torch.manual_seed(42)
264
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
265
+ b = torch.randn(128, K, dtype=torch.bfloat16, device="cuda")
266
+ torch.testing.assert_close(_gemm(a, b), _gemm(a, b), atol=0, rtol=0)
267
+
268
+
269
+# =================================================================
270
+# Cross-kernel consistency
271
+# =================================================================
272
+
273
+
274
+@pytest.mark.parametrize("K", [2048, 4096, 7168])
275
+def test_dotprod_vs_splitk(K):
276
+ """First 4 rows from split-K (M=5) match dot-product (M=4)."""
277
+ torch.manual_seed(42)
278
+ a = torch.randn(5, K, dtype=torch.bfloat16, device="cuda")
279
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda")
280
+ _assert_close(
281
+ _gemm(a, b)[:4],
282
+ _gemm(a[:4], b),
283
+ min_cos_sim=0.999,
284
+ context=f"cross-kernel K={K}",
285
+ )
286
+
287
+
288
+# =================================================================
289
+# CUDA graph
290
+# =================================================================
291
+
292
+
293
+@pytest.mark.parametrize("M,K", [(4, 2048), (8, 4096)], ids=["dotprod", "splitk"])
294
+def test_cudagraph(M, K):
295
+ torch.manual_seed(42)
296
+ a = torch.randn(M, K, dtype=torch.bfloat16, device="cuda")
297
+ b = torch.randn(64, K, dtype=torch.bfloat16, device="cuda")
298
+ _gemm(a, b)
299
+ torch.accelerator.synchronize()
300
+
301
+ g = torch.cuda.CUDAGraph()
302
+ with torch.cuda.graph(g):
303
+ out = _gemm(a, b)
304
+ for _ in range(5):
305
+ g.replay()
306
+ torch.accelerator.synchronize()
307
+ _assert_close(out, _ref(a, b), context=f"cudagraph M={M}")
308
+
309
+
310
+def test_cudagraph_20x_replay():
311
+ torch.manual_seed(42)
312
+ a = torch.randn(4, 4096, dtype=torch.bfloat16, device="cuda")
313
+ b = torch.randn(256, 4096, dtype=torch.bfloat16, device="cuda")
314
+ _gemm(a, b)
315
+ torch.accelerator.synchronize()
316
+
317
+ g = torch.cuda.CUDAGraph()
318
+ with torch.cuda.graph(g):
319
+ out = _gemm(a, b)
320
+ results = []
321
+ for _ in range(20):
322
+ g.replay()
323
+ torch.accelerator.synchronize()
324
+ results.append(out.clone())
325
+ for i in range(1, len(results)):
326
+ torch.testing.assert_close(
327
+ results[0], results[i], atol=0, rtol=0, msg=f"Replay {i} differs"
328
+ )
329
+
330
+
331
+def test_cudagraph_input_update():
332
+ torch.manual_seed(42)
333
+ a = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
334
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda")
335
+ _gemm(a, b)
336
+ torch.accelerator.synchronize()
337
+
338
+ g = torch.cuda.CUDAGraph()
339
+ with torch.cuda.graph(g):
340
+ out = _gemm(a, b)
341
+ a.copy_(torch.randn_like(a))
342
+ g.replay()
343
+ torch.accelerator.synchronize()
344
+ _assert_close(out, _ref(a, b), context="cudagraph input update")
345
+
346
+
347
+# =================================================================
348
+# GateLinear dispatch integration
349
+# =================================================================
350
+
351
+
352
+def _make_gate_linear(monkeypatch, *, params_dtype, out_dtype=torch.float32):
353
+ monkeypatch.setattr(
354
+ "vllm.model_executor.layers.linear.get_tensor_model_parallel_rank",
355
+ lambda: 0,
356
+ )
357
+ monkeypatch.setattr(
358
+ "vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size",
359
+ lambda: 1,
360
+ )
361
+ monkeypatch.setattr(
362
+ "vllm.model_executor.parameter.get_tensor_model_parallel_rank",
363
+ lambda: 0,
364
+ )
365
+ monkeypatch.setattr(
366
+ "vllm.model_executor.parameter.get_tensor_model_parallel_world_size",
367
+ lambda: 1,
368
+ )
369
+
370
+ from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear
371
+
372
+ monkeypatch.setattr(
373
+ "vllm.model_executor.kernels.linear.cute_dsl.ll_bf16.is_available",
374
+ lambda: True,
375
+ )
376
+ return GateLinear(
377
+ input_size=2048,
378
+ output_size=64,
379
+ bias=False,
380
+ out_dtype=out_dtype,
381
+ params_dtype=params_dtype,
382
+ ).cuda()
383
+
384
+
385
+def test_gate_linear_uses_ll_bf16_for_bf16_fast_path(monkeypatch):
386
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.bfloat16)
387
+ x = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
388
+ calls = []
389
+
390
+ def fake_ll_bf16_gemm(hidden_states, router_weight):
391
+ calls.append((hidden_states, router_weight))
392
+ return torch.full(
393
+ (hidden_states.shape[0], router_weight.shape[0]),
394
+ 1.0,
395
+ dtype=torch.float32,
396
+ device=hidden_states.device,
397
+ )
398
+
399
+ monkeypatch.setattr(
400
+ "vllm.model_executor.kernels.linear.cute_dsl.ll_bf16.ll_bf16_gemm",
401
+ fake_ll_bf16_gemm,
402
+ )
403
+ out, bias = gate(x)
404
+ assert bias is None
405
+ assert out.shape == (4, 64)
406
+ assert out.dtype == torch.float32
407
+ assert len(calls) == 1
408
+ assert calls[0][0] is x
409
+ assert calls[0][1] is gate.weight
410
+
411
+
412
+def test_gate_linear_fp32_weight_falls_back(monkeypatch):
413
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.float32)
414
+ assert not gate.allow_ll_bf16_gemm
415
+ x = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
416
+
417
+ def fail_ll_bf16_gemm(hidden_states, router_weight):
418
+ raise AssertionError("ll_bf16_gemm should not run for fp32 weights")
419
+
420
+ monkeypatch.setattr(
421
+ "vllm.model_executor.kernels.linear.cute_dsl.ll_bf16.ll_bf16_gemm",
422
+ fail_ll_bf16_gemm,
423
+ )
424
+ out, _ = gate(x)
425
+ assert out.shape == (4, 64)
426
+ assert out.dtype == torch.float32
427
+
428
+
429
+def test_gate_linear_non_bf16_activation_falls_back(monkeypatch):
430
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.bfloat16)
431
+ x = torch.randn(4, 2048, dtype=torch.float16, device="cuda")
432
+
433
+ def fail_ll_bf16_gemm(hidden_states, router_weight):
434
+ raise AssertionError("ll_bf16_gemm should not run for non-bf16 activations")
435
+
436
+ monkeypatch.setattr(
437
+ "vllm.model_executor.kernels.linear.cute_dsl.ll_bf16.ll_bf16_gemm",
438
+ fail_ll_bf16_gemm,
439
+ )
440
+ out, _ = gate(x)
441
+ assert out.shape == (4, 64)
442
+ assert out.dtype == torch.float32
443
+
444
+
445
+def test_gate_linear_set_out_dtype_enables_ll_bf16(monkeypatch):
446
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.bfloat16, out_dtype=None)
447
+ assert not gate.allow_ll_bf16_gemm
448
+ gate.set_out_dtype(torch.float32)
449
+ assert gate.allow_ll_bf16_gemm
450
+
451
+
452
+def test_gate_linear_non_fp32_out_dtype_disables_ll_bf16(monkeypatch):
453
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.bfloat16, out_dtype=None)
454
+ gate.set_out_dtype(torch.bfloat16)
455
+ assert not gate.allow_ll_bf16_gemm
456
+
457
+
458
+def test_gate_linear_m_gt_16_falls_back(monkeypatch):
459
+ gate = _make_gate_linear(monkeypatch, params_dtype=torch.bfloat16)
460
+ x = torch.randn(17, 2048, dtype=torch.bfloat16, device="cuda")
461
+
462
+ def fail_ll_bf16_gemm(hidden_states, router_weight):
463
+ raise AssertionError("ll_bf16_gemm should not run for M > 16")
464
+
465
+ monkeypatch.setattr(
466
+ "vllm.model_executor.kernels.linear.cute_dsl.ll_bf16.ll_bf16_gemm",
467
+ fail_ll_bf16_gemm,
468
+ )
469
+ out, _ = gate(x)
470
+ assert out.shape == (17, 64)
471
+ assert out.dtype == torch.float32
472
+
473
+
474
+# =================================================================
475
+# Negative tests — invalid inputs
476
+# =================================================================
477
+
478
+
479
+@pytest.mark.parametrize(
480
+ "M,K,N,dtype",
481
+ [
482
+ pytest.param(4, 2048, 64, torch.float32, id="fp32_input"),
483
+ pytest.param(4, 2048, 64, torch.float16, id="fp16_input"),
484
+ pytest.param(8, 4096, 64, torch.float32, id="fp32_splitk_path"),
485
+ ],
486
+)
487
+def test_invalid_dtype(M, K, N, dtype):
488
+ a = torch.randn(M, K, device="cuda", dtype=dtype)
489
+ b = torch.randn(N, K, device="cuda", dtype=dtype)
490
+ with pytest.raises(ValueError, match="dtype=bfloat16"):
491
+ _gemm(a, b)
492
+
493
+
494
+def test_invalid_device_cpu():
495
+ a = torch.randn(4, 2048, dtype=torch.bfloat16)
496
+ b = torch.randn(64, 2048, dtype=torch.bfloat16)
497
+ with pytest.raises(ValueError, match="device_type=cuda"):
498
+ _gemm(a, b)
499
+
500
+
501
+def test_invalid_1d_input():
502
+ a = torch.randn(2048, dtype=torch.bfloat16, device="cuda")
503
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda")
504
+ with pytest.raises(ValueError, match="2D tensors"):
505
+ _gemm(a, b)
506
+
507
+
508
+def test_mismatched_K():
509
+ a = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
510
+ b = torch.randn(64, 1024, dtype=torch.bfloat16, device="cuda")
511
+ with pytest.raises(ValueError, match="matching K dimensions"):
512
+ _gemm(a, b)
513
+
514
+
515
+def test_invalid_K_divisibility():
516
+ a = torch.randn(4, 2049, dtype=torch.bfloat16, device="cuda")
517
+ b = torch.randn(64, 2049, dtype=torch.bfloat16, device="cuda")
518
+
519
+ with pytest.raises(ValueError, match="K to be divisible by 8"):
520
+ _gemm(a, b)
521
+
522
+
523
+def test_non_contiguous_input():
524
+ a = torch.randn(2048, 4, dtype=torch.bfloat16, device="cuda").T
525
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda")
526
+ assert not a.is_contiguous()
527
+ with pytest.raises(ValueError, match="contiguous row-major"):
528
+ _gemm(a, b)
529
+
530
+
531
+def test_invalid_output_dtype():
532
+ a = torch.randn(4, 2048, dtype=torch.bfloat16, device="cuda")
533
+ b = torch.randn(64, 2048, dtype=torch.bfloat16, device="cuda")
534
+ with pytest.raises(ValueError, match="output_dtype=torch.float32"):
535
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import ll_bf16_gemm
536
+
537
+ ll_bf16_gemm(a, b, output_dtype=torch.bfloat16)
538
+
539
+
540
+def test_cache_miss_compiles_dotprod():
541
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import LLBf16Gemm
542
+
543
+ torch.manual_seed(42)
544
+ a = torch.randn(3, 64, dtype=torch.bfloat16, device="cuda")
545
+ b = torch.randn(17, 64, dtype=torch.bfloat16, device="cuda")
546
+ kernel = LLBf16Gemm()
547
+ out = kernel(a, b)
548
+ assert out.shape == (3, 17)
549
+ _assert_close(out, _ref(a, b), context="cache miss dotprod")
550
+
551
+
552
+if __name__ == "__main__":
553
+ pytest.main([__file__, "-v"])
vllm/model_executor/kernels/linear/cute_dsl/__init__.py
vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_dotprod.py
new
+313
@@ -0,0 +1,313 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import cutlass
5
+import cutlass.cute as cute
6
+from cuda.bindings.driver import CUstream
7
+from cutlass import const_expr
8
+
9
+
10
+class LLBf16Dotprod:
11
+ """BF16 router GEMM kernel based on CTA-local dot products.
12
+
13
+ This kernel computes C[M, N] = A[M, K] @ B[N, K]^T for bf16 inputs
14
+ and fp32 output. It launches one CTA per output column, distributes K
15
+ across CTA threads with vectorized loads, accumulates one fp32 dot product
16
+ per token, and reduces through warp shuffles plus shared memory.
17
+
18
+ :param k: Compile-time K dimension specialized into the generated kernel.
19
+ :type k: int
20
+ :param bs: Threads per CTA and K-stripe width used by the reduction.
21
+ :type bs: int
22
+ :param main_vec_width: bf16 elements loaded per thread in the main loop.
23
+ :type main_vec_width: int
24
+ :param tail_vec_width: bf16 elements loaded per thread in the vector tail.
25
+ :type tail_vec_width: int
26
+ :param use_pdl: Whether to launch with Programmatic Dependent Launch.
27
+ :type use_pdl: bool
28
+
29
+ :note: Supported A/B data types:
30
+ - BFloat16/BFloat16
31
+ :note: Supported accumulator data types:
32
+ - Float32
33
+ :note: Supported C data types:
34
+ - Float32
35
+ :note: Constraints:
36
+ - K must preserve 16-byte row alignment for contiguous bf16 inputs.
37
+
38
+ :note: K is handled as vectorized main/tail loops plus scalar remainder.
39
+
40
+ :compile-key: ``(M, K, bs)`` selects the token count, hidden size,
41
+ and CTA thread/K-stripe width specialization.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ k: int,
47
+ bs: int = 128,
48
+ main_vec_width: int = 8,
49
+ tail_vec_width: int = 4,
50
+ use_pdl: bool = False,
51
+ ):
52
+ """Initialize the dot-product kernel configuration.
53
+
54
+ This configuration fixes the CTA thread count, reduction warp count,
55
+ bf16 vector widths, and K-loop decomposition used by the generated
56
+ kernel.
57
+
58
+ :param k: Hidden size K used to specialize the K-loop decomposition.
59
+ :type k: int
60
+ :param bs: Threads per CTA and K-stripe width for the reduction.
61
+ :type bs: int
62
+ :param main_vec_width: BF16 elements loaded per thread in the main loop.
63
+ :type main_vec_width: int
64
+ :param tail_vec_width: BF16 elements loaded per thread in the vector tail.
65
+ :type tail_vec_width: int
66
+ :param use_pdl: Whether to launch with Programmatic Dependent Launch.
67
+ :type use_pdl: bool
68
+ """
69
+ self.bs = bs
70
+ self.main_vec_width = main_vec_width
71
+ self.tail_vec_width = tail_vec_width
72
+ self.use_pdl = use_pdl
73
+ self.num_warps = bs // cute.arch.WARP_SIZE
74
+ self._init_k_tiles(k)
75
+
76
+ def _vectorized_elems(self, k_extent: int, vec_width: int) -> int:
77
+ vector_tile = vec_width * self.bs
78
+ return (k_extent // vector_tile) * vector_tile
79
+
80
+ def _init_k_tiles(self, k: int) -> None:
81
+ """Split K into vector loops, scalar rounds, and ragged tail."""
82
+ self.k_main_elems = self._vectorized_elems(k, self.main_vec_width)
83
+ self.k_after_main = k - self.k_main_elems
84
+ self.k_tail_elems = self._vectorized_elems(
85
+ self.k_after_main, self.tail_vec_width
86
+ )
87
+ self.k_done_all = self.k_main_elems + self.k_tail_elems
88
+ self.scalar_rem = k - self.k_done_all
89
+ self.ks_full = self.scalar_rem // self.bs
90
+ self.ks_part = self.scalar_rem % self.bs
91
+ self.k_scalar_full = self.ks_full * self.bs
92
+ self.k_part_offset = self.k_done_all + self.k_scalar_full
93
+ self.main_tiles = self.k_main_elems // (self.main_vec_width * self.bs)
94
+ self.tail_tiles = self.k_tail_elems // (self.tail_vec_width * self.bs)
95
+
96
+ @cute.jit
97
+ def _vector_dotprod(
98
+ self,
99
+ acc: cute.Tensor,
100
+ tA: cute.Tensor,
101
+ tB: cute.Tensor,
102
+ M: cutlass.Constexpr,
103
+ num_tiles: cutlass.Constexpr,
104
+ align_bytes: cutlass.Constexpr,
105
+ ):
106
+ for tile in cutlass.range_constexpr(num_tiles):
107
+ bt = tB[None, tile]
108
+ br = cute.make_rmem_tensor_like(bt)
109
+ cute.autovec_copy(bt, br)
110
+ br_f32 = br.load().to(cutlass.Float32)
111
+
112
+ for m in cutlass.range_constexpr(M):
113
+ at = tA[m, None, tile]
114
+ ar = cute.make_rmem_tensor_like(at)
115
+ cute.autovec_copy(at, ar)
116
+ vec_width: cutlass.Constexpr = cute.size(ar)
117
+ for v in cutlass.range_constexpr(vec_width):
118
+ acc[m] = acc[m] + ar[v].to(cutlass.Float32) * br_f32[v]
119
+
120
+ def _make_thread_vector_slice(
121
+ self,
122
+ gA_vec: cute.Tensor,
123
+ gB_vec: cute.Tensor,
124
+ tidx: cutlass.Int32,
125
+ n_idx: cutlass.Int32,
126
+ bs: cutlass.Constexpr,
127
+ ):
128
+ # (M/N, K_TILE, K_LANE, K_VEC); tidx selects K_LANE.
129
+ tA = cute.logical_divide(gA_vec, (None, (None, bs)))
130
+ tB = cute.logical_divide(gB_vec, (None, (None, bs)))
131
+ return tA[None, (None, (tidx, None))], tB[n_idx, (None, (tidx, None))]
132
+
133
+ def _make_k_slice(
134
+ self,
135
+ gX: cute.Tensor,
136
+ k_offset: cutlass.Constexpr,
137
+ k_extent: cutlass.Constexpr,
138
+ ):
139
+ k_layout_extent: cutlass.Constexpr = (
140
+ 1 if const_expr(k_extent == 0) else k_extent
141
+ )
142
+
143
+ if const_expr(k_offset == 0):
144
+ return cute.local_tile(
145
+ gX, (cute.size(gX, mode=[0]), k_layout_extent), (0, 0)
146
+ )
147
+ return cute.local_tile(
148
+ cute.domain_offset((0, k_offset), gX),
149
+ (cute.size(gX, mode=[0]), k_layout_extent),
150
+ (0, 0),
151
+ )
152
+
153
+ @cute.jit
154
+ def __call__(
155
+ self,
156
+ gA: cute.Tensor,
157
+ gB: cute.Tensor,
158
+ gC: cute.Tensor,
159
+ M: cutlass.Constexpr,
160
+ K_dim: cutlass.Constexpr,
161
+ N_dim: cutlass.Int32,
162
+ stream: CUstream,
163
+ ):
164
+ """Execute the dot-product GEMM operation in steps:
165
+ - Launch one CTA per output column ``n`` with ``bs`` threads.
166
+ - Keep one FP32 accumulator per token ``m`` in each thread.
167
+ - Traverse K with vectorized 128-bit, vectorized 64-bit, scalar, and
168
+ ragged-tail loops from the precomputed K decomposition.
169
+ - Reduce each token accumulator first within the warp, then across
170
+ warps through shared memory, and store ``C[:, n]``.
171
+
172
+ :param gA: Input tensor A with shape ``[M, K]``.
173
+ :type gA: cute.Tensor
174
+ :param gB: Input tensor B with shape ``[N, K]``.
175
+ :type gB: cute.Tensor
176
+ :param gC: Output tensor C with shape ``[M, N]``.
177
+ :type gC: cute.Tensor
178
+ :param M: Token count selected by the compile key.
179
+ :type M: cutlass.Constexpr
180
+ :param K_dim: Hidden size selected by the compile key.
181
+ :type K_dim: cutlass.Constexpr
182
+ :param N_dim: Output column count used for the launch grid.
183
+ :type N_dim: cutlass.Int32
184
+ :param stream: CUDA stream for asynchronous execution.
185
+ :type stream: CUstream
186
+ """
187
+ self.kernel(
188
+ gA,
189
+ gB,
190
+ gC,
191
+ M,
192
+ self.main_vec_width,
193
+ self.tail_vec_width,
194
+ self.bs,
195
+ self.num_warps,
196
+ self.k_main_elems,
197
+ self.k_tail_elems,
198
+ self.k_done_all,
199
+ self.ks_full,
200
+ self.ks_part,
201
+ self.k_scalar_full,
202
+ self.k_part_offset,
203
+ self.main_tiles,
204
+ self.tail_tiles,
205
+ ).launch(
206
+ grid=[N_dim, 1, 1],
207
+ block=[self.bs, 1, 1],
208
+ smem=M * 4 * self.num_warps,
209
+ stream=stream,
210
+ use_pdl=self.use_pdl,
211
+ min_blocks_per_mp=1,
212
+ )
213
+
214
+ @cute.kernel
215
+ def kernel(
216
+ self,
217
+ gA: cute.Tensor,
218
+ gB: cute.Tensor,
219
+ gC: cute.Tensor,
220
+ M: cutlass.Constexpr,
221
+ main_vec_width: cutlass.Constexpr,
222
+ tail_vec_width: cutlass.Constexpr,
223
+ bs: cutlass.Constexpr,
224
+ num_warps: cutlass.Constexpr,
225
+ k_main_elems: cutlass.Constexpr,
226
+ k_tail_elems: cutlass.Constexpr,
227
+ k_done_all: cutlass.Constexpr,
228
+ ks_full: cutlass.Constexpr,
229
+ ks_part: cutlass.Constexpr,
230
+ k_scalar_full: cutlass.Constexpr,
231
+ k_part_offset: cutlass.Constexpr,
232
+ main_tiles: cutlass.Constexpr,
233
+ tail_tiles: cutlass.Constexpr,
234
+ ):
235
+ tidx, _, _ = cute.arch.thread_idx()
236
+ n_idx, _, _ = cute.arch.block_idx()
237
+ wid = cute.arch.warp_idx()
238
+
239
+ # One FP32 accumulator per token.
240
+ acc = cute.make_rmem_tensor((M,), cutlass.Float32)
241
+ acc.fill(0.0)
242
+
243
+ if const_expr(self.use_pdl):
244
+ cute.arch.griddepcontrol_wait()
245
+
246
+ # 128-bit vectorized main loop
247
+ if const_expr(k_main_elems > 0):
248
+ gA_main = self._make_k_slice(gA, 0, k_main_elems)
249
+ gB_main = self._make_k_slice(gB, 0, k_main_elems)
250
+ gA_vec = cute.logical_divide(gA_main, (None, main_vec_width))
251
+ gB_vec = cute.logical_divide(gB_main, (None, main_vec_width))
252
+ tA, tB = self._make_thread_vector_slice(gA_vec, gB_vec, tidx, n_idx, bs)
253
+ self._vector_dotprod(acc, tA, tB, M, main_tiles, 16)
254
+
255
+ # 64-bit vectorized tail (K remainder after main loop)
256
+ if const_expr(k_tail_elems > 0):
257
+ gA_tail = self._make_k_slice(gA, k_main_elems, k_tail_elems)
258
+ gB_tail = self._make_k_slice(gB, k_main_elems, k_tail_elems)
259
+ gA_tail_vec = cute.logical_divide(gA_tail, (None, tail_vec_width))
260
+ gB_tail_vec = cute.logical_divide(gB_tail, (None, tail_vec_width))
261
+ tA_t, tB_t = self._make_thread_vector_slice(
262
+ gA_tail_vec, gB_tail_vec, tidx, n_idx, bs
263
+ )
264
+ self._vector_dotprod(acc, tA_t, tB_t, M, tail_tiles, 8)
265
+
266
+ # Full scalar rounds use CuTe width-1 tiles; KS_PART is the ragged tail.
267
+ if const_expr(ks_full > 0):
268
+ gA_scalar = self._make_k_slice(gA, k_done_all, k_scalar_full)
269
+ gB_scalar = self._make_k_slice(gB, k_done_all, k_scalar_full)
270
+ gA_scalar_vec = cute.logical_divide(gA_scalar, (None, 1))
271
+ gB_scalar_vec = cute.logical_divide(gB_scalar, (None, 1))
272
+ tA_s, tB_s = self._make_thread_vector_slice(
273
+ gA_scalar_vec, gB_scalar_vec, tidx, n_idx, bs
274
+ )
275
+ self._vector_dotprod(acc, tA_s, tB_s, M, ks_full, 2)
276
+
277
+ # Only threads below KS_PART load the ragged tail.
278
+ if const_expr(ks_part > 0):
279
+ gA_part = self._make_k_slice(gA, k_part_offset, ks_part)
280
+ gB_part = self._make_k_slice(gB, k_part_offset, ks_part)
281
+ if tidx < ks_part:
282
+ bv2 = gB_part[n_idx, tidx].to(cutlass.Float32)
283
+ for m in cutlass.range_constexpr(M):
284
+ acc[m] = acc[m] + gA_part[m, tidx].to(cutlass.Float32) * bv2
285
+
286
+ # Intra-warp shuffle reduction
287
+ for m in cutlass.range_constexpr(M):
288
+ acc[m] = cute.arch.warp_reduction_sum(acc[m])
289
+
290
+ # Cross-warp reduction via shared memory
291
+ smem_red_layout = cute.make_layout((M, num_warps), stride=(num_warps, 1))
292
+ smem = cutlass.utils.SmemAllocator()
293
+ sm = smem.allocate_tensor(cutlass.Float32, smem_red_layout, byte_alignment=16)
294
+ with cute.arch.elect_one():
295
+ for m in cutlass.range_constexpr(M):
296
+ sm[m, wid] = acc[m]
297
+
298
+ # Final reduction and output
299
+ cute.arch.sync_threads()
300
+ if tidx == 0:
301
+ for m in cutlass.range_constexpr(M):
302
+ partials = sm[m, None].load()
303
+ gC[m, n_idx] = partials.reduce(
304
+ cute.ReductionOp.ADD,
305
+ init_val=cutlass.Float32(0.0),
306
+ reduction_profile=0,
307
+ )
308
+ if const_expr(self.use_pdl):
309
+ cute.arch.griddepcontrol_launch_dependents()
310
+
311
+
312
+def make_host_bf16(k_val: int, bs: int = 128):
313
+ return LLBf16Dotprod(k=k_val, bs=bs)
vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_splitk.py
new
+585
@@ -0,0 +1,585 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import math
5
+
6
+import cutlass
7
+import cutlass.cute as cute
8
+from cuda.bindings.driver import CUstream
9
+from cutlass import const_expr
10
+from cutlass._mlir import ir as _ir
11
+from cutlass._mlir.dialects import llvm as _llvm
12
+from cutlass.cutlass_dsl import dsl_user_op
13
+from cutlass.pipeline import sm90 as pipeline
14
+
15
+
16
+# Map a local smem address to the same peer CTA DSMEM offset.
17
+@dsl_user_op
18
+def set_block_rank(smem_ptr, peer_rank, *, loc=None, ip=None):
19
+ dsmem_ptr = cute.arch.map_dsmem_ptr(smem_ptr, peer_rank, loc=loc, ip=ip)
20
+ return cutlass.Int32(dsmem_ptr.toint(loc=loc, ip=ip))
21
+
22
+
23
+# Plain DSMEM store; mbarrier helpers do not model this reduction.
24
+@dsl_user_op
25
+def st_shared_remote_f32(remote_addr, val, *, loc=None, ip=None):
26
+ i32 = _ir.IntegerType.get_signless(32)
27
+ addr_ir = remote_addr.ir_value(loc=loc, ip=ip)
28
+ val_ir = val.ir_value(loc=loc, ip=ip)
29
+ _llvm.inline_asm(
30
+ i32,
31
+ [addr_ir, val_ir],
32
+ "st.shared::cluster.f32 [$0], $1; mov.u32 $2, 0;",
33
+ "r,f,=r",
34
+ has_side_effects=True, # keep the inline store ordered
35
+ loc=loc,
36
+ ip=ip,
37
+ )
38
+
39
+
40
+class LLBf16SplitK:
41
+ """BF16 router GEMM kernel based on clustered split-K MMA.
42
+
43
+ This kernel computes C[M, N] = A[M, K] @ B[N, K]^T for bf16 inputs
44
+ and fp32 output. It partitions K across a CTA cluster, uses DMA warps to
45
+ stage A/B tiles with cp.async, uses MMA warps to accumulate fp32 partials,
46
+ and reduces split-K partials through DSMEM before storing C.
47
+
48
+ :param ab_dtype: Element type for A and B operands.
49
+ :param acc_dtype: Accumulator type used by MMA and reductions.
50
+ :param out_dtype: Output element type. The public wrapper uses fp32.
51
+ :param tile_n: CTA tile size in N. M is fixed to 16 for router batches.
52
+ :type tile_n: int
53
+ :param tile_k: K tile size staged through shared memory.
54
+ :type tile_k: int
55
+ :param num_stages: Number of cp.async pipeline stages.
56
+ :type num_stages: int
57
+ :param num_dma_warps: Producer warps that issue GMEM to SMEM copies.
58
+ :type num_dma_warps: int
59
+ :param split_k: Number of CTAs in the cluster-level split-K reduction.
60
+ :type split_k: int
61
+ :param use_pdl: Whether to launch with Programmatic Dependent Launch.
62
+ :type use_pdl: bool
63
+
64
+ :note: Supported A/B data types:
65
+ - BFloat16/BFloat16
66
+ :note: Supported accumulator data types:
67
+ - Float32
68
+ :note: Supported C data types:
69
+ - Float32
70
+ :note: Constraints:
71
+ - K must preserve 16-byte row alignment for contiguous bf16 inputs.
72
+
73
+ :compile-key: ``(split_k, num_stages)`` selects the cluster split
74
+ count and cp.async pipeline depth specialization.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ ab_dtype=cutlass.BFloat16,
80
+ acc_dtype=cutlass.Float32,
81
+ out_dtype=cutlass.Float32,
82
+ tile_n: int = 16,
83
+ tile_k: int = 256,
84
+ num_stages: int = 2,
85
+ num_dma_warps: int = 4,
86
+ split_k: int = 8,
87
+ use_pdl: bool = False,
88
+ ):
89
+ """Initialize the split-K kernel configuration.
90
+
91
+ This configuration fixes the CTA tile shape, cp.async pipeline depth,
92
+ producer/consumer warp split, and CTA-cluster split count used by the
93
+ DSMEM reduction.
94
+
95
+ :param ab_dtype: Element type for A and B operands.
96
+ :param acc_dtype: Accumulator type used by MMA and reductions.
97
+ :param out_dtype: Output element type.
98
+ :param tile_n: CTA tile size in N. M is fixed to 16.
99
+ :type tile_n: int
100
+ :param tile_k: K tile size staged through shared memory.
101
+ :type tile_k: int
102
+ :param num_stages: Number of cp.async pipeline stages.
103
+ :type num_stages: int
104
+ :param num_dma_warps: Producer warps that issue GMEM to SMEM copies.
105
+ :type num_dma_warps: int
106
+ :param split_k: CTAs in the cluster-level split-K reduction.
107
+ :type split_k: int
108
+ :param use_pdl: Whether to launch with Programmatic Dependent Launch.
109
+ :type use_pdl: bool
110
+ """
111
+ self.ab_dtype = ab_dtype
112
+ self.acc_dtype = acc_dtype
113
+ self.out_dtype = out_dtype
114
+ self.tile_m = 16
115
+ self.tile_n = tile_n
116
+ self.tile_k = tile_k
117
+ self.copy_bits = 128
118
+ self.num_stages = num_stages
119
+ self.split_k = split_k
120
+ self.use_pdl = use_pdl
121
+ self.mma_shape = (16, 8, 16) # mma.sync.aligned.m16n8k16
122
+ self.atom_layout = (1, 1, 1) # one MMA atom per warp
123
+ self.num_dma_warps = num_dma_warps
124
+ self.num_mma_warps = 4
125
+ self.num_dma_threads = self.num_dma_warps * cute.arch.WARP_SIZE
126
+ self.num_mma_threads = self.num_mma_warps * cute.arch.WARP_SIZE
127
+ self.num_threads = self.num_dma_threads + self.num_mma_threads
128
+ self.num_epilogue_elems = self.tile_m * self.tile_n
129
+ self.epilogue_elems_per_thread = self.num_epilogue_elems // self.num_mma_threads
130
+
131
+ def _make_smem_layout_AB(self, dtype, copy_bits, smem_tiler):
132
+ """Build the staged swizzled SMEM layout for A or B tiles."""
133
+ major_size = min(smem_tiler[1], 64)
134
+ # Match swizzle span to contiguous K bytes, capped by CuTe 3-bit swizzle.
135
+ swizzle_bits = int(math.log2(major_size * dtype.width // copy_bits))
136
+ swizzle_bits = min(swizzle_bits, 3)
137
+ # Tile the swizzled atom across (M_or_N, K, stages).
138
+ layout_atom_outer = cute.make_layout((8, major_size), stride=(major_size, 1))
139
+ layout_atom = cute.make_composed_layout(
140
+ cute.make_swizzle(swizzle_bits, 3, 3), 0, layout_atom_outer
141
+ )
142
+ return cute.tile_to_shape(layout_atom, smem_tiler, (0, 1, 2))
143
+
144
+ def _make_gmem_tiled_copy(self, atom_copy, dtype, copy_bits, num_threads):
145
+ """Build the per-thread cp.async vector-copy layout."""
146
+ # Lay threads across K so each lane issues one vector copy.
147
+ copy_elems = copy_bits // dtype.width
148
+ k_threads = cute.size(self.tile_k) // copy_elems # threads along K
149
+ thread_layout = cute.make_layout(
150
+ (num_threads // k_threads, k_threads), stride=(k_threads, 1)
151
+ )
152
+ value_layout = cute.make_layout((1, copy_elems))
153
+ return cute.make_tiled_copy_tv(atom_copy, thread_layout, value_layout)
154
+
155
+ @cute.jit
156
+ def _fill_pred(self, pred_flat, coord_tensor, k_tile, dim_limit, K_total):
157
+ # Predicate one K tile and keep the stage-broadcast view in sync.
158
+ coord_ktile = coord_tensor[None, None, 0, k_tile]
159
+ num_vec = pred_flat.shape[0]
160
+ num_mn = pred_flat.shape[1]
161
+ for v in cutlass.range_constexpr(num_vec):
162
+ # pred_flat is (K_VEC, M/N) for one K_TILE.
163
+ for j in cutlass.range_constexpr(num_mn):
164
+ pred_flat[v, j] = cute.elem_less(
165
+ coord_ktile[(0, v), j], (dim_limit, K_total)
166
+ )
167
+
168
+ def _make_pred(self, tXcX, k_tile, dim_limit, K_total):
169
+ # pred_flat is (K_VEC, M/N); pred is (K_VEC, M/N, STAGE).
170
+ num_vec = tXcX.shape[0][1]
171
+ num_mn = cute.size(tXcX, mode=[1])
172
+ pred_flat = cute.make_rmem_tensor(
173
+ cute.make_layout((num_vec, num_mn), stride=(num_mn, 1)),
174
+ cutlass.Boolean,
175
+ )
176
+ pred = cute.make_tensor(
177
+ pred_flat.iterator,
178
+ cute.make_layout(
179
+ (num_vec, num_mn, cute.size(tXcX, mode=[2])),
180
+ stride=(num_mn, 1, 0),
181
+ ),
182
+ )
183
+ return pred_flat, pred
184
+
185
+ @cute.jit
186
+ def __call__(
187
+ self,
188
+ mA: cute.Tensor,
189
+ mB: cute.Tensor,
190
+ mC: cute.Tensor,
191
+ stream: CUstream,
192
+ scale: float = 1.0,
193
+ ):
194
+ """Execute the split-K GEMM operation in steps:
195
+ - Build swizzled staged SMEM layouts for A ``[16, tile_k, stages]``
196
+ and B ``[tile_n, tile_k, stages]``.
197
+ - Build 128-bit cp.async GMEM-to-SMEM tiled copies for DMA warps.
198
+ - Build an m16n8k16 BF16 MMA tiled across the CTA N tile.
199
+ - Launch grid ``ceil(M/16) x ceil(N/tile_n) x split_k`` with one
200
+ cluster along split-K, so each cluster rank owns K tiles
201
+ ``rank, rank + split_k, ...``.
202
+ - Reduce MMA-warp partials within the CTA, exchange split-K partials
203
+ through DSMEM, then reduce cluster partials and store C.
204
+
205
+ :param mA: Input tensor A with shape ``[M, K]``.
206
+ :type mA: cute.Tensor
207
+ :param mB: Input tensor B with shape ``[N, K]``.
208
+ :type mB: cute.Tensor
209
+ :param mC: Output tensor C with shape ``[M, N]``.
210
+ :type mC: cute.Tensor
211
+ :param stream: CUDA stream for asynchronous execution.
212
+ :type stream: CUstream
213
+ :param scale: Epilogue scale applied before storing C.
214
+ :type scale: float
215
+ """
216
+ bM, bN, bK = self.tile_m, self.tile_n, self.tile_k
217
+ copy_bits: cutlass.Constexpr = self.copy_bits
218
+ sA_layout = self._make_smem_layout_AB(
219
+ mA.element_type, copy_bits, (bM, bK, self.num_stages)
220
+ )
221
+ sB_layout = self._make_smem_layout_AB(
222
+ mB.element_type, copy_bits, (bN, bK, self.num_stages)
223
+ )
224
+
225
+ @cute.struct
226
+ class SharedStorage:
227
+ a: cute.struct.Align[
228
+ cute.struct.MemRange[mA.element_type, cute.cosize(sA_layout)], 16
229
+ ]
230
+ b: cute.struct.Align[
231
+ cute.struct.MemRange[mB.element_type, cute.cosize(sB_layout)], 16
232
+ ]
233
+ mbar: cute.struct.Align[
234
+ cute.struct.MemRange[cutlass.Int64, self.num_stages * 2], 8
235
+ ]
236
+
237
+ atom_g2s = cute.make_copy_atom(
238
+ cute.nvgpu.cpasync.CopyG2SOp(
239
+ cache_mode=cute.nvgpu.cpasync.LoadCacheMode.GLOBAL
240
+ ),
241
+ mA.element_type,
242
+ num_bits_per_copy=copy_bits,
243
+ ) # cp.async GMEM -> SMEM, bypassing L1
244
+ tiled_copy_A = self._make_gmem_tiled_copy(
245
+ atom_g2s, mA.element_type, copy_bits, self.num_dma_threads
246
+ )
247
+ tiled_copy_B = self._make_gmem_tiled_copy(
248
+ atom_g2s, mB.element_type, copy_bits, self.num_dma_threads
249
+ )
250
+ op = cute.nvgpu.warp.MmaF16BF16Op(self.ab_dtype, self.acc_dtype, self.mma_shape)
251
+ # Repeat the m16n8k16 atom along N to cover the CTA output tile.
252
+ perm_mnk = (
253
+ self.atom_layout[0] * self.mma_shape[0],
254
+ self.atom_layout[1] * self.mma_shape[1] * (self.tile_n // 8),
255
+ self.atom_layout[2] * self.mma_shape[2],
256
+ )
257
+ tiled_mma = cute.make_tiled_mma(
258
+ op, cute.make_layout(self.atom_layout), permutation_mnk=perm_mnk
259
+ )
260
+ tiler_mn = (bM, bN)
261
+ grid_m, grid_n = cute.ceil_div(mC.shape, tiler_mn)
262
+ self.kernel(
263
+ mA,
264
+ mB,
265
+ mC,
266
+ scale,
267
+ sA_layout,
268
+ sB_layout,
269
+ tiled_copy_A,
270
+ tiled_copy_B,
271
+ tiled_mma,
272
+ SharedStorage,
273
+ ).launch(
274
+ grid=[
275
+ cute.size(grid_m),
276
+ cute.size(grid_n),
277
+ self.split_k,
278
+ ],
279
+ block=[self.num_threads, 1, 1],
280
+ cluster=[
281
+ 1,
282
+ 1,
283
+ self.split_k,
284
+ ], # split-K CTAs form one cluster
285
+ stream=stream,
286
+ use_pdl=self.use_pdl,
287
+ )
288
+
289
+ @cute.kernel
290
+ def kernel(
291
+ self,
292
+ mA,
293
+ mB,
294
+ mC,
295
+ scale: cutlass.Float32,
296
+ sA_layout: cute.ComposedLayout,
297
+ sB_layout: cute.ComposedLayout,
298
+ tiled_copy_A: cute.TiledCopy,
299
+ tiled_copy_B: cute.TiledCopy,
300
+ tiled_mma: cute.TiledMma,
301
+ shared_storage: cutlass.Constexpr,
302
+ ):
303
+ bM, bN, bK = self.tile_m, self.tile_n, self.tile_k
304
+ num_stages = self.num_stages
305
+ tidx, _, _ = cute.arch.thread_idx()
306
+ bid_m, bid_n, bid_z = cute.arch.block_idx()
307
+ warp_idx = cute.arch.warp_idx()
308
+ lane_id = cute.arch.lane_idx()
309
+ num_dma_warps: cutlass.Constexpr = self.num_dma_warps
310
+ is_dma_warp = warp_idx < num_dma_warps
311
+ dma_tidx = tidx
312
+ mma_tidx = tidx - self.num_dma_threads
313
+ mma_warp_idx = warp_idx - num_dma_warps
314
+
315
+ cta_tiler = (bM, bN, bK)
316
+ coord = (bid_m, bid_n, None) # all K tiles
317
+ # CTA-local tiles.
318
+ gA = cute.local_tile(
319
+ mA, tiler=cta_tiler, coord=coord, proj=(1, None, 1)
320
+ ) # skip N
321
+ gB = cute.local_tile(
322
+ mB, tiler=cta_tiler, coord=coord, proj=(None, 1, 1)
323
+ ) # skip M
324
+ gC = cute.local_tile(
325
+ mC, tiler=cta_tiler, coord=coord, proj=(1, 1, None)
326
+ ) # skip K
327
+
328
+ mcA = cute.make_identity_tensor(mA.layout.shape)
329
+ mcB = cute.make_identity_tensor(mB.layout.shape)
330
+ mcC = cute.make_identity_tensor(mC.layout.shape)
331
+ # Coordinate modes: cA=(M,K,k_tile), cB=(N,K,k_tile), cC=(M,N).
332
+ cA = cute.local_tile(mcA, tiler=cta_tiler, coord=coord, proj=(1, None, 1))
333
+ cB = cute.local_tile(mcB, tiler=cta_tiler, coord=coord, proj=(None, 1, 1))
334
+ cC = cute.local_tile(mcC, tiler=cta_tiler, coord=coord, proj=(1, 1, None))
335
+
336
+ # 128-bit cp.async copies require 16-byte aligned GMEM views.
337
+ gA = cute.make_tensor(gA.iterator.align(16), gA.layout)
338
+ gB = cute.make_tensor(gB.iterator.align(16), gB.layout)
339
+
340
+ smem = cutlass.utils.SmemAllocator()
341
+ storage_ptr = smem.allocate(shared_storage.size_in_bytes(), byte_alignment=16) # type: ignore[attr-defined]
342
+ storage = shared_storage(storage_ptr) # type: ignore[call-arg]
343
+ sA = storage.a.get_tensor(sA_layout)
344
+ sB = storage.b.get_tensor(sB_layout)
345
+
346
+ # Pipeline cp.async producers into MMA consumers.
347
+ producer_group = pipeline.CooperativeGroup(
348
+ pipeline.Agent.Thread, self.num_dma_threads
349
+ )
350
+ consumer_group = pipeline.CooperativeGroup(
351
+ pipeline.Agent.Thread, self.num_mma_threads
352
+ )
353
+ mainloop_pipeline = pipeline.PipelineCpAsync.create(
354
+ barrier_storage=storage.mbar.data_ptr(),
355
+ num_stages=num_stages,
356
+ producer_group=producer_group,
357
+ consumer_group=consumer_group,
358
+ )
359
+
360
+ # Round-robin split-K: split z handles tiles z, z+split_k, ...
361
+ K_total = cute.size(mA, mode=[1])
362
+ k_tile_count = cute.size(gA, mode=[2])
363
+ k_start = bid_z
364
+ num_k_tiles = cute.ceil_div(k_tile_count - k_start, self.split_k)
365
+
366
+ if is_dma_warp:
367
+ # DMA warps trade registers for copy throughput.
368
+ cute.arch.setmaxregister_decrease(40)
369
+ thr_A = tiled_copy_A.get_slice(dma_tidx)
370
+ thr_B = tiled_copy_B.get_slice(dma_tidx)
371
+ tAgA = thr_A.partition_S(gA)
372
+ tAsA = thr_A.partition_D(sA)
373
+ tBgB = thr_B.partition_S(gB)
374
+ tBsB = thr_B.partition_D(sB)
375
+ tAcA = thr_A.partition_S(cA)
376
+ tBcB = thr_B.partition_S(cB)
377
+
378
+ # Build M/K and N/K predicates, broadcast across K-tile copies.
379
+ tApA_flat, tApA = self._make_pred(tAcA, k_start, mA.shape[0], K_total)
380
+ tBpB_flat, tBpB = self._make_pred(tBcB, k_start, mB.shape[0], K_total)
381
+ self._fill_pred(tApA_flat, tAcA, k_start, mA.shape[0], K_total)
382
+ self._fill_pred(tBpB_flat, tBcB, k_start, mB.shape[0], K_total)
383
+
384
+ producer_state = pipeline.make_pipeline_state(
385
+ pipeline.PipelineUserType.Producer, num_stages
386
+ )
387
+
388
+ # Prime the first pipeline stage.
389
+ mainloop_pipeline.producer_acquire(producer_state)
390
+ cute.copy(
391
+ tiled_copy_B,
392
+ tBgB[None, None, None, k_start],
393
+ tBsB[None, None, None, producer_state.index],
394
+ pred=tBpB,
395
+ )
396
+ if const_expr(self.use_pdl):
397
+ cute.arch.griddepcontrol_wait()
398
+ cute.copy(
399
+ tiled_copy_A,
400
+ tAgA[None, None, None, k_start],
401
+ tAsA[None, None, None, producer_state.index],
402
+ pred=tApA,
403
+ )
404
+ mainloop_pipeline.producer_commit(producer_state)
405
+ producer_state.advance()
406
+
407
+ for k_tile in cutlass.range(
408
+ k_start + self.split_k, k_tile_count, self.split_k, unroll=1
409
+ ):
410
+ self._fill_pred(tApA_flat, tAcA, k_tile, mA.shape[0], K_total)
411
+ self._fill_pred(tBpB_flat, tBcB, k_tile, mB.shape[0], K_total)
412
+ mainloop_pipeline.producer_acquire(producer_state)
413
+ cute.copy(
414
+ tiled_copy_A,
415
+ tAgA[None, None, None, k_tile],
416
+ tAsA[None, None, None, producer_state.index],
417
+ pred=tApA,
418
+ )
419
+ cute.copy(
420
+ tiled_copy_B,
421
+ tBgB[None, None, None, k_tile],
422
+ tBsB[None, None, None, producer_state.index],
423
+ pred=tBpB,
424
+ )
425
+ mainloop_pipeline.producer_commit(producer_state)
426
+ producer_state.advance()
427
+
428
+ mainloop_pipeline.producer_tail(producer_state)
429
+
430
+ else:
431
+ # MMA warps with k-phase interleaving
432
+ cute.arch.setmaxregister_increase(232) # large MMA fragments
433
+
434
+ num_mma_warps: cutlass.Constexpr = self.num_mma_warps
435
+
436
+ thr_mma = tiled_mma.get_slice(lane_id)
437
+ tCsA = thr_mma.partition_A(sA)
438
+ tCsB = thr_mma.partition_B(sB)
439
+ tCgC = thr_mma.partition_C(gC)
440
+
441
+ tCrA = tiled_mma.make_fragment_A(tCsA[None, None, None, 0])
442
+ tCrB = tiled_mma.make_fragment_B(tCsB[None, None, None, 0])
443
+ tCrC = tiled_mma.make_fragment_C(tCgC)
444
+ tCrC.fill(0.0)
445
+
446
+ # ldmatrix moves SMEM fragments into MMA registers.
447
+ atom_s2r_A = cute.make_copy_atom(
448
+ cute.nvgpu.warp.LdMatrix8x8x16bOp(False, 4), mA.element_type
449
+ ) # non-transposed, x4
450
+ atom_s2r_B = cute.make_copy_atom(
451
+ cute.nvgpu.warp.LdMatrix8x8x16bOp(False, 4), mB.element_type
452
+ )
453
+
454
+ # SMEM -> register copy path.
455
+ tiled_s2r_A = cute.make_tiled_copy_A(atom_s2r_A, tiled_mma)
456
+ tiled_s2r_B = cute.make_tiled_copy_B(atom_s2r_B, tiled_mma)
457
+ thr_s2r_A = tiled_s2r_A.get_slice(lane_id)
458
+ thr_s2r_B = tiled_s2r_B.get_slice(lane_id)
459
+ tCsA_v = thr_s2r_A.partition_S(sA) # views, not copies
460
+ tCrA_v = thr_s2r_A.retile(tCrA)
461
+ tCsB_v = thr_s2r_B.partition_S(sB)
462
+ tCrB_v = thr_s2r_B.retile(tCrB)
463
+ # Split the MMA K-fragments across the MMA warps.
464
+ tCsA_warp_v = cute.logical_divide(tCsA_v, (None, None, num_mma_warps, None))
465
+ tCsB_warp_v = cute.logical_divide(tCsB_v, (None, None, num_mma_warps, None))
466
+
467
+ num_k_blocks = cute.size(tCrA, mode=[2])
468
+ k_blocks_per_warp: cutlass.Constexpr = num_k_blocks // num_mma_warps
469
+
470
+ consumer_state = pipeline.make_pipeline_state(
471
+ pipeline.PipelineUserType.Consumer, num_stages
472
+ )
473
+
474
+ # Shape-dynamic split-K count, so this stays a runtime range.
475
+ for _ in cutlass.range(num_k_tiles, unroll_full=True):
476
+ mainloop_pipeline.consumer_wait(consumer_state)
477
+ for ki in cutlass.range_constexpr(k_blocks_per_warp):
478
+ cute.copy(
479
+ tiled_s2r_A,
480
+ tCsA_warp_v[
481
+ None, None, (mma_warp_idx, ki), consumer_state.index
482
+ ],
483
+ tCrA_v[None, None, 0],
484
+ ) # ldmatrix
485
+ cute.copy(
486
+ tiled_s2r_B,
487
+ tCsB_warp_v[
488
+ None, None, (mma_warp_idx, ki), consumer_state.index
489
+ ],
490
+ tCrB_v[None, None, 0],
491
+ )
492
+ cute.gemm(
493
+ tiled_mma, tCrC, tCrA[None, None, 0], tCrB[None, None, 0], tCrC
494
+ ) # mma.sync
495
+ mainloop_pipeline.consumer_release(consumer_state)
496
+ consumer_state.advance()
497
+
498
+ # Cluster reduction epilogue.
499
+ # Reduce per-warp accumulators within this CTA.
500
+ num_elems: cutlass.Constexpr = self.num_epilogue_elems
501
+ elems_per_thread: cutlass.Constexpr = self.epilogue_elems_per_thread
502
+ # Map MMA threads to linear CTA output elements.
503
+ epilogue_thread_layout = cute.make_layout(
504
+ (elems_per_thread, self.num_mma_threads),
505
+ stride=(self.num_mma_threads, 1),
506
+ )
507
+ epilogue_slots = cute.make_tensor(0, epilogue_thread_layout)
508
+ epilogue_slot_coords = cute.make_identity_tensor((bN, bM))
509
+ # Layout: (mma_warp, linear MN element).
510
+ smem_red = cute.make_tensor(
511
+ cute.arch.alloc_smem(
512
+ cutlass.Float32, num_elems * num_mma_warps, alignment=16
513
+ ),
514
+ cute.make_layout((num_mma_warps, num_elems), stride=(num_elems, 1)),
515
+ )
516
+ smem_warp = cute.make_tensor(
517
+ cute.domain_offset((mma_warp_idx, 0), smem_red).iterator,
518
+ cute.make_layout((bM, bN), stride=(bN, 1)),
519
+ )
520
+ tCsC_partial = thr_mma.partition_C(smem_warp)
521
+ cute.autovec_copy(tCrC, tCsC_partial)
522
+ cute.arch.sync_threads()
523
+
524
+ # Layout: (split-K rank, linear MN element).
525
+ partials = cute.make_tensor(
526
+ cute.arch.alloc_smem(
527
+ cutlass.Float32, num_elems * self.split_k, alignment=16
528
+ ),
529
+ cute.make_layout((self.split_k, num_elems), stride=(num_elems, 1)),
530
+ )
531
+ cta_rank = cute.arch.block_idx_in_cluster()
532
+
533
+ for ei in cutlass.range_constexpr(elems_per_thread):
534
+ elem_idx = epilogue_slots[ei, mma_tidx]
535
+ local_coord = cute.select(epilogue_slot_coords[elem_idx], mode=[1, 0])
536
+ total = cutlass.Float32(0.0)
537
+ if cute.elem_less(local_coord, gC.shape):
538
+ total = (
539
+ smem_red[None, elem_idx]
540
+ .load()
541
+ .reduce(
542
+ cute.ReductionOp.ADD,
543
+ init_val=cutlass.Float32(0.0),
544
+ reduction_profile=0,
545
+ )
546
+ )
547
+ total = total * scale
548
+ partials[cta_rank, elem_idx] = total
549
+
550
+ cute.arch.sync_threads()
551
+
552
+ # Broadcast this CTA's partials to peer DSMEM.
553
+ for ei in cutlass.range_constexpr(elems_per_thread):
554
+ elem_idx = epilogue_slots[ei, mma_tidx]
555
+ my_slot = cute.domain_offset((cta_rank, elem_idx), partials).iterator
556
+ my_val = partials[cta_rank, elem_idx]
557
+ for peer in cutlass.range_constexpr(self.split_k):
558
+ remote = set_block_rank(my_slot, cutlass.Int32(peer))
559
+ st_shared_remote_f32(remote, my_val)
560
+
561
+ cute.arch.cluster_arrive()
562
+ cute.arch.cluster_wait() # peer DSMEM stores are now visible
563
+
564
+ if const_expr(self.use_pdl) and mma_tidx == 0:
565
+ cute.arch.griddepcontrol_launch_dependents()
566
+ cute.arch.sync_threads()
567
+
568
+ # Reduce split-K partials and write global output.
569
+ for ei in cutlass.range_constexpr(elems_per_thread):
570
+ elem_idx = epilogue_slots[ei, mma_tidx]
571
+ local_coord = cute.select(epilogue_slot_coords[elem_idx], mode=[1, 0])
572
+ global_coord = cC[local_coord]
573
+ if cute.elem_less(global_coord, mC.shape):
574
+ acc = (
575
+ partials[None, elem_idx]
576
+ .load()
577
+ .reduce(
578
+ cute.ReductionOp.ADD,
579
+ init_val=cutlass.Float32(0.0),
580
+ reduction_profile=0,
581
+ )
582
+ )
583
+ gC[local_coord] = acc
584
+
585
+ cute.arch.sync_threads()
vllm/model_executor/kernels/linear/cute_dsl/ll_bf16.py
new
+282
@@ -0,0 +1,282 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from __future__ import annotations
5
+
6
+import logging
7
+import math
8
+from collections.abc import Iterable
9
+from dataclasses import dataclass
10
+from typing import Any, Literal
11
+
12
+import torch
13
+
14
+logger = logging.getLogger(__name__)
15
+
16
+_cutedsl_available: bool | None = None
17
+
18
+
19
+def is_available() -> bool:
20
+ global _cutedsl_available
21
+ if _cutedsl_available is not None:
22
+ return _cutedsl_available
23
+ try:
24
+ import cutlass # noqa: F401
25
+ import cutlass.cute # noqa: F401
26
+
27
+ _cutedsl_available = True
28
+ except ImportError:
29
+ _cutedsl_available = False
30
+ logger.info("cuteDSL (CUTLASS Python) not available, ll_bf16_gemm disabled")
31
+ return _cutedsl_available
32
+
33
+
34
+_DEFAULT_DOTPROD_BS = 128
35
+_DEFAULT_DOTPROD_MAX_M = 4
36
+_DEFAULT_SPLITK_CONFIG = (6, 4)
37
+_TUNED_DOTPROD_MAX_M: dict[tuple[int, int], int] = {
38
+ (7168, 256): 6,
39
+}
40
+_TUNED_CONFIGS: dict[tuple[int, int], dict[int, tuple[int, int]]] = {
41
+ (7168, 384): {
42
+ 5: (4, 4),
43
+ **{M: (5, 4) for M in range(6, 17)},
44
+ },
45
+}
46
+
47
+
48
+_cute_ctx = None
49
+
50
+
51
+def _cute():
52
+ global _cute_ctx
53
+ if _cute_ctx is not None:
54
+ return _cute_ctx
55
+ import cutlass.cute as cute
56
+ from cuda.bindings.driver import CUstream
57
+
58
+ _cute_ctx = (cute, CUstream)
59
+ return _cute_ctx
60
+
61
+
62
+def _stream():
63
+ _, CUstream = _cute()
64
+ from vllm.utils.torch_utils import current_stream
65
+
66
+ return CUstream(current_stream().cuda_stream)
67
+
68
+
69
+def _use_pdl() -> bool:
70
+ from vllm.platforms import current_platform
71
+
72
+ return current_platform.is_arch_support_pdl()
73
+
74
+
75
+class LLBf16Gemm:
76
+ @dataclass(frozen=True, slots=True)
77
+ class CompileKey:
78
+ backend: Literal["dotprod", "splitk"]
79
+ M: int = 0
80
+ K: int = 0
81
+ bs: int = 0
82
+ split_k: int = 0
83
+ num_stages: int = 0
84
+
85
+ def __init__(self) -> None:
86
+ # Dot-prod: keyed on (M, K, bs), because M and K are Constexpr.
87
+ self._compiled_cache: dict[tuple[int, int, int], Any] = {}
88
+ # Split-K: keyed on (split_k, num_stages), fully shape-dynamic.
89
+ self._splitk_cache: dict[tuple[int, int], Any] = {}
90
+
91
+ def dispatch(self, *, M: int, K: int, N: int) -> CompileKey:
92
+ dotprod_max_m = _TUNED_DOTPROD_MAX_M.get((K, N), _DEFAULT_DOTPROD_MAX_M)
93
+ if dotprod_max_m >= M or K < 2048:
94
+ return self.CompileKey(backend="dotprod", M=M, K=K, bs=_DEFAULT_DOTPROD_BS)
95
+
96
+ split_k, num_stages = _TUNED_CONFIGS.get((K, N), {}).get(
97
+ M, _DEFAULT_SPLITK_CONFIG
98
+ )
99
+ return self.CompileKey(backend="splitk", split_k=split_k, num_stages=num_stages)
100
+
101
+ def get_warmup_keys(
102
+ self,
103
+ *,
104
+ shapes: Iterable[tuple[int, int]],
105
+ m_values: Iterable[int],
106
+ ) -> list[CompileKey]:
107
+ return list(
108
+ dict.fromkeys(
109
+ self.dispatch(M=M, K=K, N=N) for K, N in shapes for M in m_values
110
+ )
111
+ )
112
+
113
+ @staticmethod
114
+ def _fake_gemm_tensors(*, M, K, N, divisibility: int):
115
+ from cutlass import BFloat16, Float32
116
+ from quack.compile_utils import make_fake_tensor
117
+
118
+ hidden_states = make_fake_tensor(BFloat16, (M, K), divisibility=divisibility)
119
+ router_weight = make_fake_tensor(BFloat16, (N, K), divisibility=divisibility)
120
+ output = make_fake_tensor(Float32, (M, N), divisibility=1)
121
+ return hidden_states, router_weight, output
122
+
123
+ def _compile_splitk(self, compile_key: CompileKey) -> None:
124
+ cute, _ = _cute()
125
+ from ._ll_bf16_splitk import LLBf16SplitK
126
+
127
+ hidden_states, router_weight, output = self._fake_gemm_tensors(
128
+ M=cute.sym_int(),
129
+ K=cute.sym_int(),
130
+ N=cute.sym_int(),
131
+ divisibility=8,
132
+ )
133
+ gemm = LLBf16SplitK(
134
+ tile_n=16,
135
+ tile_k=256,
136
+ num_stages=compile_key.num_stages,
137
+ num_dma_warps=4,
138
+ split_k=compile_key.split_k,
139
+ use_pdl=_use_pdl(),
140
+ )
141
+ compiled = cute.compile(
142
+ gemm,
143
+ hidden_states,
144
+ router_weight,
145
+ output,
146
+ _stream(),
147
+ options="--enable-tvm-ffi",
148
+ )
149
+ self._splitk_cache[(compile_key.split_k, compile_key.num_stages)] = compiled
150
+ logger.debug(
151
+ "Compiled ll_bf16_splitk: sk=%d ns=%d",
152
+ compile_key.split_k,
153
+ compile_key.num_stages,
154
+ )
155
+
156
+ def _compile_dotprod(self, compile_key: CompileKey) -> None:
157
+ cute, _ = _cute()
158
+ from ._ll_bf16_dotprod import LLBf16Dotprod
159
+
160
+ N = cute.sym_int()
161
+ stride_divisibility = math.gcd(8, compile_key.K)
162
+ hidden_states, router_weight, output = self._fake_gemm_tensors(
163
+ M=compile_key.M,
164
+ K=compile_key.K,
165
+ N=N,
166
+ divisibility=stride_divisibility,
167
+ )
168
+ gemm = LLBf16Dotprod(k=compile_key.K, bs=compile_key.bs, use_pdl=_use_pdl())
169
+ compiled = cute.compile(
170
+ gemm,
171
+ hidden_states,
172
+ router_weight,
173
+ output,
174
+ compile_key.M,
175
+ compile_key.K,
176
+ 1, # runtime N placeholder for fake-tensor compile
177
+ _stream(),
178
+ options="--enable-tvm-ffi --ptxas-options -maxrregcount=64",
179
+ )
180
+ self._compiled_cache[(compile_key.M, compile_key.K, compile_key.bs)] = compiled
181
+ logger.debug(
182
+ "Compiled ll_bf16_dotprod: M=%d, K=%d, bs=%d",
183
+ compile_key.M,
184
+ compile_key.K,
185
+ compile_key.bs,
186
+ )
187
+
188
+ def compile(self, compile_key: CompileKey) -> None:
189
+ if compile_key.backend == "splitk":
190
+ splitk_cache_key = (compile_key.split_k, compile_key.num_stages)
191
+ if splitk_cache_key not in self._splitk_cache:
192
+ self._compile_splitk(compile_key)
193
+ return
194
+
195
+ dotprod_cache_key = (compile_key.M, compile_key.K, compile_key.bs)
196
+ if dotprod_cache_key not in self._compiled_cache:
197
+ self._compile_dotprod(compile_key)
198
+
199
+ def warmup(
200
+ self,
201
+ *,
202
+ shapes: Iterable[tuple[int, int]],
203
+ m_values: Iterable[int],
204
+ ) -> None:
205
+ for compile_key in self.get_warmup_keys(shapes=shapes, m_values=m_values):
206
+ self.compile(compile_key)
207
+
208
+ @staticmethod
209
+ def _validate_inputs(
210
+ hidden_states: torch.Tensor,
211
+ router_weight: torch.Tensor,
212
+ output_dtype: torch.dtype,
213
+ ) -> None:
214
+ if hidden_states.dim() != 2 or router_weight.dim() != 2:
215
+ raise ValueError("hidden_states and router_weight must be 2D tensors")
216
+ if (
217
+ hidden_states.dtype != torch.bfloat16
218
+ or router_weight.dtype != torch.bfloat16
219
+ ):
220
+ raise ValueError("hidden_states and router_weight must have dtype=bfloat16")
221
+ if hidden_states.device.type != "cuda" or router_weight.device.type != "cuda":
222
+ raise ValueError(
223
+ "hidden_states and router_weight must have device_type=cuda"
224
+ )
225
+ if hidden_states.device != router_weight.device:
226
+ raise ValueError(
227
+ "hidden_states and router_weight must be on the same CUDA device"
228
+ )
229
+ if output_dtype != torch.float32:
230
+ raise ValueError("ll_bf16_gemm only supports output_dtype=torch.float32")
231
+ if hidden_states.shape[1] != router_weight.shape[1]:
232
+ raise ValueError(
233
+ "hidden_states and router_weight must have matching K dimensions"
234
+ )
235
+ # Kernels use vectorized bf16 loads and require 16-byte row alignment.
236
+ if hidden_states.shape[1] % 8 != 0:
237
+ raise ValueError("ll_bf16_gemm requires K to be divisible by 8")
238
+ if not hidden_states.is_contiguous() or not router_weight.is_contiguous():
239
+ raise ValueError(
240
+ "hidden_states and router_weight must be contiguous row-major inputs"
241
+ )
242
+
243
+ def __call__(
244
+ self,
245
+ hidden_states: torch.Tensor, # [M, K] bf16
246
+ router_weight: torch.Tensor, # [N, K] bf16
247
+ output_dtype: torch.dtype = torch.float32,
248
+ ) -> torch.Tensor: # [M, N] fp32
249
+ self._validate_inputs(hidden_states, router_weight, output_dtype)
250
+
251
+ M, K = hidden_states.shape
252
+ N = router_weight.shape[0]
253
+ compile_key = self.dispatch(M=M, K=K, N=N)
254
+ if compile_key.backend == "splitk":
255
+ splitk_cache_key = (compile_key.split_k, compile_key.num_stages)
256
+ if splitk_cache_key not in self._splitk_cache:
257
+ self.compile(compile_key)
258
+ kernel = self._splitk_cache[splitk_cache_key]
259
+ else:
260
+ dotprod_cache_key = (compile_key.M, compile_key.K, compile_key.bs)
261
+ if dotprod_cache_key not in self._compiled_cache:
262
+ self.compile(compile_key)
263
+ kernel = self._compiled_cache[dotprod_cache_key]
264
+
265
+ stream = _stream()
266
+ output = torch.empty(M, N, dtype=output_dtype, device=hidden_states.device)
267
+ if compile_key.backend == "splitk":
268
+ kernel(hidden_states, router_weight, output, stream, 1.0)
269
+ else:
270
+ kernel(hidden_states, router_weight, output, N, stream)
271
+ return output
272
+
273
+
274
+ll_bf16_gemm_kernel = LLBf16Gemm()
275
+
276
+
277
+def ll_bf16_gemm(
278
+ hidden_states: torch.Tensor,
279
+ router_weight: torch.Tensor,
280
+ output_dtype: torch.dtype = torch.float32,
281
+) -> torch.Tensor:
282
+ return ll_bf16_gemm_kernel(hidden_states, router_weight, output_dtype)
vllm/model_executor/layers/fused_moe/router/gate_linear.py
+46
-8
@@ -14,11 +14,13 @@ from vllm.utils.torch_utils import direct_register_custom_op
14
class GateLinear(ReplicatedLinear):
15
"""MoE gate linear layer with multi-tier GEMM dispatch:
16
17
- 1. DSV3 specialized kernel (SM90+, M<=16, H=7168 E=256/384, H=6144 E=256)
18
- 2. fp32 specialized kernel (SM90+, bf16/fp32 in, fp32 out, M<=32,
17
+ 1. cuteDSL ll_bf16_gemm (SM90+, M<=16, bf16 in, fp32 out,
18
+ K divisible by 8)
19
+ 2. DSV3 specialized kernel (SM90+, M<=16, H=7168 E=256/384, H=6144 E=256)
20
+ 3. fp32 specialized kernel (SM90+, bf16/fp32 in, fp32 out, M<=32,
21
(H, E) in {(3072, 256), (6144, 128), (6144, 256)})
20
- 3. cuBLAS bf16×bf16→fp32 (SM90+ + bf16 weight + fp32 out_dtype)
21
- 4. F.linear via ReplicatedLinear (ultimate fallback)
22
+ 4. cuBLAS bf16×bf16→fp32 (SM90+ + bf16 weight + fp32 out_dtype)
23
+ 5. F.linear via ReplicatedLinear (ultimate fallback)
24
25
The ``out_dtype`` attribute is mutable and can be set after init
26
(e.g. when the required dtype depends on the expert quantization
@@ -99,6 +101,21 @@ class GateLinear(ReplicatedLinear):
101
and self.out_dtype == torch.float32
102
)
103
104
+ # cuteDSL ll_bf16_gemm eligibility. Any dims supported, but SM90+ required bc:
105
+ # 1. PDL support. Both dot-product and split-K kernels.
106
+ # 2. Thread Block Clusters. Split-K kernel for cross-CTA reduction.
107
+ self.allow_ll_bf16_gemm = False
108
+ if can_use_specialized_kernels:
109
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
110
+ is_available,
111
+ )
112
+
113
+ self.allow_ll_bf16_gemm = (
114
+ self.weight.dtype == torch.bfloat16
115
+ and self.out_dtype == torch.float32
116
+ and is_available()
117
+ )
118
+
119
def set_out_dtype(self, out_dtype: torch.dtype) -> None:
120
"""Set output dtype for the router logits after init.
121
@@ -116,10 +133,31 @@ class GateLinear(ReplicatedLinear):
133
):
134
self.allow_cublas_router_gemm = self.weight.dtype == torch.bfloat16
135
136
+ # out_dtype may start as None -> recompute eligibility here
137
+ if self.allow_specialized_router_gemm:
138
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
139
+ is_available,
140
+ )
141
+
142
+ self.allow_ll_bf16_gemm = (
143
+ self.weight.dtype == torch.bfloat16
144
+ and out_dtype == torch.float32
145
+ and is_available()
146
+ )
147
+
148
def forward(
149
self, x: torch.Tensor
150
) -> torch.Tensor | tuple[torch.Tensor, Parameter | None]:
122
- # Tier 1: DSV3 specialized kernel
151
+ # Tier 1: cuteDSL ll_bf16_gemm (SM90+, any dims)
152
+ if self.allow_ll_bf16_gemm and x.shape[0] <= 16 and x.dtype == torch.bfloat16:
153
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
154
+ ll_bf16_gemm,
155
+ )
156
+
157
+ output = ll_bf16_gemm(x, self.weight)
158
+ return output, None
159
+
160
+ # Tier 2: DSV3 specialized kernel (fallback for when cuteDSL unavailable)
161
if self.allow_dsv3_router_gemm and x.shape[0] <= self._dsv3_max_batch:
162
output = ops.dsv3_router_gemm(
163
hidden_states=x,
@@ -128,7 +166,7 @@ class GateLinear(ReplicatedLinear):
166
)
167
return output, None
168
131
- # Tier 2: fp32 specialized kernel (H=3072, E=256, M<=32)
169
+ # Tier 3: fp32 specialized kernel (H=3072, E=256, M<=32)
170
# Dispatch is wrapped in a custom op so that torch.compile/CUDA-graph
171
# capture does not freeze the runtime num_tokens branch.
172
if self.allow_fp32_router_gemm and x.dtype in (
@@ -138,12 +176,12 @@ class GateLinear(ReplicatedLinear):
176
output = torch.ops.vllm.fp32_router_gemm_dispatch(x, self.weight)
177
return output, None
178
141
- # Tier 3: cuBLAS bf16→fp32
179
+ # Tier 4: cuBLAS bf16→fp32
180
if self.allow_cublas_router_gemm and x.dtype == torch.bfloat16:
181
output = torch.mm(x, self.weight.T, out_dtype=torch.float32)
182
return output, None
183
146
- # Tier 4: F.linear (ReplicatedLinear)
184
+ # Tier 5: F.linear (ReplicatedLinear)
185
if self.out_dtype is not None and x.dtype != self.weight.dtype:
186
x = x.to(self.weight.dtype)
187
output, output_bias = super().forward(x)
vllm/model_executor/warmup/kernel_warmup.py
+28
@@ -42,6 +42,31 @@ if TYPE_CHECKING:
42
43
logger = init_logger(__name__)
44
45
+_LL_BF16_WARMUP_MODEL_SHAPES: tuple[tuple[int, int], ...] = (
46
+ (7168, 256), # DSV3
47
+ (7168, 384), # DSV4-Pro
48
+ (14400, 256), # DSV4-Flash
49
+)
50
+_LL_BF16_WARMUP_M_RANGE = range(1, 17)
51
+
52
+
53
+def _warmup_ll_bf16_router_gemm() -> None:
54
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
55
+ is_available as is_ll_bf16_gemm_available,
56
+ )
57
+ from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
58
+ ll_bf16_gemm_kernel,
59
+ )
60
+
61
+ if not is_ll_bf16_gemm_available():
62
+ return
63
+
64
+ logger.info("Warming up ll_bf16 router GEMM kernels.")
65
+ ll_bf16_gemm_kernel.warmup(
66
+ shapes=_LL_BF16_WARMUP_MODEL_SHAPES,
67
+ m_values=_LL_BF16_WARMUP_M_RANGE,
68
+ )
69
+
70
71
def kernel_warmup(worker: "Worker"):
72
from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
@@ -94,6 +119,9 @@ def kernel_warmup(worker: "Worker"):
119
elif has_flashinfer() and current_platform.has_device_capability(90):
120
flashinfer_autotune(worker.model_runner)
121
122
+ if current_platform.has_device_capability(90):
123
+ _warmup_ll_bf16_router_gemm()
124
+
125
# FlashInfer attention warmup
126
# Only warmup if the model has FlashInfer attention groups
127
# and is not a pooling model