[Perf][Distributed] Add low-SM multimem reduce-scatter for SM100/SM103 (#55072)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Signed-off-by: Summer Yang <girasoleyang@gmail.com> Co-authored-by: Summer Yang <girasoleyang@gmail.com>
Yongye Zhu committed
Sep 24, 2026 at 20:27 UTC
442bd00d9feff7d6a0cebf74248674bad74ec391
10 files changed
+1243
-31
benchmarks/kernels/benchmark_multimem_reduce_scatter.py
new
+480
@@ -0,0 +1,480 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Benchmark production MNNVL reduce-scatter backends on SM100/SM103.
4
+
5
+The multimem candidate includes the device-to-device copy into its persistent
6
+symmetric input buffer. Timings use pointer-distinct CUDA graphs and report the
7
+median of the per-sample maximum across ranks.
8
+
9
+Run the default BF16 message-size sweep with:
10
+
11
+ .venv/bin/python -m torch.distributed.run --standalone --nproc-per-node=8 \
12
+ benchmarks/kernels/benchmark_multimem_reduce_scatter.py
13
+"""
14
+
15
+import argparse
16
+import json
17
+import os
18
+import statistics
19
+from collections.abc import Callable
20
+from dataclasses import dataclass
21
+
22
+import torch
23
+import torch.distributed as dist
24
+
25
+import vllm._custom_ops as ops
26
+from vllm.distributed.device_communicators.custom_all_reduce import CustomAllreduce
27
+
28
+_DTYPES = {
29
+ "bf16": torch.bfloat16,
30
+ "f16": torch.float16,
31
+ "f32": torch.float32,
32
+}
33
+
34
+
35
+@dataclass
36
+class Candidate:
37
+ name: str
38
+ runs: list[Callable[[], None]]
39
+ outputs: list[torch.Tensor]
40
+
41
+
42
+def parse_size(value: str) -> int:
43
+ suffixes = {"k": 1 << 10, "m": 1 << 20, "g": 1 << 30}
44
+ value = value.strip().lower()
45
+ multiplier = suffixes.get(value[-1], 1)
46
+ number = value[:-1] if multiplier != 1 else value
47
+ return int(float(number) * multiplier)
48
+
49
+
50
+def parse_args() -> argparse.Namespace:
51
+ parser = argparse.ArgumentParser()
52
+ parser.add_argument(
53
+ "--message-bytes",
54
+ type=parse_size,
55
+ nargs="+",
56
+ default=[
57
+ 16 << 20,
58
+ 32 << 20,
59
+ 64 << 20,
60
+ 68 << 20,
61
+ 72 << 20,
62
+ 76 << 20,
63
+ 96 << 20,
64
+ ],
65
+ help="Full input bytes on each rank. K/M/G suffixes are accepted.",
66
+ )
67
+ parser.add_argument("--dtype", choices=tuple(_DTYPES), default="bf16")
68
+ parser.add_argument(
69
+ "--blocks",
70
+ type=int,
71
+ nargs="+",
72
+ default=[8],
73
+ help="Maximum CTAs used by each multimem candidate.",
74
+ )
75
+ parser.add_argument("--num-workspaces", type=int, default=4)
76
+ parser.add_argument("--warmup-replays", type=int, default=8)
77
+ parser.add_argument("--samples", type=int, default=31)
78
+ parser.add_argument(
79
+ "--cold-l2",
80
+ action="store_true",
81
+ help="Flush approximately twice L2 capacity before each replay.",
82
+ )
83
+ return parser.parse_args()
84
+
85
+
86
+def make_lamport_run(
87
+ comm: CustomAllreduce,
88
+ input_tensor: torch.Tensor,
89
+ output: torch.Tensor,
90
+) -> Callable[[], None]:
91
+ def run() -> None:
92
+ ops.mnnvl_lamport_reduce_scatter(
93
+ comm._ptr,
94
+ input_tensor,
95
+ output,
96
+ comm.mnnvl_lamport_rs_local_ptr,
97
+ comm.mnnvl_lamport_rs_epoch_ptr,
98
+ comm.mnnvl_buffer_size,
99
+ )
100
+
101
+ return run
102
+
103
+
104
+def make_multimem_run(
105
+ comm: CustomAllreduce,
106
+ input_tensor: torch.Tensor,
107
+ output: torch.Tensor,
108
+ blocks: int,
109
+) -> Callable[[], None]:
110
+ def run() -> None:
111
+ ops.mnnvl_multimem_reduce_scatter(
112
+ comm._ptr,
113
+ input_tensor,
114
+ output,
115
+ comm.mnnvl_multimem_rs_local_ptr,
116
+ comm.mnnvl_multimem_rs_multicast_ptr,
117
+ comm.mnnvl_multimem_rs_buffer_size,
118
+ blocks,
119
+ )
120
+
121
+ return run
122
+
123
+
124
+def make_nccl_run(
125
+ input_tensor: torch.Tensor,
126
+ output: torch.Tensor,
127
+ device_group: dist.ProcessGroup,
128
+) -> Callable[[], None]:
129
+ def run() -> None:
130
+ dist.reduce_scatter_single(output, input_tensor, group=device_group)
131
+
132
+ return run
133
+
134
+
135
+def build_candidates(
136
+ comm: CustomAllreduce,
137
+ inputs: list[torch.Tensor],
138
+ local_elements: int,
139
+ blocks_values: list[int],
140
+ device_group: dist.ProcessGroup,
141
+) -> list[Candidate]:
142
+ candidates: list[Candidate] = []
143
+
144
+ lamport_outputs = [
145
+ input_tensor.new_empty(local_elements) for input_tensor in inputs
146
+ ]
147
+ candidates.append(
148
+ Candidate(
149
+ "lamport",
150
+ [
151
+ make_lamport_run(comm, input_tensor, output)
152
+ for input_tensor, output in zip(inputs, lamport_outputs)
153
+ ],
154
+ lamport_outputs,
155
+ )
156
+ )
157
+
158
+ for blocks in blocks_values:
159
+ outputs = [input_tensor.new_empty(local_elements) for input_tensor in inputs]
160
+ candidates.append(
161
+ Candidate(
162
+ f"multimem_b{blocks}",
163
+ [
164
+ make_multimem_run(comm, input_tensor, output, blocks)
165
+ for input_tensor, output in zip(inputs, outputs)
166
+ ],
167
+ outputs,
168
+ )
169
+ )
170
+
171
+ nccl_outputs = [input_tensor.new_empty(local_elements) for input_tensor in inputs]
172
+ candidates.append(
173
+ Candidate(
174
+ "nccl",
175
+ [
176
+ make_nccl_run(input_tensor, output, device_group)
177
+ for input_tensor, output in zip(inputs, nccl_outputs)
178
+ ],
179
+ nccl_outputs,
180
+ )
181
+ )
182
+ return candidates
183
+
184
+
185
+def check_correctness(
186
+ candidates: list[Candidate],
187
+ inputs: list[torch.Tensor],
188
+ rank: int,
189
+ world_size: int,
190
+ cpu_group: dist.ProcessGroup,
191
+ device_group: dist.ProcessGroup,
192
+ graphs: dict[str, list[torch.cuda.CUDAGraph]] | None = None,
193
+) -> None:
194
+ expected_outputs = []
195
+ for input_tensor in inputs:
196
+ expected = input_tensor.float().clone()
197
+ dist.all_reduce(expected, group=device_group)
198
+ expected_outputs.append(expected.to(input_tensor.dtype).chunk(world_size)[rank])
199
+
200
+ if inputs[0].dtype == torch.float32:
201
+ rtol, atol = 1e-5, 1e-5
202
+ elif inputs[0].dtype == torch.float16:
203
+ rtol, atol = 1e-2, 1e-2
204
+ else:
205
+ rtol, atol = 5e-2, 1.25e-1
206
+
207
+ for candidate in candidates:
208
+ for workspace_index, expected in enumerate(expected_outputs):
209
+ dist.barrier(group=cpu_group)
210
+ if graphs is None:
211
+ candidate.runs[workspace_index]()
212
+ else:
213
+ graphs[candidate.name][workspace_index].replay()
214
+ torch.accelerator.synchronize()
215
+ error = None
216
+ try:
217
+ torch.testing.assert_close(
218
+ candidate.outputs[workspace_index],
219
+ expected,
220
+ rtol=rtol,
221
+ atol=atol,
222
+ )
223
+ except AssertionError as assertion_error:
224
+ error = assertion_error
225
+ correct = torch.tensor(
226
+ error is None,
227
+ dtype=torch.int32,
228
+ device=candidate.outputs[workspace_index].device,
229
+ )
230
+ dist.all_reduce(correct, op=dist.ReduceOp.MIN, group=device_group)
231
+ if not correct.item():
232
+ location = f"{candidate.name} workspace {workspace_index}"
233
+ detail = str(error) if error is not None else "failed on another rank"
234
+ raise AssertionError(
235
+ f"{location} produced an incorrect shard: {detail}"
236
+ )
237
+ dist.barrier(group=cpu_group)
238
+
239
+
240
+def capture_graph(
241
+ run: Callable[[], None],
242
+ cpu_group: dist.ProcessGroup,
243
+) -> tuple[torch.cuda.CUDAGraph, torch.cuda.Stream]:
244
+ stream = torch.cuda.Stream()
245
+ stream.wait_stream(torch.cuda.current_stream())
246
+ dist.barrier(group=cpu_group)
247
+ with torch.cuda.stream(stream):
248
+ for _ in range(3):
249
+ run()
250
+ stream.synchronize()
251
+ dist.barrier(group=cpu_group)
252
+
253
+ graph = torch.cuda.CUDAGraph()
254
+ with torch.cuda.graph(graph, stream=stream):
255
+ run()
256
+ torch.cuda.current_stream().wait_stream(stream)
257
+ dist.barrier(group=cpu_group)
258
+ return graph, stream
259
+
260
+
261
+def capture_candidates(
262
+ candidates: list[Candidate],
263
+ cpu_group: dist.ProcessGroup,
264
+) -> tuple[dict[str, list[torch.cuda.CUDAGraph]], list[object]]:
265
+ graphs = {}
266
+ keepalive: list[object] = []
267
+ for candidate in candidates:
268
+ bundles = [capture_graph(run, cpu_group) for run in candidate.runs]
269
+ graphs[candidate.name] = [graph for graph, _ in bundles]
270
+ keepalive.extend(bundles)
271
+ return graphs, keepalive
272
+
273
+
274
+def benchmark_graphs(
275
+ graphs: dict[str, list[torch.cuda.CUDAGraph]],
276
+ warmup_replays: int,
277
+ samples: int,
278
+ device_group: dist.ProcessGroup,
279
+ device_barrier: Callable[[], None],
280
+ flush: torch.Tensor | None,
281
+) -> dict[str, float]:
282
+ names = list(graphs)
283
+ for round_index in range(warmup_replays):
284
+ for candidate_index in range(len(names)):
285
+ name = names[(round_index + candidate_index) % len(names)]
286
+ device_barrier()
287
+ graphs[name][round_index % len(graphs[name])].replay()
288
+ torch.accelerator.synchronize()
289
+
290
+ timings = {name: [] for name in names}
291
+ start = torch.cuda.Event(enable_timing=True)
292
+ end = torch.cuda.Event(enable_timing=True)
293
+ for sample_index in range(samples):
294
+ for candidate_index in range(len(names)):
295
+ name = names[(sample_index + candidate_index) % len(names)]
296
+ if flush is not None:
297
+ flush.zero_()
298
+ device_barrier()
299
+ start.record()
300
+ graphs[name][sample_index % len(graphs[name])].replay()
301
+ end.record()
302
+ end.synchronize()
303
+
304
+ elapsed = torch.tensor(
305
+ start.elapsed_time(end) * 1000,
306
+ dtype=torch.float64,
307
+ device=torch.accelerator.current_device_index(),
308
+ )
309
+ dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group)
310
+ timings[name].append(elapsed.item())
311
+
312
+ return {name: statistics.median(values) for name, values in timings.items()}
313
+
314
+
315
+def benchmark_size(
316
+ args: argparse.Namespace,
317
+ comm: CustomAllreduce,
318
+ message_bytes: int,
319
+ dtype: torch.dtype,
320
+ device: torch.device,
321
+ rank: int,
322
+ world_size: int,
323
+ device_group: dist.ProcessGroup,
324
+ cpu_group: dist.ProcessGroup,
325
+ device_barrier: Callable[[], None],
326
+) -> dict[str, object]:
327
+ element_size = dtype.itemsize
328
+ if message_bytes % (element_size * world_size) != 0:
329
+ raise ValueError("Message bytes must divide evenly into TP shards")
330
+ num_elements = message_bytes // element_size
331
+ local_elements = num_elements // world_size
332
+ if local_elements * element_size % 16 != 0:
333
+ raise ValueError("Each output shard must be a multiple of 16 bytes")
334
+
335
+ inputs = []
336
+ for workspace_index in range(args.num_workspaces):
337
+ generator = torch.Generator(device=device)
338
+ generator.manual_seed(1000 + rank * 101 + workspace_index)
339
+ input_tensor = torch.empty(num_elements, dtype=dtype, device=device)
340
+ input_tensor.uniform_(-1.0, 1.0, generator=generator)
341
+ inputs.append(input_tensor)
342
+
343
+ candidates = build_candidates(
344
+ comm,
345
+ inputs,
346
+ local_elements,
347
+ args.blocks,
348
+ device_group,
349
+ )
350
+ torch.accelerator.synchronize()
351
+ dist.barrier(group=cpu_group)
352
+ check_correctness(
353
+ candidates,
354
+ inputs,
355
+ rank,
356
+ world_size,
357
+ cpu_group,
358
+ device_group,
359
+ )
360
+ graphs, graph_keepalive = capture_candidates(candidates, cpu_group)
361
+ check_correctness(
362
+ candidates,
363
+ inputs,
364
+ rank,
365
+ world_size,
366
+ cpu_group,
367
+ device_group,
368
+ graphs,
369
+ )
370
+
371
+ flush = None
372
+ if args.cold_l2:
373
+ l2_bytes = torch.cuda.get_device_properties(device).L2_cache_size
374
+ flush = torch.empty(2 * l2_bytes, dtype=torch.uint8, device=device)
375
+ times = benchmark_graphs(
376
+ graphs,
377
+ args.warmup_replays,
378
+ args.samples,
379
+ device_group,
380
+ device_barrier,
381
+ flush,
382
+ )
383
+ graph_keepalive.clear()
384
+
385
+ bus_bytes = message_bytes * (world_size - 1) / world_size
386
+ fastest = min(times, key=times.__getitem__)
387
+ return {
388
+ "message_bytes": message_bytes,
389
+ "shard_bytes": message_bytes // world_size,
390
+ "latency_us": times,
391
+ "logical_bus_gbps": {
392
+ name: bus_bytes / (latency_us * 1e3) for name, latency_us in times.items()
393
+ },
394
+ "fastest": fastest,
395
+ }
396
+
397
+
398
+def main() -> None:
399
+ args = parse_args()
400
+ if not args.message_bytes or min(args.message_bytes) <= 0:
401
+ raise ValueError("--message-bytes values must be positive")
402
+ if not args.blocks or min(args.blocks) <= 0 or max(args.blocks) > 8:
403
+ raise ValueError("--blocks values must be in [1, 8]")
404
+ if min(args.num_workspaces, args.samples) <= 0:
405
+ raise ValueError("Workspace and sample counts must be positive")
406
+
407
+ local_rank = int(os.environ["LOCAL_RANK"])
408
+ torch.accelerator.set_device_index(local_rank)
409
+ dist.init_process_group("nccl")
410
+ device_group = dist.group.WORLD
411
+ cpu_group = dist.new_group(backend="gloo")
412
+ rank = dist.get_rank(device_group)
413
+ world_size = dist.get_world_size(device_group)
414
+ device = torch.device("cuda", local_rank)
415
+ capability = torch.cuda.get_device_capability(device)
416
+ if world_size not in (2, 4, 8) or capability not in ((10, 0), (10, 3)):
417
+ raise RuntimeError(
418
+ "This benchmark targets TP2/TP4/TP8 SM100/SM103, "
419
+ f"got TP{world_size} SM{capability}"
420
+ )
421
+
422
+ max_message_bytes = max(args.message_bytes)
423
+ comm = CustomAllreduce(
424
+ group=cpu_group,
425
+ device=device,
426
+ max_mnnvl_reduce_scatter_size=max_message_bytes,
427
+ max_mnnvl_multimem_reduce_scatter_size=max_message_bytes,
428
+ )
429
+ if comm.disabled:
430
+ raise RuntimeError("The production custom all-reduce backend is unavailable")
431
+ comm._init_mnnvl_multimem_reduce_scatter_buffer()
432
+ if not comm.mnnvl_multimem_rs_multicast_ptr:
433
+ raise RuntimeError("The production MNNVL multimem backend is unavailable")
434
+
435
+ dtype = _DTYPES[args.dtype]
436
+ warmup = torch.zeros(1, dtype=torch.float32, device=device)
437
+ dist.all_reduce(warmup, group=device_group)
438
+ sync = torch.zeros_like(warmup)
439
+
440
+ def device_barrier() -> None:
441
+ dist.all_reduce(sync, group=device_group)
442
+
443
+ results = [
444
+ benchmark_size(
445
+ args,
446
+ comm,
447
+ message_bytes,
448
+ dtype,
449
+ device,
450
+ rank,
451
+ world_size,
452
+ device_group,
453
+ cpu_group,
454
+ device_barrier,
455
+ )
456
+ for message_bytes in args.message_bytes
457
+ ]
458
+ if rank == 0:
459
+ metadata = {
460
+ "world_size": world_size,
461
+ "gpu": torch.cuda.get_device_name(device),
462
+ "sm": f"{capability[0]}{capability[1]}",
463
+ "num_sms": torch.cuda.get_device_properties(device).multi_processor_count,
464
+ "dtype": args.dtype,
465
+ "torch": torch.__version__,
466
+ "cuda": torch.version.cuda,
467
+ "blocks": args.blocks,
468
+ "num_workspaces": args.num_workspaces,
469
+ "cold_l2": args.cold_l2,
470
+ }
471
+ print(json.dumps({"metadata": metadata, "results": results}, indent=2))
472
+
473
+ comm.close()
474
+ dist.barrier(group=cpu_group)
475
+ dist.destroy_process_group(cpu_group)
476
+ dist.destroy_process_group()
477
+
478
+
479
+if __name__ == "__main__":
480
+ main()
csrc/custom_all_gather_reduce_scatter.cuh
+125
@@ -7,6 +7,10 @@ namespace vllm {
7
constexpr int kMnnvlLamportAgThreads = 128;
8
constexpr int kMnnvlLamportRsThreads = 256;
9
constexpr int kMnnvlLamportConcurrentPollMaxPacks = 8192;
10
+constexpr int kMnnvlMultimemRsThreads = 1024;
11
+constexpr int kMnnvlMultimemRsBlockLimit = 8;
12
+constexpr int kMnnvlMultimemRsVectorBytes = 16;
13
+constexpr int kMnnvlMultimemRsUnroll = 8;
14
15
using CopyPack = array_t<uint64_t, 2>;
16
@@ -48,6 +52,127 @@ __global__ void __launch_bounds__(512, 1)
52
barrier_at_end<ngpus, true>(sg, self_sg, rank);
53
}
54
55
+// Multimem reduce-scatter reduces through an NVLS multicast mapping, which
56
+// has no AMD equivalent. The whole path is compiled out on ROCm and the host
57
+// entry point rejects the call instead of launching anything.
58
+#if !defined(USE_ROCM)
59
+
60
+template <typename T>
61
+DINLINE void multimem_load_reduce_16(uint32_t (&result)[4], const T* address) {
62
+ #if CUDA_VERSION >= 12020 && defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
63
+ if constexpr (std::is_same<T, nv_bfloat16>::value) {
64
+ asm volatile(
65
+ "multimem.ld_reduce.relaxed.sys.global.add.acc::f32.v4.bf16x2 "
66
+ "{%0,%1,%2,%3}, [%4];"
67
+ : "=r"(result[0]), "=r"(result[1]), "=r"(result[2]), "=r"(result[3])
68
+ : "l"(address)
69
+ : "memory");
70
+ } else if constexpr (std::is_same<T, half>::value) {
71
+ asm volatile(
72
+ "multimem.ld_reduce.relaxed.sys.global.add.acc::f32.v4.f16x2 "
73
+ "{%0,%1,%2,%3}, [%4];"
74
+ : "=r"(result[0]), "=r"(result[1]), "=r"(result[2]), "=r"(result[3])
75
+ : "l"(address)
76
+ : "memory");
77
+ } else {
78
+ static_assert(std::is_same<T, float>::value);
79
+ asm volatile(
80
+ "multimem.ld_reduce.relaxed.sys.global.add.v4.f32 "
81
+ "{%0,%1,%2,%3}, [%4];"
82
+ : "=r"(result[0]), "=r"(result[1]), "=r"(result[2]), "=r"(result[3])
83
+ : "l"(address)
84
+ : "memory");
85
+ }
86
+ #else
87
+ asm volatile("trap;");
88
+ #endif
89
+}
90
+
91
+DINLINE void store_global_16(void* address, const uint32_t (&value)[4]) {
92
+ asm volatile("st.global.v4.u32 [%0], {%1,%2,%3,%4};"
93
+ :
94
+ : "l"(address), "r"(value[0]), "r"(value[1]), "r"(value[2]),
95
+ "r"(value[3])
96
+ : "memory");
97
+}
98
+
99
+DINLINE void mnnvl_multimem_publish_flag(FlagType* flag_addr, FlagType flag) {
100
+ #if CUDA_VERSION >= 12020 && defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
101
+ asm volatile("multimem.st.release.sys.global.u32 [%0], %1;"
102
+ :
103
+ : "l"(flag_addr), "r"(flag)
104
+ : "memory");
105
+ // The flag is stored through the multicast alias and polled through the
106
+ // local unicast alias in the same grid.
107
+ asm volatile("fence.proxy.alias;" ::: "memory");
108
+ #else
109
+ asm volatile("trap;");
110
+ #endif
111
+}
112
+
113
+template <bool ready, int ngpus>
114
+__global__ void mnnvl_multimem_barrier_kernel(Signal* local_signal,
115
+ Signal* multicast_signal,
116
+ int rank) {
117
+ FlagType flag = local_signal->_flag[0] + 1;
118
+ FlagType* multicast_counters =
119
+ ready ? multicast_signal->start[0] : multicast_signal->end[0];
120
+ FlagType* local_counters =
121
+ ready ? local_signal->start[0] : local_signal->end[0];
122
+ // Replicate this rank's counter into every backing allocation.
123
+ mnnvl_multimem_publish_flag(&multicast_counters[rank], flag);
124
+ #pragma unroll
125
+ for (int peer = 0; peer < ngpus; ++peer) {
126
+ while (ld_flag_acquire(&local_counters[peer]) != flag);
127
+ }
128
+ local_signal->_flag[0] = flag;
129
+}
130
+
131
+template <typename T, int ngpus>
132
+__global__ void __launch_bounds__(kMnnvlMultimemRsThreads, 1)
133
+ mnnvl_multimem_reduce_scatter_kernel(const T* __restrict__ multicast_input,
134
+ T* __restrict__ result, int rank,
135
+ int packs_per_rank) {
136
+ constexpr int kThreadsPerWarp = 32;
137
+ constexpr int kWarpsPerCta = kMnnvlMultimemRsThreads / kThreadsPerWarp;
138
+ constexpr int kPacksPerWarpIteration =
139
+ kThreadsPerWarp * kMnnvlMultimemRsUnroll;
140
+
141
+ int lane;
142
+ asm volatile("mov.u32 %0, %%laneid;" : "=r"(lane));
143
+ int warp = blockIdx.x * kWarpsPerCta + threadIdx.x / kThreadsPerWarp;
144
+ int num_warps = gridDim.x * kWarpsPerCta;
145
+ int pack_offset = warp * kPacksPerWarpIteration + lane;
146
+ int pack_stride = num_warps * kPacksPerWarpIteration;
147
+ auto* rank_input = reinterpret_cast<const char*>(multicast_input) +
148
+ rank * packs_per_rank * kMnnvlMultimemRsVectorBytes;
149
+ auto* rank_output = reinterpret_cast<char*>(result);
150
+
151
+ while (pack_offset < packs_per_rank) {
152
+ uint32_t reduced[kMnnvlMultimemRsUnroll][4];
153
+ #pragma unroll
154
+ for (int u = 0; u < kMnnvlMultimemRsUnroll; ++u) {
155
+ int pack = pack_offset + u * kThreadsPerWarp;
156
+ if (pack < packs_per_rank) {
157
+ multimem_load_reduce_16<T>(
158
+ reduced[u], reinterpret_cast<const T*>(
159
+ rank_input + pack * kMnnvlMultimemRsVectorBytes));
160
+ }
161
+ }
162
+ #pragma unroll
163
+ for (int u = 0; u < kMnnvlMultimemRsUnroll; ++u) {
164
+ int pack = pack_offset + u * kThreadsPerWarp;
165
+ if (pack < packs_per_rank) {
166
+ store_global_16(rank_output + pack * kMnnvlMultimemRsVectorBytes,
167
+ reduced[u]);
168
+ }
169
+ }
170
+ pack_offset += pack_stride;
171
+ }
172
+}
173
+
174
+#endif // !defined(USE_ROCM)
175
+
176
template <typename P>
177
union LamportPack {
178
P packed;
csrc/custom_all_reduce.cuh
+6
@@ -339,6 +339,12 @@ class CustomAllreduce {
339
void mnnvl_lamport_reduce_scatter(cudaStream_t stream, T* input, T* output,
340
void* local_buffer, uint32_t* epochs,
341
int size, int stage_size_bytes);
342
+ template <typename T>
343
+ void mnnvl_multimem_reduce_scatter(cudaStream_t stream,
344
+ const T* multicast_input, T* output,
345
+ Signal* local_signal,
346
+ Signal* multicast_signal, int size,
347
+ int block_limit);
348
349
~CustomAllreduce() {
350
for (auto [_, ptr] : ipc_handles_) {
csrc/libtorch_stable/custom_all_gather_reduce_scatter.cu
+108
@@ -191,6 +191,55 @@ void CustomAllreduce::mnnvl_lamport_reduce_scatter(cudaStream_t stream,
191
#undef MNNVL_LAMPORT_RS_LAUNCH
192
}
193
194
+template <typename T>
195
+void CustomAllreduce::mnnvl_multimem_reduce_scatter(
196
+ cudaStream_t stream, const T* multicast_input, T* output,
197
+ Signal* local_signal, Signal* multicast_signal, int size, int block_limit) {
198
+#if defined(USE_ROCM)
199
+ throw std::runtime_error(
200
+ "MNNVL multimem reduce-scatter requires NVLS multicast and is not "
201
+ "supported on ROCm");
202
+#else
203
+ if (size <= 0)
204
+ throw std::runtime_error(
205
+ "MNNVL multimem reduce-scatter requires a non-empty input");
206
+ int size_bytes = size * sizeof(T);
207
+ if (size_bytes % (kMnnvlMultimemRsVectorBytes * world_size_) != 0)
208
+ throw std::runtime_error(
209
+ "MNNVL multimem reduce-scatter requires each output shard byte size "
210
+ "to be a multiple of 16");
211
+ if (block_limit <= 0 || block_limit > kMnnvlMultimemRsBlockLimit)
212
+ throw std::runtime_error(
213
+ "MNNVL multimem reduce-scatter block limit is out of range");
214
+
215
+ int packs_per_rank = size_bytes / kMnnvlMultimemRsVectorBytes / world_size_;
216
+ int packs_per_block = kMnnvlMultimemRsThreads * kMnnvlMultimemRsUnroll;
217
+ int blocks = std::min(
218
+ block_limit, (packs_per_rank + packs_per_block - 1) / packs_per_block);
219
+
220
+ #define MNNVL_MULTIMEM_RS_CASE(ngpus) \
221
+ case ngpus: \
222
+ mnnvl_multimem_barrier_kernel<true, ngpus> \
223
+ <<<1, 1, 0, stream>>>(local_signal, multicast_signal, rank_); \
224
+ mnnvl_multimem_reduce_scatter_kernel<T, ngpus> \
225
+ <<<blocks, kMnnvlMultimemRsThreads, 0, stream>>>( \
226
+ multicast_input, output, rank_, packs_per_rank); \
227
+ mnnvl_multimem_barrier_kernel<false, ngpus> \
228
+ <<<1, 1, 0, stream>>>(local_signal, multicast_signal, rank_); \
229
+ break;
230
+
231
+ switch (world_size_) {
232
+ MNNVL_MULTIMEM_RS_CASE(2)
233
+ MNNVL_MULTIMEM_RS_CASE(4)
234
+ MNNVL_MULTIMEM_RS_CASE(8)
235
+ default:
236
+ throw std::runtime_error(
237
+ "MNNVL multimem reduce-scatter only supports num gpus in (2,4,8)");
238
+ }
239
+ #undef MNNVL_MULTIMEM_RS_CASE
240
+#endif
241
+}
242
+
243
} // namespace vllm
244
245
using fptr_t = int64_t;
@@ -360,3 +409,62 @@ void mnnvl_lamport_reduce_scatter(fptr_t _fa, torch::stable::Tensor& inp,
409
"bfloat16");
410
}
411
}
412
+
413
+void mnnvl_multimem_reduce_scatter(fptr_t _fa, torch::stable::Tensor& inp,
414
+ torch::stable::Tensor& out,
415
+ fptr_t _local_buffer,
416
+ fptr_t _multicast_buffer,
417
+ int64_t stage_sz_bytes,
418
+ int64_t block_limit) {
419
+ auto fa = reinterpret_cast<vllm::CustomAllreduce*>(_fa);
420
+ const torch::stable::accelerator::DeviceGuard device_guard(
421
+ inp.get_device_index());
422
+ const cudaStream_t stream = get_current_cuda_stream(inp.get_device_index());
423
+
424
+ STD_TORCH_CHECK((inp.scalar_type()) == (out.scalar_type()));
425
+ STD_TORCH_CHECK((out.numel() * fa->world_size_) == (inp.numel()));
426
+ STD_TORCH_CHECK(inp.numel() > 0);
427
+ STD_TORCH_CHECK(_is_weak_contiguous(out));
428
+ STD_TORCH_CHECK(_is_weak_contiguous(inp));
429
+ auto input_size = inp.numel() * inp.element_size();
430
+ STD_TORCH_CHECK(input_size <= stage_sz_bytes);
431
+ auto local_buffer = reinterpret_cast<void*>(_local_buffer);
432
+ auto multicast_buffer = reinterpret_cast<void*>(_multicast_buffer);
433
+ STD_TORCH_CHECK(local_buffer != nullptr);
434
+ STD_TORCH_CHECK(multicast_buffer != nullptr);
435
+ auto* local_signal = reinterpret_cast<vllm::Signal*>(
436
+ static_cast<char*>(local_buffer) - sizeof(vllm::Signal));
437
+ auto* multicast_signal = reinterpret_cast<vllm::Signal*>(
438
+ static_cast<char*>(multicast_buffer) - sizeof(vllm::Signal));
439
+ STD_CUDA_CHECK(cudaMemcpyAsync(local_buffer, inp.const_data_ptr(), input_size,
440
+ cudaMemcpyDeviceToDevice, stream));
441
+ switch (out.scalar_type()) {
442
+ case torch::headeronly::ScalarType::Float: {
443
+ fa->mnnvl_multimem_reduce_scatter<float>(
444
+ stream, reinterpret_cast<float*>(multicast_buffer),
445
+ reinterpret_cast<float*>(out.mutable_data_ptr()), local_signal,
446
+ multicast_signal, inp.numel(), block_limit);
447
+ break;
448
+ }
449
+ case torch::headeronly::ScalarType::Half: {
450
+ fa->mnnvl_multimem_reduce_scatter<half>(
451
+ stream, reinterpret_cast<half*>(multicast_buffer),
452
+ reinterpret_cast<half*>(out.mutable_data_ptr()), local_signal,
453
+ multicast_signal, inp.numel(), block_limit);
454
+ break;
455
+ }
456
+#if (__CUDA_ARCH__ >= 800 || !defined(__CUDA_ARCH__))
457
+ case torch::headeronly::ScalarType::BFloat16: {
458
+ fa->mnnvl_multimem_reduce_scatter<nv_bfloat16>(
459
+ stream, reinterpret_cast<nv_bfloat16*>(multicast_buffer),
460
+ reinterpret_cast<nv_bfloat16*>(out.mutable_data_ptr()), local_signal,
461
+ multicast_signal, inp.numel(), block_limit);
462
+ break;
463
+ }
464
+#endif
465
+ default:
466
+ throw std::runtime_error(
467
+ "MNNVL multimem reduce-scatter only supports float32, float16 and "
468
+ "bfloat16");
469
+ }
470
+}
csrc/libtorch_stable/custom_all_gather_reduce_scatter_ops.cpp
+6
@@ -17,6 +17,10 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C_custom_ar, custom_ag_rs) {
17
custom_ag_rs.def(
18
"mnnvl_lamport_reduce_scatter(int fa, Tensor inp, Tensor! out, int "
19
"local_buffer, int epoch_buffer, int stage_sz_bytes) -> ()");
20
+ custom_ag_rs.def(
21
+ "mnnvl_multimem_reduce_scatter(int fa, Tensor inp, Tensor! out, int "
22
+ "local_buffer, int multicast_buffer, int stage_sz_bytes, int "
23
+ "block_limit) -> ()");
24
}
25
26
STABLE_TORCH_LIBRARY_IMPL(_C_custom_ar, CUDA, custom_ag_rs) {
@@ -26,4 +30,6 @@ STABLE_TORCH_LIBRARY_IMPL(_C_custom_ar, CUDA, custom_ag_rs) {
30
custom_ag_rs.impl("custom_reduce_scatter", TORCH_BOX(&custom_reduce_scatter));
31
custom_ag_rs.impl("mnnvl_lamport_reduce_scatter",
32
TORCH_BOX(&mnnvl_lamport_reduce_scatter));
33
+ custom_ag_rs.impl("mnnvl_multimem_reduce_scatter",
34
+ TORCH_BOX(&mnnvl_multimem_reduce_scatter));
35
}
csrc/libtorch_stable/ops.h
+4
@@ -527,6 +527,10 @@ void mnnvl_lamport_reduce_scatter(fptr_t _fa, torch::stable::Tensor& inp,
527
torch::stable::Tensor& out,
528
fptr_t local_buffer, fptr_t epoch_buffer,
529
int64_t stage_sz_bytes);
530
+void mnnvl_multimem_reduce_scatter(fptr_t _fa, torch::stable::Tensor& inp,
531
+ torch::stable::Tensor& out,
532
+ fptr_t local_buffer, fptr_t multicast_buffer,
533
+ int64_t stage_sz_bytes, int64_t block_limit);
534
void dispose(fptr_t _fa);
535
int64_t meta_size();
536
void register_buffer(fptr_t _fa, const std::vector<int64_t>& fake_ipc_ptrs);
tests/distributed/test_custom_all_gather_reduce_scatter.py
+68
@@ -23,6 +23,7 @@ import torch
23
import torch.distributed as dist
24
25
from vllm.distributed.parallel_state import get_tp_group
26
+from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
27
from vllm.platforms import current_platform
28
29
from ..utils import (
@@ -53,6 +54,11 @@ def _supports_multimem():
54
)
55
56
57
+def _supports_sm100_or_sm103():
58
+ capability = current_platform.get_device_capability()
59
+ return current_platform.is_cuda() and capability in ((10, 0), (10, 3))
60
+
61
+
62
def _payload(value, device):
63
return torch.full((_ROWS, _HIDDEN), value, dtype=torch.bfloat16, device=device)
64
@@ -324,3 +330,65 @@ def test_mnnvl_lamport_stage_cleanup(monkeypatch: pytest.MonkeyPatch):
330
if torch.accelerator.device_count() < 2:
331
pytest.skip("Need at least two GPUs to run the test.")
332
multi_process_parallel(monkeypatch, 2, 1, _run_stage_cleanup_test)
333
+
334
+
335
+@ray.remote(num_gpus=1, max_calls=1)
336
+def _run_multimem_reduce_scatter_test(
337
+ monkeypatch: pytest.MonkeyPatch,
338
+ tp_size,
339
+ pp_size,
340
+ rank,
341
+ distributed_init_port,
342
+):
343
+ with monkeypatch.context() as m:
344
+ m.delenv("CUDA_VISIBLE_DEVICES", raising=False)
345
+ m.delenv("HIP_VISIBLE_DEVICES", raising=False)
346
+ device = torch.device(f"cuda:{rank}")
347
+ torch.accelerator.set_device_index(device)
348
+ init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port)
349
+ ensure_model_parallel_initialized(tp_size, pp_size)
350
+
351
+ fa = get_tp_group().device_communicator.ca_comm
352
+ assert fa is not None and not fa.disabled
353
+ assert fa.mnnvl_multimem_rs_supported
354
+ assert not fa.mnnvl_multimem_rs_initialized
355
+ assert not fa.mnnvl_multimem_rs_multicast_ptr
356
+
357
+ message_bytes = {
358
+ "mnnvl_lamport": 1 * 1024 * 1024,
359
+ "mnnvl_multimem": 20 * 1024 * 1024,
360
+ "nccl": 64 * 1024 * 1024 + 128,
361
+ }
362
+ for _ in range(2):
363
+ for backend, size_bytes in message_bytes.items():
364
+ shard_elements = size_bytes // torch.bfloat16.itemsize // tp_size
365
+ inp = torch.full(
366
+ (tp_size, shard_elements),
367
+ rank + 1,
368
+ dtype=torch.bfloat16,
369
+ device=device,
370
+ )
371
+ selected = fa._select_reduce_scatter_backend(inp)
372
+ if backend == "nccl":
373
+ assert selected is None
374
+ out = sp_reduce_scatter(inp)
375
+ else:
376
+ assert selected == backend
377
+ out = fa.custom_reduce_scatter(inp)
378
+ assert out is not None
379
+ if backend == "mnnvl_multimem":
380
+ assert fa.mnnvl_multimem_rs_initialized
381
+ assert fa.mnnvl_multimem_rs_multicast_ptr
382
+ expected = tp_size * (tp_size + 1) // 2
383
+ torch.testing.assert_close(out, torch.full_like(out, expected))
384
+
385
+
386
+@pytest.mark.skipif(
387
+ not _supports_sm100_or_sm103(),
388
+ reason="The low-SM MNNVL reduce-scatter path requires SM100 or SM103.",
389
+)
390
+@pytest.mark.parametrize("tp_size", [2, 4, 8])
391
+def test_mnnvl_multimem_reduce_scatter(monkeypatch: pytest.MonkeyPatch, tp_size: int):
392
+ if torch.accelerator.device_count() < tp_size:
393
+ pytest.skip(f"Need at least {tp_size} GPUs to run the test.")
394
+ multi_process_parallel(monkeypatch, tp_size, 1, _run_multimem_reduce_scatter_test)
tests/distributed/test_custom_all_reduce.py
+263
-1
@@ -151,9 +151,9 @@ def test_custom_allreduce_filters_dtype(
151
) -> None:
152
communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
153
communicator.disabled = False
154
+ communicator._ptr = 0
155
communicator.world_size = 2
156
communicator.max_size = 1024
156
- communicator._ptr = 0
157
158
assert communicator.should_custom_ar(torch.empty(16, dtype=dtype)) is expected
159
@@ -219,6 +219,268 @@ def test_local_multicast_support_rejects_non_cuda(monkeypatch):
219
assert not car._has_local_multicast_support(torch.device("cuda:0"))
220
221
222
+@pytest.mark.parametrize(
223
+ ("world_size", "device_capability", "local_multicast", "expected"),
224
+ [
225
+ (2, (10, 0), True, True),
226
+ (4, (10, 3), True, True),
227
+ (8, (10, 0), True, True),
228
+ (8, (10, 3), True, True),
229
+ (6, (10, 3), True, False),
230
+ (8, (10, 1), True, False),
231
+ (8, (9, 0), True, False),
232
+ (8, (10, 3), False, False),
233
+ ],
234
+)
235
+def test_mnnvl_multimem_reduce_scatter_platform_gate(
236
+ monkeypatch,
237
+ world_size,
238
+ device_capability,
239
+ local_multicast,
240
+ expected,
241
+):
242
+ def is_device_capability(capability, device_id):
243
+ assert capability in ((10, 0), (10, 3))
244
+ assert device_id == 3
245
+ return device_capability == capability
246
+
247
+ monkeypatch.setattr(
248
+ car.current_platform,
249
+ "is_device_capability",
250
+ is_device_capability,
251
+ )
252
+ monkeypatch.setattr(
253
+ car,
254
+ "_has_local_multicast_support",
255
+ lambda _device: local_multicast,
256
+ )
257
+
258
+ supported = car._supports_mnnvl_multimem_reduce_scatter(
259
+ torch.device("cuda:3"), world_size
260
+ )
261
+ assert supported is expected
262
+
263
+
264
+@pytest.mark.parametrize(
265
+ (
266
+ "message_bytes",
267
+ "multimem_ptr",
268
+ "multimem_initialized",
269
+ "batch_invariant",
270
+ "expected",
271
+ ),
272
+ [
273
+ (16 * 1024 * 1024, 1, True, False, "mnnvl_lamport"),
274
+ (16 * 1024 * 1024 + 128, 1, True, False, "mnnvl_multimem"),
275
+ (64 * 1024 * 1024, 1, True, False, "mnnvl_multimem"),
276
+ (64 * 1024 * 1024 + 128, 1, True, False, None),
277
+ (32 * 1024 * 1024, 0, True, False, None),
278
+ (32 * 1024 * 1024, 0, False, False, "mnnvl_multimem"),
279
+ (8 * 1024 * 1024, 1, True, True, "mnnvl_lamport"),
280
+ (32 * 1024 * 1024, 1, True, True, None),
281
+ ],
282
+)
283
+@pytest.mark.parametrize("world_size", [2, 4, 8])
284
+def test_mnnvl_reduce_scatter_backend_gate(
285
+ monkeypatch,
286
+ world_size,
287
+ message_bytes,
288
+ multimem_ptr,
289
+ multimem_initialized,
290
+ batch_invariant,
291
+ expected,
292
+):
293
+ monkeypatch.setattr(car.current_platform, "is_cuda", lambda: True)
294
+ monkeypatch.setattr(car.envs, "VLLM_BATCH_INVARIANT", batch_invariant)
295
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
296
+ communicator.disabled = False
297
+ communicator._ptr = 0
298
+ communicator.world_size = world_size
299
+ communicator.mnnvl_only = False
300
+ communicator.fully_connected = True
301
+ communicator.mnnvl_multicast_ptr = 1
302
+ communicator.mnnvl_multimem_rs_supported = True
303
+ communicator.mnnvl_multimem_rs_initialized = multimem_initialized
304
+ communicator.mnnvl_multimem_rs_multicast_ptr = multimem_ptr
305
+ communicator.max_mnnvl_reduce_scatter_size = 16 * 1024 * 1024
306
+ communicator.max_mnnvl_multimem_reduce_scatter_size = 64 * 1024 * 1024
307
+ communicator.max_reduce_scatter_size = 16 * 1024 * 1024
308
+ inp = torch.empty(
309
+ (world_size, message_bytes // torch.bfloat16.itemsize // world_size),
310
+ dtype=torch.bfloat16,
311
+ )
312
+
313
+ assert inp.nbytes == message_bytes
314
+ assert communicator._select_reduce_scatter_backend(inp) == expected
315
+ assert communicator.should_custom_reduce_scatter(inp) is (expected is not None)
316
+ assert communicator.should_mnnvl_multimem_reduce_scatter(inp) is (
317
+ expected == "mnnvl_multimem"
318
+ )
319
+
320
+
321
+def test_mnnvl_multimem_reduce_scatter_skips_rendezvous_after_peer_alloc_failure(
322
+ monkeypatch,
323
+):
324
+ events = []
325
+
326
+ class FakeSymmMem:
327
+ @staticmethod
328
+ def empty(*_args, **_kwargs):
329
+ events.append("empty")
330
+ return torch.empty(1, dtype=torch.uint8)
331
+
332
+ @staticmethod
333
+ def rendezvous(*_args, **_kwargs):
334
+ events.append("rendezvous")
335
+ return None
336
+
337
+ def report_peer_allocation_failure(group_value, **_kwargs):
338
+ events.append("all_reduce")
339
+ assert group_value.item() == 1
340
+ group_value.zero_()
341
+
342
+ monkeypatch.setattr(car, "torch_symm_mem", FakeSymmMem)
343
+ monkeypatch.setattr(car.ops, "meta_size", lambda: 128)
344
+ monkeypatch.setattr(car.dist, "all_reduce", report_peer_allocation_failure)
345
+ warnings = []
346
+ monkeypatch.setattr(
347
+ car.logger,
348
+ "warning_once",
349
+ lambda message, *_args, **_kwargs: warnings.append(message),
350
+ )
351
+
352
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
353
+ communicator.disabled = True
354
+ communicator._ptr = 0
355
+ communicator.group = object()
356
+ communicator.device = torch.device("cuda:0")
357
+ communicator.max_mnnvl_multimem_reduce_scatter_size = 64 * 1024 * 1024
358
+ communicator.mnnvl_multimem_rs_supported = True
359
+ communicator.mnnvl_multimem_rs_initialized = False
360
+ communicator.mnnvl_multimem_rs_buffer = None
361
+ communicator.mnnvl_multimem_rs_multicast_ptr = 0
362
+
363
+ communicator._init_mnnvl_multimem_reduce_scatter_buffer()
364
+
365
+ assert events == ["empty", "all_reduce"]
366
+ assert communicator.mnnvl_multimem_rs_initialized
367
+ assert communicator.mnnvl_multimem_rs_buffer is None
368
+ assert communicator.mnnvl_multimem_rs_multicast_ptr == 0
369
+ assert warnings == [
370
+ "MNNVL multimem reduce-scatter symmetric-memory allocation "
371
+ "failed on at least one rank; falling back to NCCL."
372
+ ]
373
+
374
+
375
+def test_mnnvl_multimem_reduce_scatter_warns_on_rendezvous_failure(monkeypatch):
376
+ events = []
377
+
378
+ class FakeSymmMem:
379
+ @staticmethod
380
+ def empty(*_args, **_kwargs):
381
+ events.append("empty")
382
+ return torch.empty(1, dtype=torch.uint8)
383
+
384
+ @staticmethod
385
+ def rendezvous(*_args, **_kwargs):
386
+ events.append("rendezvous")
387
+ raise RuntimeError("rendezvous failed")
388
+
389
+ def preserve_local_result(_group_value, **_kwargs):
390
+ events.append("all_reduce")
391
+
392
+ warnings = []
393
+ monkeypatch.setattr(car, "torch_symm_mem", FakeSymmMem)
394
+ monkeypatch.setattr(car.ops, "meta_size", lambda: 128)
395
+ monkeypatch.setattr(car.dist, "all_reduce", preserve_local_result)
396
+ monkeypatch.setattr(
397
+ car.logger,
398
+ "warning_once",
399
+ lambda message, *_args, **_kwargs: warnings.append(message),
400
+ )
401
+
402
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
403
+ communicator.disabled = True
404
+ communicator._ptr = 0
405
+ communicator.group = type("Group", (), {"group_name": "test"})()
406
+ communicator.device = torch.device("cuda:0")
407
+ communicator.max_mnnvl_multimem_reduce_scatter_size = 64 * 1024 * 1024
408
+ communicator.mnnvl_multimem_rs_supported = True
409
+ communicator.mnnvl_multimem_rs_initialized = False
410
+ communicator.mnnvl_multimem_rs_buffer = None
411
+ communicator.mnnvl_multimem_rs_multicast_ptr = 0
412
+
413
+ communicator._init_mnnvl_multimem_reduce_scatter_buffer()
414
+
415
+ assert events == ["empty", "all_reduce", "rendezvous", "all_reduce"]
416
+ assert communicator.mnnvl_multimem_rs_initialized
417
+ assert communicator.mnnvl_multimem_rs_buffer is None
418
+ assert communicator.mnnvl_multimem_rs_multicast_ptr == 0
419
+ assert warnings == [
420
+ "MNNVL multimem reduce-scatter symmetric-memory rendezvous "
421
+ "failed on at least one rank; falling back to NCCL."
422
+ ]
423
+
424
+
425
+def test_mnnvl_multimem_reduce_scatter_initializes_signals(monkeypatch):
426
+ events = []
427
+ buffers = []
428
+
429
+ class FakeHandle:
430
+ multicast_ptr = 0x3000
431
+
432
+ class FakeSymmMem:
433
+ @staticmethod
434
+ def empty(size, **_kwargs):
435
+ events.append(("empty", size))
436
+ buffer = torch.ones(size, dtype=torch.uint8)
437
+ buffers.append(buffer)
438
+ return buffer
439
+
440
+ @staticmethod
441
+ def rendezvous(*_args, **_kwargs):
442
+ events.append(("rendezvous", None))
443
+ return FakeHandle()
444
+
445
+ def preserve_local_result(*_args, **_kwargs):
446
+ events.append(("all_reduce", None))
447
+
448
+ monkeypatch.setattr(car, "torch_symm_mem", FakeSymmMem)
449
+ monkeypatch.setattr(car.ops, "meta_size", lambda: 128)
450
+ monkeypatch.setattr(
451
+ car.torch.accelerator,
452
+ "synchronize",
453
+ lambda: events.append(("synchronize", None)),
454
+ )
455
+ monkeypatch.setattr(car.dist, "all_reduce", preserve_local_result)
456
+
457
+ communicator = car.CustomAllreduce.__new__(car.CustomAllreduce)
458
+ communicator.disabled = True
459
+ communicator._ptr = 0
460
+ communicator.group = type("Group", (), {"group_name": "test"})()
461
+ communicator.device = torch.device("cpu")
462
+ communicator.max_mnnvl_multimem_reduce_scatter_size = 129
463
+ communicator.mnnvl_multimem_rs_supported = True
464
+ communicator.mnnvl_multimem_rs_initialized = False
465
+ communicator.mnnvl_multimem_rs_buffer = None
466
+ communicator.mnnvl_multimem_rs_multicast_ptr = 0
467
+
468
+ communicator._init_mnnvl_multimem_reduce_scatter_buffer()
469
+
470
+ assert events == [
471
+ ("empty", 257),
472
+ ("all_reduce", None),
473
+ ("rendezvous", None),
474
+ ("synchronize", None),
475
+ ("all_reduce", None),
476
+ ]
477
+ assert torch.all(buffers[0][:128] == 0)
478
+ assert torch.all(buffers[0][128:] == 1)
479
+ assert communicator.mnnvl_multimem_rs_buffer_size == 129
480
+ assert communicator.mnnvl_multimem_rs_local_ptr == buffers[0].data_ptr() + 128
481
+ assert communicator.mnnvl_multimem_rs_multicast_ptr == 0x3080
482
+
483
+
484
@ray.remote(num_gpus=1, max_calls=1)
485
def graph_allreduce(
486
monkeypatch: pytest.MonkeyPatch,
vllm/_custom_ops.py
+20
@@ -3159,6 +3159,26 @@ def mnnvl_lamport_reduce_scatter(
3159
)
3160
3161
3162
+def mnnvl_multimem_reduce_scatter(
3163
+ fa: int,
3164
+ inp: torch.Tensor,
3165
+ out: torch.Tensor,
3166
+ local_buffer: int,
3167
+ multicast_buffer: int,
3168
+ stage_sz_bytes: int,
3169
+ block_limit: int,
3170
+) -> None:
3171
+ torch.ops._C_custom_ar.mnnvl_multimem_reduce_scatter(
3172
+ fa,
3173
+ inp,
3174
+ out,
3175
+ local_buffer,
3176
+ multicast_buffer,
3177
+ stage_sz_bytes,
3178
+ block_limit,
3179
+ )
3180
+
3181
+
3182
def dispose(fa: int) -> None:
3183
torch.ops._C_custom_ar.dispose(fa)
3184
vllm/distributed/device_communicators/custom_all_reduce.py
+163
-30
@@ -2,7 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from contextlib import contextmanager
5
-from typing import cast
5
+from typing import Literal, cast
6
7
import torch
8
import torch.distributed as dist
@@ -32,6 +32,9 @@ except ImportError:
32
33
logger = init_logger(__name__)
34
35
+_ReduceScatterBackend = Literal["legacy", "mnnvl_lamport", "mnnvl_multimem"]
36
+_MNNVL_MULTIMEM_REDUCE_SCATTER_WORLD_SIZES = (2, 4, 8)
37
+
38
39
def _has_local_multicast_support(device: torch.device) -> bool:
40
"""Return whether this CUDA device can allocate multicast memory."""
@@ -53,6 +56,12 @@ def _has_local_multicast_support(device: torch.device) -> bool:
56
return False
57
58
59
+def _all_ranks_true(group: ProcessGroup, local_value: bool) -> bool:
60
+ group_value = torch.tensor(int(local_value), dtype=torch.int32, device="cpu")
61
+ dist.all_reduce(group_value, op=dist.ReduceOp.MIN, group=group)
62
+ return bool(group_value.item())
63
+
64
+
65
def _group_can_attempt_mnnvl(
66
group: ProcessGroup,
67
device: torch.device,
@@ -71,17 +80,7 @@ def _group_can_attempt_mnnvl(
80
and current_platform.has_device_capability(100, device_index)
81
and _has_local_multicast_support(device)
82
)
74
- group_support = torch.tensor(
75
- int(local_support),
76
- dtype=torch.int32,
77
- device="cpu",
78
- )
79
- dist.all_reduce(
80
- group_support,
81
- op=dist.ReduceOp.MIN,
82
- group=group,
83
- )
84
- return bool(group_support.item())
83
+ return _all_ranks_true(group, local_support)
84
85
86
def _can_p2p(rank: int, world_size: int) -> bool:
@@ -101,6 +100,20 @@ def _can_p2p(rank: int, world_size: int) -> bool:
100
return True
101
102
103
+def _supports_mnnvl_multimem_reduce_scatter(
104
+ device: torch.device, world_size: int
105
+) -> bool:
106
+ return (
107
+ world_size in _MNNVL_MULTIMEM_REDUCE_SCATTER_WORLD_SIZES
108
+ and device.index is not None
109
+ and (
110
+ current_platform.is_device_capability((10, 0), device.index)
111
+ or current_platform.is_device_capability((10, 3), device.index)
112
+ )
113
+ and _has_local_multicast_support(device)
114
+ )
115
+
116
+
117
from vllm.distributed.utils import is_weak_contiguous # noqa: E402
118
119
@@ -116,6 +129,8 @@ class CustomAllreduce:
129
}
130
_DEFAULT_REDUCE_SCATTER_MAX_SIZE = 16 * 1024 * 1024
131
_DEFAULT_MNNVL_REDUCE_SCATTER_MAX_SIZE = 16 * 1024 * 1024
132
+ _DEFAULT_MNNVL_MULTIMEM_REDUCE_SCATTER_MAX_SIZE = 64 * 1024 * 1024
133
+ _MNNVL_MULTIMEM_REDUCE_SCATTER_BLOCKS = 8
134
135
# max_size: max supported allreduce size
136
def __init__(
@@ -127,6 +142,9 @@ class CustomAllreduce:
142
max_mnnvl_all_gather_size=None,
143
max_reduce_scatter_size=_DEFAULT_REDUCE_SCATTER_MAX_SIZE,
144
max_mnnvl_reduce_scatter_size=_DEFAULT_MNNVL_REDUCE_SCATTER_MAX_SIZE,
145
+ max_mnnvl_multimem_reduce_scatter_size=(
146
+ _DEFAULT_MNNVL_MULTIMEM_REDUCE_SCATTER_MAX_SIZE
147
+ ),
148
symm_mem_enabled=False,
149
) -> None:
150
"""Args:
@@ -153,6 +171,13 @@ class CustomAllreduce:
171
self.mnnvl_lamport_epochs = None
172
self.mnnvl_lamport_ag_epoch_ptr = 0
173
self.mnnvl_lamport_rs_epoch_ptr = 0
174
+ self.mnnvl_multimem_rs_supported = False
175
+ self.mnnvl_multimem_rs_initialized = False
176
+ self.mnnvl_multimem_rs_buffer = None
177
+ self.mnnvl_multimem_rs_handle = None
178
+ self.mnnvl_multimem_rs_buffer_size = 0
179
+ self.mnnvl_multimem_rs_local_ptr = 0
180
+ self.mnnvl_multimem_rs_multicast_ptr = 0
181
self.mnnvl_only = False
182
183
if not custom_ar:
@@ -197,6 +222,9 @@ class CustomAllreduce:
222
# now `device` is a `torch.device` object
223
assert isinstance(device, torch.device)
224
self.device = device
225
+ mnnvl_multimem_rs_supported = _supports_mnnvl_multimem_reduce_scatter(
226
+ device, world_size
227
+ )
228
if not same_node and not _group_can_attempt_mnnvl(group, device):
229
logger.warning(
230
"Custom collectives are disabled because this multi-node "
@@ -294,6 +322,9 @@ class CustomAllreduce:
322
self.max_mnnvl_all_gather_size = max_mnnvl_all_gather_size
323
self.max_reduce_scatter_size = max_reduce_scatter_size
324
self.max_mnnvl_reduce_scatter_size = max_mnnvl_reduce_scatter_size
325
+ self.max_mnnvl_multimem_reduce_scatter_size = (
326
+ max_mnnvl_multimem_reduce_scatter_size
327
+ )
328
self.rank = rank
329
self.world_size = world_size
330
self.fully_connected = fully_connected
@@ -307,6 +338,11 @@ class CustomAllreduce:
338
max_mnnvl_reduce_scatter_size,
339
)
340
)
341
+ if world_size in _MNNVL_MULTIMEM_REDUCE_SCATTER_WORLD_SIZES:
342
+ self.mnnvl_multimem_rs_supported = _all_ranks_true(
343
+ self.group,
344
+ mnnvl_multimem_rs_supported and bool(self.mnnvl_multicast_ptr),
345
+ )
346
if not same_node and not self.mnnvl_multicast_ptr:
347
logger.warning(
348
"Custom collectives are disabled because this multi-node "
@@ -368,6 +404,59 @@ class CustomAllreduce:
404
except RuntimeError as error:
405
logger.debug("MNNVL AG/RS initialization failed: %s", error)
406
407
+ def _init_mnnvl_multimem_reduce_scatter_buffer(self) -> None:
408
+ if self.mnnvl_multimem_rs_initialized:
409
+ return
410
+ self.mnnvl_multimem_rs_initialized = True
411
+ if not self.mnnvl_multimem_rs_supported:
412
+ return
413
+
414
+ assert torch_symm_mem is not None
415
+ buffer = None
416
+ signal_size = ops.meta_size()
417
+ try:
418
+ buffer = torch_symm_mem.empty(
419
+ signal_size + self.max_mnnvl_multimem_reduce_scatter_size,
420
+ dtype=torch.uint8,
421
+ device=self.device,
422
+ )
423
+ except RuntimeError as error:
424
+ logger.debug("MNNVL multimem RS allocation failed: %s", error)
425
+ if not _all_ranks_true(self.group, buffer is not None):
426
+ logger.warning_once(
427
+ "MNNVL multimem reduce-scatter symmetric-memory allocation "
428
+ "failed on at least one rank; falling back to NCCL.",
429
+ scope="global",
430
+ )
431
+ return
432
+ assert buffer is not None
433
+
434
+ handle = None
435
+ try:
436
+ handle = torch_symm_mem.rendezvous(buffer, self.group.group_name)
437
+ if handle is not None:
438
+ buffer[:signal_size].zero_()
439
+ torch.accelerator.synchronize()
440
+ except RuntimeError as error:
441
+ logger.debug("MNNVL multimem RS rendezvous failed: %s", error)
442
+ if not _all_ranks_true(
443
+ self.group,
444
+ handle is not None and bool(handle.multicast_ptr),
445
+ ):
446
+ logger.warning_once(
447
+ "MNNVL multimem reduce-scatter symmetric-memory rendezvous "
448
+ "failed on at least one rank; falling back to NCCL.",
449
+ scope="global",
450
+ )
451
+ return
452
+ assert handle is not None
453
+
454
+ self.mnnvl_multimem_rs_buffer = buffer
455
+ self.mnnvl_multimem_rs_handle = handle
456
+ self.mnnvl_multimem_rs_buffer_size = self.max_mnnvl_multimem_reduce_scatter_size
457
+ self.mnnvl_multimem_rs_local_ptr = buffer.data_ptr() + signal_size
458
+ self.mnnvl_multimem_rs_multicast_ptr = handle.multicast_ptr + signal_size
459
+
460
@contextmanager
461
def capture(self):
462
"""The main responsibility of this context manager is the
@@ -509,35 +598,77 @@ class CustomAllreduce:
598
)
599
return out
600
512
- def should_custom_reduce_scatter(self, inp: torch.Tensor) -> bool:
601
+ def _select_reduce_scatter_backend(
602
+ self, inp: torch.Tensor
603
+ ) -> _ReduceScatterBackend | None:
604
if self.disabled or not current_platform.is_cuda():
514
- return False
605
+ return None
606
if self.world_size == 16 and not self.mnnvl_only:
516
- return False
607
+ return None
608
inp_size = inp.nbytes
609
if inp.dtype not in (torch.float32, torch.float16, torch.bfloat16):
519
- return False
610
+ return None
611
if inp.shape[0] % self.world_size != 0:
521
- return False
612
+ return None
613
output_size = inp_size // self.world_size
523
- max_size = (
524
- self.max_mnnvl_reduce_scatter_size
525
- if self.mnnvl_multicast_ptr
526
- else self.max_reduce_scatter_size
527
- )
528
- return (
529
- 0 < inp_size <= max_size
530
- and output_size % 16 == 0
531
- and is_weak_contiguous(inp)
532
- and (self.fully_connected or bool(self.mnnvl_multicast_ptr))
533
- )
614
+ if inp_size <= 0 or output_size % 16 != 0 or not is_weak_contiguous(inp):
615
+ return None
616
+
617
+ if self.mnnvl_multicast_ptr:
618
+ if inp_size <= self.max_mnnvl_reduce_scatter_size:
619
+ return "mnnvl_lamport"
620
+ if (
621
+ (
622
+ self.mnnvl_multimem_rs_multicast_ptr
623
+ or (
624
+ self.mnnvl_multimem_rs_supported
625
+ and not self.mnnvl_multimem_rs_initialized
626
+ )
627
+ )
628
+ and not envs.VLLM_BATCH_INVARIANT
629
+ and inp_size <= self.max_mnnvl_multimem_reduce_scatter_size
630
+ ):
631
+ return "mnnvl_multimem"
632
+ return None
633
+
634
+ if self.fully_connected and inp_size <= self.max_reduce_scatter_size:
635
+ return "legacy"
636
+ return None
637
+
638
+ def should_custom_reduce_scatter(self, inp: torch.Tensor) -> bool:
639
+ return self._select_reduce_scatter_backend(inp) is not None
640
+
641
+ def should_mnnvl_multimem_reduce_scatter(self, inp: torch.Tensor) -> bool:
642
+ return self._select_reduce_scatter_backend(inp) == "mnnvl_multimem"
643
644
def custom_reduce_scatter(self, inp: torch.Tensor) -> torch.Tensor | None:
536
- if not self.should_custom_reduce_scatter(inp):
645
+ backend = self._select_reduce_scatter_backend(inp)
646
+ if backend is None:
647
return None
648
+ if backend == "mnnvl_multimem" and not self.mnnvl_multimem_rs_multicast_ptr:
649
+ if self._IS_CAPTURING:
650
+ return None
651
+ self._init_mnnvl_multimem_reduce_scatter_buffer()
652
+ if not self.mnnvl_multimem_rs_multicast_ptr:
653
+ return None
654
+
655
out_shape = (inp.shape[0] // self.world_size,) + inp.shape[1:]
656
out = torch.empty(out_shape, dtype=inp.dtype, device=inp.device)
540
- if self.mnnvl_multicast_ptr:
657
+ if backend == "mnnvl_multimem":
658
+ logger.info_once(
659
+ "Using the low-SM MNNVL multimem reduce-scatter kernel.",
660
+ scope="global",
661
+ )
662
+ ops.mnnvl_multimem_reduce_scatter(
663
+ self._ptr,
664
+ inp,
665
+ out,
666
+ self.mnnvl_multimem_rs_local_ptr,
667
+ self.mnnvl_multimem_rs_multicast_ptr,
668
+ self.mnnvl_multimem_rs_buffer_size,
669
+ self._MNNVL_MULTIMEM_REDUCE_SCATTER_BLOCKS,
670
+ )
671
+ elif backend == "mnnvl_lamport":
672
logger.info_once(
673
"Using the MNNVL Lamport reduce-scatter kernel.",
674
scope="global",
@@ -571,6 +702,8 @@ class CustomAllreduce:
702
self.mnnvl_handle = None
703
self.mnnvl_buffer = None
704
self.mnnvl_lamport_epochs = None
705
+ self.mnnvl_multimem_rs_handle = None
706
+ self.mnnvl_multimem_rs_buffer = None
707
708
def __del__(self):
709
self.close()