[Kernel][DSV4.1] Fuse MXFP8 wo_b GEMM with sequence-parallel reduce-scatter (#57428)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg> Signed-off-by: Canlin <canlinguosdu@gmail.com> Co-authored-by: Thien Tran <gau.nernst@yahoo.com.sg> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
Canlin Guo committed
Sep 22, 2026 at 16:07 UTC
f92b78f6ef9c9b28f60668da77af5b65645b1a45
18 files changed
+1036
-623
.buildkite/test_areas/distributed.yaml
+6
-5
@@ -387,17 +387,18 @@ steps:
387
- pytest -v -s tests/v1/distributed/test_dbo.py
388
- pytest -v -s tests/distributed/test_mnnvl_alltoall.py
389
390
-- label: ":nvidia: (B200) Kimi-K3 GEMM-RS/AR"
391
- key: kimi-k3-gemm-rs-ar-2xb200
390
+- label: ":nvidia: (B200) GEMM-RS/AR"
391
+ key: gemm-rs-ar-2xb200
392
timeout_in_minutes: 20
393
device: b200-k8s
394
working_dir: "/vllm-workspace/"
395
num_devices: 2
396
source_file_dependencies:
397
- - vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py
398
- - tests/kernels/test_kimi_k3_gemm_rs_ar.py
397
+ - vllm/model_executor/kernels/linear/cute_dsl/gemm_rs_ar.py
398
+ - vllm/cute_utils/
399
+ - tests/kernels/test_gemm_rs_ar.py
400
commands:
400
- - pytest -v -s tests/kernels/test_kimi_k3_gemm_rs_ar.py
401
+ - pytest -v -s tests/kernels/test_gemm_rs_ar.py
402
403
- label: ":nvidia: (L4) Distributed 2-Node"
404
key: 2-node-test-4-gpus
benchmarks/kernels/benchmark_gemm_rs_ar.py
new
+407
@@ -0,0 +1,407 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Benchmark the SM100 GEMM-RS/AR kernel against the model's unfused path.
4
+
5
+The baseline is what a model runs without the fusion: the projection's own
6
+linear kernel followed by vLLM's collective (``sp_reduce_scatter`` for RS,
7
+``tensor_model_parallel_all_reduce`` for AR), with the collective backend
8
+chosen by vLLM's dispatch exactly as in serving. The same two kernels are also
9
+timed bare (the GEMM alone, the collective alone on a precomputed partial);
10
+their max is the floor a perfectly overlapped fused kernel could reach.
11
+
12
+All ranks must belong to one NVLink domain. Run a TP8 sweep over the Kimi-K3
13
+projections with:
14
+
15
+ torchrun --nproc-per-node=8 -- benchmarks/kernels/benchmark_gemm_rs_ar.py
16
+
17
+or the DeepSeek-V4.1 MXFP8 ``wo_b`` projection at TP4 with:
18
+
19
+ torchrun --nproc-per-node=4 -- benchmarks/kernels/benchmark_gemm_rs_ar.py \
20
+ --model deepseek_v41 --backend flashinfer_cutedsl
21
+"""
22
+
23
+import argparse
24
+import json
25
+import os
26
+import statistics
27
+from collections.abc import Callable
28
+
29
+import torch
30
+import torch.distributed as dist
31
+
32
+from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config
33
+from vllm.distributed import (
34
+ cleanup_dist_env_and_memory,
35
+ tensor_model_parallel_all_reduce,
36
+)
37
+from vllm.distributed.parallel_state import (
38
+ get_tp_group,
39
+ init_distributed_environment,
40
+ initialize_model_parallel,
41
+)
42
+from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
43
+from vllm.model_executor.layers.linear import RowParallelLinear
44
+from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
45
+
46
+# Per model: (hidden size N, global input widths K). Kimi-K3 lists the
47
+# shared-expert down-proj and attention O-proj; DeepSeek-V4.1 lists the
48
+# ``wo_b`` output projection (o_groups * o_lora_rank).
49
+_MODEL_PROJECTIONS = {
50
+ "kimi_k3": (7168, (6144, 12288)),
51
+ "deepseek_v41": (5120, (8192,)),
52
+}
53
+
54
+
55
+def parse_args() -> argparse.Namespace:
56
+ parser = argparse.ArgumentParser()
57
+ parser.add_argument(
58
+ "--mode",
59
+ choices=("rs", "ar"),
60
+ default="rs",
61
+ help="Collective mode to benchmark.",
62
+ )
63
+ parser.add_argument(
64
+ "--m",
65
+ type=int,
66
+ nargs="+",
67
+ default=[128, 512, 2048, 8192, 32768],
68
+ help="Global token counts to benchmark.",
69
+ )
70
+ parser.add_argument(
71
+ "--model",
72
+ choices=tuple(_MODEL_PROJECTIONS),
73
+ default="kimi_k3",
74
+ help="Model whose projection shapes set the default --n and --k.",
75
+ )
76
+ parser.add_argument(
77
+ "--backend",
78
+ choices=("bf16", "flashinfer_cutlass", "flashinfer_cutedsl"),
79
+ default="bf16",
80
+ help="Weight dtype/layout: BF16 or an online-MXFP8 FlashInfer kernel.",
81
+ )
82
+ parser.add_argument(
83
+ "--k",
84
+ type=int,
85
+ nargs="+",
86
+ help=(
87
+ "Per-rank input dimensions. By default, derive the selected "
88
+ "model's projection dimensions from the TP world size."
89
+ ),
90
+ )
91
+ parser.add_argument(
92
+ "--n",
93
+ type=int,
94
+ help="Output width. Defaults to the selected model's hidden size.",
95
+ )
96
+ parser.add_argument(
97
+ "--num-workspaces",
98
+ type=int,
99
+ default=10,
100
+ help="Pointer-distinct inputs and CUDA graphs to rotate.",
101
+ )
102
+ parser.add_argument("--warmup-replays", type=int, default=5)
103
+ parser.add_argument("--samples", type=int, default=20)
104
+ parser.add_argument("--json", type=str, help="Also write the results here.")
105
+ return parser.parse_args()
106
+
107
+
108
+def make_projection(
109
+ gemm_rs_ar: GemmRsAr,
110
+ weight: torch.Tensor,
111
+ world_size: int,
112
+ backend: str,
113
+) -> RowParallelLinear:
114
+ """Wrap a BF16 [N, K] weight the way the model would hand it to GEMM-RS.
115
+
116
+ For the MXFP8 backends this quantizes online through the FlashInfer
117
+ kernel's post-load step, so the fused kernel and the unfused baseline see
118
+ the production weight and scale layouts.
119
+ """
120
+ N, K = weight.shape
121
+ quant_config = None
122
+ if backend != "bf16":
123
+ from vllm.config.quantization import QuantizationConfigArgs
124
+ from vllm.model_executor.layers.quantization.online.base import (
125
+ OnlineQuantizationConfig,
126
+ )
127
+
128
+ quant_config = OnlineQuantizationConfig(QuantizationConfigArgs(linear="mxfp8"))
129
+ with torch.device(weight.device):
130
+ linear = RowParallelLinear(
131
+ K * world_size,
132
+ N,
133
+ bias=False,
134
+ params_dtype=weight.dtype,
135
+ quant_config=quant_config,
136
+ # The baseline reduces the local partial itself, like the models.
137
+ reduce_results=False,
138
+ return_bias=False,
139
+ )
140
+ # Select the fused path before online quantization replaces the weights.
141
+ assert gemm_rs_ar.can_run(linear)
142
+ linear.weight = torch.nn.Parameter(weight, requires_grad=False)
143
+ linear.quant_method.process_weights_after_loading(linear)
144
+ return linear
145
+
146
+
147
+def capture_graph(
148
+ op: Callable[[], torch.Tensor],
149
+ stream: torch.cuda.Stream,
150
+ cpu_group: dist.ProcessGroup,
151
+) -> torch.cuda.CUDAGraph:
152
+ stream.wait_stream(torch.cuda.current_stream())
153
+ dist.barrier(group=cpu_group)
154
+ with torch.cuda.stream(stream):
155
+ for _ in range(3):
156
+ op()
157
+ stream.synchronize()
158
+ dist.barrier(group=cpu_group)
159
+
160
+ graph = torch.cuda.CUDAGraph()
161
+ with torch.cuda.graph(graph, stream=stream):
162
+ op()
163
+ torch.cuda.current_stream().wait_stream(stream)
164
+ dist.barrier(group=cpu_group)
165
+ return graph
166
+
167
+
168
+def benchmark_graphs(
169
+ candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]],
170
+ warmup_replays: int,
171
+ samples: int,
172
+ device_group: dist.ProcessGroup,
173
+ device_barrier: Callable[[], None],
174
+) -> dict[str, float]:
175
+ """Median over samples of the slowest rank's replay time, in us."""
176
+ names = list(candidate_graphs)
177
+ for round_index in range(warmup_replays):
178
+ for offset in range(len(names)):
179
+ name = names[(round_index + offset) % len(names)]
180
+ graphs = candidate_graphs[name]
181
+ device_barrier()
182
+ graphs[round_index % len(graphs)].replay()
183
+ torch.accelerator.synchronize()
184
+
185
+ timings: dict[str, list[float]] = {name: [] for name in names}
186
+ start = torch.cuda.Event(enable_timing=True)
187
+ end = torch.cuda.Event(enable_timing=True)
188
+ for sample_index in range(samples):
189
+ for offset in range(len(names)):
190
+ name = names[(sample_index + offset) % len(names)]
191
+ graphs = candidate_graphs[name]
192
+ device_barrier()
193
+ start.record()
194
+ graphs[sample_index % len(graphs)].replay()
195
+ end.record()
196
+ end.synchronize()
197
+
198
+ elapsed = torch.tensor(
199
+ start.elapsed_time(end) * 1000,
200
+ dtype=torch.float64,
201
+ device=torch.accelerator.current_device_index(),
202
+ )
203
+ dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
204
+ timings[name].append(elapsed.item())
205
+ return {name: statistics.median(values) for name, values in timings.items()}
206
+
207
+
208
+def benchmark_shape(
209
+ gemm_rs_ar: GemmRsAr,
210
+ mode: str,
211
+ backend: str,
212
+ M: int,
213
+ N: int,
214
+ K: int,
215
+ num_workspaces: int,
216
+ warmup_replays: int,
217
+ samples: int,
218
+ device_group: dist.ProcessGroup,
219
+ cpu_group: dist.ProcessGroup,
220
+ device_barrier: Callable[[], None],
221
+) -> dict[str, float | int | str]:
222
+ all_reduce = mode == "ar"
223
+ world_size = dist.get_world_size(device_group)
224
+ rank = dist.get_rank(device_group)
225
+ device = torch.device("cuda", torch.accelerator.current_device_index())
226
+ local_M = (M + world_size - 1) // world_size
227
+
228
+ rng = torch.Generator(device=device)
229
+ rng.manual_seed(1000 + rank * 10 + M + K)
230
+ inputs = [
231
+ torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng)
232
+ for _ in range(num_workspaces)
233
+ ]
234
+ projections = [
235
+ make_projection(
236
+ gemm_rs_ar,
237
+ torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng),
238
+ world_size,
239
+ backend,
240
+ )
241
+ for _ in range(num_workspaces)
242
+ ]
243
+
244
+ def collective(partial: torch.Tensor) -> torch.Tensor:
245
+ if all_reduce:
246
+ return tensor_model_parallel_all_reduce(partial)
247
+ return sp_reduce_scatter(partial)
248
+
249
+ def gemm(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
250
+ return linear(x)
251
+
252
+ def unfused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
253
+ return collective(linear(x))
254
+
255
+ def fused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor:
256
+ return gemm_rs_ar.apply(x, linear)
257
+
258
+ # The bare collective runs on the GEMM's own output so it sees the same
259
+ # message size and layout the unfused path hands to the collective.
260
+ partials = [linear(x) for x, linear in zip(inputs, projections)]
261
+
262
+ expected = unfused(inputs[0], projections[0])
263
+ actual = fused(inputs[0], projections[0])
264
+ torch.accelerator.synchronize(device)
265
+ rows = M if all_reduce else min(max(M - rank * local_M, 0), local_M)
266
+ torch.testing.assert_close(
267
+ actual[:rows], expected[:rows], rtol=5e-2, atol=4.0, msg=f"M={M}, K={K}"
268
+ )
269
+
270
+ candidates: dict[str, list[Callable[[], torch.Tensor]]] = {
271
+ "gemm": [lambda x=x, w=w: gemm(x, w) for x, w in zip(inputs, projections)],
272
+ "collective": [lambda p=p: collective(p) for p in partials],
273
+ "vllm": [lambda x=x, w=w: unfused(x, w) for x, w in zip(inputs, projections)],
274
+ "gemm_rs_ar": [
275
+ lambda x=x, w=w: fused(x, w) for x, w in zip(inputs, projections)
276
+ ],
277
+ }
278
+
279
+ candidate_graphs = {}
280
+ keepalive: list[object] = []
281
+ for name, ops in candidates.items():
282
+ stream = torch.cuda.Stream()
283
+ candidate_graphs[name] = [capture_graph(op, stream, cpu_group) for op in ops]
284
+ keepalive.append(stream)
285
+
286
+ times = benchmark_graphs(
287
+ candidate_graphs, warmup_replays, samples, device_group, device_barrier
288
+ )
289
+ floor = max(times["gemm"], times["collective"])
290
+ return {
291
+ "mode": mode.upper(),
292
+ "backend": backend,
293
+ "M": M,
294
+ "N": N,
295
+ "K": K,
296
+ "gemm_us": times["gemm"],
297
+ "collective_us": times["collective"],
298
+ "floor_us": floor,
299
+ "vllm_us": times["vllm"],
300
+ "gemm_rs_ar_us": times["gemm_rs_ar"],
301
+ "latency_change": (times["gemm_rs_ar"] - times["vllm"]) / times["vllm"],
302
+ "floor_gap": (times["gemm_rs_ar"] - floor) / floor,
303
+ }
304
+
305
+
306
+def print_results(results: list[dict[str, float | int | str]]) -> None:
307
+ collective = results[0]["mode"]
308
+ backend = results[0]["backend"]
309
+ print(f"### GEMM-{collective} vs vLLM unfused path ({backend} weights)")
310
+ print(
311
+ f"| M | N | K | GEMM (us) | {collective} (us) | floor (us) | vLLM (us) | "
312
+ f"GEMM-{collective} (us) | vs vLLM | vs floor |"
313
+ )
314
+ print("|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|")
315
+ for r in results:
316
+ print(
317
+ f"| {r['M']} | {r['N']} | {r['K']} | {r['gemm_us']:.1f} | "
318
+ f"{r['collective_us']:.1f} | {r['floor_us']:.1f} | {r['vllm_us']:.1f} | "
319
+ f"{r['gemm_rs_ar_us']:.1f} | {r['latency_change']:+.1%} | "
320
+ f"{r['floor_gap']:+.1%} |"
321
+ )
322
+
323
+
324
+def main() -> None:
325
+ args = parse_args()
326
+ default_n, default_k = _MODEL_PROJECTIONS[args.model]
327
+ if args.n is None:
328
+ args.n = default_n
329
+ assert args.m and min(args.m) > 0
330
+ assert args.n % 128 == 0
331
+ assert args.num_workspaces > 0
332
+ assert args.warmup_replays >= 0
333
+ assert args.samples > 0
334
+
335
+ local_rank = int(os.environ["LOCAL_RANK"])
336
+ torch.accelerator.set_device_index(local_rank)
337
+ init_distributed_environment()
338
+ world_size = dist.get_world_size()
339
+ if args.k is None:
340
+ assert all(K % world_size == 0 for K in default_k)
341
+ K_values = [K // world_size for K in default_k]
342
+ else:
343
+ K_values = args.k
344
+ k_alignment = 64 if args.backend == "bf16" else 128
345
+ assert all(K % k_alignment == 0 for K in K_values)
346
+
347
+ config = VllmConfig()
348
+ config.model_config = ModelConfig(dtype="bfloat16")
349
+ # Pin the online-MXFP8 kernel the projections quantize through.
350
+ config.kernel_config.linear_backend = (
351
+ "auto" if args.backend == "bf16" else args.backend
352
+ )
353
+ with set_current_vllm_config(config):
354
+ # Builds the TP communicator as in serving, so the baseline's
355
+ # collective is whatever vLLM's dispatch selects (FlashInfer all-reduce
356
+ # first, then NCCL symmetric memory, custom all-reduce, pynccl).
357
+ initialize_model_parallel(tensor_model_parallel_size=world_size)
358
+
359
+ tp_group = get_tp_group()
360
+ pynccl_comm = tp_group.device_communicator.pynccl_comm
361
+ assert pynccl_comm is not None
362
+ sync_input = torch.zeros(1, device=torch.accelerator.current_device_index())
363
+ sync_output = torch.empty_like(sync_input)
364
+
365
+ def device_barrier() -> None:
366
+ # Order the timed launch after a device-side rank rendezvous without
367
+ # including the rendezvous itself in the measured event interval.
368
+ pynccl_comm.all_reduce(sync_input, sync_output)
369
+
370
+ gemm_rs_ar = GemmRsAr(
371
+ max_M=max(args.m),
372
+ N=args.n,
373
+ all_reduce=args.mode == "ar",
374
+ )
375
+ with set_current_vllm_config(config):
376
+ results = [
377
+ benchmark_shape(
378
+ gemm_rs_ar,
379
+ args.mode,
380
+ args.backend,
381
+ M,
382
+ args.n,
383
+ K,
384
+ args.num_workspaces,
385
+ args.warmup_replays,
386
+ args.samples,
387
+ tp_group.device_group,
388
+ tp_group.cpu_group,
389
+ device_barrier,
390
+ )
391
+ for K in K_values
392
+ for M in args.m
393
+ ]
394
+ del gemm_rs_ar
395
+
396
+ if tp_group.rank_in_group == 0:
397
+ print_results(results)
398
+ if args.json:
399
+ with open(args.json, "w") as f:
400
+ json.dump(results, f, indent=2)
401
+
402
+ dist.barrier(group=tp_group.cpu_group)
403
+ cleanup_dist_env_and_memory()
404
+
405
+
406
+if __name__ == "__main__":
407
+ main()
benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py
deleted
-469
@@ -1,469 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
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_ar.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_ar import GemmRsAr
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
- "--mode",
46
- choices=("rs", "ar"),
47
- default="rs",
48
- help="Collective mode to benchmark.",
49
- )
50
- parser.add_argument(
51
- "--m",
52
- type=int,
53
- nargs="+",
54
- default=[128, 512, 2048, 8192, 32768],
55
- help="Global token counts to benchmark.",
56
- )
57
- parser.add_argument(
58
- "--k",
59
- type=int,
60
- nargs="+",
61
- help=(
62
- "Per-rank input dimensions. By default, derive the Kimi-K3 "
63
- "shared-expert down-proj and O-proj dimensions from the TP "
64
- "world size."
65
- ),
66
- )
67
- parser.add_argument("--n", type=int, default=7168)
68
- parser.add_argument(
69
- "--num-workspaces",
70
- type=int,
71
- default=10,
72
- help="Pointer-distinct inputs and CUDA graphs to rotate.",
73
- )
74
- parser.add_argument("--warmup-replays", type=int, default=5)
75
- parser.add_argument("--samples", type=int, default=20)
76
- return parser.parse_args()
77
-
78
-
79
-def capture_graph(
80
- op: Callable[[], torch.Tensor],
81
- stream: torch.cuda.Stream,
82
- cpu_group: dist.ProcessGroup,
83
-) -> tuple[torch.cuda.CUDAGraph, list[torch.Tensor | None]]:
84
- result: list[torch.Tensor | None] = [None]
85
- stream.wait_stream(torch.cuda.current_stream())
86
- dist.barrier(group=cpu_group)
87
- with torch.cuda.stream(stream):
88
- for _ in range(3):
89
- result[0] = op()
90
- stream.synchronize()
91
- dist.barrier(group=cpu_group)
92
-
93
- graph = torch.cuda.CUDAGraph()
94
- with torch.cuda.graph(graph, stream=stream):
95
- result[0] = op()
96
- torch.cuda.current_stream().wait_stream(stream)
97
- dist.barrier(group=cpu_group)
98
- return graph, result
99
-
100
-
101
-def benchmark_graphs(
102
- candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]],
103
- warmup_replays: int,
104
- samples: int,
105
- device_group: dist.ProcessGroup,
106
- device_barrier: Callable[[], None],
107
-) -> dict[str, float]:
108
- candidate_names = list(candidate_graphs)
109
- for round_index in range(warmup_replays):
110
- for candidate_index in range(len(candidate_names)):
111
- candidate_id = (round_index + candidate_index) % len(candidate_names)
112
- name = candidate_names[candidate_id]
113
- graphs = candidate_graphs[name]
114
- device_barrier()
115
- graphs[round_index % len(graphs)].replay()
116
- torch.accelerator.synchronize()
117
-
118
- timings: dict[str, list[float]] = {name: [] for name in candidate_names}
119
- start = torch.cuda.Event(enable_timing=True)
120
- end = torch.cuda.Event(enable_timing=True)
121
- for sample_index in range(samples):
122
- for candidate_index in range(len(candidate_names)):
123
- candidate_id = (sample_index + candidate_index) % len(candidate_names)
124
- name = candidate_names[candidate_id]
125
- graphs = candidate_graphs[name]
126
- device_barrier()
127
- start.record()
128
- graphs[sample_index % len(graphs)].replay()
129
- end.record()
130
- end.synchronize()
131
-
132
- elapsed = torch.tensor(
133
- start.elapsed_time(end) * 1000,
134
- dtype=torch.float64,
135
- device=torch.accelerator.current_device_index(),
136
- )
137
- dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
138
- timings[name].append(elapsed.item())
139
- return {name: statistics.median(values) for name, values in timings.items()}
140
-
141
-
142
-def valid_rows(M: int, local_M: int, rank: int) -> int:
143
- return min(max(M - rank * local_M, 0), local_M)
144
-
145
-
146
-def benchmark_shape(
147
- gemm_rs_ar: GemmRsAr,
148
- mode: str,
149
- M: int,
150
- N: int,
151
- K: int,
152
- num_workspaces: int,
153
- warmup_replays: int,
154
- samples: int,
155
- device_group: dist.ProcessGroup,
156
- cpu_group: dist.ProcessGroup,
157
- device_barrier: Callable[[], None],
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())
163
- padded_M = (M + world_size - 1) // world_size * world_size
164
- local_M = padded_M // world_size
165
-
166
- rng = torch.Generator(device=device)
167
- rng.manual_seed(1000 + rank * 10 + M + K)
168
- inputs = [
169
- torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng)
170
- for _ in range(num_workspaces)
171
- ]
172
- weights = [
173
- torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng)
174
- for _ in range(num_workspaces)
175
- ]
176
-
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)
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):
184
- collective_input = symm_mem.empty(
185
- (padded_M, N),
186
- dtype=torch.bfloat16,
187
- device=device,
188
- )
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)
196
-
197
- if padded_M > M:
198
- partial[M:].zero_()
199
- symm_partial[M:].zero_()
200
-
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
-
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,
225
- group=device_group,
226
- )
227
- return symm_output
228
-
229
- return run
230
-
231
- def make_fused_gemm_collective(
232
- x: torch.Tensor, weight: torch.Tensor
233
- ) -> Callable[[], torch.Tensor]:
234
- def run() -> torch.Tensor:
235
- return gemm_rs_ar(x, weight)
236
-
237
- return run
238
-
239
- def make_torch_gemm(
240
- x: torch.Tensor,
241
- weight: torch.Tensor,
242
- ) -> Callable[[], torch.Tensor]:
243
- def run() -> torch.Tensor:
244
- return torch.mm(x, weight.T, out=gemm_output)
245
-
246
- return run
247
-
248
- def make_ring_ll_collective(
249
- collective_input: torch.Tensor,
250
- ) -> Callable[[], torch.Tensor]:
251
- def run() -> torch.Tensor:
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
-
262
- def make_ldmc_collective(
263
- collective_input: torch.Tensor,
264
- ) -> Callable[[], torch.Tensor]:
265
- def run() -> torch.Tensor:
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
275
-
276
- candidates = (
277
- Candidate(
278
- "ring_ll_us",
279
- [make_torch_ring_ll_gemm_collective(x, w) for x, w in zip(inputs, weights)],
280
- ),
281
- Candidate(
282
- "ldmc_us",
283
- [make_torch_ldmc_gemm_collective(x, w) for x, w in zip(inputs, weights)],
284
- ),
285
- Candidate(
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",
291
- [make_torch_gemm(x, w) for x, w in zip(inputs, weights)],
292
- check_correctness=False,
293
- ),
294
- Candidate(
295
- "ring_ll_collective_us",
296
- [make_ring_ll_collective(x) for x in collective_inputs],
297
- check_correctness=False,
298
- ),
299
- Candidate(
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]()
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
311
- actual = candidate.runs[0]()
312
- torch.accelerator.synchronize(device)
313
- torch.testing.assert_close(
314
- actual[:rows],
315
- expected[:rows],
316
- rtol=5e-2,
317
- atol=4.0,
318
- )
319
-
320
- candidate_graphs = {}
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]
325
- candidate_graphs[candidate.name] = [graph for graph, _ in bundles]
326
- graph_keepalive.extend(bundles)
327
- graph_keepalive.append(stream)
328
-
329
- times = benchmark_graphs(
330
- candidate_graphs,
331
- warmup_replays,
332
- samples,
333
- device_group,
334
- device_barrier,
335
- )
336
-
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,
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
-
350
-def print_results(results: list[dict[str, float | int | str]]) -> None:
351
- results_df = pd.DataFrame(results)
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
- ]
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
- )
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
- )
388
-
389
- print(f"### {collective} end-to-end latency")
390
- print(end_to_end.to_markdown(index=False))
391
- print(f"\n### {collective} component latency")
392
- print(components.to_markdown(index=False))
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()
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
405
- assert args.samples > 0
406
-
407
- local_rank = int(os.environ["LOCAL_RANK"])
408
- torch.accelerator.set_device_index(local_rank)
409
- init_distributed_environment()
410
- world_size = dist.get_world_size()
411
- if args.k is None:
412
- assert all(K % world_size == 0 for K in _KIMI_K3_PROJECTION_K)
413
- K_values = [K // world_size for K in _KIMI_K3_PROJECTION_K]
414
- else:
415
- K_values = args.k
416
- assert all(K % 64 == 0 for K in K_values)
417
- # Reserve symmetric memory for the NCCL-managed benchmark allocations.
418
- os.environ["VLLM_ALLREDUCE_USE_SYMM_MEM"] = "0"
419
- # NCCL-managed symmetric allocations select the NVLS/LDMC collective path.
420
- symm_mem.set_backend("NCCL")
421
- with set_current_vllm_config(VllmConfig()):
422
- initialize_model_parallel(tensor_model_parallel_size=world_size)
423
-
424
- tp_group = get_tp_group()
425
- group_warmup = torch.zeros(1, device=torch.accelerator.current_device_index())
426
- dist.all_reduce(group_warmup, group=tp_group.device_group)
427
- pynccl_comm = tp_group.device_communicator.pynccl_comm
428
- assert pynccl_comm is not None
429
- sync_input = torch.zeros(1, device=torch.accelerator.current_device_index())
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
-
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(
444
- gemm_rs_ar,
445
- args.mode,
446
- M,
447
- args.n,
448
- K,
449
- args.num_workspaces,
450
- args.warmup_replays,
451
- args.samples,
452
- tp_group.device_group,
453
- tp_group.cpu_group,
454
- device_barrier,
455
- )
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)
465
- cleanup_dist_env_and_memory()
466
-
467
-
468
-if __name__ == "__main__":
469
- main()
tests/kernels/test_gemm_rs_ar.py
renamed
+87
-26
@@ -6,7 +6,7 @@ import torch
6
import torch.distributed as dist
7
import torch.multiprocessing as mp
8
9
-from tests.utils import ensure_current_vllm_config
9
+from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config
10
from vllm.distributed import cleanup_dist_env_and_memory
11
from vllm.distributed.parallel_state import (
12
init_distributed_environment,
@@ -17,6 +17,7 @@ from vllm.utils.network_utils import get_open_port
17
from vllm.utils.system_utils import update_environment_variables
18
19
_SHAPES = (
20
+ (129, 512),
21
(129, 768),
22
(257, 768),
23
(1023, 4224),
@@ -29,6 +30,7 @@ _N = 512
30
def _reference(
31
x: torch.Tensor,
32
weight: torch.Tensor,
33
+ weight_scale: torch.Tensor | None,
34
world_size: int,
35
group: dist.ProcessGroup,
36
all_reduce: bool,
@@ -40,7 +42,21 @@ def _reference(
42
dtype=x.dtype,
43
device=x.device,
44
)
43
- torch.mm(x, weight.T, out=partial[:M])
45
+ if weight_scale is None:
46
+ torch.mm(x, weight.T, out=partial[:M])
47
+ else:
48
+ from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
49
+ mxfp8_e4m3_quantize,
50
+ )
51
+
52
+ x_q, x_sf = mxfp8_e4m3_quantize(x, is_sf_swizzled_layout=True)
53
+ partial[:M] = torch._scaled_mm(
54
+ x_q,
55
+ weight.T,
56
+ x_sf.view(torch.float8_e8m0fnu),
57
+ weight_scale.view(torch.float8_e8m0fnu),
58
+ out_dtype=x.dtype,
59
+ )
60
if padded_M > M:
61
partial[M:].zero_()
62
@@ -75,6 +91,10 @@ def _assert_valid_rows_close(
91
rtol=5e-2,
92
atol=4.0,
93
)
94
+ # Sequence-parallel callers may pass an unpadded M and read the padding
95
+ # rows back (they flow through per-token kernels before being dropped),
96
+ # so RS must leave them zero like ``sp_reduce_scatter`` does.
97
+ assert not actual[valid_rows:].any()
98
99
100
def _run_mode(
@@ -85,19 +105,52 @@ def _run_mode(
105
rank: int,
106
world_size: int,
107
weights: dict[int, torch.Tensor],
108
+ backend: str,
109
) -> None:
110
# 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
111
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
112
113
gemm_rs_ar = GemmRsAr(
114
max_M=max(M for M, _ in _SHAPES),
115
N=_N,
116
all_reduce=all_reduce,
117
)
118
+ from vllm.config.quantization import QuantizationConfigArgs
119
+ from vllm.model_executor.layers.linear import RowParallelLinear
120
+ from vllm.model_executor.layers.quantization.online.base import (
121
+ OnlineQuantizationConfig,
122
+ )
123
+
124
+ quant_config = (
125
+ None
126
+ if backend == "bf16"
127
+ else OnlineQuantizationConfig(QuantizationConfigArgs(linear="mxfp8"))
128
+ )
129
+ projections = {}
130
+ operands = {}
131
+ for K, w in weights.items():
132
+ with torch.device(device):
133
+ linear = RowParallelLinear(
134
+ K * world_size,
135
+ _N,
136
+ bias=False,
137
+ params_dtype=torch.bfloat16,
138
+ quant_config=quant_config,
139
+ )
140
+ # Select the fused path before online quantization replaces meta weights.
141
+ assert gemm_rs_ar.can_run(linear)
142
+ linear.weight = torch.nn.Parameter(w.clone(), requires_grad=False)
143
+ linear.quant_method.process_weights_after_loading(linear)
144
+ projections[K] = linear
145
+ weight = linear.weight
146
+ if backend == "flashinfer_cutedsl":
147
+ weight = weight.t()
148
+ operands[K] = (weight, getattr(linear, "weight_scale", None))
149
+
150
input_generator = torch.Generator(device=device)
151
99
- # Alternating shapes exercise producer-flag reuse across different grids,
100
- # CTA-group choices, and both BN=128 and BN=256 dispatches.
152
+ # Alternating shapes exercise producer-flag reuse across different grids
153
+ # and CTA-group choices.
154
for M, K in (*_SHAPES, *_SHAPES[::-1]):
155
input_generator.manual_seed(2000 + M + K)
156
x = torch.randn(
@@ -107,8 +160,8 @@ def _run_mode(
160
device=device,
161
generator=input_generator,
162
)
110
- expected = _reference(x, weights[K], world_size, group, all_reduce)
111
- actual = gemm_rs_ar(x, weights[K])
163
+ expected = _reference(x, *operands[K], world_size, group, all_reduce)
164
+ actual = gemm_rs_ar.apply(x, projections[K])
165
torch.accelerator.synchronize(device)
166
_assert_valid_rows_close(actual, expected, M, rank, all_reduce)
167
@@ -123,9 +176,9 @@ def _run_mode(
176
device=device,
177
generator=input_generator,
178
)
126
- first_output = gemm_rs_ar(lifetime_x, weights[lifetime_K])
179
+ first_output = gemm_rs_ar.apply(lifetime_x, projections[lifetime_K])
180
first_snapshot = first_output.clone()
128
- second_output = gemm_rs_ar(-lifetime_x, weights[lifetime_K])
181
+ second_output = gemm_rs_ar.apply(-lifetime_x, projections[lifetime_K])
182
torch.accelerator.synchronize(device)
183
assert first_output.data_ptr() != second_output.data_ptr()
184
torch.testing.assert_close(first_output, first_snapshot, rtol=0, atol=0)
@@ -141,7 +194,7 @@ def _run_mode(
194
)
195
graph_expected = _reference(
196
graph_x,
144
- weights[graph_K],
197
+ *operands[graph_K],
198
world_size,
199
group,
200
all_reduce,
@@ -152,13 +205,13 @@ def _run_mode(
205
capture_stream.wait_stream(torch.cuda.current_stream())
206
with torch.cuda.stream(capture_stream):
207
for _ in range(3):
155
- graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
208
+ graph_output.copy_(gemm_rs_ar.apply(graph_x, projections[graph_K]))
209
capture_stream.synchronize()
210
dist.barrier(group=group)
211
212
graph = torch.cuda.CUDAGraph()
213
with torch.cuda.graph(graph, stream=capture_stream):
161
- graph_output.copy_(gemm_rs_ar(graph_x, weights[graph_K]))
214
+ graph_output.copy_(gemm_rs_ar.apply(graph_x, projections[graph_K]))
215
torch.cuda.current_stream().wait_stream(capture_stream)
216
dist.barrier(group=group)
217
@@ -177,7 +230,7 @@ def _run_mode(
230
del gemm_rs_ar, capture_stream, graph, graph_output
231
232
180
-def _worker(local_rank: int, world_size: int, master_port: int) -> None:
233
+def _worker(local_rank: int, world_size: int, master_port: int, backend: str) -> None:
234
# This module pulls in cute_dsl, which is unavailable off CUDA, so importing
235
# it at module level would fail collection. Import it where it is used, as
236
# `kda.py` does.
@@ -195,7 +248,10 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
248
)
249
250
init_distributed_environment()
198
- with ensure_current_vllm_config():
251
+ config = VllmConfig()
252
+ config.model_config = ModelConfig(dtype="bfloat16")
253
+ config.kernel_config.linear_backend = "auto" if backend == "bf16" else backend
254
+ with set_current_vllm_config(config):
255
initialize_model_parallel(tensor_model_parallel_size=world_size)
256
257
group = dist.group.WORLD
@@ -215,24 +271,29 @@ def _worker(local_rank: int, world_size: int, master_port: int) -> None:
271
272
# Production binds one mode per worker; exercise both mode-bound instances
273
# 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
- )
274
+ with set_current_vllm_config(config):
275
+ for all_reduce in (False, True):
276
+ _run_mode(
277
+ all_reduce=all_reduce,
278
+ device=device,
279
+ group=group,
280
+ rank=rank,
281
+ world_size=world_size,
282
+ weights=weights,
283
+ backend=backend,
284
+ )
285
cleanup_dist_env_and_memory()
286
287
288
+@pytest.mark.parametrize(
289
+ "backend", ["bf16", "flashinfer_cutlass", "flashinfer_cutedsl"]
290
+)
291
@pytest.mark.distributed(num_gpus=2)
292
@pytest.mark.skipif(
293
not current_platform.is_device_capability_family(100),
233
- reason="Kimi-K3 GEMM-RS/AR requires SM100",
294
+ reason="GEMM-RS/AR requires SM100",
295
)
235
-def test_kimi_k3_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch) -> None:
296
+def test_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch, backend: str) -> None:
297
world_size = 2
298
if torch.accelerator.device_count() < world_size:
299
pytest.skip("GEMM-RS/AR requires two GPUs")
@@ -242,7 +303,7 @@ def test_kimi_k3_gemm_rs_ar(monkeypatch: pytest.MonkeyPatch) -> None:
303
try:
304
mp.spawn(
305
_worker,
245
- args=(world_size, get_open_port()),
306
+ args=(world_size, get_open_port(), backend),
307
nprocs=world_size,
308
)
309
finally:
vllm/cute_utils/_tcgen05.py
+71
@@ -78,6 +78,10 @@ def make_sdesc_128B_swizzle(LBO: int):
78
return Uint64((LBO >> 4 << 16) | (SBO >> 4 << 32) | (1 << 46) | (2 << 61))
79
80
81
+def make_mxfp8_idesc(MMA_M: int, MMA_N: int):
82
+ return Uint32((MMA_N >> 3 << 17) | (1 << 23) | (MMA_M >> 7 << 27))
83
+
84
+
85
@dsl_user_op
86
def mma_f16(
87
d_tmem,
@@ -130,6 +134,73 @@ def mma_ts_f16(
134
)
135
136
137
+@dsl_user_op
138
+def mma_mxfp8(
139
+ d_tmem,
140
+ a_desc,
141
+ b_desc,
142
+ idesc,
143
+ sfa_tmem,
144
+ sfb_tmem,
145
+ enable_input_d,
146
+ cta_group: int = 1,
147
+ *,
148
+ loc=None,
149
+ ip=None,
150
+) -> None:
151
+ with cute.arch.elect_one():
152
+ nvvm.tcgen05_mma_block_scale(
153
+ nvvm.Tcgen05MMAKind.MXF8F6F4,
154
+ NVVM_CTA_GROUP_MAP[cta_group],
155
+ _make_tmem_llvm_ptr(d_tmem, loc=loc, ip=ip),
156
+ Uint64(a_desc).ir_value(loc=loc, ip=ip),
157
+ Uint64(b_desc).ir_value(loc=loc, ip=ip),
158
+ Int32(idesc).ir_value(loc=loc, ip=ip),
159
+ Boolean(enable_input_d).ir_value(loc=loc, ip=ip),
160
+ _make_tmem_llvm_ptr(sfa_tmem, loc=loc, ip=ip),
161
+ _make_tmem_llvm_ptr(sfb_tmem, loc=loc, ip=ip),
162
+ scale_vec_size=nvvm.Tcgen05MMAScaleVecSize.X1,
163
+ loc=loc,
164
+ ip=ip,
165
+ )
166
+
167
+
168
+@dsl_user_op
169
+def cp(
170
+ tmem,
171
+ sdesc,
172
+ shape: str,
173
+ mcast: str,
174
+ cta_group: int = 1,
175
+ *,
176
+ loc=None,
177
+ ip=None,
178
+) -> None:
179
+ SHAPE_MAP = {
180
+ "128x256b": nvvm.Tcgen05CpShape.SHAPE_128x256b,
181
+ "4x256b": nvvm.Tcgen05CpShape.SHAPE_4x256b,
182
+ "128x128b": nvvm.Tcgen05CpShape.SHAPE_128x128b,
183
+ "64x128b": nvvm.Tcgen05CpShape.SHAPE_64x128b,
184
+ "32x128b": nvvm.Tcgen05CpShape.SHAPE_32x128b,
185
+ }
186
+ MCAST_MAP = {
187
+ "none": nvvm.Tcgen05CpMulticast.NONE,
188
+ "warpx2::02_13": nvvm.Tcgen05CpMulticast.WARPX2_02_13,
189
+ "warpx2::01_23": nvvm.Tcgen05CpMulticast.WARPX2_01_23,
190
+ "warpx4": nvvm.Tcgen05CpMulticast.WARPX4,
191
+ }
192
+ with cute.arch.elect_one():
193
+ nvvm.tcgen05_cp(
194
+ SHAPE_MAP[shape],
195
+ _make_tmem_llvm_ptr(tmem, loc=loc, ip=ip),
196
+ Uint64(sdesc).ir_value(loc=loc, ip=ip),
197
+ group=NVVM_CTA_GROUP_MAP[cta_group],
198
+ multicast=MCAST_MAP[mcast],
199
+ loc=loc,
200
+ ip=ip,
201
+ )
202
+
203
+
204
@dsl_user_op
205
def commit(mbar, cta_mask=None, cta_group: int = 1, *, loc=None, ip=None):
206
mbar_llvm = mbar.to_llvm_ptr(loc=loc, ip=ip)
vllm/envs.py
+6
-4
@@ -213,7 +213,7 @@ if TYPE_CHECKING:
213
VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT: bool = False
214
VLLM_KIMI_K3_AUX_ATTN_RES_STREAM: bool = False
215
VLLM_KIMI_K3_GEMM_AR: bool = True
216
- VLLM_KIMI_K3_GEMM_RS: bool = False
216
+ VLLM_ENABLE_GEMM_RS: bool = False
217
VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER: bool = True
218
VLLM_USE_FLASHINFER_MOE_INT4: bool = False
219
VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR: str | None = None
@@ -1629,9 +1629,11 @@ environment_variables: dict[str, Callable[[], Any]] = {
1629
# Use the SM100 BF16 GEMM-AR kernel for eligible Kimi-K3 row-parallel
1630
# attention projections. All TP ranks must belong to one NVLink domain.
1631
"VLLM_KIMI_K3_GEMM_AR": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_AR", "1"))),
1632
- # Use the SM100 BF16 GEMM-RS kernel for eligible Kimi-K3 sequence-parallel
1633
- # row-parallel projections. All TP ranks must belong to one NVLink domain.
1634
- "VLLM_KIMI_K3_GEMM_RS": lambda: bool(int(os.getenv("VLLM_KIMI_K3_GEMM_RS", "0"))),
1632
+ # Fuse eligible sequence-parallel row-parallel projections with their TP
1633
+ # reduce-scatter using the SM100 BF16/MXFP8 GEMM-RS kernel (Kimi-K3
1634
+ # attention/shared-expert projections, DeepSeek-V4.1 ``wo_b``). All TP
1635
+ # ranks must belong to one NVLink domain.
1636
+ "VLLM_ENABLE_GEMM_RS": lambda: bool(int(os.getenv("VLLM_ENABLE_GEMM_RS", "0"))),
1637
# Allow use of FlashInfer FP8 block-scale GEMM for linear layers.
1638
# This uses TensorRT-LLM kernels and requires SM90+ (Hopper).
1639
"VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER": lambda: bool(
vllm/model_executor/kernels/linear/cute_dsl/gemm_rs_ar.py
renamed
+340
-57
@@ -1,18 +1,36 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-"""SM100 BF16 GEMM with fused tensor-parallel reduce-scatter/all-reduce."""
4
+"""SM100 BF16/MXFP8 GEMM with fused tensor-parallel reduce-scatter/all-reduce.
5
+
6
+Model-neutral: any row-parallel projection whose output is reduced across TP
7
+(Kimi-K3 ``o_proj``/``down_proj``, DeepSeek-V4.1 ``wo_b``) can bind to the
8
+process-wide workspace through ``maybe_init_gemm_rs_ar`` + ``GemmRsAr.apply``.
9
+"""
10
11
# Based on CUTLASS's Blackwell distributed GEMM-RS example at dcf215a.
12
# See https://github.com/NVIDIA/cutlass/issues/3117 for memory semantics.
13
+# The MXFP8 path follows the block-scaled SM100 GEMM template from
14
+# https://github.com/gau-nernst/gn-kernels (sm100_mm_mxfp8.py).
15
16
from functools import cache
17
+from typing import TYPE_CHECKING
18
19
import cutlass
20
import torch
21
import torch.distributed._symmetric_memory as symm_mem
22
from cuda.bindings.driver import CUstream
15
-from cutlass import BFloat16, Int32, Int64, Uint16, cute, utils
23
+from cutlass import (
24
+ BFloat16,
25
+ Float8E4M3FN,
26
+ Float8E8M0FNU,
27
+ Int32,
28
+ Int64,
29
+ Uint16,
30
+ Uint64,
31
+ cute,
32
+ utils,
33
+)
34
from cutlass._mlir import ir
35
from cutlass._mlir.dialects import llvm, nvvm, vector
36
from cutlass.cute.nvgpu import cpasync, tcgen05
@@ -24,6 +42,10 @@ from vllm.cute_utils import _tcgen05, mbarrier, simple_tma_copy, to_cta0_smem
42
from vllm.distributed import get_tp_group
43
from vllm.logger import init_logger
44
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
45
+from vllm.platforms import current_platform
46
+
47
+if TYPE_CHECKING:
48
+ from vllm.config import VllmConfig
49
50
logger = init_logger(__name__)
51
@@ -88,24 +110,31 @@ def multimem_st_16B(dst: cute.Tensor, value: cute.Tensor, *, loc=None, ip=None)
110
)
111
112
91
-class Sm100GemmRsArBF16:
113
+class Sm100GemmRsAr:
114
def __init__(
115
self,
116
rank: int,
117
num_ranks: int,
96
- BN: int = 128,
118
cta_group: int = 1,
119
all_reduce: bool = False,
120
+ dtype: str = "bf16",
121
) -> None:
122
+ assert dtype in ("bf16", "mxfp8")
123
self.rank = rank
124
self.num_ranks = num_ranks
102
- BM, BK = 128, 64
125
+ BM = 128
126
+ BN = 128
127
+ BK = {"bf16": 64, "mxfp8": 128}[dtype] # 128B
128
self.cta_tile = (BM, BN, BK)
129
self.cta_group = cta_group
130
self.all_reduce = all_reduce
131
+ self.dtype = dtype
132
133
smem_bytes = get_smem_capacity_in_bytes()
108
- self.stage_size = (BM + (BN // cta_group)) * BK * 2
134
+ if dtype == "bf16":
135
+ self.stage_size = (BM + (BN // cta_group)) * BK * 2
136
+ elif dtype == "mxfp8":
137
+ self.stage_size = (BM + (BN // cta_group)) * BK + (BM + BN) * (BK // 32)
138
self.num_stages = smem_bytes // self.stage_size
139
140
@cute.jit
@@ -124,11 +153,36 @@ class Sm100GemmRsArBF16:
153
layout = cute.make_composed_layout(swizzle_128b, 0, layout)
154
return cpasync.make_tiled_tma_atom(tma_op, tensor, layout, (BM, BK))
155
156
+ @cute.jit
157
+ def prepare_sf_tma(
158
+ self,
159
+ SF: cute.Tensor,
160
+ R: Int32,
161
+ Ksf: Int32,
162
+ BR: cutlass.Constexpr,
163
+ BKsf: cutlass.Constexpr,
164
+ ) -> cpasync.TmaInfo:
165
+ tma_group = (
166
+ tcgen05.CtaGroup.TWO if self.cta_group == 2 else tcgen05.CtaGroup.ONE
167
+ )
168
+ tma_op = cpasync.CopyBulkTensorTileG2SOp(cta_group=tma_group)
169
+ # NVIDIA SF gmem layout: an [R, Ksf] scale grid padded to R % 128 == 0
170
+ # and permuted as [R/128, Ksf/4, 32, 4, 4]. The trailing [32, 4, 4]
171
+ # atom is the opaque 512B block tcgen05 expects, so treat it as bytes.
172
+ # The Int64 recast keeps the TMA boxDim <= 256.
173
+ g_layout = cute.make_layout((Ksf * 128, cute.ceil_div(R, 128)))
174
+ SF_i64 = cute.recast_tensor(cute.make_tensor(SF.iterator, g_layout), Int64)
175
+ tiler = (128 * BKsf // 8, BR // 128)
176
+ layout = cute.make_layout((*tiler, self.num_stages))
177
+ return cpasync.make_tiled_tma_atom(tma_op, SF_i64, layout, tiler)
178
+
179
@cute.jit
180
def __call__(
181
self,
182
A: cute.Tensor,
183
B: cute.Tensor,
184
+ SFA: cute.Tensor,
185
+ SFB: cute.Tensor,
186
partial_uc: cute.Tensor,
187
partial_mc_ptr: cute.Pointer,
188
output: cute.Tensor,
@@ -138,10 +192,17 @@ class Sm100GemmRsArBF16:
192
grid_size: Int32,
193
stream: CUstream,
194
) -> None:
195
+ M, K = A.shape
196
N = B.shape[0]
197
BM, BN, BK = self.cta_tile
198
A_tma = self.prepare_tma(A, BM, BK)
199
B_tma = self.prepare_tma(B, BN // self.cta_group, BK)
200
+ if cutlass.const_expr(self.dtype == "mxfp8"):
201
+ SFA_tma = self.prepare_sf_tma(SFA, M, K // 32, BM, BK // 32)
202
+ SFB_tma = self.prepare_sf_tma(SFB, N, K // 32, BN, BK // 32)
203
+ else:
204
+ SFA_tma = None
205
+ SFB_tma = None
206
padded_M = partial_uc.shape[0]
207
partial_mc = cute.make_tensor(
208
partial_mc_ptr,
@@ -158,6 +219,8 @@ class Sm100GemmRsArBF16:
219
self.kernel(
220
A_tma,
221
B_tma,
222
+ SFA_tma,
223
+ SFB_tma,
224
partial_uc,
225
partial_mc,
226
output,
@@ -171,6 +234,8 @@ class Sm100GemmRsArBF16:
234
self,
235
A_tma: cpasync.TmaInfo,
236
B_tma: cpasync.TmaInfo,
237
+ SFA_tma: cpasync.TmaInfo,
238
+ SFB_tma: cpasync.TmaInfo,
239
partial_uc: cute.Tensor,
240
partial_mc: cute.Tensor,
241
output: cute.Tensor,
@@ -190,25 +255,33 @@ class Sm100GemmRsArBF16:
255
256
is_2cta = cta_group == 2
257
cta_rank = raw_bid % self.cta_group
193
- num_tmem_stages = 512 // BN
258
+
259
+ # MXFP8 reserves the last 16 tmem columns (496-512) for the A/B scale
260
+ # factors.
261
+ num_tmem_stages = (496 if self.dtype == "mxfp8" else 512) // BN
262
263
smem = utils.SmemAllocator()
264
sA = smem.allocate_tensor(
197
- BFloat16,
265
+ A_tma.atom.value_type,
266
A_tma.smem_layout.outer,
267
byte_alignment=128,
268
swizzle=A_tma.smem_layout.inner,
269
)
270
sB = smem.allocate_tensor(
203
- BFloat16,
271
+ B_tma.atom.value_type,
272
B_tma.smem_layout.outer,
273
byte_alignment=128,
274
swizzle=B_tma.smem_layout.inner,
275
)
276
+ if cutlass.const_expr(self.dtype == "mxfp8"):
277
+ sSFA = smem.allocate_tensor(Int64, SFA_tma.smem_layout, byte_alignment=128)
278
+ sSFB = smem.allocate_tensor(Int64, SFB_tma.smem_layout, byte_alignment=128)
279
+
280
tma_full_mbar = smem.allocate_array(Int64, num_stages)
281
tma_empty_mbar = smem.allocate_array(Int64, num_stages)
282
tmem_full_mbar = smem.allocate_array(Int64, num_tmem_stages)
283
tmem_empty_mbar = smem.allocate_array(Int64, num_tmem_stages)
284
+
285
taddr = smem.allocate(Int32, 4)
286
287
# Named barriers
@@ -238,6 +311,9 @@ class Sm100GemmRsArBF16:
311
elif warp_id == 1:
312
cpasync.prefetch_descriptor(A_tma.atom)
313
cpasync.prefetch_descriptor(B_tma.atom)
314
+ if cutlass.const_expr(self.dtype == "mxfp8"):
315
+ cpasync.prefetch_descriptor(SFA_tma.atom)
316
+ cpasync.prefetch_descriptor(SFB_tma.atom)
317
318
if cutlass.const_expr(is_2cta):
319
cute.arch.cluster_arrive_relaxed()
@@ -261,12 +337,22 @@ class Sm100GemmRsArBF16:
337
# [(BM, BK), (M/BM, K/BK)]
338
gA_tiles = cute.zipped_divide(A_tma.tma_tensor, (BM, BK))
339
gB_tiles = cute.zipped_divide(B_tma.tma_tensor, (BN // cta_group, BK))
340
+ if cutlass.const_expr(self.dtype == "mxfp8"):
341
+ BKsf = BK // 32
342
+ gSFA_tiles = cute.zipped_divide(
343
+ SFA_tma.tma_tensor, (128 * BKsf // 8, BM // 128)
344
+ )
345
+ gSFB_tiles = cute.zipped_divide(
346
+ SFB_tma.tma_tensor, (128 * BKsf // 8, BN // 128)
347
+ )
348
349
for bid in range(raw_bid, total_tiles, num_bids):
350
bid_m = bid % grid_m
351
bid_n = bid // grid_m
352
if cutlass.const_expr(cta_group == 2):
269
- bid_n = bid_n * cta_group + cta_rank
353
+ bid_n_b = bid_n * cta_group + cta_rank
354
+ else:
355
+ bid_n_b = bid_n
356
357
for iter_k in cutlass.range(cute.ceil_div(K, BK), unroll=1):
358
mbar = tma_full_mbar_ + tma_stage
@@ -282,10 +368,23 @@ class Sm100GemmRsArBF16:
368
)
369
simple_tma_copy(
370
B_tma.atom,
285
- gB_tiles[None, (bid_n, iter_k)],
371
+ gB_tiles[None, (bid_n_b, iter_k)],
372
sB[None, None, tma_stage],
373
mbar,
374
)
375
+ if cutlass.const_expr(self.dtype == "mxfp8"):
376
+ simple_tma_copy(
377
+ SFA_tma.atom,
378
+ gSFA_tiles[None, (iter_k, bid_m)],
379
+ sSFA[None, None, tma_stage],
380
+ mbar,
381
+ )
382
+ simple_tma_copy(
383
+ SFB_tma.atom,
384
+ gSFB_tiles[None, (iter_k, bid_n)],
385
+ sSFB[None, None, tma_stage],
386
+ mbar,
387
+ )
388
389
tma_stage = (tma_stage + 1) % num_stages
390
if tma_stage == 0:
@@ -303,8 +402,15 @@ class Sm100GemmRsArBF16:
402
403
MMA_M = BM * cta_group
404
MMA_N = BN
306
- idesc = _tcgen05.make_bf16_idesc(MMA_M, MMA_N)
405
sdesc = _tcgen05.make_sdesc_128B_swizzle(0)
406
+ if cutlass.const_expr(self.dtype == "mxfp8"):
407
+ # SF smem descriptor: no swizzle, SBO = 8 * 16 bytes.
408
+ sdesc_sf = Uint64((8 << 32) | (1 << 46))
409
+ sfa_tmem = 496
410
+ sfb_tmem = sfa_tmem + 4
411
+ idesc = _tcgen05.make_mxfp8_idesc(MMA_M, MMA_N)
412
+ else:
413
+ idesc = _tcgen05.make_bf16_idesc(MMA_M, MMA_N)
414
multicast_mask = Uint16((1 << self.cta_group) - 1)
415
416
for bid in range(raw_bid, total_tiles, num_bids):
@@ -319,17 +425,54 @@ class Sm100GemmRsArBF16:
425
b_addr = sB[None, None, tma_stage].iterator.toint()
426
a_desc = sdesc | (a_addr >> 4)
427
b_desc = sdesc | (b_addr >> 4)
428
+ if cutlass.const_expr(self.dtype == "mxfp8"):
429
+ sfa_desc = sdesc_sf | (
430
+ sSFA[None, None, tma_stage].iterator.toint() >> 4
431
+ )
432
+ sfb_desc = sdesc_sf | (
433
+ sSFB[None, None, tma_stage].iterator.toint() >> 4
434
+ )
435
436
cute.arch.mbarrier_wait(
437
tma_full_mbar + tma_stage, tma_full_parity
438
)
439
_tcgen05.fence_after_thread_sync()
440
328
- for mma_k in cutlass.range_constexpr(BK // 16):
329
- enable_d = iter_k > 0 or mma_k > 0
330
- _tcgen05.mma_f16(
331
- d_tmem, a_desc, b_desc, idesc, enable_d, cta_group
441
+ # load SF from smem->tmem
442
+ if cutlass.const_expr(self.dtype == "mxfp8"):
443
+ _tcgen05.cp(
444
+ sfa_tmem, sfa_desc, "32x128b", "warpx4", cta_group
445
)
446
+ for j in cutlass.range_constexpr(BN // 128):
447
+ _tcgen05.cp(
448
+ sfb_tmem + j * 4,
449
+ sfb_desc,
450
+ "32x128b",
451
+ "warpx4",
452
+ cta_group,
453
+ )
454
+ sfb_desc += (128 * 4) >> 4
455
+
456
+ # 4 = 128B / 32B
457
+ for mma_k in cutlass.range_constexpr(4):
458
+ enable_d = iter_k > 0 or mma_k > 0
459
+ if cutlass.const_expr(self.dtype == "mxfp8"):
460
+ # select SF IDs
461
+ idesc_k = idesc + ((mma_k << 4) | (mma_k << 29))
462
+ _tcgen05.mma_mxfp8(
463
+ d_tmem,
464
+ a_desc,
465
+ b_desc,
466
+ idesc_k,
467
+ sfa_tmem,
468
+ sfb_tmem,
469
+ enable_d,
470
+ cta_group,
471
+ )
472
+ else:
473
+ _tcgen05.mma_f16(
474
+ d_tmem, a_desc, b_desc, idesc, enable_d, cta_group
475
+ )
476
a_desc += 32 >> 4
477
b_desc += 32 >> 4
478
_tcgen05.commit(
@@ -638,9 +781,9 @@ class Sm100GemmRsArBF16:
781
def compile(
782
rank: int,
783
num_ranks: int,
641
- BN: int,
784
cta_group: int,
785
all_reduce: bool = False,
786
+ dtype: str = "bf16",
787
):
788
M = cute.sym_int()
789
padded_M = cute.sym_int()
@@ -649,12 +792,37 @@ class Sm100GemmRsArBF16:
792
local_M = cute.sym_int()
793
num_flags = cute.sym_int()
794
795
+ ab_dtype = Float8E4M3FN if dtype == "mxfp8" else BFloat16
796
+ # Row stride must keep rows 16B-aligned: 8 BF16 or 16 FP8 elements.
797
+ row_div = 16 if dtype == "mxfp8" else 8
798
A = make_fake_tensor(
653
- BFloat16, (M, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
799
+ ab_dtype,
800
+ (M, K),
801
+ (cute.sym_int64(divisibility=row_div), 1),
802
+ assumed_align=16,
803
)
804
B = make_fake_tensor(
656
- BFloat16, (N, K), (cute.sym_int64(divisibility=8), 1), assumed_align=16
805
+ ab_dtype,
806
+ (N, K),
807
+ (cute.sym_int64(divisibility=row_div), 1),
808
+ assumed_align=16,
809
)
810
+ if dtype == "mxfp8":
811
+ SFA = make_fake_tensor(
812
+ Float8E8M0FNU,
813
+ (cute.sym_int(divisibility=512),),
814
+ (1,),
815
+ assumed_align=16,
816
+ )
817
+ SFB = make_fake_tensor(
818
+ Float8E8M0FNU,
819
+ (cute.sym_int(divisibility=512),),
820
+ (1,),
821
+ assumed_align=16,
822
+ )
823
+ else:
824
+ SFA = None
825
+ SFB = None
826
partial = make_fake_tensor(
827
BFloat16,
828
(padded_M, N),
@@ -677,13 +845,15 @@ class Sm100GemmRsArBF16:
845
peer_flag_ptr = nullptr(Int64, cute.AddressSpace.gmem, assumed_align=8)
846
847
stream = make_fake_stream(use_tvm_ffi_env_stream=True)
680
- kernel = Sm100GemmRsArBF16(
681
- rank, num_ranks, BN, cta_group, all_reduce=all_reduce
848
+ kernel = Sm100GemmRsAr(
849
+ rank, num_ranks, cta_group, all_reduce=all_reduce, dtype=dtype
850
)
851
return cute.compile(
852
kernel,
853
A,
854
B,
855
+ SFA,
856
+ SFB,
857
partial,
858
partial_mc_ptr,
859
output,
@@ -697,7 +867,7 @@ class Sm100GemmRsArBF16:
867
868
869
class GemmRsAr:
700
- """Own the symmetric workspace for Kimi-K3 GEMM-RS/AR launches.
870
+ """Own the symmetric workspace for GEMM-RS/AR launches.
871
872
All TP ranks must belong to one NVLink domain for multimem instructions.
873
@@ -727,6 +897,7 @@ class GemmRsAr:
897
self.N = N
898
self.device = device
899
self.all_reduce = all_reduce
900
+ self.dtypes: set[str] = set()
901
902
self.partial = symm_mem.empty((max_M, N), dtype=torch.bfloat16, device=device)
903
self.partial_handle = symm_mem.rendezvous(self.partial, group)
@@ -766,21 +937,40 @@ class GemmRsAr:
937
tp_group.barrier()
938
939
def can_run(self, linear: LinearBase) -> bool:
769
- # Validate projection-invariant requirements once during model init.
770
- # only supports BF16 for now
771
- if not isinstance(linear.quant_method, UnquantizedLinearMethod):
772
- return False
940
+ from vllm.model_executor.kernels.linear.mxfp8.flashinfer import (
941
+ FlashInferCutedslMxfp8LinearKernel,
942
+ FlashInferCutlassMxfp8LinearKernel,
943
+ )
944
+
945
+ # Called before weight loading; online quantization starts on meta.
946
+ if isinstance(linear.quant_method, UnquantizedLinearMethod):
947
+ dtype, k_alignment = "bf16", 64
948
+ if linear.weight.dtype != torch.bfloat16:
949
+ return False
950
+ else:
951
+ method = getattr(linear, "scheme", linear.quant_method)
952
+ kernel = getattr(method, "kernel", None)
953
+ if not isinstance(
954
+ kernel,
955
+ (
956
+ FlashInferCutedslMxfp8LinearKernel,
957
+ FlashInferCutlassMxfp8LinearKernel,
958
+ ),
959
+ ):
960
+ return False
961
+ dtype, k_alignment = "mxfp8", 128
962
+
963
w = linear.weight
774
- if w.ndim != 2:
964
+ if (
965
+ w.ndim != 2
966
+ or w.shape[0] != self.N
967
+ or w.shape[1] % k_alignment != 0
968
+ or (w.device != self.device and not w.is_meta)
969
+ or not w.is_contiguous()
970
+ ):
971
return False
776
- K = w.shape[1]
777
- return (
778
- w.shape == (self.N, K)
779
- and K % 64 == 0
780
- and w.dtype == torch.bfloat16
781
- and w.device == self.device
782
- and w.is_contiguous()
783
- )
972
+ self.dtypes.add(dtype)
973
+ return True
974
975
def warn_incompatible_projection(self) -> None:
976
logger.warning_once(
@@ -794,18 +984,51 @@ class GemmRsAr:
984
# supported but faster on the existing LL path.
985
return x.shape[0] >= 128
986
797
- def __call__(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
987
+ def apply(self, x: torch.Tensor, linear: LinearBase) -> torch.Tensor:
988
+ from vllm.model_executor.kernels.linear.mxfp8.flashinfer import (
989
+ FlashInferCutedslMxfp8LinearKernel,
990
+ )
991
+
992
+ method = getattr(linear, "scheme", linear.quant_method)
993
+ w = linear.weight
994
+ if isinstance(
995
+ getattr(method, "kernel", None), FlashInferCutedslMxfp8LinearKernel
996
+ ):
997
+ # This backend stores a column-major [K, N] view after loading.
998
+ w = w.t()
999
+ w_sf = getattr(linear, "weight_scale", None)
1000
+
1001
assert x.ndim == 2
1002
M, K = x.shape
1003
assert 0 < M <= self.max_M
801
- assert w.shape == (self.N, K) and K % 64 == 0
802
- assert w.dtype == torch.bfloat16
803
- assert w.device == self.device
804
- assert w.is_contiguous()
1004
assert x.dtype == torch.bfloat16
1005
assert x.device == self.device
1006
assert x.is_contiguous()
808
- N = w.shape[0]
1007
+ assert w.device == self.device
1008
+ N = self.N
1009
+ dtype = "mxfp8" if w.dtype == torch.float8_e4m3fn else "bf16"
1010
+ x_sf = None
1011
+ if dtype == "mxfp8":
1012
+ from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
1013
+ mxfp8_e4m3_quantize,
1014
+ )
1015
+
1016
+ assert w_sf is not None
1017
+ assert w_sf.dtype == torch.uint8 and w_sf.device == self.device
1018
+ assert w_sf.ndim == 1 and w_sf.is_contiguous()
1019
+ assert w_sf.numel() == N * (K // 32)
1020
+ assert w.shape == (N, K) and K % 128 == 0
1021
+ assert w.is_contiguous()
1022
+ x, x_sf = mxfp8_e4m3_quantize(x, is_sf_swizzled_layout=True)
1023
+ # The kernel declares the scales as E8M0; the stored/quantized
1024
+ # tensors carry the same bits as uint8.
1025
+ x_sf = x_sf.view(torch.float8_e8m0fnu)
1026
+ w_sf = w_sf.view(torch.float8_e8m0fnu)
1027
+ else:
1028
+ assert w_sf is None
1029
+ assert w.shape == (N, K) and K % 64 == 0
1030
+ assert w.dtype == torch.bfloat16
1031
+ assert w.is_contiguous()
1032
padded_M = (M + self.world_size - 1) // self.world_size
1033
padded_M *= self.world_size
1034
local_M = padded_M // self.world_size
@@ -814,10 +1037,8 @@ class GemmRsAr:
1037
# Avoid padding small odd grids; 2-CTA wins consistently for M >= 1024.
1038
cta_group = 2 if M >= 1024 or grid_m % 2 == 0 else 1
1039
grid_m = (grid_m + cta_group - 1) // cta_group * cta_group
817
- BN = 256 if M * K >= 24 * 1024 * 1024 else 128
818
- assert N % BN == 0
1040
820
- num_tiles = grid_m * (N // BN)
1041
+ num_tiles = grid_m * (N // 128)
1042
num_ctas = min(num_tiles, self.num_sms)
1043
num_ctas = num_ctas // cta_group * cta_group
1044
assert self.flags.numel() >= num_tiles + num_ctas
@@ -825,16 +1046,26 @@ class GemmRsAr:
1046
output = None
1047
if not self.all_reduce:
1048
output = torch.empty((local_M, N), dtype=torch.bfloat16, device=self.device)
828
- compiled = Sm100GemmRsArBF16.compile(
1049
+ # The kernel never touches rows at or beyond M. Callers that hand
1050
+ # in an unpadded M (DeepSeek-V4.1 slices the SP all-gather back to
1051
+ # the real token count) expect the same zero padding rows that
1052
+ # ``sp_reduce_scatter`` produces, so clear them here. This is a
1053
+ # no-op whenever M is already a multiple of the TP size.
1054
+ valid_rows = min(max(M - self.rank * local_M, 0), local_M)
1055
+ if valid_rows < local_M:
1056
+ output[valid_rows:].zero_()
1057
+ compiled = Sm100GemmRsAr.compile(
1058
self.rank,
1059
self.world_size,
831
- BN,
1060
cta_group,
1061
self.all_reduce,
1062
+ dtype=dtype,
1063
)
1064
compiled(
1065
x,
1066
w,
1067
+ x_sf,
1068
+ w_sf,
1069
self.partial[:padded_M],
1070
self.partial_mc_ptr,
1071
output,
@@ -871,22 +1102,74 @@ def init_gemm_rs_ar(max_M: int, N: int, *, all_reduce: bool = False) -> None:
1102
_gemm_rs_ar = GemmRsAr(max_M=max_M, N=N, all_reduce=all_reduce)
1103
1104
1105
+def maybe_init_gemm_rs_ar(
1106
+ vllm_config: "VllmConfig", *, N: int, all_reduce: bool
1107
+) -> bool:
1108
+ """Collectively initialize the mode-bound GEMM-RS/AR state if supported.
1109
+
1110
+ Callers gate on their own feature flag first; this checks the worker
1111
+ topology (SM100-family CUDA, BF16 model dtype, no ubatching, a TP size in
1112
+ 2-16 that divides 128) and the NVLink multicast rendezvous. Returns whether
1113
+ projections may bind to the kernel through ``get_gemm_rs_ar().can_run``.
1114
+ """
1115
+ mode = "GEMM-AR" if all_reduce else "GEMM-RS"
1116
+ parallel_config = vllm_config.parallel_config
1117
+ tp_size = parallel_config.tensor_parallel_size
1118
+ if parallel_config.use_ubatching:
1119
+ reason = "ubatching is enabled"
1120
+ elif vllm_config.model_config.dtype != torch.bfloat16:
1121
+ reason = "the model dtype is not BF16"
1122
+ elif not current_platform.is_cuda():
1123
+ reason = "the device is not CUDA"
1124
+ elif not current_platform.is_device_capability_family(100):
1125
+ reason = "the device is not SM100-family"
1126
+ elif not 1 < tp_size <= 16:
1127
+ reason = "TP size is not in the supported range 2-16"
1128
+ elif 128 % tp_size != 0:
1129
+ reason = "TP size does not divide 128"
1130
+ elif N % 128 != 0:
1131
+ reason = f"the output width {N} is not a multiple of 128"
1132
+ else:
1133
+ reason = None
1134
+
1135
+ if reason is not None:
1136
+ logger.warning_once("%s is disabled because %s.", mode, reason)
1137
+ return False
1138
+
1139
+ try:
1140
+ init_gemm_rs_ar(
1141
+ max_M=vllm_config.scheduler_config.max_num_batched_tokens,
1142
+ N=N,
1143
+ all_reduce=all_reduce,
1144
+ )
1145
+ except RuntimeError as e:
1146
+ logger.warning_once(
1147
+ "%s is disabled because initialization failed: %s. This may mean "
1148
+ "the TP ranks do not share one NVLink domain.",
1149
+ mode,
1150
+ e,
1151
+ )
1152
+ return False
1153
+ logger.info_once("%s is enabled.", mode)
1154
+ return True
1155
+
1156
+
1157
def warmup_gemm_rs_ar() -> int:
1158
"""Compile every reachable dispatch for the initialized GEMM-RS/AR mode."""
1159
# Initialization can be disabled or fail when multicast is unavailable.
1160
if _gemm_rs_ar is None:
1161
return 0
879
- # Keep these profiles in sync with the dispatch in GemmRsAr.__call__.
880
- profiles = ((128, 1), (128, 2), (256, 2))
881
- for BN, cta_group in profiles:
882
- Sm100GemmRsArBF16.compile(
883
- _gemm_rs_ar.rank,
884
- _gemm_rs_ar.world_size,
885
- BN,
886
- cta_group,
887
- _gemm_rs_ar.all_reduce,
888
- )
889
- return len(profiles)
1162
+
1163
+ for dtype in sorted(_gemm_rs_ar.dtypes):
1164
+ for cta_group in (1, 2):
1165
+ Sm100GemmRsAr.compile(
1166
+ _gemm_rs_ar.rank,
1167
+ _gemm_rs_ar.world_size,
1168
+ cta_group,
1169
+ _gemm_rs_ar.all_reduce,
1170
+ dtype=dtype,
1171
+ )
1172
+ return 2 * len(_gemm_rs_ar.dtypes)
1173
1174
1175
def get_gemm_rs_ar() -> GemmRsAr:
vllm/model_executor/warmup/kernel_warmup.py
+8
-7
@@ -136,16 +136,17 @@ def _warmup_bf16x3_router_gemm(
136
logger.info_once("Warmed up BF16x3 router GEMM configs: %s.", configs)
137
138
139
-def _warmup_kimi_k3_gemm_rs_ar() -> None:
140
- # Kimi-K3 model construction imports this module only when GEMM-RS/AR is
141
- # enabled and initializes its singleton before kernel_warmup runs. Avoid
142
- # importing it here so other models do not compile the RS/AR variants.
143
- module = sys.modules.get("vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar")
139
+def _warmup_gemm_rs_ar() -> None:
140
+ # Model construction (Kimi-K3, DeepSeek-V4.1) imports this module only
141
+ # when GEMM-RS/AR is enabled and initializes its singleton before
142
+ # kernel_warmup runs. Avoid importing it here so other models do not
143
+ # compile the RS/AR variants.
144
+ module = sys.modules.get("vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar")
145
if module is None:
146
return
147
compiled = module.warmup_gemm_rs_ar()
148
if compiled:
148
- logger.info_once("Warmed up %d Kimi-K3 GEMM-RS/AR variants.", compiled)
149
+ logger.info_once("Warmed up %d GEMM-RS/AR variants.", compiled)
150
151
152
def _autotune_kimi_k3_kda_qkvg(model: torch.nn.Module) -> None:
@@ -208,7 +209,7 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
209
if current_platform.has_device_capability(90):
210
_warmup_ll_bf16_router_gemm(worker.get_model())
211
211
- _warmup_kimi_k3_gemm_rs_ar()
212
+ _warmup_gemm_rs_ar()
213
214
if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
215
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
vllm/models/deepseek_v4/nvidia/ops/o_proj.py
+6
-2
@@ -1,5 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from collections.abc import Callable
4
+
5
import torch
6
import torch.nn as nn
7
@@ -31,7 +33,7 @@ def deep_gemm_fp8_o_proj(
33
positions: torch.Tensor,
34
cos_sin_cache: torch.Tensor,
35
wo_a: nn.Module,
34
- wo_b: nn.Module,
36
+ wo_b: Callable[[torch.Tensor], torch.Tensor],
37
*,
38
n_groups: int,
39
heads_per_group: int,
@@ -44,7 +46,9 @@ def deep_gemm_fp8_o_proj(
46
"""O projection: inverse RoPE + grouped wo_a + wo_b.
47
48
Shared by the FlashMLA and FlashInfer CUDA backends. The attention
47
- layer selects the recipe at initialization.
49
+ layer selects the recipe at initialization. ``wo_b`` is any callable over
50
+ the flattened ``z``: the projection module itself, or a wrapper that also
51
+ reduce-scatters its output (DeepSeek-V4.1 GEMM-RS).
52
"""
53
use_fp8 = wo_a.weight.dtype == torch.float8_e4m3fn
54
o_proj_input, o_scale = fused_inv_rope_fp8_quant(
vllm/models/deepseek_v41/attention.py
+40
@@ -26,6 +26,7 @@ from vllm.model_executor.layers.linear import (
26
from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
27
from vllm.model_executor.layers.sparse_mqa_indexer import SparseMQAIndexer
28
from vllm.models.common.ops import fused_q_kv_rmsnorm
29
+from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
30
from vllm.models.deepseek_v41.common.ops import (
31
MXFP4_BLOCK_SIZE,
32
fused_indexer_q_rope_quant,
@@ -33,6 +34,7 @@ from vllm.models.deepseek_v41.common.ops import (
34
)
35
36
if TYPE_CHECKING:
37
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
38
from vllm.v1.attention.backends.mla.sparse_swa import (
39
DeepseekSparseSWAMetadata,
40
)
@@ -389,6 +391,9 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
391
return_bias=False,
392
prefix=f"{prefix}.wo_b",
393
)
394
+ # Set by ``bind_gemm_rs`` when the decoder layer runs sequence
395
+ # parallel and the fused GEMM + reduce-scatter kernel accepts wo_b.
396
+ self.gemm_rs: GemmRsAr | None = None
397
398
# Initialize rotary embedding before the indexer/compressor consume it.
399
self.rotary_emb = build_deepseek_v4_rope(
@@ -672,6 +677,41 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
677
)
678
return self._o_proj(attn_out, positions)
679
680
+ def bind_gemm_rs(self) -> None:
681
+ """Fuse ``wo_b`` with the sequence-parallel TP reduce-scatter.
682
+
683
+ The decoder layer calls this after the model initialized the
684
+ process-wide GEMM-RS workspace and before weights load: the
685
+ eligibility check inspects the projection's linear kernel and weight
686
+ shape, which online quantization may later re-layout.
687
+ """
688
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
689
+ get_gemm_rs_ar,
690
+ )
691
+
692
+ # The layer owns the reduction under sequence parallel.
693
+ assert not self.wo_b.reduce_results
694
+ gemm_rs = get_gemm_rs_ar()
695
+ if gemm_rs.can_run(self.wo_b):
696
+ self.gemm_rs = gemm_rs
697
+ else:
698
+ gemm_rs.warn_incompatible_projection()
699
+
700
+ def _wo_b_proj(self, z: torch.Tensor) -> torch.Tensor:
701
+ """Apply ``wo_b``; with GEMM-RS bound, also reduce-scatter the result.
702
+
703
+ Every ``_o_proj`` implementation projects through this so the decoder
704
+ layer sees one contract: when ``gemm_rs`` is bound the output is
705
+ already the local sequence-parallel shard, otherwise it is the
706
+ unreduced TP partial.
707
+ """
708
+ if self.gemm_rs is None:
709
+ return self.wo_b(z)
710
+ if self.gemm_rs.should_run(z):
711
+ return self.gemm_rs.apply(z, self.wo_b)
712
+ # Small batches stay on the unfused path, which is faster there.
713
+ return sp_reduce_scatter(self.wo_b(z))
714
+
715
def _alloc_attn_out(
716
self, num_tokens: int, hidden_states: torch.Tensor
717
) -> "torch.Tensor | QuantizedActivation":
vllm/models/deepseek_v41/nvidia/dspark.py
+7
@@ -58,6 +58,7 @@ from .model import (
58
_linear_scale_param_name,
59
_use_sequence_parallel,
60
make_deepseek_v4_expert_params_mapping,
61
+ maybe_init_gemm_rs,
62
prepare_mega_gate_routing_metadata,
63
)
64
@@ -113,12 +114,18 @@ class DSparkDeepseekV4Model(nn.Module):
114
)
115
116
current_vllm_config = get_current_vllm_config()
117
+ # The target model already holds the GEMM-RS workspace (same hidden
118
+ # size, same TP group); this only re-checks and binds the draft layers.
119
+ run_gemm_rs = maybe_init_gemm_rs(
120
+ current_vllm_config, self.use_sequence_parallel
121
+ )
122
self.layers = nn.ModuleList(
123
[
124
DeepseekV4DecoderLayer(
125
current_vllm_config,
126
prefix=maybe_prefix(prefix, f"layers.{self.num_hidden_layers + i}"),
127
topk_indices_buffer=self.topk_indices_buffer,
128
+ run_gemm_rs=run_gemm_rs,
129
)
130
for i in range(self.num_dspark_layers)
131
]
vllm/models/deepseek_v41/nvidia/flash_mla_mega_attn.py
+1
-1
@@ -225,7 +225,7 @@ class DeepseekV4MegaAttnAttention(DeepseekV4FlashMLAAttention):
225
z,
226
recipe=self._einsum_recipe,
227
)
228
- return self.wo_b(z.flatten(1))
228
+ return self._wo_b_proj(z.flatten(1))
229
230
# ---- weights -----------------------------------------------------------
231
vllm/models/deepseek_v41/nvidia/flashinfer_sparse.py
+2
-2
@@ -248,7 +248,7 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
248
positions,
249
self.rotary_emb.cos_sin_cache,
250
self.wo_a,
251
- self.wo_b,
251
+ self._wo_b_proj,
252
n_groups=self.n_local_groups,
253
heads_per_group=self.n_local_heads // self.n_local_groups,
254
nope_dim=self.nope_head_dim,
@@ -627,7 +627,7 @@ class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
627
positions,
628
self.rotary_emb.cos_sin_cache,
629
self.wo_a,
630
- self.wo_b,
630
+ self._wo_b_proj,
631
n_groups=self.n_local_groups,
632
heads_per_group=self.n_local_heads // self.n_local_groups,
633
nope_dim=self.nope_head_dim,
vllm/models/deepseek_v41/nvidia/flashmla.py
+1
-1
@@ -65,7 +65,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
65
positions,
66
self.rotary_emb.cos_sin_cache,
67
self.wo_a,
68
- self.wo_b,
68
+ self._wo_b_proj,
69
n_groups=self.n_local_groups,
70
heads_per_group=self.n_local_heads // self.n_local_groups,
71
nope_dim=self.nope_head_dim,
vllm/models/deepseek_v41/nvidia/model.py
+37
-1
@@ -193,6 +193,30 @@ def _use_sequence_parallel(vllm_config: VllmConfig) -> bool:
193
)
194
195
196
+def maybe_init_gemm_rs(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
197
+ """Set up the fused ``wo_b`` GEMM + reduce-scatter when opted in.
198
+
199
+ Sequence parallel is the only topology where ``wo_b`` ends in a
200
+ reduce-scatter, so the kernel is bound to RS mode. The workspace is a
201
+ process-wide singleton shared with any other model code in this worker
202
+ (the DSpark drafter reuses it), and its NVLink multicast rendezvous is
203
+ collective, so every TP rank must take the same decision here.
204
+ """
205
+ if not (use_sequence_parallel and envs.VLLM_ENABLE_GEMM_RS):
206
+ return False
207
+
208
+ # The kernel module pulls in cute_dsl, so import it only once opted in.
209
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
210
+ maybe_init_gemm_rs_ar,
211
+ )
212
+
213
+ hidden_size = vllm_config.model_config.hf_config.hidden_size
214
+ if not maybe_init_gemm_rs_ar(vllm_config, N=hidden_size, all_reduce=False):
215
+ return False
216
+ logger.info_once("To disable DeepSeek-V4.1 GEMM-RS, set VLLM_ENABLE_GEMM_RS=0.")
217
+ return True
218
+
219
+
220
class DeepseekV4DecoderLayer(nn.Module):
221
def __init__(
222
self,
@@ -202,6 +226,7 @@ class DeepseekV4DecoderLayer(nn.Module):
226
aux_stream_list: list[torch.cuda.Stream] | None = None,
227
candidate_block_buffer: torch.Tensor | None = None,
228
engram_layout: EngramLayout | None = None,
229
+ run_gemm_rs: bool = False,
230
mhc_stream: torch.cuda.Stream | None = None,
231
fuse_mhc_all_reduce: bool = False,
232
):
@@ -236,6 +261,10 @@ class DeepseekV4DecoderLayer(nn.Module):
261
)
262
if self.use_sequence_parallel or fuse_mhc_all_reduce:
263
self.attn.wo_b.reduce_results = False
264
+ if run_gemm_rs:
265
+ # Binds only when wo_b's kernel and shape qualify; otherwise
266
+ # forward keeps the separate reduce-scatter below.
267
+ self.attn.bind_gemm_rs()
268
self.ffn = DeepseekV4MoE(
269
vllm_config,
270
prefix=f"{prefix}.ffn",
@@ -492,7 +521,9 @@ class DeepseekV4DecoderLayer(nn.Module):
521
x = sp_all_gather(x)[: positions.shape[0]]
522
523
x = self.attn(positions, x, None)
495
- if self.use_sequence_parallel:
524
+ # With GEMM-RS bound, the attention output is already this rank's
525
+ # sequence-parallel shard (see DeepseekV4Attention._wo_b_proj).
526
+ if self.use_sequence_parallel and self.attn.gemm_rs is None:
527
x = sp_reduce_scatter(x)
528
529
if mhc_stream is not None:
@@ -597,6 +628,10 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
628
):
629
engram_config.dp_shared_memory = can_share_engram_tables(self.engram_layout)
630
631
+ # GEMM-RS uses NCCL symmetric-memory multicast, which requires all TP
632
+ # ranks to belong to one NVLink domain. Collective: run before layers.
633
+ self.run_gemm_rs = maybe_init_gemm_rs(vllm_config, self.use_sequence_parallel)
634
+
635
self.start_layer, self.end_layer, self.layers = make_layers(
636
config.num_hidden_layers,
637
lambda prefix: DeepseekV4DecoderLayer(
@@ -606,6 +641,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
641
aux_stream_list=aux_stream_list,
642
candidate_block_buffer=self.candidate_block_buffer,
643
engram_layout=self.engram_layout,
644
+ run_gemm_rs=self.run_gemm_rs,
645
mhc_stream=mhc_stream,
646
fuse_mhc_all_reduce=self.fuse_mhc_all_reduce,
647
),
vllm/models/kimi_k3/nvidia/kda.py
+2
-2
@@ -724,7 +724,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
724
)
725
self.gemm_rs_ar = None
726
if run_gemm_rs_ar:
727
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
727
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
728
get_gemm_rs_ar,
729
)
730
@@ -809,7 +809,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
809
)
810
core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
811
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(core_attn_out):
812
- return self.gemm_rs_ar(core_attn_out, self.o_proj.weight)
812
+ return self.gemm_rs_ar.apply(core_attn_out, self.o_proj)
813
return self.o_proj(core_attn_out)[0]
814
815
@eager_break_during_capture
vllm/models/kimi_k3/nvidia/mla.py
+2
-2
@@ -301,7 +301,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
301
)
302
self.gemm_rs_ar = None
303
if run_gemm_rs_ar:
304
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
304
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
305
get_gemm_rs_ar,
306
)
307
@@ -631,7 +631,7 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
631
attn_out = _gate_sigmoid_mul(attn_out, gate)
632
633
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(attn_out):
634
- return self.gemm_rs_ar(attn_out, self.o_proj.weight)
634
+ return self.gemm_rs_ar.apply(attn_out, self.o_proj)
635
636
return self.o_proj(attn_out)[0]
637
vllm/models/kimi_k3/nvidia/model.py
+13
-44
@@ -160,54 +160,23 @@ def maybe_init_gemm_rs_ar(vllm_config: VllmConfig, use_sequence_parallel: bool)
160
# its singleton to exactly one mode.
161
all_reduce = not use_sequence_parallel
162
mode = "GEMM-AR" if all_reduce else "GEMM-RS"
163
- enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_KIMI_K3_GEMM_RS
163
+ enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_ENABLE_GEMM_RS
164
if not enabled:
165
return False
166
167
- parallel_config = vllm_config.parallel_config
168
- tp_size = parallel_config.tensor_parallel_size
169
- if parallel_config.use_ubatching:
170
- reason = "ubatching is enabled"
171
- elif vllm_config.model_config.dtype != torch.bfloat16:
172
- reason = "the model dtype is not BF16"
173
- elif not current_platform.is_cuda():
174
- reason = "the device is not CUDA"
175
- elif not current_platform.is_device_capability_family(100):
176
- reason = "the device is not SM100-family"
177
- elif not 1 < tp_size <= 16:
178
- reason = "TP size is not in the supported range 2-16"
179
- elif 128 % tp_size != 0:
180
- reason = "TP size does not divide 128"
181
- else:
182
- reason = None
183
-
184
- if reason is not None:
185
- logger.warning_once("%s is disabled because %s.", mode, reason)
186
- return False
187
-
188
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import init_gemm_rs_ar
167
+ # The kernel module pulls in cute_dsl, so import it only once the flag
168
+ # asks for it.
169
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
170
+ maybe_init_gemm_rs_ar as maybe_init_shared_gemm_rs_ar,
171
+ )
172
173
config = vllm_config.model_config.hf_text_config
191
- try:
192
- init_gemm_rs_ar(
193
- max_M=vllm_config.scheduler_config.max_num_batched_tokens,
194
- N=config.hidden_size,
195
- all_reduce=all_reduce,
196
- )
197
- except RuntimeError as e:
198
- logger.warning_once(
199
- "%s is disabled because initialization failed: %s. This may mean "
200
- "the TP ranks do not share one NVLink domain.",
201
- mode,
202
- e,
203
- )
174
+ if not maybe_init_shared_gemm_rs_ar(
175
+ vllm_config, N=config.hidden_size, all_reduce=all_reduce
176
+ ):
177
return False
205
- if all_reduce:
206
- logger.info_once(
207
- "GEMM-AR is enabled. To disable it, set VLLM_KIMI_K3_GEMM_AR=0."
208
- )
209
- else:
210
- logger.info_once("GEMM-RS is enabled.")
178
+ flag = "VLLM_KIMI_K3_GEMM_AR" if all_reduce else "VLLM_ENABLE_GEMM_RS"
179
+ logger.info_once("To disable %s, set %s=0.", mode, flag)
180
return True
181
182
@@ -276,7 +245,7 @@ class KimiMLP(nn.Module):
245
not use_sequence_parallel and reduce_results
246
)
247
if use_gemm_rs_ar and run_gemm_rs_ar:
279
- from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import (
248
+ from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
249
get_gemm_rs_ar,
250
)
251
@@ -309,7 +278,7 @@ class KimiMLP(nn.Module):
278
x = self.act_fn(gate_up)
279
280
if self.gemm_rs_ar is not None and self.gemm_rs_ar.should_run(x):
312
- return self.gemm_rs_ar(x, self.down_proj.weight)
281
+ return self.gemm_rs_ar.apply(x, self.down_proj)
282
283
x, _ = self.down_proj(x)
284
if self.shard_sequence_parallel: