[Kimi-K3] Extend GEMM-RS to GEMM-AR (#53053)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: wzhao18 <wzhao18.sz@gmail.com>
Thien Tran committed
Aug 21, 2026 at 11:44 UTC
2785c72a1497dcd4db53d7c3bc557e379a518e0c
9 files changed
+481
-232
.buildkite/test_areas/distributed.yaml
+11
-2
@@ -244,11 +244,20 @@ steps:
244
commands:
245
- pytest -v -s tests/distributed/test_context_parallel.py
246
- pytest -v -s tests/distributed/test_nccl_symm_mem.py
247
- - pytest -v -s tests/kernels/test_kimi_k3_gemm_rs.py
247
- pytest -v -s tests/v1/distributed/test_dbo.py
248
- pytest -v -s tests/distributed/test_mnnvl_alltoall.py
249
251
-
250
+- label: ":nvidia: (B200) Kimi-K3 GEMM-RS/AR"
251
+ key: kimi-k3-gemm-rs-ar-2xb200
252
+ timeout_in_minutes: 20
253
+ device: b200-k8s
254
+ working_dir: "/vllm-workspace/"
255
+ num_devices: 2
256
+ source_file_dependencies:
257
+ - vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py
258
+ - tests/kernels/test_kimi_k3_gemm_rs_ar.py
259
+ commands:
260
+ - pytest -v -s tests/kernels/test_kimi_k3_gemm_rs_ar.py
261
262
- label: ":nvidia: (L4) Distributed 2-Node"
263
key: 2-node-test-4-gpus
benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py
renamed
+114
-78
@@ -1,11 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Benchmark the SM100 Kimi-K3 GEMM-RS kernel.
3
+"""Benchmark the SM100 Kimi-K3 GEMM-RS/AR kernel.
4
5
All ranks must belong to one NVLink domain. For example, run a TP8 sweep with:
6
7
torchrun --nproc-per-node=8 \
8
- benchmarks/kernels/benchmark_kimi_k3_gemm_rs.py
8
+ benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py
9
"""
10
11
import argparse
@@ -26,7 +26,7 @@ from vllm.distributed.parallel_state import (
26
init_distributed_environment,
27
initialize_model_parallel,
28
)
29
-from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import GemmRS
29
+from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr
30
31
# Shared-expert down-proj and attention O-proj.
32
_KIMI_K3_PROJECTION_K = (6144, 12288)
@@ -41,6 +41,12 @@ class Candidate:
41
42
def parse_args() -> argparse.Namespace:
43
parser = argparse.ArgumentParser()
44
+ parser.add_argument(
45
+ "--mode",
46
+ choices=("rs", "ar"),
47
+ default="rs",
48
+ help="Collective mode to benchmark.",
49
+ )
50
parser.add_argument(
51
"--m",
52
type=int,
@@ -138,7 +144,8 @@ def valid_rows(M: int, local_M: int, rank: int) -> int:
144
145
146
def benchmark_shape(
141
- gemm_rs: GemmRS,
147
+ gemm_rs_ar: GemmRsAr,
148
+ mode: str,
149
M: int,
150
N: int,
151
K: int,
@@ -148,7 +155,8 @@ def benchmark_shape(
155
device_group: dist.ProcessGroup,
156
cpu_group: dist.ProcessGroup,
157
device_barrier: Callable[[], None],
151
-) -> dict[str, float | int]:
158
+) -> dict[str, float | int | str]:
159
+ all_reduce = mode == "ar"
160
world_size = dist.get_world_size(device_group)
161
rank = dist.get_rank(device_group)
162
device = torch.device("cuda", torch.accelerator.current_device_index())
@@ -169,17 +177,19 @@ def benchmark_shape(
177
partial = torch.empty((padded_M, N), dtype=torch.bfloat16, device=device)
178
symm_partial = symm_mem.empty((padded_M, N), dtype=torch.bfloat16, device=device)
179
symm_partial_handle = symm_mem.rendezvous(symm_partial, device_group)
172
- rs_inputs = [torch.empty_like(partial) for _ in range(num_workspaces)]
173
- symm_rs_inputs = []
174
- symm_rs_handles = []
180
+ collective_inputs = [torch.empty_like(partial) for _ in range(num_workspaces)]
181
+ symm_collective_inputs = []
182
+ symm_collective_handles = []
183
for _ in range(num_workspaces):
176
- rs_input = symm_mem.empty(
184
+ collective_input = symm_mem.empty(
185
(padded_M, N),
186
dtype=torch.bfloat16,
187
device=device,
188
)
181
- symm_rs_inputs.append(rs_input)
182
- symm_rs_handles.append(symm_mem.rendezvous(rs_input, device_group))
189
+ symm_collective_inputs.append(collective_input)
190
+ symm_collective_handles.append(
191
+ symm_mem.rendezvous(collective_input, device_group)
192
+ )
193
torch_output = torch.empty((local_M, N), dtype=torch.bfloat16, device=device)
194
symm_output = torch.empty_like(torch_output)
195
gemm_output = torch.empty((M, N), dtype=torch.bfloat16, device=device)
@@ -188,21 +198,27 @@ def benchmark_shape(
198
partial[M:].zero_()
199
symm_partial[M:].zero_()
200
191
- def make_torch_ring_ll_gemm_rs(
201
+ def make_torch_ring_ll_gemm_collective(
202
x: torch.Tensor, weight: torch.Tensor
203
) -> Callable[[], torch.Tensor]:
204
def run() -> torch.Tensor:
205
torch.mm(x, weight.T, out=partial[:M])
206
+ if all_reduce:
207
+ dist.all_reduce(partial, group=device_group)
208
+ return partial[:M]
209
dist.reduce_scatter_single(torch_output, partial, group=device_group)
210
return torch_output
211
212
return run
213
201
- def make_torch_ldmc_gemm_rs(
214
+ def make_torch_ldmc_gemm_collective(
215
x: torch.Tensor, weight: torch.Tensor
216
) -> Callable[[], torch.Tensor]:
217
def run() -> torch.Tensor:
218
torch.mm(x, weight.T, out=symm_partial[:M])
219
+ if all_reduce:
220
+ dist.all_reduce(symm_partial, group=device_group)
221
+ return symm_partial[:M]
222
dist.reduce_scatter_single(
223
symm_output,
224
symm_partial,
@@ -212,11 +228,11 @@ def benchmark_shape(
228
229
return run
230
215
- def make_fused_gemm_rs(
231
+ def make_fused_gemm_collective(
232
x: torch.Tensor, weight: torch.Tensor
233
) -> Callable[[], torch.Tensor]:
234
def run() -> torch.Tensor:
219
- return gemm_rs(x, weight)
235
+ return gemm_rs_ar(x, weight)
236
237
return run
238
@@ -229,18 +245,30 @@ def benchmark_shape(
245
246
return run
247
232
- def make_ring_ll_rs(rs_input: torch.Tensor) -> Callable[[], torch.Tensor]:
248
+ def make_ring_ll_collective(
249
+ collective_input: torch.Tensor,
250
+ ) -> Callable[[], torch.Tensor]:
251
def run() -> torch.Tensor:
234
- dist.reduce_scatter_single(torch_output, rs_input, group=device_group)
252
+ if all_reduce:
253
+ dist.all_reduce(collective_input, group=device_group)
254
+ return collective_input
255
+ dist.reduce_scatter_single(
256
+ torch_output, collective_input, group=device_group
257
+ )
258
return torch_output
259
260
return run
261
239
- def make_ldmc_rs(
240
- rs_input: torch.Tensor,
262
+ def make_ldmc_collective(
263
+ collective_input: torch.Tensor,
264
) -> Callable[[], torch.Tensor]:
265
def run() -> torch.Tensor:
243
- dist.reduce_scatter_single(symm_output, rs_input, group=device_group)
266
+ if all_reduce:
267
+ dist.all_reduce(collective_input, group=device_group)
268
+ return collective_input
269
+ dist.reduce_scatter_single(
270
+ symm_output, collective_input, group=device_group
271
+ )
272
return symm_output
273
274
return run
@@ -248,15 +276,15 @@ def benchmark_shape(
276
candidates = (
277
Candidate(
278
"ring_ll_us",
251
- [make_torch_ring_ll_gemm_rs(x, w) for x, w in zip(inputs, weights)],
279
+ [make_torch_ring_ll_gemm_collective(x, w) for x, w in zip(inputs, weights)],
280
),
281
Candidate(
282
"ldmc_us",
255
- [make_torch_ldmc_gemm_rs(x, w) for x, w in zip(inputs, weights)],
283
+ [make_torch_ldmc_gemm_collective(x, w) for x, w in zip(inputs, weights)],
284
),
285
Candidate(
258
- "gemm_rs_us",
259
- [make_fused_gemm_rs(x, w) for x, w in zip(inputs, weights)],
286
+ "gemm_rs_ar_us",
287
+ [make_fused_gemm_collective(x, w) for x, w in zip(inputs, weights)],
288
),
289
Candidate(
290
"torch_gemm_us",
@@ -264,19 +292,19 @@ def benchmark_shape(
292
check_correctness=False,
293
),
294
Candidate(
267
- "ring_ll_rs_us",
268
- [make_ring_ll_rs(x) for x in rs_inputs],
295
+ "ring_ll_collective_us",
296
+ [make_ring_ll_collective(x) for x in collective_inputs],
297
check_correctness=False,
298
),
299
Candidate(
272
- "ldmc_rs_us",
273
- [make_ldmc_rs(x) for x in symm_rs_inputs],
300
+ "ldmc_collective_us",
301
+ [make_ldmc_collective(x) for x in symm_collective_inputs],
302
check_correctness=False,
303
),
304
)
305
306
expected = candidates[0].runs[0]()
279
- rows = valid_rows(M, local_M, rank)
307
+ rows = M if all_reduce else valid_rows(M, local_M, rank)
308
for candidate in candidates[1:]:
309
if not candidate.check_correctness:
310
continue
@@ -290,7 +318,7 @@ def benchmark_shape(
318
)
319
320
candidate_graphs = {}
293
- graph_keepalive: list[object] = [symm_partial_handle, *symm_rs_handles]
321
+ graph_keepalive: list[object] = [symm_partial_handle, *symm_collective_handles]
322
for candidate in candidates:
323
stream = torch.cuda.Stream()
324
bundles = [capture_graph(run, stream, cpu_group) for run in candidate.runs]
@@ -306,70 +334,71 @@ def benchmark_shape(
334
device_barrier,
335
)
336
309
- best_nccl_rs_us = min(times["ring_ll_rs_us"], times["ldmc_rs_us"])
337
+ best_nccl_us = min(times["ring_ll_collective_us"], times["ldmc_collective_us"])
338
return {
339
+ "mode": mode.upper(),
340
"M": M,
341
"N": N,
342
"K": K,
343
**times,
315
- "best_nccl_rs_us": best_nccl_rs_us,
316
- "speedup_vs_ring_ll": times["ring_ll_us"] / times["gemm_rs_us"],
317
- "speedup_vs_ldmc": times["ldmc_us"] / times["gemm_rs_us"],
344
+ "best_nccl_us": best_nccl_us,
345
+ "speedup_vs_ring_ll": times["ring_ll_us"] / times["gemm_rs_ar_us"],
346
+ "speedup_vs_ldmc": times["ldmc_us"] / times["gemm_rs_ar_us"],
347
}
348
349
321
-def print_results(results: list[dict[str, float | int]]) -> None:
350
+def print_results(results: list[dict[str, float | int | str]]) -> None:
351
results_df = pd.DataFrame(results)
323
- end_to_end = results_df[
324
- [
325
- "M",
326
- "N",
327
- "K",
328
- "ring_ll_us",
329
- "ldmc_us",
330
- "gemm_rs_us",
331
- "speedup_vs_ring_ll",
332
- "speedup_vs_ldmc",
352
+ collective = str(results_df["mode"].iloc[0])
353
+ end_to_end = (
354
+ results_df[
355
+ [
356
+ "M",
357
+ "N",
358
+ "K",
359
+ "ring_ll_us",
360
+ "ldmc_us",
361
+ "gemm_rs_ar_us",
362
+ "speedup_vs_ring_ll",
363
+ "speedup_vs_ldmc",
364
+ ]
365
]
334
- ].rename(
335
- columns={
336
- "ring_ll_us": "Torch GEMM + NCCL RS (RING_LL) (us)",
337
- "ldmc_us": "Torch GEMM + NCCL RS (LDMC) (us)",
338
- "gemm_rs_us": "GEMM-RS (us)",
339
- "speedup_vs_ring_ll": "Speedup vs RING_LL",
340
- "speedup_vs_ldmc": "Speedup vs LDMC",
341
- }
342
- )
343
- end_to_end = end_to_end.round(
344
- {
345
- "Torch GEMM + NCCL RS (RING_LL) (us)": 2,
346
- "Torch GEMM + NCCL RS (LDMC) (us)": 2,
347
- "GEMM-RS (us)": 2,
348
- "Speedup vs RING_LL": 3,
349
- "Speedup vs LDMC": 3,
350
- }
366
+ .rename(
367
+ columns={
368
+ "ring_ll_us": f"Torch GEMM + NCCL {collective} (RING_LL) (us)",
369
+ "ldmc_us": f"Torch GEMM + NCCL {collective} (LDMC) (us)",
370
+ "gemm_rs_ar_us": f"GEMM-{collective} (us)",
371
+ "speedup_vs_ring_ll": "Speedup vs RING_LL",
372
+ "speedup_vs_ldmc": "Speedup vs LDMC",
373
+ }
374
+ )
375
+ .round(3)
376
)
352
- components = results_df[
353
- ["M", "N", "K", "torch_gemm_us", "best_nccl_rs_us", "gemm_rs_us"]
354
- ].rename(
355
- columns={
356
- "torch_gemm_us": "Torch GEMM (us)",
357
- "best_nccl_rs_us": "NCCL RS (best) (us)",
358
- "gemm_rs_us": "GEMM-RS (us)",
359
- }
377
+ components = (
378
+ results_df[["M", "N", "K", "torch_gemm_us", "best_nccl_us", "gemm_rs_ar_us"]]
379
+ .rename(
380
+ columns={
381
+ "torch_gemm_us": "Torch GEMM (us)",
382
+ "best_nccl_us": f"NCCL {collective} (best) (us)",
383
+ "gemm_rs_ar_us": f"GEMM-{collective} (us)",
384
+ }
385
+ )
386
+ .round(2)
387
)
361
- components = components.round(2)
388
363
- print("### End-to-end latency")
389
+ print(f"### {collective} end-to-end latency")
390
print(end_to_end.to_markdown(index=False))
365
- print("\n### Component latency")
391
+ print(f"\n### {collective} component latency")
392
print(components.to_markdown(index=False))
367
- print("\nNCCL RS (best) is the faster of RING_LL and LDMC for each shape.")
393
+ print(
394
+ f"\nNCCL {collective} (best) is the faster of RING_LL and LDMC "
395
+ "for each shape.\n"
396
+ )
397
398
399
def main() -> None:
400
args = parse_args()
372
- assert args.m and min(args.m) >= 128
401
+ assert args.m and min(args.m) > 0
402
assert args.n % 256 == 0
403
assert args.num_workspaces > 0
404
assert args.warmup_replays >= 0
@@ -401,12 +430,19 @@ def main() -> None:
430
sync_output = torch.empty_like(sync_input)
431
432
def device_barrier() -> None:
433
+ # Order the timed launch after a device-side rank rendezvous without
434
+ # including the rendezvous itself in the measured event interval.
435
pynccl_comm.all_reduce(sync_input, sync_output)
436
406
- gemm_rs = GemmRS(max_M=max(args.m), N=args.n)
437
+ gemm_rs_ar = GemmRsAr(
438
+ max_M=max(args.m),
439
+ N=args.n,
440
+ all_reduce=args.mode == "ar",
441
+ )
442
results = [
443
benchmark_shape(
409
- gemm_rs,
444
+ gemm_rs_ar,
445
+ args.mode,
446
M,
447
args.n,
448
K,
@@ -420,12 +456,12 @@ def main() -> None:
456
for K in K_values
457
for M in args.m
458
]
459
+ del gemm_rs_ar
460
461
if tp_group.rank_in_group == 0:
462
print_results(results)
463
464
dist.barrier(group=tp_group.cpu_group)
428
- del gemm_rs
465
cleanup_dist_env_and_memory()
466
467
tests/kernels/test_kimi_k3_gemm_rs_ar.py
renamed
+114
-47
@@ -31,6 +31,7 @@ def _reference(
31
weight: torch.Tensor,
32
world_size: int,
33
group: dist.ProcessGroup,
34
+ all_reduce: bool,
35
) -> torch.Tensor:
36
M = x.shape[0]
37
padded_M = (M + world_size - 1) // world_size * world_size
@@ -43,6 +44,10 @@ def _reference(
44
if padded_M > M:
45
partial[M:].zero_()
46
47
+ if all_reduce:
48
+ dist.all_reduce(partial, group=group)
49
+ return partial[:M]
50
+
51
output = torch.empty(
52
(padded_M // world_size, weight.shape[0]),
53
dtype=x.dtype,
@@ -57,7 +62,11 @@ def _assert_valid_rows_close(
62
expected: torch.Tensor,
63
M: int,
64
rank: int,
65
+ all_reduce: bool,
66
) -> None:
67
+ if all_reduce:
68
+ torch.testing.assert_close(actual, expected, rtol=5e-2, atol=4.0)
69
+ return
70
rank_start = rank * actual.shape[0]
71
valid_rows = min(max(M - rank_start, 0), actual.shape[0])
72
torch.testing.assert_close(
@@ -68,44 +77,24 @@ def _assert_valid_rows_close(
77
)
78
79
71
-def _worker(local_rank: int, world_size: int, master_port: int) -> None:
72
- # This module pulls in cute_dsl, which is unavailable off CUDA, so importing
73
- # it at module level would fail collection. Import it where it is used, as
74
- # `kda.py` does.
75
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import GemmRS
80
+def _run_mode(
81
+ *,
82
+ all_reduce: bool,
83
+ device: torch.device,
84
+ group: dist.ProcessGroup,
85
+ rank: int,
86
+ world_size: int,
87
+ weights: dict[int, torch.Tensor],
88
+) -> None:
89
+ # cute_dsl is unavailable off CUDA, so import it only inside the GPU worker.
90
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr
91
77
- device = torch.device("cuda", local_rank)
78
- torch.accelerator.set_device_index(device)
79
- update_environment_variables(
80
- {
81
- "RANK": str(local_rank),
82
- "LOCAL_RANK": str(local_rank),
83
- "WORLD_SIZE": str(world_size),
84
- "MASTER_ADDR": "localhost",
85
- "MASTER_PORT": str(master_port),
86
- }
92
+ gemm_rs_ar = GemmRsAr(
93
+ max_M=max(M for M, _ in _SHAPES),
94
+ N=_N,
95
+ all_reduce=all_reduce,
96
)
88
-
89
- init_distributed_environment()
90
- with ensure_current_vllm_config():
91
- initialize_model_parallel(tensor_model_parallel_size=world_size)
92
-
93
- group = dist.group.WORLD
94
- rank = dist.get_rank(group)
95
- gemm_rs = GemmRS(max_M=max(M for M, _ in _SHAPES), N=_N)
96
-
97
- weight_generator = torch.Generator(device=device)
97
input_generator = torch.Generator(device=device)
99
- weights = {}
100
- for K in {K for _, K in _SHAPES}:
101
- weight_generator.manual_seed(1000 + rank * 10 + K)
102
- weights[K] = torch.randn(
103
- _N,
104
- K,
105
- dtype=torch.bfloat16,
106
- device=device,
107
- generator=weight_generator,
108
- )
98
99
# Alternating shapes exercise producer-flag reuse across different grids,
100
# CTA-group choices, and both BN=128 and BN=256 dispatches.
@@ -118,12 +107,29 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
107
device=device,
108
generator=input_generator,
109
)
121
- expected = _reference(x, weights[K], world_size, group)
122
- actual = gemm_rs(x, weights[K])
110
+ expected = _reference(x, weights[K], world_size, group, all_reduce)
111
+ actual = gemm_rs_ar(x, weights[K])
112
+ torch.accelerator.synchronize(device)
113
+ _assert_valid_rows_close(actual, expected, M, rank, all_reduce)
114
+
115
+ if all_reduce:
116
+ # AR outputs must remain valid after the symmetric workspace is reused.
117
+ lifetime_M, lifetime_K = 257, 768
118
+ input_generator.manual_seed(2500)
119
+ lifetime_x = torch.randn(
120
+ lifetime_M,
121
+ lifetime_K,
122
+ dtype=torch.bfloat16,
123
+ device=device,
124
+ generator=input_generator,
125
+ )
126
+ first_output = gemm_rs_ar(lifetime_x, weights[lifetime_K])
127
+ first_snapshot = first_output.clone()
128
+ second_output = gemm_rs_ar(-lifetime_x, weights[lifetime_K])
129
torch.accelerator.synchronize(device)
124
- _assert_valid_rows_close(actual, expected, M, rank)
130
+ assert first_output.data_ptr() != second_output.data_ptr()
131
+ torch.testing.assert_close(first_output, first_snapshot, rtol=0, atol=0)
132
126
- # Exercise GEMM-RS under CUDA graph capture and replay.
133
graph_M, graph_K = 1025, 4224
134
input_generator.manual_seed(3000)
135
graph_x = torch.randn(
@@ -133,42 +139,103 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
139
device=device,
140
generator=input_generator,
141
)
136
- graph_expected = _reference(graph_x, weights[graph_K], world_size, group)
142
+ graph_expected = _reference(
143
+ graph_x,
144
+ weights[graph_K],
145
+ world_size,
146
+ group,
147
+ all_reduce,
148
+ )
149
graph_output = torch.empty_like(graph_expected)
150
151
capture_stream = torch.cuda.Stream()
152
capture_stream.wait_stream(torch.cuda.current_stream())
153
with torch.cuda.stream(capture_stream):
154
for _ in range(3):
143
- graph_output.copy_(gemm_rs(graph_x, weights[graph_K]))
155
+ graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
156
capture_stream.synchronize()
157
dist.barrier(group=group)
158
159
graph = torch.cuda.CUDAGraph()
160
with torch.cuda.graph(graph, stream=capture_stream):
149
- graph_output.copy_(gemm_rs(graph_x, weights[graph_K]))
161
+ graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
162
torch.cuda.current_stream().wait_stream(capture_stream)
163
dist.barrier(group=group)
164
165
for _ in range(3):
166
graph.replay()
167
torch.accelerator.synchronize(device)
156
- _assert_valid_rows_close(graph_output, graph_expected, graph_M, rank)
168
+ _assert_valid_rows_close(
169
+ graph_output,
170
+ graph_expected,
171
+ graph_M,
172
+ rank,
173
+ all_reduce,
174
+ )
175
176
dist.barrier(group=group)
159
- del gemm_rs
177
+ del gemm_rs_ar, capture_stream, graph, graph_output
178
+
179
+
180
+def _worker(local_rank: int, world_size: int, master_port: int) -> None:
181
+ # This module pulls in cute_dsl, which is unavailable off CUDA, so importing
182
+ # it at module level would fail collection. Import it where it is used, as
183
+ # `kda.py` does.
184
+
185
+ device = torch.device("cuda", local_rank)
186
+ torch.accelerator.set_device_index(device)
187
+ update_environment_variables(
188
+ {
189
+ "RANK": str(local_rank),
190
+ "LOCAL_RANK": str(local_rank),
191
+ "WORLD_SIZE": str(world_size),
192
+ "MASTER_ADDR": "localhost",
193
+ "MASTER_PORT": str(master_port),
194
+ }
195
+ )
196
+
197
+ init_distributed_environment()
198
+ with ensure_current_vllm_config():
199
+ initialize_model_parallel(tensor_model_parallel_size=world_size)
200
+
201
+ group = dist.group.WORLD
202
+ rank = dist.get_rank(group)
203
+
204
+ weight_generator = torch.Generator(device=device)
205
+ weights = {}
206
+ for K in {K for _, K in _SHAPES}:
207
+ weight_generator.manual_seed(1000 + rank * 10 + K)
208
+ weights[K] = torch.randn(
209
+ _N,
210
+ K,
211
+ dtype=torch.bfloat16,
212
+ device=device,
213
+ generator=weight_generator,
214
+ )
215
+
216
+ # Production binds one mode per worker; exercise both mode-bound instances
217
+ # sequentially in this test without implying that both are initialized.
218
+ for all_reduce in (False, True):
219
+ _run_mode(
220
+ all_reduce=all_reduce,
221
+ device=device,
222
+ group=group,
223
+ rank=rank,
224
+ world_size=world_size,
225
+ weights=weights,
226
+ )
227
cleanup_dist_env_and_memory()
228
229
230
@pytest.mark.distributed(num_gpus=2)
231
@pytest.mark.skipif(
232
not current_platform.is_device_capability_family(100),
166
- reason="Kimi-K3 GEMM-RS requires SM100",
233
+ reason="Kimi-K3 GEMM-RS/AR requires SM100",
234
)
168
-def test_kimi_k3_gemm_rs(monkeypatch: pytest.MonkeyPatch) -> None:
235
+def test_kimi_k3_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch) -> None:
236
world_size = 2
237
if torch.accelerator.device_count() < world_size:
171
- pytest.skip("GEMM-RS requires two GPUs")
238
+ pytest.skip("GEMM-RS/AR requires two GPUs")
239
240
monkeypatch.setenv("NCCL_CUMEM_ENABLE", "1")
241
monkeypatch.setenv("NCCL_NVLS_ENABLE", "1")
vllm/envs.py
+4
@@ -210,6 +210,7 @@ if TYPE_CHECKING:
210
VLLM_MOE_SKIP_PADDING: bool = True
211
VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT: bool = False
212
VLLM_KIMI_K3_AUX_ATTN_RES_STREAM: bool = False
213
+ VLLM_KIMI_K3_GEMM_AR: bool = True
214
VLLM_KIMI_K3_GEMM_RS: bool = False
215
VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER: bool = True
216
VLLM_USE_FLASHINFER_MOE_INT4: bool = False
@@ -1608,6 +1609,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
1609
"VLLM_KIMI_K3_AUX_ATTN_RES_STREAM": lambda: bool(
1610
int(os.getenv("VLLM_KIMI_K3_AUX_ATTN_RES_STREAM", "0"))
1611
),
1612
+ # Use the SM100 BF16 GEMM-AR kernel for eligible Kimi-K3 row-parallel
1613
+ # attention projections. All TP ranks must belong to one NVLink domain.
1614
+ "VLLM_KIMI_K3_GEMM_AR": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_AR", "1"))),
1615
# Use the SM100 BF16 GEMM-RS kernel for eligible Kimi-K3 sequence-parallel
1616
# row-parallel projections. All TP ranks must belong to one NVLink domain.
1617
"VLLM_KIMI_K3_GEMM_RS": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_RS", "0"))),
vllm/model_executor/warmup/kernel_warmup.py
+15
@@ -6,6 +6,7 @@ This is useful specifically for JIT'ed kernels as we don't want JIT'ing to
6
happen during model execution.
7
"""
8
9
+import sys
10
import time
11
from typing import TYPE_CHECKING
12
@@ -95,6 +96,18 @@ def _warmup_ll_bf16_router_gemm(model: torch.nn.Module) -> None:
96
)
97
98
99
+def _warmup_kimi_k3_gemm_rs_ar() -> None:
100
+ # Kimi-K3 model construction imports this module only when GEMM-RS/AR is
101
+ # enabled and initializes its singleton before kernel_warmup runs. Avoid
102
+ # importing it here so other models do not compile the RS/AR variants.
103
+ module = sys.modules.get("vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar")
104
+ if module is None:
105
+ return
106
+ compiled = module.warmup_gemm_rs_ar()
107
+ if compiled:
108
+ logger.info_once("Warmed up %d Kimi-K3 GEMM-RS/AR variants.", compiled)
109
+
110
+
111
def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
112
from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
113
minimax_m3_msa_warmup,
@@ -146,6 +159,8 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
159
if current_platform.has_device_capability(90):
160
_warmup_ll_bf16_router_gemm(worker.get_model())
161
162
+ _warmup_kimi_k3_gemm_rs_ar()
163
+
164
if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
165
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
166
# to the shared JIT warmup infrastructure.
vllm/models/kimi_k3/nvidia/kda.py
+13
-13
@@ -319,7 +319,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
319
config: KimiLinearConfig,
320
vllm_config: VllmConfig,
321
prefix: str = "",
322
- run_gemm_rs: bool = False,
322
+ run_gemm_rs_ar: bool = False,
323
) -> None:
324
super().__init__(config, vllm_config, prefix)
325
self.use_recoverssm = self.cache_config.use_kda_recoverssm
@@ -491,14 +491,18 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
491
quant_config=self.quant_config,
492
prefix=f"{prefix}.o_proj",
493
)
494
- self.run_gemm_rs = run_gemm_rs
495
- if self.run_gemm_rs:
496
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
494
+ self.gemm_rs_ar = None
495
+ if run_gemm_rs_ar:
496
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
497
+ get_gemm_rs_ar,
498
+ )
499
498
- self.run_gemm_rs = get_gemm_rs().can_run(self.o_proj)
499
- if not self.run_gemm_rs:
500
+ gemm_rs_ar = get_gemm_rs_ar()
501
+ if gemm_rs_ar.can_run(self.o_proj):
502
+ self.gemm_rs_ar = gemm_rs_ar
503
+ else:
504
logger.warning_once(
501
- "GEMM-RS is disabled for %s due to an incompatible projection.",
505
+ "GEMM-RS/AR is disabled for %s due to an incompatible projection.",
506
prefix,
507
)
508
compilation_config = vllm_config.compilation_config
@@ -541,12 +545,8 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
545
core_attn_out=core_attn_out,
546
)
547
core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
544
- if self.run_gemm_rs:
545
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
546
-
547
- gemm_rs = get_gemm_rs()
548
- if gemm_rs.should_run(core_attn_out):
549
- return gemm_rs(core_attn_out, self.o_proj.weight)
548
+ if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(core_attn_out):
549
+ return self.gemm_rs_ar(core_attn_out, self.o_proj.weight)
550
return self.o_proj(core_attn_out)[0]
551
552
@eager_break_during_capture
vllm/models/kimi_k3/nvidia/mla.py
+13
-13
@@ -134,7 +134,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
134
aux_stream: torch.cuda.Stream | None = None,
135
use_rope: bool = False,
136
non_causal_multi_token_decode: bool = False,
137
- run_gemm_rs: bool = False,
137
+ run_gemm_rs_ar: bool = False,
138
) -> None:
139
super().__init__()
140
self.hidden_size = hidden_size
@@ -271,14 +271,18 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
271
quant_config=quant_config,
272
prefix=f"{prefix}.o_proj",
273
)
274
- self.run_gemm_rs = run_gemm_rs
275
- if self.run_gemm_rs:
276
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
274
+ self.gemm_rs_ar = None
275
+ if run_gemm_rs_ar:
276
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
277
+ get_gemm_rs_ar,
278
+ )
279
278
- self.run_gemm_rs = get_gemm_rs().can_run(self.o_proj)
279
- if not self.run_gemm_rs:
280
+ gemm_rs_ar = get_gemm_rs_ar()
281
+ if gemm_rs_ar.can_run(self.o_proj):
282
+ self.gemm_rs_ar = gemm_rs_ar
283
+ else:
284
logger.warning_once(
281
- "GEMM-RS is disabled for %s due to an incompatible projection.",
285
+ "GEMM-RS/AR is disabled for %s due to an incompatible projection.",
286
prefix,
287
)
288
@@ -549,12 +553,8 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
553
if gate is not None:
554
attn_out = _gate_sigmoid_mul(attn_out, gate)
555
552
- if self.run_gemm_rs:
553
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
554
-
555
- gemm_rs = get_gemm_rs()
556
- if gemm_rs.should_run(attn_out):
557
- return gemm_rs(attn_out, self.o_proj.weight)
556
+ if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(attn_out):
557
+ return self.gemm_rs_ar(attn_out, self.o_proj.weight)
558
559
return self.o_proj(attn_out)[0]
560
vllm/models/kimi_k3/nvidia/model.py
+60
-36
@@ -154,15 +154,18 @@ def shard_sequence_parallel_mlp(
154
)
155
156
157
-def maybe_init_gemm_rs(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
158
- if not envs.VLLM_KIMI_K3_GEMM_RS:
157
+def maybe_init_gemm_rs_ar(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
158
+ # Both feature flags may be enabled; the worker's static SP topology binds
159
+ # its singleton to exactly one mode.
160
+ all_reduce = not use_sequence_parallel
161
+ mode = "GEMM-AR" if all_reduce else "GEMM-RS"
162
+ enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_KIMI_K3_GEMM_RS
163
+ if not enabled:
164
return False
165
166
parallel_config = vllm_config.parallel_config
167
tp_size = parallel_config.tensor_parallel_size
163
- if not use_sequence_parallel:
164
- reason = "sequence parallelism is disabled"
165
- elif parallel_config.use_ubatching:
168
+ if parallel_config.use_ubatching:
169
reason = "ubatching is enabled"
170
elif vllm_config.model_config.dtype != torch.bfloat16:
171
reason = "the model dtype is not BF16"
@@ -178,17 +181,32 @@ def maybe_init_gemm_rs(vllm_config: VllmConfig, use_sequence_parallel: bool) ->
181
reason = None
182
183
if reason is not None:
181
- logger.warning_once("GEMM-RS was requested but is disabled because %s.", reason)
184
+ logger.warning_once("%s is disabled because %s.", mode, reason)
185
return False
186
184
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import init_gemm_rs
187
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import init_gemm_rs_ar
188
189
config = vllm_config.model_config.hf_text_config
187
- init_gemm_rs(
188
- max_M=vllm_config.scheduler_config.max_num_batched_tokens,
189
- N=config.hidden_size,
190
- )
191
- logger.info_once("GEMM-RS is enabled.")
190
+ try:
191
+ init_gemm_rs_ar(
192
+ max_M=vllm_config.scheduler_config.max_num_batched_tokens,
193
+ N=config.hidden_size,
194
+ all_reduce=all_reduce,
195
+ )
196
+ except RuntimeError as e:
197
+ logger.warning_once(
198
+ "%s is disabled because initialization failed: %s. This may mean "
199
+ "the TP ranks do not share one NVLink domain.",
200
+ mode,
201
+ e,
202
+ )
203
+ return False
204
+ if all_reduce:
205
+ logger.info_once(
206
+ "GEMM-AR is enabled. To disable it, set VLLM_KIMI_K3_GEMM_AR=0."
207
+ )
208
+ else:
209
+ logger.info_once("GEMM-RS is enabled.")
210
return True
211
212
@@ -217,7 +235,7 @@ class KimiMLP(nn.Module):
235
reduce_results: bool = True,
236
use_sequence_parallel: bool = False,
237
can_shard_sequence_parallel: bool = False,
220
- run_gemm_rs: bool = False,
238
+ run_gemm_rs_ar: bool = False,
239
prefix: str = "",
240
activation_situ_beta: float | None = None,
241
activation_situ_linear_beta: float | None = None,
@@ -230,7 +248,6 @@ class KimiMLP(nn.Module):
248
use_sequence_parallel,
249
can_shard_sequence_parallel,
250
)
233
- self.run_gemm_rs = self.shard_sequence_parallel and run_gemm_rs
251
replicate = use_sequence_parallel and not self.shard_sequence_parallel
252
253
self.gate_up_proj = MergedColumnParallelLinear(
@@ -252,13 +269,22 @@ class KimiMLP(nn.Module):
269
disable_tp=replicate,
270
prefix=f"{prefix}.down_proj",
271
)
255
- if self.run_gemm_rs:
256
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
272
+ self.gemm_rs_ar = None
273
+ # RS requires sequence sharding; AR operates on replicated tokens.
274
+ use_gemm_rs_ar = self.shard_sequence_parallel or (
275
+ not use_sequence_parallel and reduce_results
276
+ )
277
+ if use_gemm_rs_ar and run_gemm_rs_ar:
278
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
279
+ get_gemm_rs_ar,
280
+ )
281
258
- self.run_gemm_rs = get_gemm_rs().can_run(self.down_proj)
259
- if not self.run_gemm_rs:
282
+ gemm_rs_ar = get_gemm_rs_ar()
283
+ if gemm_rs_ar.can_run(self.down_proj):
284
+ self.gemm_rs_ar = gemm_rs_ar
285
+ else:
286
logger.warning_once(
261
- "GEMM-RS is disabled for %s due to an incompatible projection.",
287
+ "GEMM-RS/AR is disabled for %s due to an incompatible projection.",
288
prefix,
289
)
290
if hidden_act == "silu":
@@ -284,12 +310,8 @@ class KimiMLP(nn.Module):
310
gate_up, _ = self.gate_up_proj(x)
311
x = self.act_fn(gate_up)
312
287
- if self.run_gemm_rs:
288
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
289
-
290
- gemm_rs = get_gemm_rs()
291
- if gemm_rs.should_run(x):
292
- return gemm_rs(x, self.down_proj.weight)
313
+ if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(x):
314
+ return self.gemm_rs_ar(x, self.down_proj.weight)
315
316
x, _ = self.down_proj(x)
317
if self.shard_sequence_parallel:
@@ -527,7 +549,7 @@ class KimiMoE(nn.Module):
549
prefix: str = "",
550
layer_idx: int = 0,
551
use_sequence_parallel: bool = False,
530
- run_gemm_rs: bool = False,
552
+ run_gemm_rs_ar: bool = False,
553
):
554
super().__init__()
555
hidden_size = config.hidden_size
@@ -616,7 +638,7 @@ class KimiMoE(nn.Module):
638
# FusedMoE path below hands them to the runner, which fuses
639
# their reduction and assumes the replicated layout.
640
can_shard_sequence_parallel=self.use_mega_moe,
619
- run_gemm_rs=run_gemm_rs,
641
+ run_gemm_rs_ar=run_gemm_rs_ar,
642
prefix=f"{prefix}.shared_experts",
643
activation_situ_beta=activation_situ_beta,
644
activation_situ_linear_beta=activation_situ_linear_beta,
@@ -838,7 +860,7 @@ class KimiDecoderLayer(nn.Module):
860
vllm_config: VllmConfig,
861
prefix: str = "",
862
aux_stream: torch.cuda.Stream | None = None,
841
- run_gemm_rs: bool = False,
863
+ run_gemm_rs_ar: bool = False,
864
) -> None:
865
super().__init__()
866
self.hidden_size = config.hidden_size
@@ -873,7 +895,7 @@ class KimiDecoderLayer(nn.Module):
895
config,
896
vllm_config,
897
prefix=f"{prefix}.self_attn",
876
- run_gemm_rs=run_gemm_rs,
898
+ run_gemm_rs_ar=run_gemm_rs_ar,
899
)
900
self._self_attn_writes_output = False
901
else:
@@ -910,7 +932,7 @@ class KimiDecoderLayer(nn.Module):
932
quant_config=quant_config,
933
prefix=f"{prefix}.self_attn",
934
aux_stream=aux_stream,
913
- run_gemm_rs=run_gemm_rs,
935
+ run_gemm_rs_ar=run_gemm_rs_ar,
936
)
937
self._self_attn_writes_output = False
938
@@ -925,7 +947,7 @@ class KimiDecoderLayer(nn.Module):
947
prefix=f"{prefix}.block_sparse_moe",
948
layer_idx=layer_idx,
949
use_sequence_parallel=self.use_sequence_parallel,
928
- run_gemm_rs=run_gemm_rs,
950
+ run_gemm_rs_ar=run_gemm_rs_ar,
951
)
952
self.mlp = self.block_sparse_moe
953
else:
@@ -937,7 +959,7 @@ class KimiDecoderLayer(nn.Module):
959
prefix=f"{prefix}.mlp",
960
use_sequence_parallel=self.use_sequence_parallel,
961
can_shard_sequence_parallel=True,
940
- run_gemm_rs=run_gemm_rs,
962
+ run_gemm_rs_ar=run_gemm_rs_ar,
963
activation_situ_beta=config.activation_situ_beta,
964
activation_situ_linear_beta=config.activation_situ_linear_beta,
965
)
@@ -1118,9 +1140,11 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1140
1141
self.vocab_size = config.vocab_size
1142
1121
- # GEMM-RS uses NCCL symmetric-memory multicast, which requires all TP
1122
- # ranks to belong to one NVLink domain.
1123
- self.run_gemm_rs = maybe_init_gemm_rs(vllm_config, self.use_sequence_parallel)
1143
+ # GEMM-RS/AR uses NCCL symmetric-memory multicast, which requires all
1144
+ # TP ranks to belong to one NVLink domain.
1145
+ self.run_gemm_rs_ar = maybe_init_gemm_rs_ar(
1146
+ vllm_config, self.use_sequence_parallel
1147
+ )
1148
1149
if get_pp_group().is_first_rank:
1150
self.embed_tokens = VocabParallelEmbedding(
@@ -1142,7 +1166,7 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1166
vllm_config,
1167
prefix,
1168
aux_stream=aux_stream,
1145
- run_gemm_rs=self.run_gemm_rs,
1169
+ run_gemm_rs_ar=self.run_gemm_rs_ar,
1170
)
1171
1172
self.start_layer, self.end_layer, self.layers = make_layers(
vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py
renamed
+137
-43
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-"""SM100 BF16 GEMM with a fused tensor-parallel reduce-scatter."""
4
+"""SM100 BF16 GEMM with fused tensor-parallel reduce-scatter/all-reduce."""
5
6
# Based on CUTLASS's Blackwell distributed GEMM-RS example at dcf215a.
7
# See https://github.com/NVIDIA/cutlass/issues/3117 for memory semantics.
@@ -32,7 +32,7 @@ def nanosleep(ns: int, *, loc=None, ip=None) -> None:
32
33
@dsl_user_op
34
def multimem_ld_reduce_16B(x: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
35
- # NOTE: assume x is contiguous
35
+ # The vector instruction assumes x is contiguous and 16-byte aligned.
36
assert x.element_type == BFloat16
37
vec_type = ".v4.bf16x2"
38
@@ -66,15 +66,40 @@ def multimem_ld_reduce_16B(x: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
66
return cute.recast_tensor(y, x.element_type)
67
68
69
-class Sm100GemmRsBF16:
69
+@dsl_user_op
70
+def multimem_st_16B(dst: cute.Tensor, value: cute.Tensor, *, loc=None, ip=None) -> None:
71
+ # The vector instruction assumes dst and value are contiguous BF16x8 vectors
72
+ # and dst is 16-byte aligned.
73
+ assert dst.element_type == BFloat16
74
+ assert value.element_type == BFloat16
75
+ ptr = dst.iterator.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
76
+ regs = cute.recast_tensor(value, Int32, loc=loc, ip=ip)
77
+ llvm.inline_asm(
78
+ None,
79
+ [ptr] + [regs[i].ir_value(loc=loc, ip=ip) for i in range(4)],
80
+ "multimem.st.relaxed.sys.global.v4.f32 [$0], {$1, $2, $3, $4};",
81
+ "l,r,r,r,r",
82
+ has_side_effects=True,
83
+ loc=loc,
84
+ ip=ip,
85
+ )
86
+
87
+
88
+class Sm100GemmRsArBF16:
89
def __init__(
71
- self, rank: int, num_ranks: int, BN: int = 128, cta_group: int = 1
90
+ self,
91
+ rank: int,
92
+ num_ranks: int,
93
+ BN: int = 128,
94
+ cta_group: int = 1,
95
+ all_reduce: bool = False,
96
) -> None:
97
self.rank = rank
98
self.num_ranks = num_ranks
99
BM, BK = 128, 64
100
self.cta_tile = (BM, BN, BK)
101
self.cta_group = cta_group
102
+ self.all_reduce = all_reduce
103
104
smem_bytes = get_smem_capacity_in_bytes()
105
self.stage_size = (BM + (BN // cta_group)) * BK * 2
@@ -190,6 +215,9 @@ class Sm100GemmRsBF16:
215
216
M, K = A_tma.tma_tensor.shape
217
N, _ = B_tma.tma_tensor.shape
218
+ local_M = (
219
+ partial_uc.shape[0] // num_ranks if self.all_reduce else output.shape[0]
220
+ )
221
grid_m = cute.ceil_div(M, BM)
222
# Keep 2-CTA clusters within a single N tile.
223
grid_m = cute.ceil_div(grid_m, cta_group) * cta_group
@@ -322,7 +350,6 @@ class Sm100GemmRsBF16:
350
# Keep epilogue in warps 0-3 and communication in warps 4-7.
351
# Swapping the warpgroups can hang due to warp scheduling.
352
tid_ = tid % 128
325
- local_M = output.shape[0]
353
354
# Offset in the [M, N] GEMM result.
355
rank_start = self.rank * local_M
@@ -338,7 +365,8 @@ class Sm100GemmRsBF16:
365
max_tile_rows = BM // num_ranks
366
vecs_per_tile = max_tile_rows * vec_cols
367
partial_vecs = cute.zipped_divide(partial_mc, (1, vec_width))
341
- output_vecs = cute.zipped_divide(output, (1, vec_width))
368
+ if cutlass.const_expr(not self.all_reduce):
369
+ output_vecs = cute.zipped_divide(output, (1, vec_width))
370
371
for tile_id in range(raw_bid, total_tiles, num_bids):
372
rs_bid_m = tile_id % grid_m
@@ -400,7 +428,6 @@ class Sm100GemmRsBF16:
428
reduced_vec.store(tmp.load())
429
reduced_vecs.append(reduced_vec)
430
403
- # Store the result to local L2.
431
for vec_iter in cutlass.range_constexpr(vecs_per_tile // 128):
432
vec_idx = tid_ + vec_iter * 128
433
@@ -409,11 +436,19 @@ class Sm100GemmRsBF16:
436
col = bid_n * vec_cols + vec_idx % vec_cols
437
438
if local_row < local_row_end and global_row < M:
412
- cute.copy(
413
- st_atom,
414
- reduced_vecs[vec_iter],
415
- output_vecs[None, (local_row, col)],
416
- )
439
+ if cutlass.const_expr(self.all_reduce):
440
+ # Broadcast the result to all ranks.
441
+ multimem_st_16B(
442
+ partial_vecs[None, (global_row, col)],
443
+ reduced_vecs[vec_iter],
444
+ )
445
+ else:
446
+ # Store the result to local L2.
447
+ cute.copy(
448
+ st_atom,
449
+ reduced_vecs[vec_iter],
450
+ output_vecs[None, (local_row, col)],
451
+ )
452
cute.arch.barrier(barrier_id=BAR_COMM, number_of_threads=128)
453
454
# Release each GEMM tile consumed by this RS tile. The last
@@ -481,19 +516,21 @@ class Sm100GemmRsBF16:
516
num_ranks,
517
)
518
484
- # Exit barrier. GPU scope is sufficient because the kernel writes
485
- # local global memory and only needs to flush it to local L2.
519
+ # Exit barrier. GPU scope is sufficient for RS because the kernel
520
+ # writes local global memory and only needs to flush it to local L2.
521
+ # AR uses system scope to flush the multimem stores to remote L2.
522
cute.arch.barrier(barrier_id=BAR_COMM, number_of_threads=128)
523
if tid_ == 0:
524
exit_flag = total_tiles + raw_bid
525
+ scope = "sys" if self.all_reduce else "gpu"
526
utils.distributed.multimem_red_add1(
490
- flags_mc_ptr + exit_flag, order="release", scope="gpu"
527
+ flags_mc_ptr + exit_flag, order="release", scope=scope
528
)
529
utils.distributed.spin_lock_atom_cas_acquire_wait(
530
flags_uc_ptr + exit_flag,
531
expected_val=num_ranks,
532
reset_val=0,
496
- scope="gpu",
533
+ scope=scope,
534
)
535
536
else:
@@ -564,8 +601,8 @@ class Sm100GemmRsBF16:
601
if tid_ == 0 and bid_m * BM < M:
602
gemm_start = bid_m * BM
603
gemm_end = min(gemm_start + BM, M)
567
- first_owner = gemm_start // output.shape[0]
568
- last_owner = (gemm_end - 1) // output.shape[0]
604
+ first_owner = gemm_start // local_M
605
+ last_owner = (gemm_end - 1) // local_M
606
607
# signal to all consuming ranks (can be more than 1)
608
for rank in cutlass.range_constexpr(num_ranks):
@@ -595,7 +632,13 @@ class Sm100GemmRsBF16:
632
633
@cache
634
@staticmethod
598
- def compile(rank: int, num_ranks: int, BN: int, cta_group: int):
635
+ def compile(
636
+ rank: int,
637
+ num_ranks: int,
638
+ BN: int,
639
+ cta_group: int,
640
+ all_reduce: bool = False,
641
+ ):
642
M = cute.sym_int()
643
padded_M = cute.sym_int()
644
N = cute.sym_int()
@@ -616,18 +659,24 @@ class Sm100GemmRsBF16:
659
assumed_align=32,
660
)
661
partial_mc_ptr = nullptr(BFloat16, cute.AddressSpace.gmem, assumed_align=32)
619
- output = make_fake_tensor(
620
- BFloat16,
621
- (local_M, N),
622
- (cute.sym_int64(divisibility=16), 1),
623
- assumed_align=32,
662
+ output = (
663
+ None
664
+ if all_reduce
665
+ else make_fake_tensor(
666
+ BFloat16,
667
+ (local_M, N),
668
+ (cute.sym_int64(divisibility=16), 1),
669
+ assumed_align=32,
670
+ )
671
)
672
flags = make_fake_tensor(Int32, (num_flags,), (1,), assumed_align=16)
673
flags_mc_ptr = nullptr(Int32, cute.AddressSpace.gmem, assumed_align=16)
674
peer_flag_ptr = nullptr(Int64, cute.AddressSpace.gmem, assumed_align=8)
675
676
stream = make_fake_stream(use_tvm_ffi_env_stream=True)
630
- kernel = Sm100GemmRsBF16(rank, num_ranks, BN, cta_group)
677
+ kernel = Sm100GemmRsArBF16(
678
+ rank, num_ranks, BN, cta_group, all_reduce=all_reduce
679
+ )
680
return cute.compile(
681
kernel,
682
A,
@@ -644,13 +693,20 @@ class Sm100GemmRsBF16:
693
)
694
695
647
-class GemmRS:
648
- """Own the max-sized symmetric workspace for Kimi-K3 GEMM-RS launches.
696
+class GemmRsAr:
697
+ """Own the symmetric workspace for Kimi-K3 GEMM-RS/AR launches.
698
699
All TP ranks must belong to one NVLink domain for multimem instructions.
700
+
701
+ Each instance is bound to either RS or AR. A vLLM worker has one static
702
+ sequence-parallel topology, so the process-wide singleton only needs one
703
+ mode. Two independent mode-specific singletons would lift that restriction
704
+ but duplicate the large symmetric workspace. A future mixed-mode design
705
+ should instead use lightweight RS/AR frontends over one shared multicast
706
+ workspace; that is outside this integration's current scope.
707
"""
708
653
- def __init__(self, *, max_M: int, N: int) -> None:
709
+ def __init__(self, *, max_M: int, N: int, all_reduce: bool = False) -> None:
710
tp_group = get_tp_group()
711
group = tp_group.device_group
712
rank = tp_group.rank_in_group
@@ -667,9 +723,12 @@ class GemmRS:
723
self.max_M = max_M
724
self.N = N
725
self.device = device
726
+ self.all_reduce = all_reduce
727
728
self.partial = symm_mem.empty((max_M, N), dtype=torch.bfloat16, device=device)
729
self.partial_handle = symm_mem.rendezvous(self.partial, group)
730
+ if self.partial_handle.multicast_ptr == 0:
731
+ raise RuntimeError("GEMM-RS/AR requires NVLink multicast memory")
732
self.partial_mc_ptr = make_ptr(
733
BFloat16,
734
self.partial_handle.multicast_ptr,
@@ -684,6 +743,8 @@ class GemmRS:
743
max_flags = grid_m * (N // 128) + self.num_sms
744
self.flags = symm_mem.empty(max_flags, dtype=torch.int32, device=device)
745
self.flags_handle = symm_mem.rendezvous(self.flags, group)
746
+ if self.flags_handle.multicast_ptr == 0:
747
+ raise RuntimeError("GEMM-RS/AR requires NVLink multicast memory")
748
self.flags.zero_()
749
self.flags_mc_ptr = make_ptr(
750
Int32,
@@ -698,8 +759,6 @@ class GemmRS:
759
assumed_align=8,
760
)
761
701
- assert self.partial_handle.multicast_ptr != 0
702
- assert self.flags_handle.multicast_ptr != 0
762
torch.accelerator.synchronize(device)
763
tp_group.barrier()
764
@@ -721,7 +780,8 @@ class GemmRS:
780
)
781
782
def should_run(self, x: torch.Tensor) -> bool:
724
- # Small-M shapes are supported but faster on the existing LL path.
783
+ # Use the same threshold for RS and AR for now. Small-M shapes are
784
+ # supported but faster on the existing LL path.
785
return x.shape[0] >= 128
786
787
def __call__(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
@@ -752,12 +812,15 @@ class GemmRS:
812
num_ctas = num_ctas // cta_group * cta_group
813
assert self.flags.numel() >= num_tiles + num_ctas
814
755
- output = torch.empty((local_M, N), dtype=torch.bfloat16, device=self.device)
756
- compiled = Sm100GemmRsBF16.compile(
815
+ output = None
816
+ if not self.all_reduce:
817
+ output = torch.empty((local_M, N), dtype=torch.bfloat16, device=self.device)
818
+ compiled = Sm100GemmRsArBF16.compile(
819
self.rank,
820
self.world_size,
821
BN,
822
cta_group,
823
+ self.all_reduce,
824
)
825
compiled(
826
x,
@@ -770,21 +833,52 @@ class GemmRS:
833
self.peer_flag_ptr,
834
num_ctas,
835
)
836
+ if self.all_reduce:
837
+ # AttnRes may retain output past the next workspace reuse.
838
+ # A future kernel could overlap this copy using an extra warp or
839
+ # the communication warp.
840
+ return self.partial[:M].clone()
841
+ assert output is not None
842
return output
843
844
776
-_gemm_rs: GemmRS | None = None
845
+_gemm_rs_ar: GemmRsAr | None = None
846
847
779
-def init_gemm_rs(max_M: int, N: int) -> None:
780
- """Collectively initialize the process-wide GEMM-RS state."""
781
- global _gemm_rs
782
- if _gemm_rs is not None:
783
- assert _gemm_rs.max_M >= max_M and _gemm_rs.N == N
848
+def init_gemm_rs_ar(max_M: int, N: int, *, all_reduce: bool = False) -> None:
849
+ """Collectively initialize the process-wide, mode-bound GEMM-RS/AR state."""
850
+ global _gemm_rs_ar
851
+ if _gemm_rs_ar is not None:
852
+ if _gemm_rs_ar.all_reduce != all_reduce:
853
+ current = "AR" if _gemm_rs_ar.all_reduce else "RS"
854
+ requested = "AR" if all_reduce else "RS"
855
+ raise RuntimeError(
856
+ f"GEMM-RS/AR is already initialized for {current}; "
857
+ f"a worker cannot reinitialize it for {requested}"
858
+ )
859
+ assert _gemm_rs_ar.max_M >= max_M and _gemm_rs_ar.N == N
860
return
785
- _gemm_rs = GemmRS(max_M=max_M, N=N)
861
+ _gemm_rs_ar = GemmRsAr(max_M=max_M, N=N, all_reduce=all_reduce)
862
+
863
+
864
+def warmup_gemm_rs_ar() -> int:
865
+ """Compile every reachable dispatch for the initialized GEMM-RS/AR mode."""
866
+ # Initialization can be disabled or fail when multicast is unavailable.
867
+ if _gemm_rs_ar is None:
868
+ return 0
869
+ # Keep these profiles in sync with the dispatch in GemmRsAr.__call__.
870
+ profiles = ((128, 1), (128, 2), (256, 2))
871
+ for BN, cta_group in profiles:
872
+ Sm100GemmRsArBF16.compile(
873
+ _gemm_rs_ar.rank,
874
+ _gemm_rs_ar.world_size,
875
+ BN,
876
+ cta_group,
877
+ _gemm_rs_ar.all_reduce,
878
+ )
879
+ return len(profiles)
880
881
788
-def get_gemm_rs() -> GemmRS:
789
- assert _gemm_rs is not None, "GEMM-RS is not initialized"
790
- return _gemm_rs
882
+def get_gemm_rs_ar() -> GemmRsAr:
883
+ assert _gemm_rs_ar is not None, "GEMM-RS/AR is not initialized"
884
+ return _gemm_rs_ar