[Perf][ROCm] Add AITER custom AG/RS (DP only) (#48247)
Signed-off-by: simondanielsson <simon.danielsson99@hotmail.com>
Simon Danielsson committed
Sep 11, 2026 at 07:19 UTC
ae48466cf33b4d7cfbe0f32370c425a3aa9fde61
8 files changed
+552
-77
benchmarks/kernels/benchmark_device_communicators.py
+204
-51
@@ -29,6 +29,10 @@ import torch
29
import torch.distributed as dist
30
from torch.distributed import ProcessGroup
31
32
+from vllm._aiter_ops import rocm_aiter_ops
33
+from vllm.distributed.device_communicators.aiter_custom_all_reduce import (
34
+ AiterCustomAllreduce,
35
+)
36
from vllm.distributed.device_communicators.custom_all_reduce import CustomAllreduce
37
from vllm.distributed.device_communicators.flashinfer_all_reduce import (
38
FlashInferAllReduce,
@@ -42,6 +46,7 @@ from vllm.distributed.device_communicators.pynccl_allocator import (
46
)
47
from vllm.distributed.device_communicators.symm_mem import SymmMemCommunicator
48
from vllm.logger import init_logger
49
+from vllm.platforms import current_platform
50
from vllm.utils.argparse_utils import FlexibleArgumentParser
51
52
logger = init_logger(__name__)
@@ -56,6 +61,30 @@ BENCHMARK_DTYPE = torch.bfloat16
61
# CUDA graph settings
62
CUDA_GRAPH_CAPTURE_CYCLES = 10
63
64
+# All-gather and reduce-scatter are distinct collectives from all-reduce, so
65
+# results are compared within a collective (each against its own NCCL/RCCL
66
+# baseline) rather than across collectives.
67
+COMM_COLLECTIVE = {
68
+ "ca_1stage": "all-reduce",
69
+ "ca_2stage": "all-reduce",
70
+ "pynccl": "all-reduce",
71
+ "pynccl-symm": "all-reduce",
72
+ "symm_mem_multimem": "all-reduce",
73
+ "symm_mem_two_shot": "all-reduce",
74
+ "flashinfer_trtllm": "all-reduce",
75
+ "flashinfer_mnnvl": "all-reduce",
76
+ "aiter_ag": "all-gather",
77
+ "pynccl_ag": "all-gather",
78
+ "aiter_rs": "reduce-scatter",
79
+ "pynccl_rs": "reduce-scatter",
80
+}
81
+COLLECTIVE_ORDER = ["all-reduce", "all-gather", "reduce-scatter"]
82
+COLLECTIVE_BASELINE = {
83
+ "all-reduce": "pynccl",
84
+ "all-gather": "pynccl_ag",
85
+ "reduce-scatter": "pynccl_rs",
86
+}
87
+
88
89
class CommunicatorBenchmark:
90
"""Benchmark class for testing device communicators."""
@@ -77,6 +106,11 @@ class CommunicatorBenchmark:
106
max_seq_len = max(sequence_lengths)
107
max_tensor_elements = max_seq_len * HIDDEN_SIZE
108
self.max_size_override = max_tensor_elements * BENCHMARK_DTYPE.itemsize + 1
109
+ # AITER has effective max size of half the max size, so we double it, and
110
+ # account for AG output size (which is world_size larger)
111
+ self.aiter_max_size = (
112
+ 2 * world_size * max_tensor_elements * BENCHMARK_DTYPE.itemsize + 1
113
+ )
114
115
# Initialize communicators
116
self.custom_allreduce = None
@@ -85,6 +119,7 @@ class CommunicatorBenchmark:
119
self.symm_mem_comm_multimem = None
120
self.symm_mem_comm_two_shot = None
121
self.fi_ar_comm = None
122
+ self.aiter_comm = None
123
124
self._init_communicators()
125
@@ -181,6 +216,27 @@ class CommunicatorBenchmark:
216
)
217
self.fi_ar_comm = None
218
219
+ if current_platform.is_rocm() and rocm_aiter_ops.is_custom_all_reduce_enabled():
220
+ try:
221
+ # Also used for AG/RS for DP
222
+ self.aiter_comm = AiterCustomAllreduce(
223
+ group=self.cpu_group,
224
+ device=self.device,
225
+ max_size=self.aiter_max_size,
226
+ )
227
+ if not self.aiter_comm.disabled:
228
+ logger.info("Rank %s: AITER custom AG/RS initialized", self.rank)
229
+ else:
230
+ logger.info("Rank %s: AITER custom AG/RS disabled", self.rank)
231
+ self.aiter_comm = None
232
+ except Exception as e:
233
+ logger.warning(
234
+ "Rank %s: Failed to initialize AITER custom AG/RS: %s",
235
+ self.rank,
236
+ e,
237
+ )
238
+ self.aiter_comm = None
239
+
240
def benchmark_allreduce(
241
self, sequence_length: int, num_warmup: int, num_trials: int
242
) -> dict[str, float]:
@@ -385,29 +441,116 @@ class CommunicatorBenchmark:
441
f"CUDA graph benchmark failed for communicator: {e}"
442
) from e
443
444
+ def benchmark_ag_rs(
445
+ self, sequence_length: int, num_warmup: int, num_trials: int
446
+ ) -> dict[str, float]:
447
+ """Benchmark all-gather and reduce-scatter: custom (AITER) vs NCCL/RCCL.
448
+
449
+ Each collective is measured for both the AITER custom kernel and the
450
+ PyNccl baseline so they compare like-for-like (print_results groups
451
+ columns by collective). All-gather maps ``(seq, hidden)`` ->
452
+ ``(world_size * seq, hidden)``; reduce-scatter maps ``(seq, hidden)`` ->
453
+ ``(seq // world_size, hidden)`` and so only runs when ``seq`` divides
454
+ evenly across ranks. Both reuse ``benchmark_allreduce_single`` timed on a
455
+ ``(seq, hidden)`` input; baselines write into a pre-allocated buffer.
456
+ """
457
+ results: dict[str, float] = {}
458
+ entries = []
459
+
460
+ # ---- all-gather: (seq, hidden) -> (world_size * seq, hidden) ----
461
+ if self.aiter_comm is not None:
462
+ c = self.aiter_comm
463
+ entries.append(
464
+ (
465
+ "aiter_ag",
466
+ lambda t, c=c: c.custom_all_gather(t, dim=0),
467
+ lambda t, c=c: c.should_custom_ag(t),
468
+ c.capture(),
469
+ )
470
+ )
471
+ if self.pynccl_comm is not None:
472
+ c = self.pynccl_comm
473
+ ag_out = torch.empty(
474
+ sequence_length * self.world_size,
475
+ HIDDEN_SIZE,
476
+ dtype=BENCHMARK_DTYPE,
477
+ device=self.device,
478
+ )
479
+ entries.append(
480
+ (
481
+ "pynccl_ag",
482
+ lambda t, c=c, o=ag_out: c.all_gather(o, t),
483
+ lambda t: True,
484
+ nullcontext(),
485
+ )
486
+ )
487
+
488
+ # ---- reduce-scatter: (seq, hidden) -> (seq // world_size, hidden) ----
489
+ if sequence_length % self.world_size == 0:
490
+ rs_shape = (sequence_length // self.world_size, HIDDEN_SIZE)
491
+ if self.aiter_comm is not None:
492
+ c = self.aiter_comm
493
+ rs_out = torch.empty(
494
+ rs_shape, dtype=BENCHMARK_DTYPE, device=self.device
495
+ )
496
+ entries.append(
497
+ (
498
+ "aiter_rs",
499
+ lambda t, c=c, o=rs_out: c.custom_reduce_scatter(t, o, dim=0),
500
+ lambda t, c=c: c.should_custom_rs(t, dim=0),
501
+ c.capture(),
502
+ )
503
+ )
504
+ if self.pynccl_comm is not None:
505
+ c = self.pynccl_comm
506
+ rs_out = torch.empty(
507
+ rs_shape, dtype=BENCHMARK_DTYPE, device=self.device
508
+ )
509
+ entries.append(
510
+ (
511
+ "pynccl_rs",
512
+ lambda t, c=c, o=rs_out: c.reduce_scatter(o, t),
513
+ lambda t: True,
514
+ nullcontext(),
515
+ )
516
+ )
517
389
-def _calculate_speedup_info(comm_results: dict[str, float]) -> str:
390
- """Calculate speedup information for a single tensor size."""
518
+ for name, fn, should_use_fn, context in entries:
519
+ latency = self.benchmark_allreduce_single(
520
+ sequence_length, fn, should_use_fn, context, num_warmup, num_trials
521
+ )
522
+ if latency is not None:
523
+ results[name] = latency
524
+
525
+ return results
526
+
527
+
528
+def _calculate_speedup_info(comm_results: dict[str, float], baseline: str) -> str:
529
+ """Fastest comm in a collective group and its speedup vs ``baseline``."""
530
if not comm_results:
531
return "N/A"
532
394
- # Find the fastest communicator
533
fastest_comm = min(comm_results.keys(), key=lambda k: comm_results[k])
534
fastest_time = comm_results[fastest_comm]
535
398
- # Calculate speedup vs PyNccl if available
399
- if "pynccl" in comm_results:
400
- pynccl_time = comm_results["pynccl"]
401
- speedup = pynccl_time / fastest_time
536
+ if baseline in comm_results and fastest_time > 0:
537
+ speedup = comm_results[baseline] / fastest_time
538
return f"{fastest_comm} ({speedup:.2f}x)"
403
- else:
404
- return f"{fastest_comm} (N/A)"
539
+ return f"{fastest_comm} (N/A)"
540
541
542
def print_results(
408
- results: dict[str, dict[str, float]], sequence_lengths: list[int], world_size: int
543
+ results: dict[int, dict[str, float]], sequence_lengths: list[int], world_size: int
544
):
410
- """Print benchmark results in a formatted table."""
545
+ """Print benchmark results as one table per collective.
546
+
547
+ All-reduce, all-gather and reduce-scatter are different collectives, so each
548
+ gets its own table and its speedup is computed against that collective's
549
+ NCCL/RCCL baseline rather than across collectives.
550
+ """
551
+ present: set[str] = set()
552
+ for size_results in results.values():
553
+ present.update(size_results.keys())
554
555
print(f"\n{'=' * 130}")
556
print("Device Communicator Benchmark Results")
@@ -415,53 +558,45 @@ def print_results(
558
f"World Size: {world_size}, Data Type: {BENCHMARK_DTYPE}, "
559
f"Hidden Size: {HIDDEN_SIZE}"
560
)
561
+ print("Tensor Shape is the per-rank input; times are ms per operation.")
562
print(f"{'=' * 130}")
563
420
- # Get all communicator names
421
- all_comms = set()
422
- for size_results in results.values():
423
- all_comms.update(size_results.keys())
424
-
425
- all_comms = sorted(list(all_comms))
426
-
427
- # Print header
428
- header = f"{'Tensor Shape':<20}{'Tensor Size':<15}"
429
- for comm in all_comms:
430
- header += f"{comm:<20}"
431
- header += f"{'Best (Speedup vs PyNccl)':<30}"
432
- print(header)
433
- print("-" * len(header))
434
-
435
- # Print results for each sequence length
436
- for seq_len in sequence_lengths:
437
- if seq_len in results:
438
- # Calculate tensor size in elements and bytes
439
- tensor_elements = seq_len * HIDDEN_SIZE
440
- tensor_bytes = tensor_elements * BENCHMARK_DTYPE.itemsize
441
-
442
- # Format tensor size (MB)
443
- tensor_size_mb = tensor_bytes / (1024 * 1024)
444
- tensor_size_str = f"{tensor_size_mb:.2f} MB"
445
-
446
- # Format tensor shape
564
+ for collective in COLLECTIVE_ORDER:
565
+ comms = sorted(c for c in present if COMM_COLLECTIVE.get(c) == collective)
566
+ if not comms:
567
+ continue
568
+ baseline = COLLECTIVE_BASELINE[collective]
569
+
570
+ print(f"\n{collective} (speedup vs {baseline})")
571
+ header = f"{'Tensor Shape':<20}{'Tensor Size':<15}"
572
+ for comm in comms:
573
+ header += f"{comm:<20}"
574
+ header += f"{'Best':<30}"
575
+ print(header)
576
+ print("-" * len(header))
577
+
578
+ for seq_len in sequence_lengths:
579
+ if seq_len not in results:
580
+ continue
581
+ row_results = {
582
+ c: results[seq_len][c] for c in comms if c in results[seq_len]
583
+ }
584
+
585
+ tensor_bytes = seq_len * HIDDEN_SIZE * BENCHMARK_DTYPE.itemsize
586
+ tensor_size_str = f"{tensor_bytes / (1024 * 1024):.2f} MB"
587
tensor_shape = f"({seq_len}, {HIDDEN_SIZE})"
588
589
row = f"{tensor_shape:<20}{tensor_size_str:<15}"
450
- for comm in all_comms:
451
- if comm in results[seq_len]:
452
- row += f"{results[seq_len][comm]:<20.3f}"
590
+ for comm in comms:
591
+ if comm in row_results:
592
+ row += f"{row_results[comm]:<20.3f}"
593
else:
594
row += f"{'N/A':<20}"
455
-
456
- # Calculate speedup information
457
- speedup_info = _calculate_speedup_info(results[seq_len])
458
- row += f"{speedup_info:<30}"
459
-
595
+ row += f"{_calculate_speedup_info(row_results, baseline):<30}"
596
print(row)
597
462
- print(f"{'=' * 130}")
463
- print("All times are in milliseconds (ms) per allreduce operation")
464
- print("Speedup column shows: fastest_algorithm (speedup_vs_pynccl)")
598
+ print(f"\n{'=' * 130}")
599
+ print("All times are in milliseconds (ms) per operation")
600
601
602
def main():
@@ -526,6 +661,13 @@ def main():
661
num_warmup=args.num_warmup,
662
num_trials=args.num_trials,
663
)
664
+ results.update(
665
+ benchmark.benchmark_ag_rs(
666
+ sequence_length=seq_len,
667
+ num_warmup=args.num_warmup,
668
+ num_trials=args.num_trials,
669
+ )
670
+ )
671
672
all_results[seq_len] = results
673
@@ -538,12 +680,23 @@ def main():
680
681
# Save to JSON if requested
682
if args.output_json:
541
- # Add speedup information to results
683
+ # Add per-collective speedup information to results.
684
enhanced_results = {}
685
for seq_len, comm_results in all_results.items():
686
+ speedup_info = {}
687
+ for collective in COLLECTIVE_ORDER:
688
+ group = {
689
+ k: v
690
+ for k, v in comm_results.items()
691
+ if COMM_COLLECTIVE.get(k) == collective
692
+ }
693
+ if group:
694
+ speedup_info[collective] = _calculate_speedup_info(
695
+ group, COLLECTIVE_BASELINE[collective]
696
+ )
697
enhanced_results[seq_len] = {
698
"timings": comm_results,
546
- "speedup_info": _calculate_speedup_info(comm_results),
699
+ "speedup_info": speedup_info,
700
}
701
702
output_data = {
tests/distributed/test_comm_ops.py
+15
@@ -410,6 +410,21 @@ def test_flashinfer_all_reduce_precedes_nccl(monkeypatch: pytest.MonkeyPatch) ->
410
nccl_selector.assert_not_called()
411
412
413
+def test_aiter_all_gather_precedes_pynccl(monkeypatch: pytest.MonkeyPatch) -> None:
414
+ """Ensure can use aiter all_gather if available even if pynccl is not available."""
415
+ output = torch.empty(2)
416
+ aiter_comm = Mock()
417
+ aiter_comm.should_custom_ag.return_value = True
418
+ aiter_comm.custom_all_gather.return_value = output
419
+ communicator = CudaCommunicator.__new__(CudaCommunicator)
420
+ communicator.world_size = 2
421
+ communicator.aiter_ar_comm = aiter_comm
422
+ communicator.pynccl_comm = Mock(disabled=True)
423
+ monkeypatch.setattr(communicator, "_can_use_aiter_ag_rs", Mock(return_value=True))
424
+
425
+ assert communicator.all_gatherv(torch.empty(1)) is output
426
+
427
+
428
def test_isend_object_posts_size_then_object_and_releases_on_wait(
429
monkeypatch: pytest.MonkeyPatch,
430
) -> None:
tests/distributed/test_rocm_aiter_custom_ar.py
+171
-1
@@ -8,7 +8,7 @@ import torch.distributed as dist
8
9
from vllm._aiter_ops import is_aiter_found, rocm_aiter_ops
10
from vllm.distributed.communication_op import tensor_model_parallel_all_reduce # noqa
11
-from vllm.distributed.parallel_state import get_tp_group, graph_capture
11
+from vllm.distributed.parallel_state import get_dp_group, get_tp_group, graph_capture
12
from vllm.envs import disable_envs_cache
13
from vllm.platforms import current_platform
14
@@ -132,3 +132,173 @@ def test_rocm_aiter_custom_allreduce(
132
test_target,
133
):
134
multi_process_parallel(monkeypatch, tp_size, pipeline_parallel_size, test_target)
135
+
136
+
137
+def _get_aiter_ag_rs_comm():
138
+ device_communicator = get_dp_group().device_communicator
139
+ assert device_communicator.use_aiter_ag_rs, (
140
+ "AITER custom AG/RS was not enabled on the DP group."
141
+ )
142
+ aiter_comm = device_communicator.aiter_ar_comm
143
+ assert aiter_comm is not None, "AITER custom AG/RS was not initialized."
144
+ assert not aiter_comm.disabled, "AITER custom AG/RS is disabled."
145
+ return aiter_comm
146
+
147
+
148
+def _assert_aiter_handles_ag(aiter_comm, inp: torch.Tensor) -> None:
149
+ assert aiter_comm.should_custom_ag(inp), (
150
+ f"AITER custom all-gather does not support input shape {inp.shape}."
151
+ )
152
+
153
+
154
+def _assert_aiter_handles_rs(aiter_comm, inp: torch.Tensor) -> None:
155
+ assert aiter_comm.should_custom_rs(inp, dim=0), (
156
+ f"AITER custom reduce-scatter does not support input shape {inp.shape}."
157
+ )
158
+
159
+
160
+@ray.remote(num_gpus=1, max_calls=1)
161
+def eager_ag_rs(
162
+ monkeypatch: pytest.MonkeyPatch,
163
+ tp_size,
164
+ pp_size,
165
+ rank,
166
+ distributed_init_port,
167
+ data_parallel_size,
168
+ data_parallel_master_port,
169
+) -> None:
170
+ with monkeypatch.context() as m:
171
+ _configure_aiter_custom_ar_env(m)
172
+
173
+ device = torch.device(f"cuda:{rank}")
174
+ torch.accelerator.set_device_index(device)
175
+ init_test_distributed_environment(
176
+ tp_size,
177
+ pp_size,
178
+ rank,
179
+ distributed_init_port,
180
+ data_parallel_size=data_parallel_size,
181
+ data_parallel_master_port=data_parallel_master_port,
182
+ )
183
+
184
+ dp_group = get_dp_group()
185
+ group = dp_group.device_group
186
+ dp_world = dp_group.world_size
187
+ aiter_comm = _get_aiter_ag_rs_comm()
188
+
189
+ for shape, dtype in test_cases:
190
+ num_tokens, hidden = shape
191
+
192
+ # all-gather: each rank contributes (num_tokens, hidden).
193
+ inp = torch.ones(shape, dtype=dtype, device=device) * (rank + 1)
194
+ _assert_aiter_handles_ag(aiter_comm, inp)
195
+ expected = torch.empty(
196
+ (num_tokens * dp_world, hidden), dtype=dtype, device=device
197
+ )
198
+ dist.all_gather_into_tensor(expected, inp, group=group)
199
+ out = aiter_comm.custom_all_gather(inp, dim=0)
200
+ assert out is not None
201
+ torch.testing.assert_close(out, expected)
202
+
203
+ # reduce-scatter: each rank contributes (num_tokens * dp, hidden).
204
+ rs_in = torch.ones(
205
+ (num_tokens * dp_world, hidden), dtype=dtype, device=device
206
+ ) * (rank + 1)
207
+ _assert_aiter_handles_rs(aiter_comm, rs_in)
208
+ rs_expected = torch.empty((num_tokens, hidden), dtype=dtype, device=device)
209
+ dist.reduce_scatter_tensor(rs_expected, rs_in, group=group)
210
+ rs_out = torch.empty((num_tokens, hidden), dtype=dtype, device=device)
211
+ aiter_comm.custom_reduce_scatter(rs_in, rs_out, dim=0)
212
+ torch.testing.assert_close(rs_out, rs_expected)
213
+
214
+
215
+@ray.remote(num_gpus=1, max_calls=1)
216
+def graph_ag_rs(
217
+ monkeypatch: pytest.MonkeyPatch,
218
+ tp_size,
219
+ pp_size,
220
+ rank,
221
+ distributed_init_port,
222
+ data_parallel_size,
223
+ data_parallel_master_port,
224
+) -> None:
225
+ with monkeypatch.context() as m:
226
+ _configure_aiter_custom_ar_env(m)
227
+
228
+ device = torch.device(f"cuda:{rank}")
229
+ torch.accelerator.set_device_index(device)
230
+ init_test_distributed_environment(
231
+ tp_size,
232
+ pp_size,
233
+ rank,
234
+ distributed_init_port,
235
+ data_parallel_size=data_parallel_size,
236
+ data_parallel_master_port=data_parallel_master_port,
237
+ )
238
+
239
+ dp_group = get_dp_group()
240
+ group = dp_group.device_group
241
+ dp_world = dp_group.world_size
242
+ aiter_comm = _get_aiter_ag_rs_comm()
243
+
244
+ # Warmup so DP comms is initialized before graph capture
245
+ data = torch.zeros(1, device=device)
246
+ dist.all_reduce(data, group=group)
247
+ torch.accelerator.synchronize()
248
+ del data
249
+
250
+ for shape, dtype in test_cases:
251
+ num_tokens, hidden = shape
252
+
253
+ # all-gather under graph capture.
254
+ inp = torch.ones(shape, dtype=dtype, device=device) * (rank + 1)
255
+ _assert_aiter_handles_ag(aiter_comm, inp)
256
+ ag_expected = torch.empty(
257
+ (num_tokens * dp_world, hidden), dtype=dtype, device=device
258
+ )
259
+ dist.all_gather_into_tensor(ag_expected, inp, group=group)
260
+ with graph_capture(device=device) as graph_capture_context:
261
+ torch.accelerator.synchronize()
262
+ graph = torch.cuda.CUDAGraph()
263
+ with torch.cuda.graph(graph, stream=graph_capture_context.stream):
264
+ ag_out = aiter_comm.custom_all_gather(inp, dim=0)
265
+ graph.replay()
266
+ torch.testing.assert_close(ag_out, ag_expected)
267
+
268
+ # reduce-scatter under graph capture.
269
+ rs_in = torch.ones(
270
+ (num_tokens * dp_world, hidden), dtype=dtype, device=device
271
+ ) * (rank + 1)
272
+ _assert_aiter_handles_rs(aiter_comm, rs_in)
273
+ rs_expected = torch.empty((num_tokens, hidden), dtype=dtype, device=device)
274
+ dist.reduce_scatter_tensor(rs_expected, rs_in, group=group)
275
+ rs_out = torch.empty((num_tokens, hidden), dtype=dtype, device=device)
276
+ with graph_capture(device=device) as graph_capture_context:
277
+ torch.accelerator.synchronize()
278
+ graph = torch.cuda.CUDAGraph()
279
+ with torch.cuda.graph(graph, stream=graph_capture_context.stream):
280
+ aiter_comm.custom_reduce_scatter(rs_in, rs_out, dim=0)
281
+ graph.replay()
282
+ torch.testing.assert_close(rs_out, rs_expected)
283
+
284
+
285
+@pytest.mark.skipif(not is_aiter_found(), reason="AITER is not installed")
286
+@multi_gpu_test(num_gpus=2)
287
+@pytest.mark.parametrize("tp_size", [1])
288
+@pytest.mark.parametrize("pipeline_parallel_size", [1])
289
+@pytest.mark.parametrize("data_parallel_size", [2])
290
+@pytest.mark.parametrize("test_target", [eager_ag_rs, graph_ag_rs])
291
+def test_rocm_aiter_custom_ag_rs(
292
+ monkeypatch: pytest.MonkeyPatch,
293
+ tp_size,
294
+ pipeline_parallel_size,
295
+ data_parallel_size,
296
+ test_target,
297
+):
298
+ multi_process_parallel(
299
+ monkeypatch,
300
+ tp_size,
301
+ pipeline_parallel_size,
302
+ test_target,
303
+ data_parallel_size=data_parallel_size,
304
+ )
tests/utils.py
+52
-10
@@ -1416,6 +1416,8 @@ def init_test_distributed_environment(
1416
rank: int,
1417
distributed_init_port: str,
1418
local_rank: int = -1,
1419
+ data_parallel_size: int = 1,
1420
+ data_parallel_master_port: int | None = None,
1421
) -> None:
1422
# Note: This function is often called from Ray worker processes, so we
1423
# can't rely on pytest fixtures to set the config. We check if the config
@@ -1428,6 +1430,29 @@ def init_test_distributed_environment(
1430
1431
distributed_init_method = f"tcp://localhost:{distributed_init_port}"
1432
1433
+ if data_parallel_size > 1:
1434
+ # For DP we need to set a common DP master port
1435
+ from vllm.config.parallel import ParallelConfig
1436
+
1437
+ assert data_parallel_master_port is not None, (
1438
+ "data_parallel_master_port is required when data_parallel_size > 1"
1439
+ )
1440
+ tp_pp_world = tp_size * pp_size
1441
+ parallel_config = ParallelConfig(
1442
+ data_parallel_size=data_parallel_size,
1443
+ data_parallel_rank=rank // tp_pp_world,
1444
+ _data_parallel_master_port_list=[int(data_parallel_master_port)],
1445
+ )
1446
+ with set_current_vllm_config(VllmConfig(parallel_config=parallel_config)):
1447
+ init_distributed_environment(
1448
+ world_size=tp_pp_world,
1449
+ rank=rank % tp_pp_world,
1450
+ distributed_init_method=distributed_init_method,
1451
+ local_rank=local_rank if local_rank >= 0 else rank,
1452
+ )
1453
+ ensure_model_parallel_initialized(tp_size, pp_size)
1454
+ return
1455
+
1456
if get_current_vllm_config_or_none() is not None:
1457
# Config already set, use it directly
1458
init_distributed_environment(
@@ -1454,6 +1479,7 @@ def multi_process_parallel(
1479
tp_size: int,
1480
pp_size: int,
1481
test_target: Any,
1482
+ data_parallel_size: int = 1,
1483
) -> None:
1484
import ray
1485
@@ -1474,18 +1500,34 @@ def multi_process_parallel(
1500
)
1501
1502
distributed_init_port = get_open_port()
1503
+ # Separate port for the DP master group; only used when data_parallel_size > 1.
1504
+ data_parallel_master_port = get_open_port() if data_parallel_size > 1 else None
1505
+ world_size = data_parallel_size * tp_size * pp_size
1506
try:
1507
refs = []
1479
- for rank in range(tp_size * pp_size):
1480
- refs.append(
1481
- test_target.remote(
1482
- monkeypatch,
1483
- tp_size,
1484
- pp_size,
1485
- rank,
1486
- distributed_init_port,
1487
- ),
1488
- )
1508
+ for rank in range(world_size):
1509
+ if data_parallel_size > 1:
1510
+ refs.append(
1511
+ test_target.remote(
1512
+ monkeypatch,
1513
+ tp_size,
1514
+ pp_size,
1515
+ rank,
1516
+ distributed_init_port,
1517
+ data_parallel_size,
1518
+ data_parallel_master_port,
1519
+ ),
1520
+ )
1521
+ else:
1522
+ refs.append(
1523
+ test_target.remote(
1524
+ monkeypatch,
1525
+ tp_size,
1526
+ pp_size,
1527
+ rank,
1528
+ distributed_init_port,
1529
+ ),
1530
+ )
1531
ray.get(refs)
1532
finally:
1533
ray.shutdown()
vllm/distributed/device_communicators/aiter_custom_all_reduce.py
+14
@@ -56,6 +56,20 @@ class AiterCustomAllreduce:
56
def custom_all_reduce(self, inp: torch.Tensor) -> torch.Tensor | None:
57
return self._impl.custom_all_reduce(inp)
58
59
+ def should_custom_ag(self, inp: torch.Tensor) -> bool:
60
+ return self._impl.should_custom_ag(inp)
61
+
62
+ def should_custom_rs(self, inp: torch.Tensor, dim: int) -> bool:
63
+ return self._impl.should_custom_rs(inp, dim)
64
+
65
+ def custom_all_gather(self, inp: torch.Tensor, dim: int = 0) -> torch.Tensor | None:
66
+ return self._impl.custom_all_gather(inp, dim=dim)
67
+
68
+ def custom_reduce_scatter(
69
+ self, inp: torch.Tensor, out: torch.Tensor, dim: int = 0
70
+ ) -> torch.Tensor | None:
71
+ return self._impl.custom_reduce_scatter(inp, out, dim=dim)
72
+
73
def use_1stage_fused_ar_rms(self, inp: torch.Tensor) -> bool:
74
"""Whether AITER's fused allreduce+RMSNorm runs as its one-stage kernel.
75
vllm/distributed/device_communicators/cuda_communicator.py
+83
-4
@@ -109,6 +109,7 @@ class CudaCommunicator(DeviceCommunicatorBase):
109
self.fi_ar_comm: FlashInferAllReduce | None = None
110
self.fi_pcie_ipc_ar_comm: FlashInferPcieIpcAllReduce | None = None
111
self.aiter_ar_comm: AiterCustomAllreduce | None = None
112
+ self.use_aiter_ag_rs: bool = False
113
114
if use_torch_symm_mem and current_platform.is_cuda():
115
self.symm_mem_comm = SymmMemCommunicator(
@@ -149,6 +150,22 @@ class CudaCommunicator(DeviceCommunicatorBase):
150
),
151
)
152
153
+ # AITER custom all-gather/reduce-scatter DP-attention dispatch/combine
154
+ if (
155
+ "dp" in unique_name
156
+ and self.world_size in (2, 4, 8)
157
+ and current_platform.is_rocm()
158
+ and rocm_aiter_ops.is_custom_all_reduce_enabled()
159
+ ):
160
+ self.aiter_ar_comm = AiterCustomAllreduce(
161
+ group=self.cpu_group,
162
+ device=self.device,
163
+ )
164
+ if self.aiter_ar_comm.disabled:
165
+ self.aiter_ar_comm = None
166
+ else:
167
+ self.use_aiter_ag_rs = True
168
+
169
if use_custom_allreduce and self.world_size > 1 and current_platform.is_rocm():
170
# Initialize a custom quick all-reduce implementation for AMD.
171
# Quick reduce is designed as a complement to custom allreduce
@@ -285,7 +302,11 @@ class CudaCommunicator(DeviceCommunicatorBase):
302
enabled_ar_backends.append("NCCL_SYMM_MEM")
303
if self.qr_comm is not None and not self.qr_comm.disabled:
304
enabled_ar_backends.append("QUICK_REDUCE")
288
- if self.aiter_ar_comm is not None and not self.aiter_ar_comm.disabled:
305
+ if (
306
+ self.use_aiter_allreduce
307
+ and self.aiter_ar_comm is not None
308
+ and not self.aiter_ar_comm.disabled
309
+ ):
310
enabled_ar_backends.append("AITER_CUSTOM")
311
if self.ca_comm is not None and not self.ca_comm.disabled:
312
enabled_ar_backends.append("CUSTOM")
@@ -340,7 +361,8 @@ class CudaCommunicator(DeviceCommunicatorBase):
361
return out
362
aiter_ar_comm = self.aiter_ar_comm
363
if (
343
- aiter_ar_comm is not None
364
+ self.use_aiter_allreduce
365
+ and aiter_ar_comm is not None
366
and not aiter_ar_comm.disabled
367
and aiter_ar_comm.should_custom_ar(input_)
368
):
@@ -462,6 +484,11 @@ class CudaCommunicator(DeviceCommunicatorBase):
484
# Convert negative dim to positive.
485
dim += input_.dim()
486
487
+ # 'sizes' is not needed if all inputs in the same group have the same
488
+ # shape
489
+ if sizes is not None and all(s == sizes[0] for s in sizes):
490
+ sizes = None
491
+
492
# Note: This will produce an incorrect answer if we don't make
493
# the input_tensor contiguous. Possible bug in reduce_scatter_tensor?
494
input_tensor = input_.movedim(0, dim).contiguous()
@@ -475,6 +502,16 @@ class CudaCommunicator(DeviceCommunicatorBase):
502
chunk_size = input_tensor.shape[0] // world_size
503
output_shape = (chunk_size,) + input_tensor.shape[1:]
504
505
+ if self._can_use_aiter_ag_rs(sizes):
506
+ aiter_comm = self.aiter_ar_comm
507
+ assert aiter_comm is not None
508
+ if aiter_comm.should_custom_rs(input_tensor, dim=0):
509
+ output = torch.empty(
510
+ output_shape, dtype=input_tensor.dtype, device=input_tensor.device
511
+ )
512
+ aiter_comm.custom_reduce_scatter(input_tensor, output, dim=0)
513
+ return output.movedim(0, dim).contiguous()
514
+
515
# Symmetric memory is only used when all ranks have uniform sizes.
516
# ncclCommWindowRegister is collective: asymmetric pool allocations
517
# from variable per-rank sizes cause deadlocks.
@@ -632,6 +669,34 @@ class CudaCommunicator(DeviceCommunicatorBase):
669
self.all2all_manager.destroy()
670
self.all2all_manager = None # type: ignore[assignment]
671
672
+ def _can_use_aiter_ag_rs(self, sizes: list[int] | None) -> bool:
673
+ """Whether the AITER custom AG/RS fast path may run for this collective.
674
+
675
+ Requires:
676
+ - uniform batches
677
+ - FULL CUDAgraphs
678
+ """
679
+ if (
680
+ not self.use_aiter_ag_rs
681
+ or self.aiter_ar_comm is None
682
+ or self.aiter_ar_comm.disabled
683
+ ):
684
+ return False
685
+ if sizes is not None:
686
+ return False
687
+
688
+ from vllm.config.compilation import CUDAGraphMode
689
+ from vllm.forward_context import get_forward_context
690
+
691
+ try:
692
+ ctx = get_forward_context()
693
+ except AssertionError:
694
+ return False
695
+ if ctx.cudagraph_runtime_mode != CUDAGraphMode.FULL:
696
+ return False
697
+ bd = ctx.batch_descriptor
698
+ return bd is not None and bd.uniform
699
+
700
def suspend(self) -> None:
701
if self.pynccl_comm is not None:
702
self.pynccl_comm.suspend()
@@ -665,14 +730,28 @@ class CudaCommunicator(DeviceCommunicatorBase):
730
if dim != 0:
731
raise NotImplementedError("only dim 0 all-gatherv is supported")
732
world_size = self.world_size
668
- pynccl_comm = self.pynccl_comm
669
- assert pynccl_comm is not None and not pynccl_comm.disabled
733
734
# 'sizes' is not needed if all inputs in the same group have the same
735
# shape
736
if sizes is not None and all(s == sizes[0] for s in sizes):
737
sizes = None
738
739
+ if self._can_use_aiter_ag_rs(sizes):
740
+ aiter_comm = self.aiter_ar_comm
741
+ assert aiter_comm is not None
742
+ if isinstance(input_, torch.Tensor):
743
+ if aiter_comm.should_custom_ag(input_):
744
+ out = aiter_comm.custom_all_gather(input_, dim=0)
745
+ if out is not None:
746
+ return out
747
+ elif all(aiter_comm.should_custom_ag(inp) for inp in input_):
748
+ outs = [aiter_comm.custom_all_gather(inp, dim=0) for inp in input_]
749
+ if all(o is not None for o in outs):
750
+ return outs
751
+
752
+ pynccl_comm = self.pynccl_comm
753
+ assert pynccl_comm is not None and not pynccl_comm.disabled
754
+
755
# Symmetric memory is only used when all ranks have uniform sizes.
756
# ncclCommWindowRegister is collective: asymmetric pool allocations
757
# from variable per-rank sizes cause deadlocks.
vllm/distributed/parallel_state.py
+12
-10
@@ -673,11 +673,11 @@ class GroupCoordinator:
673
else:
674
stream = graph_capture_context.stream
675
676
- # only cuda uses this function,
676
+ # only cuda/rocm uses this function,
677
# so we don't abstract it into the base class
678
maybe_ca_context = nullcontext()
679
maybe_fi_pcie_ipc_context: AbstractContextManager[Any] = nullcontext()
680
- maybe_aiter_context = nullcontext()
680
+ maybe_aiter_ar_context = nullcontext()
681
from vllm.distributed.device_communicators.cuda_communicator import (
682
CudaCommunicator,
683
)
@@ -698,12 +698,10 @@ class GroupCoordinator:
698
if fi_pcie_ipc_ar_comm is not None:
699
maybe_fi_pcie_ipc_context = fi_pcie_ipc_ar_comm.capture()
700
701
- from vllm._aiter_ops import rocm_aiter_ops
702
-
703
- if rocm_aiter_ops.is_enabled():
704
- aiter_ar = rocm_aiter_ops.get_aiter_allreduce()
705
- if aiter_ar is not None:
706
- maybe_aiter_context = aiter_ar.capture() # type: ignore
701
+ # Capture each group's own comm. A global lookup would double-capture
702
+ aiter_ar_comm = getattr(self.device_communicator, "aiter_ar_comm", None)
703
+ if aiter_ar_comm is not None:
704
+ maybe_aiter_ar_context = aiter_ar_comm.capture() # type: ignore
705
706
# ensure all initialization operations complete before attempting to
707
# capture the graph on another stream
@@ -715,7 +713,7 @@ class GroupCoordinator:
713
torch.cuda.stream(stream),
714
maybe_ca_context,
715
maybe_fi_pcie_ipc_context,
718
- maybe_aiter_context,
716
+ maybe_aiter_ar_context,
717
):
718
yield graph_capture_context
719
@@ -1640,7 +1638,11 @@ def graph_capture(
1638
context = graph_capture_context or GraphCaptureContext(
1639
torch.cuda.Stream(device=device)
1640
)
1643
- with get_tp_group().graph_capture(context), get_pp_group().graph_capture(context):
1641
+ with (
1642
+ get_tp_group().graph_capture(context),
1643
+ get_pp_group().graph_capture(context),
1644
+ get_dp_group().graph_capture(context),
1645
+ ):
1646
yield context
1647
1648
vllm/envs.py
+1
-1
@@ -1261,7 +1261,7 @@ environment_variables: dict[str, Callable[[], Any]] = {
1261
os.getenv("VLLM_ROCM_USE_AITER", "False").lower() in ("true", "1")
1262
),
1263
# Use AITER's CustomAllreduce as the custom-allreduce backend inside vLLM's
1264
- # CudaCommunicator on ROCm.
1264
+ # CudaCommunicator on ROCm. Also enables AITER AG/RS for DP communication.
1265
"VLLM_ROCM_USE_AITER_CUSTOM_AR": lambda: (
1266
os.getenv("VLLM_ROCM_USE_AITER_CUSTOM_AR", "True").lower() in ("true", "1")
1267
),