Debug slow DDP training and rank stragglers¶
Use this guide when PyTorch DDP training is slow, uneven, or controlled by one culprit rank.
TraceML looks for visible rank skew in distributed runs, identifies a culprit rank and a victim/reference rank, then attributes material culprit excess to input, H2D, DDP forward compute, or sync/unattributed work.
For whole-run triage, start with Find why PyTorch training is slow. This page stays focused on DDP rank skew.
Run DDP with TraceML¶
For single-node DDP:
traceml run train.py --nproc-per-node=4
For multi-node DDP, use the same --run-name, --nnodes,
--nproc-per-node, --master-addr, --master-port, and script args on every
node. See
Distributed Training.
On Slurm, use the Slurm guide instead of writing the node flags by hand.
Reproduce rank stragglers¶
The repo includes a small DDP demo with a compute-heavy baseline and two rank-local straggler modes.
Balanced baseline:
traceml run examples/distributed/ddp_rank_straggler_demo.py --mode=summary --nproc-per-node=2 --run-name ddp_balanced --args --scenario balanced
Input straggler:
traceml run examples/distributed/ddp_rank_straggler_demo.py --mode=summary --nproc-per-node=2 --run-name ddp_input_straggler --args --scenario input-straggler --straggler-rank 0 --input-sleep-ms 200
Compute straggler:
traceml run examples/distributed/ddp_rank_straggler_demo.py --mode=summary --nproc-per-node=2 --run-name ddp_compute_straggler --args --scenario compute-straggler --straggler-rank 0 --compute-extra-matmuls 8
For two nodes with one GPU each, run the same script on both nodes with
--nnodes=2 --nproc-per-node=1, the same --master-addr,
--master-port, and --run-name, and a different --node-rank on each node.
Example node 0:
traceml run examples/distributed/ddp_rank_straggler_demo.py --mode=summary --nnodes=2 --nproc-per-node=1 --node-rank=0 --master-addr <NODE_0_PRIVATE_IP> --master-port 29546 --run-name ddp_compute_straggler --args --scenario compute-straggler --straggler-rank 0 --compute-extra-matmuls 8
Example node 1:
traceml run examples/distributed/ddp_rank_straggler_demo.py --mode=summary --nnodes=2 --nproc-per-node=1 --node-rank=1 --master-addr <NODE_0_PRIVATE_IP> --master-port 29546 --run-name ddp_compute_straggler --args --scenario compute-straggler --straggler-rank 0 --compute-extra-matmuls 8
Demo fingerprints¶
These fingerprints came from the DDP demo on two nodes with one GPU per node.
| Scenario | Diagnosis | Key signal |
|---|---|---|
| Balanced | COMPUTE-BOUND |
Step Time 124.6/124.6ms, Input Wait 1.4/1.4ms, compute 122.4/122.4ms |
| Input straggler | INPUT STRAGGLER |
r0 input wait 201.6ms vs r1 1.4ms |
| Compute straggler | COMPUTE STRAGGLER |
r0 optimizer 33.1ms vs r1 14.5ms |
Read the balanced run as the control: both ranks have similar Step Time, Input Wait is small, and the run is mostly compute.
Read the input-straggler run as a rank-local input issue: rank 0 reaches compute late because its input path is slower.
Read the compute-straggler run as a rank-local compute issue: rank 0 spends more time in optimizer work than the peer rank.
What to look for¶
Start with the Step Time diagnosis.
| Diagnosis | Meaning |
|---|---|
INPUT STRAGGLER |
one rank spends much longer waiting for input than peer ranks |
COMPUTE STRAGGLER |
in DDP, the culprit rank has materially more forward-time burden than the victim rank |
H2D STRAGGLER |
the culprit rank has meaningfully more host-to-device transfer burden than the victim rank |
STRAGGLER |
visible rank skew exists, but input wait, H2D, and DDP forward do not explain it |
INPUT-BOUND |
input work is broad, not just one bad rank |
RESIDUAL-HEAVY |
meaningful residual time is not attributed to input wait, H2D, forward, backward, or optimizer work |
For rank stragglers, TraceML first looks for visible wait cost: in DDP this is the gap between the upper-median backward time and the rank with the smallest backward time. The smallest-backward rank is the likely culprit because it arrived late and waited least. TraceML then compares that culprit with the victim rank to see whether input wait, H2D, or DDP forward time explains the cost.
Then inspect:
- culprit rank
- victim/reference rank
- visible rank skew
Input WaitForwardBackwardOptimizer StepResidual
How to triage the culprit rank¶
If the diagnosis is INPUT STRAGGLER, inspect input loading on the culprit
rank:
- uneven shards or batch construction
- rank-local preprocessing jitter
- slow storage path on one host
- custom collation or tokenization on one rank
If the diagnosis is COMPUTE STRAGGLER, inspect DDP forward work on the
culprit rank:
- uneven input shapes
- rank-local branching
- extra forward work
- framework hooks or callbacks that run on one rank
If the diagnosis is H2D STRAGGLER, inspect host-to-device transfer on the
culprit rank: batch shapes, transfer placement, pinned memory, and transfer
jitter.
If the diagnosis is STRAGGLER, inspect synchronization, collectives, and
unattributed work around the culprit rank. Explicit collective timing is not
available yet, so this is the honest fallback when input, H2D, and DDP forward
do not explain the visible wait cost.
If the diagnosis is RESIDUAL-HEAVY, remember that TraceML reports residual
time as a derived bucket. It is not direct NCCL or all-reduce timing. Inspect logging,
checkpointing, validation, CPU stalls, framework orchestration, and unobserved
transfer paths before assuming the cause.
FSDP note¶
TraceML supports single-node multi-GPU DDP and FSDP in the current public docs. Multi-node DDP summary reports are supported. Multi-node FSDP uses the same distributed launch path as DDP, but should be validated in your environment.
TraceML uses backward as the visible rank-skew phase for DDP/default strategy.
For FSDP, it uses forward + backward because sharding communication can appear
in both phases. FSDP rank stragglers can still be attributed to input wait or
H2D, but TraceML does not emit COMPUTE STRAGGLER from the FSDP rank-skew
rule without explicit all-gather or reduce-scatter timing.
Compare a fix¶
After changing data loading, batching, model logic, or host placement, compare the old and new summaries:
traceml compare old_run/final_summary.json new_run/final_summary.json
Look for changes in common-clock Step Time, visible rank skew, Input Wait, compute time, residual time, and diagnosis.