Skip to content

Commit 5f107ce

Browse files
authored
feat: add intranode examples (#31)
Signed-off-by: chang-ning <spiderpower02@gmail.com>
1 parent 7b9fd3e commit 5f107ce

7 files changed

Lines changed: 179 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ to when you pressed `r`, so the trace spans exactly your record window.
6666
Use `rdmatop` to monitor RDMA traffic while running GPU
6767
communication benchmarks:
6868

69+
- [PyTorch](examples/pytorch/) — intranode NVLink/XGMI traffic
6970
- [IB Perftest](examples/ib/) — two-node `ib_write_bw` benchmark
7071
- [UCX Perftest](examples/ucx/) — two-node `ucx_perftest` bandwidth / latency
7172
- [NCCL](examples/nccl/) — collective communication

examples/pytorch/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PyTorch Collectives
2+
3+
Small `torch.distributed` loops that drive sustained traffic over the
4+
intranode GPU interconnect — **XGMI** on AMD (RCCL) or **NVLink** on
5+
NVIDIA (NCCL). Run one in a shell and watch the `amdgpu<N>` /
6+
`nvidia<N>` rows in `rdmatop` from another.
7+
8+
Collectives are symmetric: every GPU talks to every peer, so all
9+
interconnect rows light up with roughly equal TX/RX, and the per-peer
10+
detail pane shows traffic on every link.
11+
12+
| Script | Collective | TX per GPU per iteration |
13+
|--------------------|-------------------------|-----------------------------|
14+
| `allgather.py` | `all_gather` | `(world-1) * buffer` |
15+
| `allreduce.py` | `all_reduce` | `~2*(world-1)/world * buffer` |
16+
| `alltoall.py` | `all_to_all_single` | `(world-1)/world * buffer` |
17+
| `reducescatter.py` | `reduce_scatter_tensor` | `(world-1)/world * buffer` |
18+
19+
## Prerequisites
20+
21+
- 2+ GPUs on one node with PyTorch matching your GPU stack:
22+
```bash
23+
# AMD (ROCm)
24+
pip install torch --index-url https://download.pytorch.org/whl/rocm6.4
25+
# NVIDIA (CUDA)
26+
pip install torch
27+
```
28+
- `rdmatop` built with the matching feature to see the GPU rows:
29+
```bash
30+
cargo install --path . --features xgmi # AMD
31+
cargo install --path . --features nvlink # NVIDIA
32+
```
33+
34+
## Usage
35+
36+
```bash
37+
cd examples/pytorch
38+
torchrun --nproc_per_node=8 allgather.py # 60s, 256 MiB/rank
39+
torchrun --nproc_per_node=8 allreduce.py --mb 512 --secs 300
40+
torchrun --nproc_per_node=4 alltoall.py # only 4 GPUs
41+
torchrun --nproc_per_node=8 reducescatter.py
42+
```
43+
44+
Rank 0 prints an estimated per-GPU transmit rate each batch
45+
(`~N Gbps TX/GPU`, from the ring-algorithm model in the table above).
46+
Compare it against the per-row TX in `rdmatop`.
47+
48+
## Notes
49+
50+
- The printed rate is a ring-algorithm estimate; RCCL/NCCL may pick
51+
other algorithms, so treat it as a sanity reference. Ground truth on
52+
AMD is `amd-smi xgmi` accumulator deltas, which read the same
53+
counters `rdmatop` samples.
54+
- The `nccl` backend name is historical — on ROCm builds of PyTorch it
55+
is backed by RCCL, no code changes needed.
56+
57+
## Related Links
58+
59+
- [torch.distributed](https://pytorch.org/docs/stable/distributed.html)
60+
— collective API used by the scripts
61+
- [rccl](https://github.com/ROCm/rccl) — AMD collective library
62+
- [nccl](https://github.com/NVIDIA/nccl) — NVIDIA collective library

examples/pytorch/allgather.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import torch
2+
import torch.distributed as dist
3+
4+
import common
5+
6+
7+
def make(world, n):
8+
x = torch.randn(n, dtype=torch.float16, device="cuda")
9+
out = [torch.empty_like(x) for _ in range(world)]
10+
# ring all-gather: each GPU transmits (world-1) shards per iteration
11+
tx = x.numel() * x.element_size() * (world - 1)
12+
return lambda: dist.all_gather(out, x), tx
13+
14+
15+
if __name__ == "__main__":
16+
common.run("all_gather", make)

examples/pytorch/allreduce.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import torch
2+
import torch.distributed as dist
3+
4+
import common
5+
6+
7+
def make(world, n):
8+
x = torch.randn(n, dtype=torch.float16, device="cuda")
9+
# ring all-reduce: each GPU transmits ~2*(world-1)/world of its buffer
10+
tx = int(x.numel() * x.element_size() * 2 * (world - 1) / world)
11+
return lambda: dist.all_reduce(x), tx
12+
13+
14+
if __name__ == "__main__":
15+
common.run("all_reduce", make)

examples/pytorch/alltoall.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import torch
2+
import torch.distributed as dist
3+
4+
import common
5+
6+
7+
def make(world, n):
8+
# all_to_all_single sends one equal slice to each rank, so the buffer
9+
# is built as `world` slices of n//world elements.
10+
slice_len = n // world
11+
x = torch.randn(slice_len * world, dtype=torch.float16, device="cuda")
12+
out = torch.empty_like(x)
13+
# each GPU transmits (world-1) slices per iteration (own slice stays)
14+
tx = slice_len * x.element_size() * (world - 1)
15+
return lambda: dist.all_to_all_single(out, x), tx
16+
17+
18+
if __name__ == "__main__":
19+
common.run("all_to_all", make)

examples/pytorch/common.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Shared harness for the intranode collective benchmarks.
2+
3+
Each benchmark defines its collective and a per-iteration TX-bytes model;
4+
run() handles setup, warmup, the timed loop, and rank-0 rate reporting.
5+
"""
6+
7+
import argparse
8+
import time
9+
10+
import torch
11+
import torch.distributed as dist
12+
13+
14+
def run(name, make):
15+
p = argparse.ArgumentParser(description=f"{name} loop for rdmatop monitoring")
16+
p.add_argument("--mb", type=int, default=256, help="per-rank buffer size in MiB")
17+
p.add_argument("--secs", type=float, default=60, help="run duration in seconds")
18+
args = p.parse_args()
19+
20+
dist.init_process_group("nccl") # NCCL on CUDA, RCCL on ROCm
21+
rank = dist.get_rank()
22+
world = dist.get_world_size()
23+
torch.cuda.set_device(rank % torch.cuda.device_count())
24+
25+
n = args.mb * (1 << 20) // 2 # fp16 elements per rank
26+
step, tx_bytes_per_iter = make(world, n)
27+
28+
for _ in range(3): # warmup
29+
step()
30+
torch.cuda.synchronize()
31+
32+
t0 = time.time()
33+
iters = 0
34+
while time.time() - t0 < args.secs:
35+
for _ in range(10):
36+
step()
37+
iters += 1
38+
torch.cuda.synchronize()
39+
if rank == 0:
40+
elapsed = time.time() - t0
41+
gbps = iters * tx_bytes_per_iter * 8 / 1e9 / elapsed
42+
print(
43+
f"{elapsed:6.1f}s {iters:5d} iters ~{gbps:7.1f} Gbps TX/GPU",
44+
flush=True,
45+
)
46+
47+
dist.destroy_process_group()

examples/pytorch/reducescatter.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import torch
2+
import torch.distributed as dist
3+
4+
import common
5+
6+
7+
def make(world, n):
8+
# reduce_scatter_tensor reduces the input into one shard per rank, so
9+
# the buffer is built as `world` shards of n//world elements.
10+
shard_len = n // world
11+
x = torch.randn(shard_len * world, dtype=torch.float16, device="cuda")
12+
out = torch.empty(shard_len, dtype=torch.float16, device="cuda")
13+
# ring reduce-scatter: each GPU transmits (world-1) shards per iteration
14+
tx = shard_len * x.element_size() * (world - 1)
15+
return lambda: dist.reduce_scatter_tensor(out, x), tx
16+
17+
18+
if __name__ == "__main__":
19+
common.run("reduce_scatter", make)

0 commit comments

Comments
 (0)