The usual answer to a slow training run is “just throw more GPUs at it.” Without profiling your distributed training data transfer first, that mostly buys you idle silicon. I have seen more than one 8-GPU node lose to a well-tuned single-GPU setup because nobody looked at NCCL overhead or the PCIe bus.
Train something large like a Vision Transformer (ViT) and the communication-to-compute ratio gets ugly. The GPUs spend more time synchronizing gradients than they spend calculating weights, so the hardware topology, NVLink or PCIe, is the real ceiling on your performance.
The bottleneck: PCIe versus NVLink
Across multiple GPUs, everything moves through the NVIDIA Collective Communications Library (NCCL). On an AWS g6e instance with L40S cards, that traffic often crosses the PCIe bus or even CPU shared memory. A p4d instance with A100s gets dedicated NVLink interconnects instead.
Run nvidia-smi topo -m to see what you actually have. “NODE” or “SYS” in that matrix, rather than “NV#” or “PIX,” means latency you cannot code your way out of. It also means the software side has to work a lot harder to keep the cards fed.
What default DDP costs you
Most people wrap the model in DistributedDataParallel (DDP) and ship it. The defaults are wasteful. DDP makes device-to-device (DtoD) memory copies after gradient reduction, which is work you do not need.
# The standard approach that most people use (and shouldn't)
model = DDP(model, device_ids=[rank])
On a single GPU, DDP costs roughly 3-7% overhead, and that grows once you scale out. If your graph is static, meaning no conditional logic decides which parameters get gradients, you are also paying for dynamic graph tracking you never use.
Tuning the distributed training data transfer
Two things to do: move less memory around, and compress what does go across the wire. This is how I set up a production DDP wrapper so the distributed training data transfer keeps up with the compute.
import torch.distributed.algorithms.ddp_comm_hooks.powerSGD_hook as powerSGD
def bbioon_configure_ddp(model, rank):
# Enable static graph to skip unused parameter checks
# Use gradient_as_bucket_view to eliminate DtoD copies
model = DDP(model,
device_ids=[rank],
static_graph=True,
gradient_as_bucket_view=True,
bucket_cap_mb=100) # Tuned for overlap
# The secret sauce: PowerSGD Gradient Compression
state = powerSGD.PowerSGDState(process_group=None)
model.register_comm_hook(state, powerSGD.powerSGD_hook)
return model
gradient_as_bucket_view=True points the gradients straight at the NCCL buffers, so the reduction happens in place with no copy-back step at the end. On a badly bottlenecked PCIe setup, PowerSGD compression shrinks the payload enough to lift throughput by more than 5X.
I have written before about how specialist model performance rests on these infrastructure choices. And if you are not measuring GPU utilization with NVIDIA Nsight Systems (nsys), you are guessing.
Overlapping the reduction with compute
Bucket capacity is the other trap. DDP may group every gradient into one huge bucket, which means the distributed training data transfer cannot start until all gradients have been computed. Shrink bucket_cap_mb and you get smaller transfers that overlap with the computation of the later layers.
If this is eating your billable hours, I can take it on. I have been working on WordPress, high-performance APIs and AI infrastructure since the 4.x days.
What to check first
- Profile first: Run
nsys profileand see whetherncclAllReducecalls dominate your backward pass. - Hardware first: On PCIe you must compress gradients, with BF16 or PowerSGD.
- Defaults: Set
static_graph=Trueandgradient_as_bucket_view=Trueunless the architecture really is dynamic. - Overlap: Tune the bucket size so communication happens while the next layer is still calculating.
For benchmarking this kind of workload, we track performance across different environments with the WP-Bench AI standard.