PyTorch Distributed Training

Distributed training splits a model's training workload across multiple GPUs or multiple machines. It reduces training time from days to hours for large models and datasets. PyTorch provides built-in tools — DataParallel and DistributedDataParallel — that handle the complexity of splitting data, syncing gradients, and aggregating results.

Why Distributed Training Is Needed

Single GPU:
  16GB VRAM, batch_size=32
  Training ResNet50 on ImageNet: ~5 days

4 × GPUs (DataParallel):
  Effective batch_size=128
  Training time: ~30 hours

8 × GPUs across 2 machines (DDP):
  Effective batch_size=256
  Training time: ~10 hours

Scaling compute reduces training time proportionally.

Two Approaches in PyTorch

DataParallel (DP):
  Single machine, multiple GPUs
  Easiest to set up — one line of code
  Less efficient: bottleneck on the master GPU
  Uses one Python process

DistributedDataParallel (DDP):
  Single or multiple machines, multiple GPUs
  More complex setup
  More efficient: each GPU has its own process
  Industry standard for serious training

DataParallel — Quick Multi-GPU

Wrap your model in nn.DataParallel and PyTorch replicates the model on all available GPUs. Each GPU processes a chunk of the batch in parallel, and results are gathered back on GPU 0.

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(1024, 512),
    nn.ReLU(),
    nn.Linear(512, 10)
)

# Wrap for multi-GPU (uses all available GPUs by default)
if torch.cuda.device_count() > 1:
    print(f"Using {torch.cuda.device_count()} GPUs")
    model = nn.DataParallel(model)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model  = model.to(device)

x      = torch.rand(128, 1024).to(device)
output = model(x)
print(output.shape)   # torch.Size([128, 10])

DataParallel — How the Batch Is Split

batch_size = 128, using 4 GPUs:

  GPU 0: processes samples  0– 31   (32 samples)
  GPU 1: processes samples 32– 63   (32 samples)
  GPU 2: processes samples 64– 95   (32 samples)
  GPU 3: processes samples 96–127   (32 samples)
       ↓ all results gathered to GPU 0
  GPU 0: computes loss, backward, optimizer step

Bottleneck: GPU 0 does extra work (gathering + optimizer).

DistributedDataParallel — Production Standard

DDP launches one process per GPU. Each process trains on its own data shard and its own model copy. After every backward pass, DDP uses an all-reduce operation to average gradients across all processes — so all models stay identical. This approach eliminates the GPU 0 bottleneck and scales linearly.

import torch
import torch.nn as nn
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
import os

def train(rank, world_size):
    # Initialize process group
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    dist.init_process_group('nccl', rank=rank, world_size=world_size)

    # Create model on this GPU
    model = nn.Linear(10, 2).to(rank)
    model = DDP(model, device_ids=[rank])   # wrap with DDP

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_fn   = nn.MSELoss()

    for epoch in range(10):
        x = torch.rand(32, 10).to(rank)
        y = torch.rand(32, 2).to(rank)

        optimizer.zero_grad()
        loss = loss_fn(model(x), y)
        loss.backward()          # DDP auto-syncs gradients here
        optimizer.step()

        if rank == 0:            # only print from main process
            print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")

    dist.destroy_process_group()

# Launch with torch.multiprocessing
if __name__ == '__main__':
    world_size = torch.cuda.device_count()
    torch.multiprocessing.spawn(train, args=(world_size,), nprocs=world_size)

DDP Data Splitting with DistributedSampler

Each process must receive a different, non-overlapping subset of the training data. The DistributedSampler handles this automatically.

from torch.utils.data import DataLoader, DistributedSampler

sampler = DistributedSampler(
    dataset,
    num_replicas=world_size,   # total number of processes
    rank=rank                  # this process's rank
)

loader = DataLoader(
    dataset,
    batch_size=32,
    sampler=sampler,   # replaces shuffle=True
    num_workers=4,
    pin_memory=True
)

# At each epoch, reshuffle:
for epoch in range(num_epochs):
    sampler.set_epoch(epoch)   # ensures different shuffle per epoch
    for batch in loader:
        ...   # training step

DDP Gradient Synchronization Diagram

Process 0 (GPU 0):              Process 1 (GPU 1):
  Forward pass                    Forward pass
  Backward → local grads          Backward → local grads
         │                               │
         └─────── All-Reduce ────────────┘
                (average grads across GPUs)
         │                               │
  Optimizer.step()               Optimizer.step()
  (identical update)             (identical update)

Saving and Loading with DDP

# Save only from rank 0 to avoid file conflicts
if rank == 0:
    torch.save(model.module.state_dict(), 'model.pth')
    # model.module gives the underlying model (not the DDP wrapper)

# Load on any rank
model = MyModel()
model.load_state_dict(torch.load('model.pth', map_location=f'cuda:{rank}'))
model = DDP(model, device_ids=[rank])

Launching DDP with torchrun

The simplest way to launch a DDP training script across multiple GPUs is with the torchrun command.

# 4 GPUs on one machine:
torchrun --nproc_per_node=4 train.py

# 2 machines, 4 GPUs each (8 total):
torchrun --nproc_per_node=4 --nnodes=2 --node_rank=0 \
         --master_addr=192.168.1.1 --master_port=12355 train.py

Summary

Distributed training reduces training time by running computation across multiple GPUs or machines in parallel. nn.DataParallel is the quickest multi-GPU setup but bottlenecks on the master GPU. DistributedDataParallel is the production standard — one process per GPU, gradients averaged via all-reduce, scales efficiently. Use DistributedSampler to ensure each process sees a different data shard. Save checkpoints only from rank 0. Launch with torchrun for the simplest multi-GPU and multi-node setup. DDP is the approach used in training every large-scale model in industry today.

Leave a Comment

Your email address will not be published. Required fields are marked *