Skip to content

Chapter 8: NCCL -- The Application Layer of AI Networking · Part 5 of 9

Act 4 -- nccl-tests: the fabric validation benchmark

Building the model…

Ring AllReduce across eight ranks, step by step. Brightness shows how many ranks have contributed to each chunk.

Every AI cluster commissioning process runs nccl-tests before handover to production. It is the standard benchmark suite that validates the full stack: NCCL configuration, NIC drivers, RDMA transport, switch fabric, and topology -- simultaneously.

Reading nccl-tests output — click any column header to understand it

This is allreduce_perf output for a 32-node (256 GPU) BasePOD cluster at 400G

size (B)time (µs)algbw (GB/s)busbw (GB/s) ◀#wrong
8388608412.520.338.10
16777216743.722.642.40
1342177284891.227.451.40
5368709129814.254.7102.60
107374182417234.862.3116.80
8589934592109842.178.2146.60
Bus bandwidth (GB/s) — THE number that matters

algbw × 2(N-1)/N. This corrects for the ring algorithm's communication volume. Each DGX H100 node has 8 × 400G NICs (~400 GB/s aggregate per node). The busbw in a 32-node / 256-GPU test reflects per-node aggregate throughput across all 8 NICs. Compare busbw to 8 × 50 GB/s = 400 GB/s per node max. A healthy 32-node cluster shows 130–160 GB/s busbw at large tensor sizes (33–40% of theoretical max, expected due to ring overhead and fabric hops).

102.6 GB/s busbw on a 32-node BasePOD ≈ 26% of 400 GB/s per-node NIC capacity. At 512 MB messages, fabric is the limit. At 8 GB messages, 146 GB/s busbw approaches ~37% — healthy for this cluster size.
Small tensors (<1MB)

Latency-dominated. Low busbw is normal.

Large tensors (>128MB)

Bandwidth-dominated. Compare busbw to NIC line rate.

#wrong > 0

STOP. Data corruption. Do not run training.

Running allreduce_perf

# On DGX node A -- run across all nodes in the cluster
mpirun --hostfile /etc/mpi/hostfile \
  -np 256 \
  --map-by ppr:8:node \
  -x NCCL_IB_HCA=mlx5_0:1,mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1 \
  -x NCCL_DEBUG=INFO \
  -x NCCL_IB_GID_INDEX=3 \
  ./build/all_reduce_perf \
    -b 8 \           # Start at 8 bytes
    -e 8G \          # End at 8 GB
    -f 2 \           # Double message size each step
    -g 1 \           # 1 GPU per process
    -c 1             # Check results for correctness

Flags explained:

  • -b 8 -e 8G -f 2: Sweeps from 8 bytes to 8 GB, doubling each step -- reveals performance across all tensor sizes
  • -g 1: Each MPI process owns 1 GPU
  • -c 1: Enables the #wrong correctness check -- critical, must be zero

Before you believe any number: pin the clocks

A busbw figure that moves by ten percent between runs is not telling you about the fabric. It is telling you the GPUs boosted differently.

Pin the application clocks before the first run and unpin them afterwards:

# Pin memory and SM clocks (values are per-generation; these are H100)
sudo nvidia-smi -ac 2619,1980
# Applications clocks set to (MEM 2619, SM 1980)

# ...run the benchmark...

# Return to the default boost behaviour
sudo nvidia-smi -rac

Without this, run-to-run variance is thermal and power behaviour, and it will be attributed to the network -- because the network is what everyone is looking at. Pin the clocks, and any remaining variance is real.

Two related preconditions, for the same reason:

  • Run on a whole node. Under Slurm, --exclusive. A busbw number measured while another job shares the node is not a fabric measurement.
  • Health-check the node first. Run the node's own health gate (Chapter 3, Act 1) before benchmarking it. Benchmarking a node with an open hardware alert wastes the run and, worse, produces a number somebody will quote later.

Isolating the network from everything else: the loopback test

Here is the test that is missing from most runbooks, and it needs exactly one node.

all_reduce_perf on a single node normally never touches the NIC -- NCCL routes the traffic over NVLink or shared memory because those are faster. That is the right behaviour for a real job and the wrong behaviour for a diagnosis, because it means a single-node result tells you nothing about the network stack.

You can force the traffic out of the NIC and back:

srun -N 1 --exclusive \
  --container-image=nvcr.io/nvidia/pytorch:24.10-py3 \
  bash -c '
    export NCCL_P2P_DISABLE=1      # no peer-to-peer over PCIe
    export NCCL_SHM_DISABLE=1      # no shared-memory transport
    export NCCL_NVLS_ENABLE=0      # no NVLink SHARP
    export NCCL_MNNVL_ENABLE=0     # no multi-node NVLink
    export UCX_TLS=tcp
    export NCCL_DEBUG=INFO
    ./build/all_reduce_perf -b 8 -e 8G -f 2 -g 8
  '

With every faster path disabled, NCCL has nowhere to go but the NIC. On an HDR (200 Gb/s) fabric a healthy node returns roughly 175 Gbps on this test.

What that buys you is a clean bisection:

Loopback resultMulti-node resultConclusion
HealthyHealthyNot a network problem -- look at the job
HealthyPoorThe fabric between nodes -- switch, cable, congestion, routing
PoorPoorThe node itself -- NIC, driver, firmware, PCIe, GID configuration
Loopback resultHealthy
Multi-node resultHealthy
ConclusionNot a network problem -- look at the job
Loopback resultHealthy
Multi-node resultPoor
ConclusionThe fabric between nodes -- switch, cable, congestion, routing
Loopback resultPoor
Multi-node resultPoor
ConclusionThe node itself -- NIC, driver, firmware, PCIe, GID configuration

That third row is the valuable one. It tells you to stop opening switch counters and start on the host, using one node, with no switch access and no maintenance window. It is the difference between an afternoon of fabric archaeology and a ten-minute answer.

A note on containers

Every example here launches through Slurm's container integration, which arranges host networking and IPC for you. If you run nccl-tests under plain Docker instead, you have to arrange it yourself:

docker run --rm --net=host --uts=host --ipc=host --pid=host \
  --device=/dev/infiniband --cap-add=IPC_LOCK \
  --gpus all <image> ...

Omitting --net=host or --ipc=host is a common and confusing failure: NCCL initialises, the job runs, and the numbers are terrible -- because the container's network namespace hid the RDMA devices and NCCL quietly fell back to TCP. The NCCL_DEBUG=INFO output will name the transport it chose. Read it before blaming the fabric.

Reading the output table -- field by field

#                                                              out-of-place
#       size         count    type    redop    root     time   algbw   busbw  #wrong
      8388608       2097152   float     sum      -1    412.5    20.3    38.1      0
     16777216       4194304   float     sum      -1    743.7    22.6    42.4      0
    134217728      33554432   float     sum      -1   4891.2    27.4    51.4      0
    536870912     134217728   float     sum      -1   9814.2    54.7   102.6      0
   1073741824     268435456   float     sum      -1  17234.8    62.3   116.8      0
   8589934592    2147483648   float     sum      -1 109842.1    78.2   146.6      0

size: Message size in bytes (this is what one GPU is contributing -- the total AllReduce processes N x this).

time: Elapsed microseconds for the operation. Includes all N GPUs completing the full AllReduce.

algbw (algorithm bandwidth): size / time. Raw throughput -- bytes moved per second per GPU.

busbw (bus bandwidth): algbw x 2(N-1)/N. This is the number that matters. It corrects algbw by the mathematical efficiency of the ring algorithm, giving you the effective utilisation of the NIC bandwidth. At large N, busbw ~= 2 x algbw. This is what you compare against line rate.

#wrong: The number of gradient values that did not match after the AllReduce. This must always be zero. A non-zero value means data corruption -- training would produce silently wrong results. Causes: ECC error, RDMA memory corruption, fabric bit-flip. Any non-zero value requires immediate investigation before the cluster is used for training.

What "expected busbw" means for different scales

The theoretical maximum busbw equals the NIC line rate (400 Gb/s = 50 GB/s per NIC). In practice, protocol overhead, algorithm efficiency, and multiple NICs sharing a path reduce this. Healthy targets:

Cluster sizeTopologyExpected busbw (large tensors)Alarm threshold
8 GPUs, 1 nodeNVLink only800–880 GB/s (NVLink)< 600 GB/s
32 GPUs, 4 nodes2-stage leaf-spine350–390 GB/s< 280 GB/s
256 GPUs, 32 nodes (BasePOD)2-stage leaf-spine130–160 GB/s< 100 GB/s
256 GPUs, SuperPOD3-stage280–340 GB/s< 200 GB/s
2048 GPUs, large cluster5-stage200–280 GB/s< 150 GB/s
Cluster size8 GPUs, 1 node
TopologyNVLink only
Expected busbw (large tensors)800–880 GB/s (NVLink)
Alarm threshold< 600 GB/s
Cluster size32 GPUs, 4 nodes
Topology2-stage leaf-spine
Expected busbw (large tensors)350–390 GB/s
Alarm threshold< 280 GB/s
Cluster size256 GPUs, 32 nodes (BasePOD)
Topology2-stage leaf-spine
Expected busbw (large tensors)130–160 GB/s
Alarm threshold< 100 GB/s
Cluster size256 GPUs, SuperPOD
Topology3-stage
Expected busbw (large tensors)280–340 GB/s
Alarm threshold< 200 GB/s
Cluster size2048 GPUs, large cluster
Topology5-stage
Expected busbw (large tensors)200–280 GB/s
Alarm threshold< 150 GB/s

Why busbw decreases with cluster size: More GPUs means more hops, more protocol overhead, and more QP management. A 256-GPU SuperPOD will always show lower busbw than a 32-GPU BasePOD -- this is expected and does not indicate a problem.

The small-message / large-message distinction

The output table covers 8 bytes through 8 GB. These are not equivalent:

  • Small tensors (< 1 MB): NCCL uses LL (Low Latency) protocol. Latency dominates. busbw is low -- not because the fabric is slow, but because the per-message overhead is large relative to the message. This is normal.
  • Large tensors (> 128 MB): NCCL uses SIMPLE protocol. Bandwidth dominates. This is where you compare against line rate expectations.

When an ML engineer says "training is slow," first ask: what size are the gradient tensors_2 If the model produces many small gradients (small layer sizes), you may be in the latency-dominated regime and load balancing / hop count improvements will not help as much as algorithmic changes.


Read aloudPage 5