Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

235 lines
8.6 KiB
Markdown

# Distributed training with PyTorch DDP on mirage
This tutorial trains a small MLP with PyTorch
[`DistributedDataParallel`](https://pytorch.org/docs/stable/notes/ddp.html)
(DDP) across **multiple emulated MI350X GPUs**, launched with
[`torchrun`](https://pytorch.org/docs/stable/elastic/run.html). No
physical GPU is required — [`rocjitsu`](architecture.md) emulates the
devices, so the same launch command you would use on a real 8-GPU box
runs on a laptop.
By the end you will have run:
```sh
mirage run --profile mi350x --gpus-per-node 2 \
-- torchrun --standalone --nproc_per_node=2 ddp_mlp.py
```
and watched two ranks all-reduce their gradients and converge to
byte-identical weights.
The ready-to-run pieces live in the repo:
| File | Purpose |
|------|---------|
| [`tests/fixtures/ml/ddp_mlp.py`](../tests/fixtures/ml/ddp_mlp.py) | the DDP training workload (one process per rank) |
| [`tests/run_ddp_mlp_mi350.sh`](../tests/run_ddp_mlp_mi350.sh) | end-to-end runner: sets up a venv and launches the demo |
## TL;DR
```sh
cd emulation/mirage
./tests/run_ddp_mlp_mi350.sh # first run installs a venv; prints ddp_mlp_ok
SKIP_INSTALL=1 ./tests/run_ddp_mlp_mi350.sh # reuse the venv on later runs
```
A successful run ends with:
```text
[rank 0] loss: 30.5666 -> 0.2634 over 50 steps
[rank 0] all ranks converged with identical replicas
ddp_mlp_ok
==> PASS: DDP MLP trained on 2 emulated mi350x GPUs via torchrun
```
## How the pieces fit together
```mermaid
flowchart LR
subgraph host["mirage host (1 emulated node)"]
tr["torchrun --nproc_per_node=2"]
subgraph r0["rank 0"]
p0["python ddp_mlp.py"] --> g0["emulated GPU 0"]
end
subgraph r1["rank 1"]
p1["python ddp_mlp.py"] --> g1["emulated GPU 1"]
end
tr --> p0
tr --> p1
p0 <-- "all-reduce grads (RCCL)" --> p1
end
```
Three things make this work:
1. **Multiple emulated GPUs.** `mirage run --gpus-per-node N` tells
rocjitsu to synthesize `N` KFD device nodes, so inside the workload
`torch.cuda.device_count() == N`. Every rank must pin itself to a
*distinct* device — RCCL refuses to run if two ranks land on the same
GPU (`Duplicate GPU detected`). The fixture picks
`rank % torch.cuda.device_count()`, so each rank owns its own GPU.
2. **`torchrun` rendezvous works out of the box.** mirage exports the
standard `torch.distributed` variables `MASTER_ADDR` and
`MASTER_PORT` on every node (aliasing mirage's own
`MIRAGE_HEAD_ADDR` / `MIRAGE_HEAD_PORT`). For a single node,
`torchrun --standalone` picks its own loopback rendezvous; for
multi-node you can point `torchrun --rdzv-endpoint` at
`$MASTER_ADDR:$MASTER_PORT`.
3. **The daemon emulator (default).** mirage runs the emulator as a
separate daemon process by default, so the rank processes share GPU
memory through it — which is what lets RCCL set up its transports
across ranks. Pass `mirage run --in-process` to instead give every
process its own in-process emulator (no shared GPU memory; multi-GPU
RCCL cannot work in that mode).
## Step by step
### 1. Set up a venv
The demo uses PyTorch and the ROCm SDK (which ships rocjitsu) from the
`gfx950-dcgpu` (MI350) nightly index. The runner script does this for
you, but to do it by hand:
```sh
cd emulation/mirage
python3 -m venv .venv-mi350
source .venv-mi350/bin/activate
pip install --index-url https://rocm.nightlies.amd.com/v2/gfx950-dcgpu/ \
"rocm[libraries,devel]" torch numpy
rocm-sdk init # unpacks librocjitsu.so + configs into the venv
```
`rocm-sdk init` is required: the `devel` package ships its contents
compressed, and rocjitsu's KMD interposer only appears under
`site-packages/_rocm_sdk_devel` after this step. mirage auto-detects
that library from the active venv — no `LD_LIBRARY_PATH` wiring needed.
### 2. Launch the training
From the workspace (so mirage's own rocjitsu discovery works), run:
```sh
cargo run --quiet -- run \
--profile mi350x \
--gpus-per-node 2 \
-- .venv-mi350/bin/torchrun --standalone --nproc_per_node=2 \
tests/fixtures/ml/ddp_mlp.py
```
The first `--` ends `cargo run`'s arguments; the second ends mirage's,
so everything after it is the workload `torchrun` launches once per
rank. mirage already runs the shared-memory daemon emulator by default
(pass `--in-process` to opt out).
### 3. Read the output
Each rank logs the loss it started and ended with, and rank 0 confirms
that every replica converged to identical weights before printing
`ddp_mlp_ok`. The fixture fails loudly (non-zero exit) if the loss does
not drop or any rank's weights diverge.
## What the workload does
[`ddp_mlp.py`](../tests/fixtures/ml/ddp_mlp.py) is a standard,
self-contained DDP program:
1. Reads `RANK`, `WORLD_SIZE`, `LOCAL_RANK` (set by `torchrun`) and
`MASTER_ADDR` / `MASTER_PORT` (set by mirage).
2. Pins to GPU `LOCAL_RANK` and joins the process group.
3. Builds an identical MLP on every rank, wraps it in
`DistributedDataParallel` (which broadcasts the initial weights and
all-reduces gradients every backward pass).
4. Trains on a per-rank shard of a fixed synthetic regression task, so
the gradients genuinely differ and the all-reduce matters.
5. Verifies the loss dropped **and** that an `all_gather` of the final
weight checksum is identical on every rank — proof that DDP kept the
replicas in lock-step.
## Tuning the run
The runner script and fixture honor these environment variables:
| Variable | Default | Meaning |
|----------|---------|---------|
| `NPROC` | `2` | GPUs / ranks per node (`torchrun --nproc_per_node`, `mirage --gpus-per-node`). |
| `STEPS` | `50` | Optimizer steps. |
| `PROFILE` | `mi350x` | mirage profile / emulated GPU. |
| `VENV` | `.venv-mi350` | venv location. |
| `SKIP_INSTALL` | unset | set to `1` to reuse an already-populated venv. |
Examples:
```sh
# 4 emulated GPUs, 100 steps
NPROC=4 STEPS=100 SKIP_INSTALL=1 ./tests/run_ddp_mlp_mi350.sh
```
## Multi-node DDP (no launcher)
mirage emulates each *node* as its own rank process and exports the full
set of `torch.distributed` `env://` variables — `RANK`, `WORLD_SIZE`,
`LOCAL_RANK`, `MASTER_ADDR`, `MASTER_PORT` — on every node. That means
you can run the workload **directly**, with no `torchrun` launcher: each
node runs `python ddp_mlp.py` once and rendezvouses through `env://`.
```sh
mirage run --profile mi350x --num-nodes 2 --gpus-per-node 2 \
--env NCCL_P2P_DISABLE=1 --env NCCL_SHM_DISABLE=1 --env NCCL_SOCKET_IFNAME=lo \
-- .venv-mi350/bin/python3 tests/fixtures/ml/ddp_mlp.py
```
- `--num-nodes 2` makes `WORLD_SIZE == 2` and runs the script twice, once
per rank, with distinct `RANK`s.
- `--gpus-per-node 2` exposes two GPUs so each rank can pin to a distinct
device (`rank % device_count`).
- The `NCCL_*` variables force RCCL onto its loopback socket transport,
which is what the daemon emulator supports.
### Quick connectivity check
Before the full training run, a minimal smoke test
([`tests/fixtures/ml/dist_smoke.py`](../tests/fixtures/ml/dist_smoke.py))
does a single `all_reduce` and prints `dist_smoke_ok`. It is heavily
logged with timestamps so any stall is easy to localize:
```sh
mirage run --profile mi350x --num-nodes 2 --gpus-per-node 2 \
--env NCCL_P2P_DISABLE=1 --env NCCL_SHM_DISABLE=1 --env NCCL_SOCKET_IFNAME=lo \
-- .venv-mi350/bin/python3 tests/fixtures/ml/dist_smoke.py
```
A successful run shows each rank on its own device and the reduced sum:
```text
[rank 0] current device = 0 (cuda:0)
[rank 1] current device = 1 (cuda:1)
[rank 0] post all_reduce: tensor = 3.0
dist_smoke_ok
```
## Troubleshooting
- **`torch.cuda.is_available()` is False.** The venv is missing the ROCm
runtime or `rocm-sdk init` was not run. Re-run the install step in the
active venv.
- **A rank SIGSEGV/SIGABRTs immediately with `--nproc_per_node > 1`.**
You are likely in `--in-process` mode, where each rank has its own
emulator and cannot share GPU memory. Drop `--in-process` so the
default daemon emulator is used.
- **`Duplicate GPU detected` / `ncclInvalidUsage` at the first
collective.** Two ranks pinned to the same emulated GPU. Give the
session at least as many GPUs as ranks (`--gpus-per-node N`) so each
rank can own a distinct device (the fixtures use
`rank % torch.cuda.device_count()`).
- **Multi-GPU run hangs / times out after a few steps.** Known
limitation: RCCL initializes and a single collective completes (the
`dist_smoke.py` smoke test passes), but DDP's repeated, bucketed
collectives stall on a later one over the daemon emulator. Single-rank
runs train end to end.
- **`KMD preload library not found`.** You are running an installed
`mirage` from `PATH` instead of the workspace build. Run via
`cargo run --` from `emulation/mirage` (see
[building.md](building.md)).