[Kimi-K3] Add GEMM-RS for sequence parallelism (#52079)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>

Thien Tran committed Aug 14, 2026 at 00:02 UTC 6014f9e67291ea96548e263b8784a33a3f055954
11 files changed +1591 -13
.buildkite/test_areas/distributed.yaml
+1
@@ -242,6 +242,7 @@ steps:
242 commands:
243 - pytest -v -s tests/distributed/test_context_parallel.py
244 - pytest -v -s tests/distributed/test_nccl_symm_mem.py
245 + - pytest -v -s tests/kernels/test_kimi_k3_gemm_rs.py
246 - pytest -v -s tests/v1/distributed/test_dbo.py
247 - pytest -v -s tests/distributed/test_mnnvl_alltoall.py
248
benchmarks/kernels/benchmark_kimi_k3_gemm_rs.py new
+433
@@ -0,0 +1,433 @@
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.
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
9 +"""
10 +
11 +import argparse
12 +import os
13 +import statistics
14 +from collections.abc import Callable
15 +from dataclasses import dataclass
16 +
17 +import pandas as pd
18 +import torch
19 +import torch.distributed as dist
20 +import torch.distributed._symmetric_memory as symm_mem
21 +
22 +from vllm.config import VllmConfig, set_current_vllm_config
23 +from vllm.distributed import cleanup_dist_env_and_memory
24 +from vllm.distributed.parallel_state import (
25 + get_tp_group,
26 + init_distributed_environment,
27 + initialize_model_parallel,
28 +)
29 +from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import GemmRS
30 +
31 +# Shared-expert down-proj and attention O-proj.
32 +_KIMI_K3_PROJECTION_K = (6144, 12288)
33 +
34 +
35 +@dataclass
36 +class Candidate:
37 + name: str
38 + runs: list[Callable[[], torch.Tensor]]
39 + check_correctness: bool = True
40 +
41 +
42 +def parse_args() -> argparse.Namespace:
43 + parser = argparse.ArgumentParser()
44 + parser.add_argument(
45 + "--m",
46 + type=int,
47 + nargs="+",
48 + default=[128, 512, 2048, 8192, 32768],
49 + help="Global token counts to benchmark.",
50 + )
51 + parser.add_argument(
52 + "--k",
53 + type=int,
54 + nargs="+",
55 + help=(
56 + "Per-rank input dimensions. By default, derive the Kimi-K3 "
57 + "shared-expert down-proj and O-proj dimensions from the TP "
58 + "world size."
59 + ),
60 + )
61 + parser.add_argument("--n", type=int, default=7168)
62 + parser.add_argument(
63 + "--num-workspaces",
64 + type=int,
65 + default=10,
66 + help="Pointer-distinct inputs and CUDA graphs to rotate.",
67 + )
68 + parser.add_argument("--warmup-replays", type=int, default=5)
69 + parser.add_argument("--samples", type=int, default=20)
70 + return parser.parse_args()
71 +
72 +
73 +def capture_graph(
74 + op: Callable[[], torch.Tensor],
75 + stream: torch.cuda.Stream,
76 + cpu_group: dist.ProcessGroup,
77 +) -> tuple[torch.cuda.CUDAGraph, list[torch.Tensor | None]]:
78 + result: list[torch.Tensor | None] = [None]
79 + stream.wait_stream(torch.cuda.current_stream())
80 + dist.barrier(group=cpu_group)
81 + with torch.cuda.stream(stream):
82 + for _ in range(3):
83 + result[0] = op()
84 + stream.synchronize()
85 + dist.barrier(group=cpu_group)
86 +
87 + graph = torch.cuda.CUDAGraph()
88 + with torch.cuda.graph(graph, stream=stream):
89 + result[0] = op()
90 + torch.cuda.current_stream().wait_stream(stream)
91 + dist.barrier(group=cpu_group)
92 + return graph, result
93 +
94 +
95 +def benchmark_graphs(
96 + candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]],
97 + warmup_replays: int,
98 + samples: int,
99 + device_group: dist.ProcessGroup,
100 + device_barrier: Callable[[], None],
101 +) -> dict[str, float]:
102 + candidate_names = list(candidate_graphs)
103 + for round_index in range(warmup_replays):
104 + for candidate_index in range(len(candidate_names)):
105 + candidate_id = (round_index + candidate_index) % len(candidate_names)
106 + name = candidate_names[candidate_id]
107 + graphs = candidate_graphs[name]
108 + device_barrier()
109 + graphs[round_index % len(graphs)].replay()
110 + torch.accelerator.synchronize()
111 +
112 + timings: dict[str, list[float]] = {name: [] for name in candidate_names}
113 + start = torch.cuda.Event(enable_timing=True)
114 + end = torch.cuda.Event(enable_timing=True)
115 + for sample_index in range(samples):
116 + for candidate_index in range(len(candidate_names)):
117 + candidate_id = (sample_index + candidate_index) % len(candidate_names)
118 + name = candidate_names[candidate_id]
119 + graphs = candidate_graphs[name]
120 + device_barrier()
121 + start.record()
122 + graphs[sample_index % len(graphs)].replay()
123 + end.record()
124 + end.synchronize()
125 +
126 + elapsed = torch.tensor(
127 + start.elapsed_time(end) * 1000,
128 + dtype=torch.float64,
129 + device=torch.accelerator.current_device_index(),
130 + )
131 + dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
132 + timings[name].append(elapsed.item())
133 + return {name: statistics.median(values) for name, values in timings.items()}
134 +
135 +
136 +def valid_rows(M: int, local_M: int, rank: int) -> int:
137 + return min(max(M - rank * local_M, 0), local_M)
138 +
139 +
140 +def benchmark_shape(
141 + gemm_rs: GemmRS,
142 + M: int,
143 + N: int,
144 + K: int,
145 + num_workspaces: int,
146 + warmup_replays: int,
147 + samples: int,
148 + device_group: dist.ProcessGroup,
149 + cpu_group: dist.ProcessGroup,
150 + device_barrier: Callable[[], None],
151 +) -> dict[str, float | int]:
152 + world_size = dist.get_world_size(device_group)
153 + rank = dist.get_rank(device_group)
154 + device = torch.device("cuda", torch.accelerator.current_device_index())
155 + padded_M = (M + world_size - 1) // world_size * world_size
156 + local_M = padded_M // world_size
157 +
158 + rng = torch.Generator(device=device)
159 + rng.manual_seed(1000 + rank * 10 + M + K)
160 + inputs = [
161 + torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng)
162 + for _ in range(num_workspaces)
163 + ]
164 + weights = [
165 + torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng)
166 + for _ in range(num_workspaces)
167 + ]
168 +
169 + partial = torch.empty((padded_M, N), dtype=torch.bfloat16, device=device)
170 + symm_partial = symm_mem.empty((padded_M, N), dtype=torch.bfloat16, device=device)
171 + 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 = []
175 + for _ in range(num_workspaces):
176 + rs_input = symm_mem.empty(
177 + (padded_M, N),
178 + dtype=torch.bfloat16,
179 + device=device,
180 + )
181 + symm_rs_inputs.append(rs_input)
182 + symm_rs_handles.append(symm_mem.rendezvous(rs_input, device_group))
183 + torch_output = torch.empty((local_M, N), dtype=torch.bfloat16, device=device)
184 + symm_output = torch.empty_like(torch_output)
185 + gemm_output = torch.empty((M, N), dtype=torch.bfloat16, device=device)
186 +
187 + if padded_M > M:
188 + partial[M:].zero_()
189 + symm_partial[M:].zero_()
190 +
191 + def make_torch_ring_ll_gemm_rs(
192 + x: torch.Tensor, weight: torch.Tensor
193 + ) -> Callable[[], torch.Tensor]:
194 + def run() -> torch.Tensor:
195 + torch.mm(x, weight.T, out=partial[:M])
196 + dist.reduce_scatter_single(torch_output, partial, group=device_group)
197 + return torch_output
198 +
199 + return run
200 +
201 + def make_torch_ldmc_gemm_rs(
202 + x: torch.Tensor, weight: torch.Tensor
203 + ) -> Callable[[], torch.Tensor]:
204 + def run() -> torch.Tensor:
205 + torch.mm(x, weight.T, out=symm_partial[:M])
206 + dist.reduce_scatter_single(
207 + symm_output,
208 + symm_partial,
209 + group=device_group,
210 + )
211 + return symm_output
212 +
213 + return run
214 +
215 + def make_fused_gemm_rs(
216 + x: torch.Tensor, weight: torch.Tensor
217 + ) -> Callable[[], torch.Tensor]:
218 + def run() -> torch.Tensor:
219 + return gemm_rs(x, weight)
220 +
221 + return run
222 +
223 + def make_torch_gemm(
224 + x: torch.Tensor,
225 + weight: torch.Tensor,
226 + ) -> Callable[[], torch.Tensor]:
227 + def run() -> torch.Tensor:
228 + return torch.mm(x, weight.T, out=gemm_output)
229 +
230 + return run
231 +
232 + def make_ring_ll_rs(rs_input: torch.Tensor) -> Callable[[], torch.Tensor]:
233 + def run() -> torch.Tensor:
234 + dist.reduce_scatter_single(torch_output, rs_input, group=device_group)
235 + return torch_output
236 +
237 + return run
238 +
239 + def make_ldmc_rs(
240 + rs_input: torch.Tensor,
241 + ) -> Callable[[], torch.Tensor]:
242 + def run() -> torch.Tensor:
243 + dist.reduce_scatter_single(symm_output, rs_input, group=device_group)
244 + return symm_output
245 +
246 + return run
247 +
248 + candidates = (
249 + Candidate(
250 + "ring_ll_us",
251 + [make_torch_ring_ll_gemm_rs(x, w) for x, w in zip(inputs, weights)],
252 + ),
253 + Candidate(
254 + "ldmc_us",
255 + [make_torch_ldmc_gemm_rs(x, w) for x, w in zip(inputs, weights)],
256 + ),
257 + Candidate(
258 + "gemm_rs_us",
259 + [make_fused_gemm_rs(x, w) for x, w in zip(inputs, weights)],
260 + ),
261 + Candidate(
262 + "torch_gemm_us",
263 + [make_torch_gemm(x, w) for x, w in zip(inputs, weights)],
264 + check_correctness=False,
265 + ),
266 + Candidate(
267 + "ring_ll_rs_us",
268 + [make_ring_ll_rs(x) for x in rs_inputs],
269 + check_correctness=False,
270 + ),
271 + Candidate(
272 + "ldmc_rs_us",
273 + [make_ldmc_rs(x) for x in symm_rs_inputs],
274 + check_correctness=False,
275 + ),
276 + )
277 +
278 + expected = candidates[0].runs[0]()
279 + rows = valid_rows(M, local_M, rank)
280 + for candidate in candidates[1:]:
281 + if not candidate.check_correctness:
282 + continue
283 + actual = candidate.runs[0]()
284 + torch.accelerator.synchronize(device)
285 + torch.testing.assert_close(
286 + actual[:rows],
287 + expected[:rows],
288 + rtol=5e-2,
289 + atol=4.0,
290 + )
291 +
292 + candidate_graphs = {}
293 + graph_keepalive: list[object] = [symm_partial_handle, *symm_rs_handles]
294 + for candidate in candidates:
295 + stream = torch.cuda.Stream()
296 + bundles = [capture_graph(run, stream, cpu_group) for run in candidate.runs]
297 + candidate_graphs[candidate.name] = [graph for graph, _ in bundles]
298 + graph_keepalive.extend(bundles)
299 + graph_keepalive.append(stream)
300 +
301 + times = benchmark_graphs(
302 + candidate_graphs,
303 + warmup_replays,
304 + samples,
305 + device_group,
306 + device_barrier,
307 + )
308 +
309 + best_nccl_rs_us = min(times["ring_ll_rs_us"], times["ldmc_rs_us"])
310 + return {
311 + "M": M,
312 + "N": N,
313 + "K": K,
314 + **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"],
318 + }
319 +
320 +
321 +def print_results(results: list[dict[str, float | int]]) -> None:
322 + 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",
333 + ]
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 + }
351 + )
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 + }
360 + )
361 + components = components.round(2)
362 +
363 + print("### End-to-end latency")
364 + print(end_to_end.to_markdown(index=False))
365 + print("\n### Component latency")
366 + print(components.to_markdown(index=False))
367 + print("\nNCCL RS (best) is the faster of RING_LL and LDMC for each shape.")
368 +
369 +
370 +def main() -> None:
371 + args = parse_args()
372 + assert args.m and min(args.m) >= 128
373 + assert args.n % 256 == 0
374 + assert args.num_workspaces > 0
375 + assert args.warmup_replays >= 0
376 + assert args.samples > 0
377 +
378 + local_rank = int(os.environ["LOCAL_RANK"])
379 + torch.accelerator.set_device_index(local_rank)
380 + init_distributed_environment()
381 + world_size = dist.get_world_size()
382 + if args.k is None:
383 + assert all(K % world_size == 0 for K in _KIMI_K3_PROJECTION_K)
384 + K_values = [K // world_size for K in _KIMI_K3_PROJECTION_K]
385 + else:
386 + K_values = args.k
387 + assert all(K % 64 == 0 for K in K_values)
388 + # Reserve symmetric memory for the NCCL-managed benchmark allocations.
389 + os.environ["VLLM_ALLREDUCE_USE_SYMM_MEM"] = "0"
390 + # NCCL-managed symmetric allocations select the NVLS/LDMC collective path.
391 + symm_mem.set_backend("NCCL")
392 + with set_current_vllm_config(VllmConfig()):
393 + initialize_model_parallel(tensor_model_parallel_size=world_size)
394 +
395 + tp_group = get_tp_group()
396 + group_warmup = torch.zeros(1, device=torch.accelerator.current_device_index())
397 + dist.all_reduce(group_warmup, group=tp_group.device_group)
398 + pynccl_comm = tp_group.device_communicator.pynccl_comm
399 + assert pynccl_comm is not None
400 + sync_input = torch.zeros(1, device=torch.accelerator.current_device_index())
401 + sync_output = torch.empty_like(sync_input)
402 +
403 + def device_barrier() -> None:
404 + pynccl_comm.all_reduce(sync_input, sync_output)
405 +
406 + gemm_rs = GemmRS(max_M=max(args.m), N=args.n)
407 + results = [
408 + benchmark_shape(
409 + gemm_rs,
410 + M,
411 + args.n,
412 + K,
413 + args.num_workspaces,
414 + args.warmup_replays,
415 + args.samples,
416 + tp_group.device_group,
417 + tp_group.cpu_group,
418 + device_barrier,
419 + )
420 + for K in K_values
421 + for M in args.m
422 + ]
423 +
424 + if tp_group.rank_in_group == 0:
425 + print_results(results)
426 +
427 + dist.barrier(group=tp_group.cpu_group)
428 + del gemm_rs
429 + cleanup_dist_env_and_memory()
430 +
431 +
432 +if __name__ == "__main__":
433 + main()
tests/kernels/test_kimi_k3_gemm_rs.py new
+178
@@ -0,0 +1,178 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +import torch
6 +import torch.distributed as dist
7 +import torch.multiprocessing as mp
8 +
9 +from tests.utils import ensure_current_vllm_config
10 +from vllm.distributed import cleanup_dist_env_and_memory
11 +from vllm.distributed.parallel_state import (
12 + init_distributed_environment,
13 + initialize_model_parallel,
14 +)
15 +from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import GemmRS
16 +from vllm.platforms import current_platform
17 +from vllm.utils.network_utils import get_open_port
18 +from vllm.utils.system_utils import update_environment_variables
19 +
20 +_SHAPES = (
21 + (129, 768),
22 + (257, 768),
23 + (1023, 4224),
24 + (1024, 4224),
25 + (8191, 4224),
26 +)
27 +_N = 512
28 +
29 +
30 +def _reference(
31 + x: torch.Tensor,
32 + weight: torch.Tensor,
33 + world_size: int,
34 + group: dist.ProcessGroup,
35 +) -> torch.Tensor:
36 + M = x.shape[0]
37 + padded_M = (M + world_size - 1) // world_size * world_size
38 + partial = torch.empty(
39 + (padded_M, weight.shape[0]),
40 + dtype=x.dtype,
41 + device=x.device,
42 + )
43 + torch.mm(x, weight.T, out=partial[:M])
44 + if padded_M > M:
45 + partial[M:].zero_()
46 +
47 + output = torch.empty(
48 + (padded_M // world_size, weight.shape[0]),
49 + dtype=x.dtype,
50 + device=x.device,
51 + )
52 + dist.reduce_scatter_single(output, partial, group=group)
53 + return output
54 +
55 +
56 +def _assert_valid_rows_close(
57 + actual: torch.Tensor,
58 + expected: torch.Tensor,
59 + M: int,
60 + rank: int,
61 +) -> None:
62 + rank_start = rank * actual.shape[0]
63 + valid_rows = min(max(M - rank_start, 0), actual.shape[0])
64 + torch.testing.assert_close(
65 + actual[:valid_rows],
66 + expected[:valid_rows],
67 + rtol=5e-2,
68 + atol=4.0,
69 + )
70 +
71 +
72 +def _worker(local_rank: int, world_size: int, master_port: int) -> None:
73 + device = torch.device("cuda", local_rank)
74 + torch.accelerator.set_device_index(device)
75 + update_environment_variables(
76 + {
77 + "RANK": str(local_rank),
78 + "LOCAL_RANK": str(local_rank),
79 + "WORLD_SIZE": str(world_size),
80 + "MASTER_ADDR": "localhost",
81 + "MASTER_PORT": str(master_port),
82 + }
83 + )
84 +
85 + init_distributed_environment()
86 + with ensure_current_vllm_config():
87 + initialize_model_parallel(tensor_model_parallel_size=world_size)
88 +
89 + group = dist.group.WORLD
90 + rank = dist.get_rank(group)
91 + gemm_rs = GemmRS(max_M=max(M for M, _ in _SHAPES), N=_N)
92 +
93 + weight_generator = torch.Generator(device=device)
94 + input_generator = torch.Generator(device=device)
95 + weights = {}
96 + for K in {K for _, K in _SHAPES}:
97 + weight_generator.manual_seed(1000 + rank * 10 + K)
98 + weights[K] = torch.randn(
99 + _N,
100 + K,
101 + dtype=torch.bfloat16,
102 + device=device,
103 + generator=weight_generator,
104 + )
105 +
106 + # Alternating shapes exercise producer-flag reuse across different grids,
107 + # CTA-group choices, and both BN=128 and BN=256 dispatches.
108 + for M, K in (*_SHAPES, *_SHAPES[::-1]):
109 + input_generator.manual_seed(2000 + M + K)
110 + x = torch.randn(
111 + M,
112 + K,
113 + dtype=torch.bfloat16,
114 + device=device,
115 + generator=input_generator,
116 + )
117 + expected = _reference(x, weights[K], world_size, group)
118 + actual = gemm_rs(x, weights[K])
119 + torch.accelerator.synchronize(device)
120 + _assert_valid_rows_close(actual, expected, M, rank)
121 +
122 + # Exercise GEMM-RS under CUDA graph capture and replay.
123 + graph_M, graph_K = 1025, 4224
124 + input_generator.manual_seed(3000)
125 + graph_x = torch.randn(
126 + graph_M,
127 + graph_K,
128 + dtype=torch.bfloat16,
129 + device=device,
130 + generator=input_generator,
131 + )
132 + graph_expected = _reference(graph_x, weights[graph_K], world_size, group)
133 + graph_output = torch.empty_like(graph_expected)
134 +
135 + capture_stream = torch.cuda.Stream()
136 + capture_stream.wait_stream(torch.cuda.current_stream())
137 + with torch.cuda.stream(capture_stream):
138 + for _ in range(3):
139 + graph_output.copy_(gemm_rs(graph_x, weights[graph_K]))
140 + capture_stream.synchronize()
141 + dist.barrier(group=group)
142 +
143 + graph = torch.cuda.CUDAGraph()
144 + with torch.cuda.graph(graph, stream=capture_stream):
145 + graph_output.copy_(gemm_rs(graph_x, weights[graph_K]))
146 + torch.cuda.current_stream().wait_stream(capture_stream)
147 + dist.barrier(group=group)
148 +
149 + for _ in range(3):
150 + graph.replay()
151 + torch.accelerator.synchronize(device)
152 + _assert_valid_rows_close(graph_output, graph_expected, graph_M, rank)
153 +
154 + dist.barrier(group=group)
155 + del gemm_rs
156 + cleanup_dist_env_and_memory()
157 +
158 +
159 +@pytest.mark.distributed(num_gpus=2)
160 +@pytest.mark.skipif(
161 + not current_platform.is_device_capability_family(100),
162 + reason="Kimi-K3 GEMM-RS requires SM100",
163 +)
164 +def test_kimi_k3_gemm_rs(monkeypatch: pytest.MonkeyPatch) -> None:
165 + world_size = 2
166 + if torch.accelerator.device_count() < world_size:
167 + pytest.skip("GEMM-RS requires two GPUs")
168 +
169 + monkeypatch.setenv("NCCL_CUMEM_ENABLE", "1")
170 + monkeypatch.setenv("NCCL_NVLS_ENABLE", "1")
171 + try:
172 + mp.spawn(
173 + _worker,
174 + args=(world_size, get_open_port()),
175 + nprocs=world_size,
176 + )
177 + finally:
178 + cleanup_dist_env_and_memory()
tests/models/kimi_k3/test_sequence_parallel.py
+9 -6
@@ -267,19 +267,21 @@ def test_kimi_mtp_restores_sequence_parallel_output(monkeypatch):
267
268
269 @pytest.mark.parametrize(
270 - ("enabled", "use_sequence_parallel", "tp_size", "expected"),
270 + ("enabled", "use_sequence_parallel", "eligible", "tp_size", "expected"),
271 [
272 - (True, True, 8, True),
273 - (False, True, 8, False), # opt-in only
274 - (True, False, 8, False), # replication only exists under SP
275 - (True, True, 1, False), # nothing to shard
276 - (True, True, 5, False), # 6144 % 5 -- would fail divide()
272 + (True, True, True, 8, True),
273 + (False, True, True, 8, False), # opt-in only
274 + (True, False, True, 8, False), # replication only exists under SP
275 + (True, True, False, 8, False), # FusedMoE path owns the reduction
276 + (True, True, True, 1, False), # nothing to shard
277 + (True, True, True, 5, False), # 6144 % 5 -- would fail divide()
278 ],
279 )
280 def test_shard_sequence_parallel_mlp_gating(
281 monkeypatch,
282 enabled: bool,
283 use_sequence_parallel: bool,
284 + eligible: bool,
285 tp_size: int,
286 expected: bool,
287 ):
@@ -293,6 +295,7 @@ def test_shard_sequence_parallel_mlp_gating(
295 hidden_size=7168,
296 intermediate_size=6144,
297 use_sequence_parallel=use_sequence_parallel,
298 + eligible=eligible,
299 )
300 is expected
301 )
vllm/cute_utils/__init__.py
+12
@@ -40,6 +40,18 @@ def recast_val(x, dtype, *, loc=None, ip=None):
40 return dtype(llvm.bitcast(dtype.mlir_type, x.ir_value(loc=loc, ip=ip)))
41
42
43 +@dsl_user_op
44 +def to_cta0_smem(ptr: cute.Pointer, *, loc=None, ip=None):
45 + return cute.make_ptr(
46 + ptr.dtype,
47 + ptr.toint(loc=loc, ip=ip) & 0xFEFF_FFFF,
48 + cute.AddressSpace.smem,
49 + assumed_align=8,
50 + loc=loc,
51 + ip=ip,
52 + )
53 +
54 +
55 def simple_tma_copy(atom, src, dst, mbar=None, cache_policy=None):
56 """A simple helper that wraps group_modes() and tma_partition()
57 NOTE: this should be called WITHOUT cute.elect_one()
vllm/cute_utils/mbarrier.py new
+47
@@ -0,0 +1,47 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from cutlass import Int32
5 +from cutlass._mlir.dialects import nvvm
6 +from cutlass.cutlass_dsl import dsl_user_op
7 +
8 +_SPACE_MAP = {
9 + "cta": nvvm.MBarrierSpaceKind.CTA,
10 + "cluster": nvvm.MBarrierSpaceKind.CLUSTER,
11 +}
12 +
13 +_MEMORY_ORDER_MAP = {
14 + "weak": nvvm.MemOrderKind.WEAK,
15 + "relaxed": nvvm.MemOrderKind.RELAXED,
16 + "acquire": nvvm.MemOrderKind.ACQUIRE,
17 + "release": nvvm.MemOrderKind.RELEASE,
18 + "acq_rel": nvvm.MemOrderKind.ACQ_REL,
19 +}
20 +
21 +
22 +@dsl_user_op
23 +def arrive(mbar, space: str = "cta", order: str = "relaxed", *, loc=None, ip=None):
24 + nvvm.mbarrier_txn(
25 + mbar.to_llvm_ptr(loc=loc, ip=ip),
26 + Int32(1).ir_value(loc=loc, ip=ip),
27 + kind=nvvm.MBarrierTxnKind.ARRIVE,
28 + space=_SPACE_MAP[space],
29 + order=_MEMORY_ORDER_MAP[order],
30 + loc=loc,
31 + ip=ip,
32 + )
33 +
34 +
35 +@dsl_user_op
36 +def arrive_expect_tx(
37 + mbar, size, space: str = "cta", order: str = "relaxed", *, loc=None, ip=None
38 +):
39 + nvvm.mbarrier_txn(
40 + mbar.to_llvm_ptr(loc=loc, ip=ip),
41 + Int32(size).ir_value(loc=loc, ip=ip),
42 + kind=nvvm.MBarrierTxnKind.ARRIVE_EXPECT_TX,
43 + space=_SPACE_MAP[space],
44 + order=_MEMORY_ORDER_MAP[order],
45 + loc=loc,
46 + ip=ip,
47 + )
vllm/envs.py
+4
@@ -206,6 +206,7 @@ if TYPE_CHECKING:
206 VLLM_USE_FUSED_MOE_GROUPED_TOPK: bool = True
207 VLLM_MOE_SKIP_PADDING: bool = True
208 VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT: bool = False
209 + VLLM_KIMI_K3_GEMM_RS: bool = False
210 VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER: bool = True
211 VLLM_USE_FLASHINFER_MOE_INT4: bool = False
212 VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR: str | None = None
@@ -1594,6 +1595,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
1595 "VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT": lambda: bool(
1596 int(os.getenv("VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT", "0"))
1597 ),
1598 + # Use the SM100 BF16 GEMM-RS kernel for eligible Kimi-K3 sequence-parallel
1599 + # row-parallel projections. All TP ranks must belong to one NVLink domain.
1600 + "VLLM_KIMI_K3_GEMM_RS": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_RS", "0"))),
1601 # Allow use of FlashInfer FP8 block-scale GEMM for linear layers.
1602 # This uses TensorRT-LLM kernels and requires SM90+ (Hopper).
1603 "VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER": lambda: bool(
vllm/models/kimi_k3/nvidia/kda.py
+17 -1
@@ -305,6 +305,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
305 config: KimiLinearConfig,
306 vllm_config: VllmConfig,
307 prefix: str = "",
308 + run_gemm_rs: bool = False,
309 ) -> None:
310 super().__init__(config, vllm_config, prefix)
311
@@ -470,7 +471,16 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
471 quant_config=self.quant_config,
472 prefix=f"{prefix}.o_proj",
473 )
473 -
474 + self.run_gemm_rs = run_gemm_rs
475 + if self.run_gemm_rs:
476 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
477 +
478 + self.run_gemm_rs = get_gemm_rs().can_run(self.o_proj)
479 + if not self.run_gemm_rs:
480 + logger.warning_once(
481 + "GEMM-RS is disabled for %s due to an incompatible projection.",
482 + prefix,
483 + )
484 compilation_config = vllm_config.compilation_config
485 if prefix in compilation_config.static_forward_context:
486 raise ValueError(f"Duplicate layer name: {prefix}")
@@ -511,6 +521,12 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
521 core_attn_out=core_attn_out,
522 )
523 core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
524 + if self.run_gemm_rs:
525 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
526 +
527 + gemm_rs = get_gemm_rs()
528 + if gemm_rs.should_run(core_attn_out):
529 + return gemm_rs(core_attn_out, self.o_proj.weight)
530 return self.o_proj(core_attn_out)[0]
531
532 @eager_break_during_capture
vllm/models/kimi_k3/nvidia/mla.py
+18 -4
@@ -134,6 +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,
138 ) -> None:
139 super().__init__()
140 self.hidden_size = hidden_size
@@ -270,6 +271,16 @@ 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
277 +
278 + self.run_gemm_rs = get_gemm_rs().can_run(self.o_proj)
279 + if not self.run_gemm_rs:
280 + logger.warning_once(
281 + "GEMM-RS is disabled for %s due to an incompatible projection.",
282 + prefix,
283 + )
284
285 # ---- Attention backend / impl / KV cache ----
286 self.quant_config = quant_config
@@ -541,10 +552,13 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
552 if gate is not None:
553 attn_out = _gate_sigmoid_mul(attn_out, gate)
554
544 - # ``o_proj`` (RowParallelLinear + out-of-place all-reduce) returns a
545 - # fresh private tensor, so return it directly rather than copying into a
546 - # caller buffer -- the previous ``output[:] = ...`` convention forced an
547 - # extra [num_tokens, hidden] copy per layer.
555 + if self.run_gemm_rs:
556 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
557 +
558 + gemm_rs = get_gemm_rs()
559 + if gemm_rs.should_run(attn_out):
560 + return gemm_rs(attn_out, self.o_proj.weight)
561 +
562 return self.o_proj(attn_out)[0]
563
564 @eager_break_during_capture
vllm/models/kimi_k3/nvidia/model.py
+82 -2
@@ -133,6 +133,7 @@ def shard_sequence_parallel_mlp(
133 hidden_size: int,
134 intermediate_size: int,
135 use_sequence_parallel: bool,
136 + eligible: bool,
137 ) -> bool:
138 """Whether to TP-shard a sequence-parallel MLP instead of replicating it.
139
@@ -140,7 +141,7 @@ def shard_sequence_parallel_mlp(
141 the trade-off and :mod:`vllm.envs` for when it is worth enabling.
142 """
143 enabled = envs.VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT
143 - if not (use_sequence_parallel and enabled):
144 + if not (use_sequence_parallel and eligible and enabled):
145 return False
146 tp_size = get_tensor_model_parallel_world_size()
147 return (
@@ -148,6 +149,44 @@ def shard_sequence_parallel_mlp(
149 )
150
151
152 +def maybe_init_gemm_rs(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
153 + if not envs.VLLM_KIMI_K3_GEMM_RS:
154 + return False
155 +
156 + parallel_config = vllm_config.parallel_config
157 + tp_size = parallel_config.tensor_parallel_size
158 + if not use_sequence_parallel:
159 + reason = "sequence parallelism is disabled"
160 + elif parallel_config.use_ubatching:
161 + reason = "ubatching is enabled"
162 + elif vllm_config.model_config.dtype != torch.bfloat16:
163 + reason = "the model dtype is not BF16"
164 + elif not current_platform.is_cuda():
165 + reason = "the device is not CUDA"
166 + elif not current_platform.is_device_capability_family(100):
167 + reason = "the device is not SM100-family"
168 + elif not 1 < tp_size <= 16:
169 + reason = "TP size is not in the supported range 2-16"
170 + elif 128 % tp_size != 0:
171 + reason = "TP size does not divide 128"
172 + else:
173 + reason = None
174 +
175 + if reason is not None:
176 + logger.warning_once("GEMM-RS was requested but is disabled because %s.", reason)
177 + return False
178 +
179 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import init_gemm_rs
180 +
181 + config = vllm_config.model_config.hf_text_config
182 + init_gemm_rs(
183 + max_M=vllm_config.scheduler_config.max_num_batched_tokens,
184 + N=config.hidden_size,
185 + )
186 + logger.info_once("GEMM-RS is enabled.")
187 + return True
188 +
189 +
190 class KimiMLP(nn.Module):
191 """Dense / shared-expert MLP, optionally TP-sharded under sequence parallel.
192
@@ -172,6 +211,8 @@ class KimiMLP(nn.Module):
211 quant_config: QuantizationConfig | None = None,
212 reduce_results: bool = True,
213 use_sequence_parallel: bool = False,
214 + can_shard_sequence_parallel: bool = False,
215 + run_gemm_rs: bool = False,
216 prefix: str = "",
217 activation_situ_beta: float | None = None,
218 activation_situ_linear_beta: float | None = None,
@@ -182,7 +223,9 @@ class KimiMLP(nn.Module):
223 hidden_size,
224 intermediate_size,
225 use_sequence_parallel,
226 + can_shard_sequence_parallel,
227 )
228 + self.run_gemm_rs = self.shard_sequence_parallel and run_gemm_rs
229 replicate = use_sequence_parallel and not self.shard_sequence_parallel
230
231 self.gate_up_proj = MergedColumnParallelLinear(
@@ -204,6 +247,15 @@ class KimiMLP(nn.Module):
247 disable_tp=replicate,
248 prefix=f"{prefix}.down_proj",
249 )
250 + if self.run_gemm_rs:
251 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
252 +
253 + self.run_gemm_rs = get_gemm_rs().can_run(self.down_proj)
254 + if not self.run_gemm_rs:
255 + logger.warning_once(
256 + "GEMM-RS is disabled for %s due to an incompatible projection.",
257 + prefix,
258 + )
259 if hidden_act == "silu":
260 self.act_fn = SiluAndMul()
261 elif hidden_act == "situ":
@@ -226,6 +278,14 @@ class KimiMLP(nn.Module):
278 x = sp_all_gather(x)
279 gate_up, _ = self.gate_up_proj(x)
280 x = self.act_fn(gate_up)
281 +
282 + if self.run_gemm_rs:
283 + from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs import get_gemm_rs
284 +
285 + gemm_rs = get_gemm_rs()
286 + if gemm_rs.should_run(x):
287 + return gemm_rs(x, self.down_proj.weight)
288 +
289 x, _ = self.down_proj(x)
290 if self.shard_sequence_parallel:
291 x = sp_reduce_scatter(x)
@@ -462,6 +522,7 @@ class KimiMoE(nn.Module):
522 prefix: str = "",
523 layer_idx: int = 0,
524 use_sequence_parallel: bool = False,
525 + run_gemm_rs: bool = False,
526 ):
527 super().__init__()
528 hidden_size = config.hidden_size
@@ -546,6 +607,11 @@ class KimiMoE(nn.Module):
607 quant_config=quant_config,
608 reduce_results=False,
609 use_sequence_parallel=use_sequence_parallel,
610 + # Only the MegaMoE path calls the shared experts directly; the
611 + # FusedMoE path below hands them to the runner, which fuses
612 + # their reduction and assumes the replicated layout.
613 + can_shard_sequence_parallel=self.use_mega_moe,
614 + run_gemm_rs=run_gemm_rs,
615 prefix=f"{prefix}.shared_experts",
616 activation_situ_beta=activation_situ_beta,
617 activation_situ_linear_beta=activation_situ_linear_beta,
@@ -767,6 +833,7 @@ class KimiDecoderLayer(nn.Module):
833 vllm_config: VllmConfig,
834 prefix: str = "",
835 aux_stream: torch.cuda.Stream | None = None,
836 + run_gemm_rs: bool = False,
837 ) -> None:
838 super().__init__()
839 self.hidden_size = config.hidden_size
@@ -801,6 +868,7 @@ class KimiDecoderLayer(nn.Module):
868 config,
869 vllm_config,
870 prefix=f"{prefix}.self_attn",
871 + run_gemm_rs=run_gemm_rs,
872 )
873 self._self_attn_writes_output = False
874 else:
@@ -837,6 +905,7 @@ class KimiDecoderLayer(nn.Module):
905 quant_config=quant_config,
906 prefix=f"{prefix}.self_attn",
907 aux_stream=aux_stream,
908 + run_gemm_rs=run_gemm_rs,
909 )
910 self._self_attn_writes_output = False
911
@@ -851,6 +920,7 @@ class KimiDecoderLayer(nn.Module):
920 prefix=f"{prefix}.block_sparse_moe",
921 layer_idx=layer_idx,
922 use_sequence_parallel=self.use_sequence_parallel,
923 + run_gemm_rs=run_gemm_rs,
924 )
925 self.mlp = self.block_sparse_moe
926 else:
@@ -861,6 +931,8 @@ class KimiDecoderLayer(nn.Module):
931 quant_config=quant_config,
932 prefix=f"{prefix}.mlp",
933 use_sequence_parallel=self.use_sequence_parallel,
934 + can_shard_sequence_parallel=True,
935 + run_gemm_rs=run_gemm_rs,
936 activation_situ_beta=config.activation_situ_beta,
937 activation_situ_linear_beta=config.activation_situ_linear_beta,
938 )
@@ -991,15 +1063,18 @@ class KimiDecoderLayer(nn.Module):
1063 )
1064 assert hidden_states is not None
1065
1066 + M = None
1067 if self.use_sequence_parallel:
1068 hidden_states = sp_all_gather(hidden_states)
1069 # Remove SP padding before attention.
1070 hidden_states = hidden_states[: positions.shape[0]]
1071 + M = hidden_states.shape[0]
1072
1073 # Attention.
1074 hidden_states = self._run_self_attn(positions, hidden_states)
1075
1002 - if self.use_sequence_parallel:
1076 + # GEMM-RS returns the local sequence shard; standard O-proj preserves M.
1077 + if self.use_sequence_parallel and hidden_states.shape[0] == M:
1078 # Add SP padding if needed, and then perform reduce scatter.
1079 hidden_states = sp_reduce_scatter(hidden_states)
1080
@@ -1038,6 +1113,10 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1113
1114 self.vocab_size = config.vocab_size
1115
1116 + # GEMM-RS uses NCCL symmetric-memory multicast, which requires all TP
1117 + # ranks to belong to one NVLink domain.
1118 + self.run_gemm_rs = maybe_init_gemm_rs(vllm_config, self.use_sequence_parallel)
1119 +
1120 if get_pp_group().is_first_rank:
1121 self.embed_tokens = VocabParallelEmbedding(
1122 config.vocab_size,
@@ -1058,6 +1137,7 @@ class KimiLinearModel(nn.Module, EagleModelMixin, SupportsQuant):
1137 vllm_config,
1138 prefix,
1139 aux_stream=aux_stream,
1140 + run_gemm_rs=self.run_gemm_rs,
1141 )
1142
1143 self.start_layer, self.end_layer, self.layers = make_layers(
vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs.py new
+790
@@ -0,0 +1,790 @@
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."""
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.
8 +
9 +from functools import cache
10 +
11 +import cutlass
12 +import torch
13 +import torch.distributed._symmetric_memory as symm_mem
14 +from cuda.bindings.driver import CUstream
15 +from cutlass import BFloat16, Int32, Int64, Uint16, cute, utils
16 +from cutlass._mlir import ir
17 +from cutlass._mlir.dialects import llvm, nvvm, vector
18 +from cutlass.cute.nvgpu import cpasync, tcgen05
19 +from cutlass.cute.runtime import make_fake_stream, make_fake_tensor, make_ptr, nullptr
20 +from cutlass.cutlass_dsl import dsl_user_op
21 +from cutlass.utils import get_smem_capacity_in_bytes
22 +
23 +from vllm.cute_utils import _tcgen05, mbarrier, simple_tma_copy, to_cta0_smem
24 +from vllm.distributed import get_tp_group
25 +from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
26 +
27 +
28 +@dsl_user_op
29 +def nanosleep(ns: int, *, loc=None, ip=None) -> None:
30 + nvvm.nanosleep(Int32(ns).ir_value(loc=loc, ip=ip), loc=loc, ip=ip)
31 +
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
36 + assert x.element_type == BFloat16
37 + vec_type = ".v4.bf16x2"
38 +
39 + ptr = x.iterator.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
40 + asm = (
41 + "multimem.ld_reduce.relaxed.gpu.global.add.acc::f32"
42 + f"{vec_type} {{$0, $1, $2, $3}}, [$4];"
43 + )
44 + struct = llvm.inline_asm(
45 + llvm.StructType.get_literal([Int32.mlir_type] * 4),
46 + [ptr],
47 + asm,
48 + "=r,=r,=r,=r,l",
49 + has_side_effects=True,
50 + loc=loc,
51 + ip=ip,
52 + )
53 + vec = vector.from_elements(
54 + ir.VectorType.get([4], Int32.mlir_type, loc=loc),
55 + [
56 + llvm.extractvalue(Int32.mlir_type, struct, [i], loc=loc, ip=ip)
57 + for i in range(4)
58 + ],
59 + loc=loc,
60 + ip=ip,
61 + )
62 + ssa = cute.TensorSSA(vec, 4, Int32)
63 +
64 + y = cute.make_rmem_tensor(4, Int32)
65 + y.store(ssa)
66 + return cute.recast_tensor(y, x.element_type)
67 +
68 +
69 +class Sm100GemmRsBF16:
70 + def __init__(
71 + self, rank: int, num_ranks: int, BN: int = 128, cta_group: int = 1
72 + ) -> None:
73 + self.rank = rank
74 + self.num_ranks = num_ranks
75 + BM, BK = 128, 64
76 + self.cta_tile = (BM, BN, BK)
77 + self.cta_group = cta_group
78 +
79 + smem_bytes = get_smem_capacity_in_bytes()
80 + self.stage_size = (BM + (BN // cta_group)) * BK * 2
81 + self.num_stages = smem_bytes // self.stage_size
82 +
83 + @cute.jit
84 + def prepare_tma(
85 + self, tensor: cute.Tensor, BM: cutlass.Constexpr, BK: cutlass.Constexpr
86 + ) -> cpasync.TmaInfo:
87 + tma_group = (
88 + tcgen05.CtaGroup.TWO if self.cta_group == 2 else tcgen05.CtaGroup.ONE
89 + )
90 + tma_op = cpasync.CopyBulkTensorTileG2SOp(cta_group=tma_group)
91 + swizzle_128b = cute.make_swizzle(3, 4, 3)
92 + layout = cute.make_layout(
93 + (BM, BK, self.num_stages),
94 + stride=(BK, 1, BM * BK),
95 + )
96 + layout = cute.make_composed_layout(swizzle_128b, 0, layout)
97 + return cpasync.make_tiled_tma_atom(tma_op, tensor, layout, (BM, BK))
98 +
99 + @cute.jit
100 + def __call__(
101 + self,
102 + A: cute.Tensor,
103 + B: cute.Tensor,
104 + partial_uc: cute.Tensor,
105 + partial_mc_ptr: cute.Pointer,
106 + output: cute.Tensor,
107 + flags_uc: cute.Tensor,
108 + flags_mc_ptr: cute.Pointer,
109 + peer_flag_ptr: cute.Pointer,
110 + grid_size: Int32,
111 + stream: CUstream,
112 + ) -> None:
113 + N = B.shape[0]
114 + BM, BN, BK = self.cta_tile
115 + A_tma = self.prepare_tma(A, BM, BK)
116 + B_tma = self.prepare_tma(B, BN // self.cta_group, BK)
117 + padded_M = partial_uc.shape[0]
118 + partial_mc = cute.make_tensor(
119 + partial_mc_ptr,
120 + cute.make_layout((padded_M, N), stride=(N, 1)),
121 + )
122 + peer_flags = cute.make_tensor(
123 + peer_flag_ptr,
124 + cute.make_layout(self.num_ranks),
125 + )
126 +
127 + grid = (grid_size, 1, 1)
128 + block = (10 * 32, 1, 1)
129 + cluster = (self.cta_group, 1, 1)
130 + self.kernel(
131 + A_tma,
132 + B_tma,
133 + partial_uc,
134 + partial_mc,
135 + output,
136 + flags_uc.iterator,
137 + flags_mc_ptr,
138 + peer_flags,
139 + ).launch(grid=grid, block=block, cluster=cluster, stream=stream)
140 +
141 + @cute.kernel
142 + def kernel(
143 + self,
144 + A_tma: cpasync.TmaInfo,
145 + B_tma: cpasync.TmaInfo,
146 + partial_uc: cute.Tensor,
147 + partial_mc: cute.Tensor,
148 + output: cute.Tensor,
149 + flags_uc_ptr: cute.Pointer,
150 + flags_mc_ptr: cute.Pointer,
151 + peer_flags: cute.Tensor,
152 + ) -> None:
153 + tid, _, _ = cute.arch.thread_idx()
154 + raw_bid, _, _ = cute.arch.block_idx()
155 + num_bids, _, _ = cute.arch.grid_dim()
156 + warp_id = cute.arch.make_warp_uniform(tid // 32)
157 +
158 + BM, BN, BK = self.cta_tile
159 + cta_group = self.cta_group
160 + num_stages = self.num_stages
161 + num_ranks = self.num_ranks
162 +
163 + is_2cta = cta_group == 2
164 + cta_rank = raw_bid % self.cta_group
165 + num_tmem_stages = 512 // BN
166 +
167 + smem = utils.SmemAllocator()
168 + sA = smem.allocate_tensor(
169 + BFloat16,
170 + A_tma.smem_layout.outer,
171 + byte_alignment=128,
172 + swizzle=A_tma.smem_layout.inner,
173 + )
174 + sB = smem.allocate_tensor(
175 + BFloat16,
176 + B_tma.smem_layout.outer,
177 + byte_alignment=128,
178 + swizzle=B_tma.smem_layout.inner,
179 + )
180 + tma_full_mbar = smem.allocate_array(Int64, num_stages)
181 + tma_empty_mbar = smem.allocate_array(Int64, num_stages)
182 + tmem_full_mbar = smem.allocate_array(Int64, num_tmem_stages)
183 + tmem_empty_mbar = smem.allocate_array(Int64, num_tmem_stages)
184 + taddr = smem.allocate(Int32, 4)
185 +
186 + # Named barriers
187 + BAR_TMEM_ALLOC = 1
188 + BAR_EPILOGUE = 2
189 + BAR_COMM = 3
190 +
191 + M, K = A_tma.tma_tensor.shape
192 + N, _ = B_tma.tma_tensor.shape
193 + grid_m = cute.ceil_div(M, BM)
194 + # Keep 2-CTA clusters within a single N tile.
195 + grid_m = cute.ceil_div(grid_m, cta_group) * cta_group
196 + grid_n = cute.ceil_div(N, BN)
197 +
198 + if warp_id == 0:
199 + with cute.arch.elect_one():
200 + for i in cutlass.range_constexpr(num_stages):
201 + cute.arch.mbarrier_init(tma_full_mbar + i, cta_group)
202 + cute.arch.mbarrier_init(tma_empty_mbar + i, 1)
203 + for i in cutlass.range_constexpr(num_tmem_stages):
204 + cute.arch.mbarrier_init(tmem_full_mbar + i, 1)
205 + cute.arch.mbarrier_init(tmem_empty_mbar + i, 128 * cta_group)
206 + cute.arch.mbarrier_init_fence()
207 + elif warp_id == 1:
208 + cpasync.prefetch_descriptor(A_tma.atom)
209 + cpasync.prefetch_descriptor(B_tma.atom)
210 +
211 + if cutlass.const_expr(is_2cta):
212 + cute.arch.cluster_arrive_relaxed()
213 + cute.arch.cluster_wait()
214 + else:
215 + cute.arch.sync_threads()
216 +
217 + total_tiles = grid_m * grid_n
218 +
219 + if warp_id == 9:
220 + # TMA warp
221 + tma_stage = 0
222 + parity = 1
223 +
224 + if cutlass.const_expr(is_2cta):
225 + tma_full_mbar_ = to_cta0_smem(tma_full_mbar)
226 + else:
227 + tma_full_mbar_ = tma_full_mbar
228 +
229 + # Select global-memory tiles.
230 + # [(BM, BK), (M/BM, K/BK)]
231 + gA_tiles = cute.zipped_divide(A_tma.tma_tensor, (BM, BK))
232 + gB_tiles = cute.zipped_divide(B_tma.tma_tensor, (BN // cta_group, BK))
233 +
234 + for bid in range(raw_bid, total_tiles, num_bids):
235 + bid_m = bid % grid_m
236 + bid_n = bid // grid_m
237 + if cutlass.const_expr(cta_group == 2):
238 + bid_n = bid_n * cta_group + cta_rank
239 +
240 + for iter_k in cutlass.range(cute.ceil_div(K, BK), unroll=1):
241 + mbar = tma_full_mbar_ + tma_stage
242 + cute.arch.mbarrier_wait(tma_empty_mbar + tma_stage, parity)
243 +
244 + with cute.arch.elect_one():
245 + mbarrier.arrive_expect_tx(mbar, self.stage_size, "cluster")
246 + simple_tma_copy(
247 + A_tma.atom,
248 + gA_tiles[None, (bid_m, iter_k)],
249 + sA[None, None, tma_stage],
250 + mbar,
251 + )
252 + simple_tma_copy(
253 + B_tma.atom,
254 + gB_tiles[None, (bid_n, iter_k)],
255 + sB[None, None, tma_stage],
256 + mbar,
257 + )
258 +
259 + tma_stage = (tma_stage + 1) % num_stages
260 + if tma_stage == 0:
261 + parity ^= 1
262 +
263 + elif warp_id == 8:
264 + # MMA warp
265 + cute.arch.barrier(barrier_id=BAR_TMEM_ALLOC, number_of_threads=5 * 32)
266 +
267 + if cta_rank == 0:
268 + tma_stage = 0
269 + tma_full_parity = 0
270 + tmem_stage = 0
271 + tmem_empty_parity = 1
272 +
273 + MMA_M = BM * cta_group
274 + MMA_N = BN
275 + idesc = _tcgen05.make_bf16_idesc(MMA_M, MMA_N)
276 + sdesc = _tcgen05.make_sdesc_128B_swizzle(0)
277 + multicast_mask = Uint16((1 << self.cta_group) - 1)
278 +
279 + for bid in range(raw_bid, total_tiles, num_bids):
280 + cute.arch.mbarrier_wait(
281 + tmem_empty_mbar + tmem_stage, tmem_empty_parity
282 + )
283 + _tcgen05.fence_after_thread_sync()
284 +
285 + for iter_k in cutlass.range(cute.ceil_div(K, BK), unroll=1):
286 + d_tmem = BN * tmem_stage
287 + a_addr = sA[None, None, tma_stage].iterator.toint()
288 + b_addr = sB[None, None, tma_stage].iterator.toint()
289 + a_desc = sdesc | (a_addr >> 4)
290 + b_desc = sdesc | (b_addr >> 4)
291 +
292 + cute.arch.mbarrier_wait(
293 + tma_full_mbar + tma_stage, tma_full_parity
294 + )
295 + _tcgen05.fence_after_thread_sync()
296 +
297 + for mma_k in cutlass.range_constexpr(BK // 16):
298 + enable_d = iter_k > 0 or mma_k > 0
299 + _tcgen05.mma_f16(
300 + d_tmem, a_desc, b_desc, idesc, enable_d, cta_group
301 + )
302 + a_desc += 32 >> 4
303 + b_desc += 32 >> 4
304 + _tcgen05.commit(
305 + tma_empty_mbar + tma_stage, multicast_mask, cta_group
306 + )
307 +
308 + tma_stage = (tma_stage + 1) % num_stages
309 + if tma_stage == 0:
310 + tma_full_parity ^= 1
311 +
312 + _tcgen05.commit(
313 + tmem_full_mbar + tmem_stage, multicast_mask, cta_group
314 + )
315 +
316 + tmem_stage = (tmem_stage + 1) % num_tmem_stages
317 + if tmem_stage == 0:
318 + tmem_empty_parity ^= 1
319 +
320 + elif warp_id >= 4:
321 + # Communication warps
322 + # Keep epilogue in warps 0-3 and communication in warps 4-7.
323 + # Swapping the warpgroups can hang due to warp scheduling.
324 + tid_ = tid % 128
325 + local_M = output.shape[0]
326 +
327 + # Offset in the [M, N] GEMM result.
328 + rank_start = self.rank * local_M
329 + rank_end = min(rank_start + local_M, M)
330 +
331 + st_atom = cute.make_copy_atom(
332 + cute.nvgpu.CopyUniversalOp(), BFloat16, num_bits_per_copy=128
333 + )
334 +
335 + # Each thread issues one BF16x8 multimem reduction per vector.
336 + vec_width = 8
337 + vec_cols = BN // vec_width
338 + max_tile_rows = BM // num_ranks
339 + vecs_per_tile = max_tile_rows * vec_cols
340 + partial_vecs = cute.zipped_divide(partial_mc, (1, vec_width))
341 + output_vecs = cute.zipped_divide(output, (1, vec_width))
342 +
343 + for tile_id in range(raw_bid, total_tiles, num_bids):
344 + rs_bid_m = tile_id % grid_m
345 + bid_n = tile_id // grid_m
346 +
347 + local_row_start = rs_bid_m * local_M // grid_m
348 + local_row_end = (rs_bid_m + 1) * local_M // grid_m
349 + global_row_start = rank_start + local_row_start
350 + global_row_end = min(rank_start + local_row_end, M)
351 +
352 + if global_row_start < M:
353 + # Map the current RS tile to the required GEMM tiles.
354 + # Since an RS tile is smaller than a GEMM tile, each RS tile
355 + # can overlap at most 2 GEMM tiles.
356 + gemm_bid_m0 = global_row_start // BM
357 + gemm_bid_m1 = max(global_row_end - 1, global_row_start) // BM
358 +
359 + # Poll local L2 with relaxed GPU-scope loads. The following
360 + # multimem reduction reads every rank's L2 directly, so no
361 + # acquire fence or L1 invalidation is needed.
362 + if tid_ == 0:
363 + # Poll the 1st GEMM tile.
364 + flag_ptr = flags_uc_ptr + bid_n * grid_m + gemm_bid_m0
365 + arrivals = cute.arch.load(
366 + flag_ptr, Int32, sem="relaxed", scope="gpu"
367 + )
368 + while arrivals < num_ranks:
369 + nanosleep(64)
370 + arrivals = cute.arch.load(
371 + flag_ptr, Int32, sem="relaxed", scope="gpu"
372 + )
373 + elif tid_ == 32 and gemm_bid_m1 != gemm_bid_m0:
374 + # Poll the 2nd GEMM tile with another warp if needed.
375 + flag_ptr = flags_uc_ptr + bid_n * grid_m + gemm_bid_m1
376 + arrivals = cute.arch.load(
377 + flag_ptr, Int32, sem="relaxed", scope="gpu"
378 + )
379 + while arrivals < num_ranks:
380 + nanosleep(64)
381 + arrivals = cute.arch.load(
382 + flag_ptr, Int32, sem="relaxed", scope="gpu"
383 + )
384 + cute.arch.barrier(barrier_id=BAR_COMM, number_of_threads=128)
385 +
386 + # Issue multimem.ld_reduce before storing any result.
387 + reduced_vecs = []
388 + for vec_iter in cutlass.range_constexpr(vecs_per_tile // 128):
389 + vec_idx = tid_ + vec_iter * 128
390 +
391 + local_row = local_row_start + vec_idx // vec_cols
392 + global_row = rank_start + local_row
393 + col = bid_n * vec_cols + vec_idx % vec_cols
394 +
395 + reduced_vec = cute.make_rmem_tensor(vec_width, BFloat16)
396 + if local_row < local_row_end and global_row < M:
397 + tmp = multimem_ld_reduce_16B(
398 + partial_vecs[None, (global_row, col)]
399 + )
400 + reduced_vec.store(tmp.load())
401 + reduced_vecs.append(reduced_vec)
402 +
403 + # Store the result to local L2.
404 + for vec_iter in cutlass.range_constexpr(vecs_per_tile // 128):
405 + vec_idx = tid_ + vec_iter * 128
406 +
407 + local_row = local_row_start + vec_idx // vec_cols
408 + global_row = rank_start + local_row
409 + col = bid_n * vec_cols + vec_idx % vec_cols
410 +
411 + 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 + )
417 + cute.arch.barrier(barrier_id=BAR_COMM, number_of_threads=128)
418 +
419 + # Release each GEMM tile consumed by this RS tile. The last
420 + # consumer resets its producer flag for the next launch.
421 + def release_gemm_tile(
422 + gemm_bid_m,
423 + tile_M,
424 + logical_M,
425 + rank_start,
426 + rank_end,
427 + local_M,
428 + grid_m,
429 + tile_N,
430 + flags,
431 + num_ranks,
432 + ):
433 + gemm_start = gemm_bid_m * tile_M
434 + gemm_end = min(gemm_start + tile_M, logical_M)
435 + local_start = max(gemm_start, rank_start) - rank_start
436 + local_end = min(gemm_end, rank_end) - rank_start
437 +
438 + # Compute the number of consumers to identify the last
439 + # arrival, which resets the producer flag.
440 + first_rs_tile = (
441 + cute.ceil_div((local_start + 1) * grid_m, local_M) - 1
442 + )
443 + last_rs_tile = cute.ceil_div(local_end * grid_m, local_M) - 1
444 + num_consumers = last_rs_tile - first_rs_tile + 1
445 +
446 + flag_ptr = flags + tile_N * grid_m + gemm_bid_m
447 + old_count = cute.arch.atomic_add(
448 + flag_ptr, Int32(1), sem="relaxed", scope="gpu"
449 + )
450 + if old_count == num_ranks + num_consumers - 1:
451 + cute.arch.store(
452 + flag_ptr, Int32(0), sem="relaxed", scope="gpu"
453 + )
454 +
455 + if tid_ == 0:
456 + # Arrive at the 1st GEMM tile.
457 + release_gemm_tile(
458 + gemm_bid_m0,
459 + BM,
460 + M,
461 + rank_start,
462 + rank_end,
463 + local_M,
464 + grid_m,
465 + bid_n,
466 + flags_uc_ptr,
467 + num_ranks,
468 + )
469 + elif tid_ == 32 and gemm_bid_m1 != gemm_bid_m0:
470 + # Arrive at the 2nd GEMM tile if necessary.
471 + release_gemm_tile(
472 + gemm_bid_m1,
473 + BM,
474 + M,
475 + rank_start,
476 + rank_end,
477 + local_M,
478 + grid_m,
479 + bid_n,
480 + flags_uc_ptr,
481 + num_ranks,
482 + )
483 +
484 + # Exit barrier. GPU scope is sufficient because the kernel writes
485 + # local global memory and only needs to flush it to local L2.
486 + cute.arch.barrier(barrier_id=BAR_COMM, number_of_threads=128)
487 + if tid_ == 0:
488 + exit_flag = total_tiles + raw_bid
489 + utils.distributed.multimem_red_add1(
490 + flags_mc_ptr + exit_flag, order="release", scope="gpu"
491 + )
492 + utils.distributed.spin_lock_atom_cas_acquire_wait(
493 + flags_uc_ptr + exit_flag,
494 + expected_val=num_ranks,
495 + reset_val=0,
496 + scope="gpu",
497 + )
498 +
499 + else:
500 + # Epilogue warps
501 + warp_id_ = warp_id % 4
502 + tid_ = tid % 128
503 +
504 + peer_flag_bases = cute.make_rmem_tensor(num_ranks, Int64)
505 + if tid_ == 0:
506 + for rank in cutlass.range_constexpr(num_ranks):
507 + peer_flag_bases[rank] = cute.arch.load(
508 + (peer_flags.iterator + rank).llvm_ptr,
509 + Int64,
510 + )
511 +
512 + if warp_id_ == 0:
513 + _tcgen05.alloc(taddr, cta_group)
514 + cute.arch.barrier(barrier_id=BAR_TMEM_ALLOC, number_of_threads=5 * 32)
515 +
516 + WIDTH = cutlass.const_expr(16)
517 + partial_vecs = cute.zipped_divide(partial_uc, (1, WIDTH))
518 +
519 + bf16x16_atom = cute.make_copy_atom(
520 + cute.nvgpu.CopyUniversalOp(),
521 + BFloat16,
522 + num_bits_per_copy=256,
523 + l1c_evict_priority=cute.nvgpu.CacheEvictionPriority.NO_ALLOCATE,
524 + )
525 +
526 + tmem_stage = 0
527 + parity = 0
528 +
529 + if cutlass.const_expr(is_2cta):
530 + tmem_empty_mbar_ = to_cta0_smem(tmem_empty_mbar)
531 + else:
532 + tmem_empty_mbar_ = tmem_empty_mbar
533 +
534 + for bid in range(raw_bid, total_tiles, num_bids):
535 + bid_m = bid % grid_m
536 + bid_n = bid // grid_m
537 +
538 + if warp_id_ == 0:
539 + cute.arch.mbarrier_wait(tmem_full_mbar + tmem_stage, parity)
540 + cute.arch.barrier(barrier_id=BAR_EPILOGUE, number_of_threads=128)
541 + _tcgen05.fence_after_thread_sync()
542 +
543 + for i in cutlass.range_constexpr(BN // WIDTH):
544 + tcol = tmem_stage * BN + i * WIDTH
545 + regs = _tcgen05.ld(warp_id_ * 32, tcol, "32x32b", WIDTH)
546 + _tcgen05.wait_ld()
547 +
548 + if cutlass.const_expr(i == BN // WIDTH - 1):
549 + _tcgen05.fence_before_thread_sync()
550 + mbarrier.arrive(tmem_empty_mbar_ + tmem_stage, "cluster")
551 +
552 + tmp = cute.make_rmem_tensor(WIDTH, BFloat16)
553 + tmp.store(regs.to(BFloat16))
554 +
555 + global_row = bid_m * BM + tid_
556 + if global_row < M:
557 + coord = (global_row, bid_n * (BN // WIDTH) + i)
558 + cute.copy(bf16x16_atom, tmp, partial_vecs[None, coord])
559 +
560 + cute.arch.barrier(barrier_id=BAR_EPILOGUE, number_of_threads=128)
561 +
562 + # Signal GEMM completion. GPU scope is sufficient because the
563 + # partial output only needs to be flushed to local L2.
564 + if tid_ == 0 and bid_m * BM < M:
565 + gemm_start = bid_m * BM
566 + gemm_end = min(gemm_start + BM, M)
567 + first_owner = gemm_start // output.shape[0]
568 + last_owner = (gemm_end - 1) // output.shape[0]
569 +
570 + # signal to all consuming ranks (can be more than 1)
571 + for rank in cutlass.range_constexpr(num_ranks):
572 + if first_owner <= rank and rank <= last_owner:
573 + ptr = cute.make_ptr(
574 + Int32,
575 + peer_flag_bases[rank],
576 + cute.AddressSpace.gmem,
577 + assumed_align=16,
578 + )
579 + ptr += bid_m + bid_n * grid_m
580 + utils.distributed.red_add1(
581 + ptr, order="release", scope="gpu"
582 + )
583 +
584 + tmem_stage = (tmem_stage + 1) % num_tmem_stages
585 + if tmem_stage == 0:
586 + parity ^= 1
587 +
588 + if cutlass.const_expr(is_2cta):
589 + cute.arch.cluster_arrive_relaxed()
590 + cute.arch.cluster_wait()
591 + else:
592 + cute.arch.barrier(barrier_id=BAR_EPILOGUE, number_of_threads=128)
593 + if warp_id_ == 0:
594 + _tcgen05.dealloc(cta_group)
595 +
596 + @cache
597 + @staticmethod
598 + def compile(rank: int, num_ranks: int, BN: int, cta_group: int):
599 + M = cute.sym_int()
600 + padded_M = cute.sym_int()
601 + N = cute.sym_int()
602 + K = cute.sym_int()
603 + local_M = cute.sym_int()
604 + num_flags = cute.sym_int()
605 +
606 + A = make_fake_tensor(
607 + BFloat16, (M, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
608 + )
609 + B = make_fake_tensor(
610 + BFloat16, (N, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
611 + )
612 + partial = make_fake_tensor(
613 + BFloat16,
614 + (padded_M, N),
615 + (cute.sym_int64(divisibility=16), 1),
616 + assumed_align=32,
617 + )
618 + 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,
624 + )
625 + flags = make_fake_tensor(Int32, (num_flags,), (1,), assumed_align=16)
626 + flags_mc_ptr = nullptr(Int32, cute.AddressSpace.gmem, assumed_align=16)
627 + peer_flag_ptr = nullptr(Int64, cute.AddressSpace.gmem, assumed_align=8)
628 +
629 + stream = make_fake_stream(use_tvm_ffi_env_stream=True)
630 + kernel = Sm100GemmRsBF16(rank, num_ranks, BN, cta_group)
631 + return cute.compile(
632 + kernel,
633 + A,
634 + B,
635 + partial,
636 + partial_mc_ptr,
637 + output,
638 + flags,
639 + flags_mc_ptr,
640 + peer_flag_ptr,
641 + 128,
642 + stream,
643 + options="--enable-tvm-ffi",
644 + )
645 +
646 +
647 +class GemmRS:
648 + """Own the max-sized symmetric workspace for Kimi-K3 GEMM-RS launches.
649 +
650 + All TP ranks must belong to one NVLink domain for multimem instructions.
651 + """
652 +
653 + def __init__(self, *, max_M: int, N: int) -> None:
654 + tp_group = get_tp_group()
655 + group = tp_group.device_group
656 + rank = tp_group.rank_in_group
657 + world_size = tp_group.world_size
658 + device = torch.device("cuda", torch.accelerator.current_device_index())
659 +
660 + assert 1 < world_size <= 16
661 + assert 128 % world_size == 0
662 + assert max_M >= 128 and N % 128 == 0
663 +
664 + max_M = (max_M + world_size - 1) // world_size * world_size
665 + self.rank = rank
666 + self.world_size = world_size
667 + self.max_M = max_M
668 + self.N = N
669 + self.device = device
670 +
671 + self.partial = symm_mem.empty((max_M, N), dtype=torch.bfloat16, device=device)
672 + self.partial_handle = symm_mem.rendezvous(self.partial, group)
673 + self.partial_mc_ptr = make_ptr(
674 + BFloat16,
675 + self.partial_handle.multicast_ptr,
676 + cute.AddressSpace.gmem,
677 + assumed_align=32,
678 + )
679 +
680 + grid_m = (max_M + 127) // 128
681 + cta_group = 2 if max_M >= 1024 or grid_m % 2 == 0 else 1
682 + grid_m = (grid_m + cta_group - 1) // cta_group * cta_group
683 + self.num_sms = torch.cuda.get_device_properties(device).multi_processor_count
684 + max_flags = grid_m * (N // 128) + self.num_sms
685 + self.flags = symm_mem.empty(max_flags, dtype=torch.int32, device=device)
686 + self.flags_handle = symm_mem.rendezvous(self.flags, group)
687 + self.flags.zero_()
688 + self.flags_mc_ptr = make_ptr(
689 + Int32,
690 + self.flags_handle.multicast_ptr,
691 + cute.AddressSpace.gmem,
692 + assumed_align=16,
693 + )
694 + self.peer_flag_ptr = make_ptr(
695 + Int64,
696 + self.flags_handle.buffer_ptrs_dev,
697 + cute.AddressSpace.gmem,
698 + assumed_align=8,
699 + )
700 +
701 + assert self.partial_handle.multicast_ptr != 0
702 + assert self.flags_handle.multicast_ptr != 0
703 + torch.accelerator.synchronize(device)
704 + tp_group.barrier()
705 +
706 + def can_run(self, linear: LinearBase) -> bool:
707 + # Validate projection-invariant requirements once during model init.
708 + # only supports BF16 for now
709 + if not isinstance(linear.quant_method, UnquantizedLinearMethod):
710 + return False
711 + w = linear.weight
712 + if w.ndim != 2:
713 + return False
714 + K = w.shape[1]
715 + return (
716 + w.shape == (self.N, K)
717 + and K % 64 == 0
718 + and w.dtype == torch.bfloat16
719 + and w.device == self.device
720 + and w.is_contiguous()
721 + )
722 +
723 + def should_run(self, x: torch.Tensor) -> bool:
724 + # Small-M shapes are supported but faster on the existing LL path.
725 + return x.shape[0] >= 128
726 +
727 + def __call__(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
728 + assert x.ndim == 2
729 + M, K = x.shape
730 + assert 0 < M <= self.max_M
731 + assert w.shape == (self.N, K) and K % 64 == 0
732 + assert w.dtype == torch.bfloat16
733 + assert w.device == self.device
734 + assert w.is_contiguous()
735 + assert x.dtype == torch.bfloat16
736 + assert x.device == self.device
737 + assert x.is_contiguous()
738 + N = w.shape[0]
739 + padded_M = (M + self.world_size - 1) // self.world_size
740 + padded_M *= self.world_size
741 + local_M = padded_M // self.world_size
742 +
743 + grid_m = (M + 127) // 128
744 + # Avoid padding small odd grids; 2-CTA wins consistently for M >= 1024.
745 + cta_group = 2 if M >= 1024 or grid_m % 2 == 0 else 1
746 + grid_m = (grid_m + cta_group - 1) // cta_group * cta_group
747 + BN = 256 if M * K >= 24 * 1024 * 1024 else 128
748 + assert N % BN == 0
749 +
750 + num_tiles = grid_m * (N // BN)
751 + num_ctas = min(num_tiles, self.num_sms)
752 + num_ctas = num_ctas // cta_group * cta_group
753 + assert self.flags.numel() >= num_tiles + num_ctas
754 +
755 + output = torch.empty((local_M, N), dtype=torch.bfloat16, device=self.device)
756 + compiled = Sm100GemmRsBF16.compile(
757 + self.rank,
758 + self.world_size,
759 + BN,
760 + cta_group,
761 + )
762 + compiled(
763 + x,
764 + w,
765 + self.partial[:padded_M],
766 + self.partial_mc_ptr,
767 + output,
768 + self.flags,
769 + self.flags_mc_ptr,
770 + self.peer_flag_ptr,
771 + num_ctas,
772 + )
773 + return output
774 +
775 +
776 +_gemm_rs: GemmRS | None = None
777 +
778 +
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
784 + return
785 + _gemm_rs = GemmRS(max_M=max_M, N=N)
786 +
787 +
788 +def get_gemm_rs() -> GemmRS:
789 + assert _gemm_rs is not None, "GEMM-RS is not initialized"
790 + return _gemm_rs