Distributed Training Research¶
When you'd use this
DDP, FSDP, model sharding, pipeline parallelism and multi-GPU strategies.
Train large models across multiple GPUs/nodes — data and model parallelism — when one device isn't enough.
Why distribute training?¶
A core question explored in Distributed Training: Why distribute training.
| Scenario | Solution |
|---|---|
| Data doesn't fit in GPU memory | Gradient accumulation, data parallelism |
| Model doesn't fit on one GPU | Model parallelism, FSDP |
| Training takes too long | Data parallelism across multiple GPUs |
| Model + optimizer state too large | ZeRO / FSDP sharding |
Data Parallelism (DDP)¶
Replicate the model on each GPU, split the batch, and sync gradients — the default way to scale training.
The simplest form: same model on each GPU, different data batches, synchronized gradients.
import torch
import torch.nn as nn
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler
import os
def setup(rank, world_size):
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "12355"
dist.init_process_group("nccl", rank=rank, world_size=world_size)
torch.cuda.set_device(rank)
def cleanup():
dist.destroy_process_group()
def train(rank, world_size):
setup(rank, world_size)
# Model on this GPU
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10),
).to(rank)
model = DDP(model, device_ids=[rank])
# Distributed sampler ensures each GPU gets different data
dataset = load_dataset()
sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
dataloader = DataLoader(dataset, batch_size=64, sampler=sampler)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
for epoch in range(10):
sampler.set_epoch(epoch) # shuffle differently each epoch
for x, y in dataloader:
x, y = x.to(rank), y.to(rank)
optimizer.zero_grad()
output = model(x)
loss = criterion(output, y)
loss.backward() # gradients auto-synchronized by DDP!
optimizer.step()
if rank == 0:
print(f"Epoch {epoch}: loss={loss.item():.4f}")
cleanup()
# Launch
import torch.multiprocessing as mp
world_size = torch.cuda.device_count()
mp.spawn(train, args=(world_size,), nprocs=world_size)
How DDP works internally:¶
GPU 0: forward → loss → backward → [all-reduce gradients] → optimizer.step()
GPU 1: forward → loss → backward → [all-reduce gradients] → optimizer.step()
GPU 2: forward → loss → backward → [all-reduce gradients] → optimizer.step()
All GPUs end up with identical model parameters after each step.
FSDP (Fully Sharded Data Parallel)¶
Shard model params/optimizer state across GPUs so models too big for one device fit.
For models too large for one GPU — shards model parameters, gradients AND optimizer states:
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy
def train_fsdp(rank, world_size):
setup(rank, world_size)
model = LargeModel().to(rank)
# Wrap with FSDP — parameters are sharded across GPUs
model = FSDP(
model,
sharding_strategy=ShardingStrategy.FULL_SHARD,
device_id=rank,
)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
for batch in dataloader:
optimizer.zero_grad()
output = model(batch["input_ids"].to(rank))
loss = output.loss
loss.backward()
optimizer.step()
cleanup()
FSDP memory savings:¶
Model: 7B parameters (28 GB in fp32)
Optimizer (Adam): 2x model = 56 GB
Gradients: 28 GB
Total per GPU without FSDP: 112 GB (impossible on 80GB A100!)
With FSDP (8 GPUs): 112 / 8 = 14 GB per GPU ✓
Gradient accumulation (simulating larger batches)¶
Accumulate gradients over several mini-batches to emulate a big batch on small memory.
accumulation_steps = 4 # effective batch = 4 * batch_size
optimizer.zero_grad()
for i, (x, y) in enumerate(dataloader):
output = model(x)
loss = criterion(output, y) / accumulation_steps # normalize
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
Mixed precision training¶
Use fp16/bf16 for speed and memory while keeping fp32 where precision matters.
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for x, y in dataloader:
optimizer.zero_grad()
# Forward pass in float16 (2x faster, half memory)
with autocast():
output = model(x.to(device))
loss = criterion(output, y.to(device))
# Backward with scaled gradients (avoid underflow in fp16)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
Pipeline parallelism¶
Split model layers across devices and stream micro-batches through them.
Split model layers across GPUs — micro-batches flow through the pipeline:
# GPipe-style pipeline
# GPU 0: layers 0-11
# GPU 1: layers 12-23
# GPU 2: layers 24-35
# GPU 3: layers 36-47
# While GPU 0 processes micro-batch 2,
# GPU 1 processes micro-batch 1 (from GPU 0's output)
# → Reduces idle time (bubble)
Checkpointing large models¶
Save/restore sharded state so long training runs survive interruptions.
import torch.distributed.checkpoint as dcp
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import StateDictType
# Save (each rank saves its shard)
with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT):
state = {"model": model.state_dict(), "optimizer": optimizer.state_dict()}
dcp.save(state, checkpoint_id="checkpoints/epoch_5")
# Load
with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT):
state = {"model": model.state_dict(), "optimizer": optimizer.state_dict()}
dcp.load(state, checkpoint_id="checkpoints/epoch_5")
model.load_state_dict(state["model"])
optimizer.load_state_dict(state["optimizer"])
Launch scripts¶
How to start multi-GPU/multi-node runs with torchrun and friends.
# torchrun (recommended)
torchrun --nproc_per_node=4 train.py
# Multi-node
torchrun --nproc_per_node=4 --nnodes=2 --node_rank=0 \
--master_addr=192.168.1.1 --master_port=12355 train.py
Practice Exercises¶
- Convert a single-GPU training script to use DDP with 2+ GPUs.
- Implement gradient accumulation and verify it matches a larger batch size.
- Use mixed precision and measure the speedup and memory savings.
- Wrap a model with FSDP and verify memory is sharded across GPUs.
- Implement checkpointing that saves/resumes training across crashes.
- Benchmark DDP scaling: 1 GPU vs 2 vs 4 — measure throughput (samples/sec).
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.