LLM + RoboticsModule 08 · Stage 06

Distributed training & systems ML

When a model and its optimiser no longer fit on one GPU, you split them up: copies of the data, slices of each matrix, groups of layers, shards of the optimiser state. This stage is about choosing those splits, the communication each one costs, and how to find out where the time actually goes.

PrerequisitesTraining loop internals, mixed precision, Stage 01, GPipe (Day 24)
Time3–4 weeks part-time (about 40–60 h)
HardwareLaptop CPU for the collectives exercise · free 2-GPU notebook (Kaggle) for real NCCL · a 4-GPU node for the 7B version
Key papersMegatron-LM (Shoeybi et al., 2019) · ZeRO (Rajbhandari et al., 2020) · 3D parallelism (Narayanan et al., 2021) · FSDP (Zhao et al., 2023)

Source: Suraj Sharma, X post (stage 6) · roadmap hub: Module 08

Start here (no maths needed)

Big AI models are too large for one computer chip, so we cut the work into pieces and give each piece to a different chip. The chips then have to keep talking to each other.

Think of it like this

Imagine your class has to copy out a 1,000-page book by hand. One person would take forever. You could give everyone a different chapter. Or, for a giant table, one person writes the left columns and another the right columns. Or you form an assembly line: one person writes, the next checks, the next binds. Every plan saves time, but now people must pass pages around and wait for each other. If passing pages is slow, the whole class waits. Distributed training is choosing the plan where the most people are writing and the fewest are waiting.

How it works

  1. Same model, different data: every chip has a full copy and learns from different examples. They share what they learned after each step.
  2. Split each layer: one big multiplication is cut into slices that run on several chips at once.
  3. Split the layers: chip 1 runs the first layers, chip 2 the next ones, like an assembly line.
  4. Don't store things twice: each chip keeps only its share of the model's notes, and borrows the rest when it needs it.
  5. Measure the waiting: time how long chips spend talking versus computing, and change the plan to cut the waiting.

Why robots care

The newest robot brains read camera images and instructions, and they're built from big language models. Training them needs many GPUs working together, and robot video is heavy to move around. Once trained, the model usually has to be squeezed back onto one small computer inside the robot.

Try it

Words to know

GPU
A chip that does lots of maths at once. AI models train on them.
Parallelism
Doing many parts of a job at the same time.
Shard
One piece of something that's been split up across chips.
Interconnect
The wires and links that let chips send data to each other.
Optimizer state
Extra notes the training program keeps for every number in the model.

Going deeper: everything below is the full version, with some maths. On a first read, skim the headings and the "Why it matters for robotics" box, and come back for the rest later.

1. What this stage covers

TL;DR

There are three ways to split training. Data parallelism replicates the model and splits the batch. Tensor parallelism splits each weight matrix. Pipeline parallelism splits the stack of layers. ZeRO and FSDP make data parallelism memory-efficient by sharding the optimiser states, gradients and finally the parameters themselves, at the price of more communication. Every scheme is built from a few NCCL collectives (all-reduce, all-gather, reduce-scatter, all-to-all, send/recv), and the art is matching each one to the right link: tensor parallelism on fast intra-node links, data parallelism across nodes. Gradient checkpointing trades extra compute for activation memory.

2. Concepts, explained

Where the memory goes

Start here, because every technique below attacks one of these terms. With mixed-precision Adam, each parameter costs about 16 bytes before you count activations (ZeRO paper's accounting):

fp16/bf16 weights      2 bytes
fp16/bf16 gradients    2 bytes
fp32 master weights    4 bytes  ┐
fp32 Adam momentum     4 bytes  ├ optimizer state, K = 12
fp32 Adam variance     4 bytes  ┘
total                 16 bytes / parameter  →  7B params ≈ 112 GB

So a full fine-tune of a 7B model does not fit on one 80 GB GPU even before activations. Activations scale with batch × sequence length × hidden size × layers, and at long context they can dominate.

Data parallelism (DP / DDP)

Every GPU holds the full model and processes a different slice of the batch. After the backward pass, gradients are averaged with an all-reduce, so all copies take the same step. PyTorch's DDP buckets gradients and starts all-reducing early buckets while the backward pass is still running, hiding much of the communication. It scales throughput, but memory per GPU doesn't shrink at all.

Tensor parallelism (TP)

Shoeybi, Patwary, Puri, LeGresley, Casper and Catanzaro (2019), Megatron-LM, split each transformer block's matrices so that only a couple of communications are needed:

MLP:   Y = GeLU(X · A) · B
       A split by columns  → each GPU computes GeLU(X · A_i) locally
       B split by rows     → each GPU gets a partial sum Y_i
       all-reduce(Y_i)     → Y           (one all-reduce forward, one backward)
Attention: split by heads, same column-then-row pattern

That's two all-reduces per layer in the forward pass, on activation-sized tensors, every layer. TP therefore needs the fastest links you have, which in practice means NVLink inside one node. They reported 8.3B-parameter models on 512 GPUs at 76% scaling efficiency. Sequence parallelism (Korthikanti et al., 2022) extends TP to split the layer-norm and dropout regions along the sequence, replacing each all-reduce with a reduce-scatter plus all-gather of the same total volume and cutting activation memory.

Pipeline parallelism (PP)

Put consecutive layers on different GPUs and stream micro-batches through them. Day 24 (GPipe) covers the idea and its demo. The cost is the bubble: with p stages and m micro-batches, the idle fraction is about (p − 1)/m. The 1F1B schedule bounds how many micro-batches' activations are in flight. The interleaved schedule from Narayanan et al. (2021) gives each GPU several smaller chunks of layers, cutting the bubble by that factor at the cost of more communication. PP only sends activations between neighbours (point-to-point send/recv), so it tolerates slower links than TP.

3D parallelism

Narayanan et al. composed all three: TP inside a node, PP across nodes, DP over the whole thing. Total GPUs = t × p × d. Their rules of thumb: keep the tensor-parallel degree within a server, use pipeline parallelism to go beyond that, and use data parallelism for the rest. They trained up to a trillion parameters on thousands of GPUs this way. Modern stacks add context parallelism (splitting long sequences, see Stage 03's ring attention) and expert parallelism for MoE.

DeepSpeed ZeRO

Rajbhandari, Rasley, Ruwase and He (2020) noticed that data parallelism stores N identical copies of everything. ZeRO removes the redundancy in three stages. With Ψ parameters and N GPUs:

stage                    per-GPU model-state memory     comm vs DP
DP (no ZeRO)             16Ψ                             1×
ZeRO-1  (P_os)           4Ψ + 12Ψ/N                      1×
ZeRO-2  (P_os+g)         2Ψ + 14Ψ/N                      1×
ZeRO-3  (P_os+g+p)       16Ψ/N                           1.5×

Stages 1 and 2 cost no extra communication, because an all-reduce is already a reduce-scatter followed by an all-gather, and ZeRO just changes what each GPU keeps in between. Stage 3 must all-gather each layer's parameters right before its forward and backward passes, then free them, which adds about 50% more traffic. The paper's example: a 7.5B model on 64 GPUs drops from 120 GB to about 1.9 GB of model state per GPU under stage 3. ZeRO-Offload and ZeRO-Infinity extend this to CPU and NVMe memory.

FSDP

PyTorch's Fully Sharded Data Parallel (Zhao et al., 2023) is the native take on ZeRO-3. You wrap units of the model (usually one per transformer block). Each unit's parameters live sharded, are all-gathered just before use, and are freed afterwards. Gradients are reduce-scattered. Choices that matter: how you wrap (too coarse wastes memory, too fine adds latency), prefetching the next unit's all-gather, mixed-precision policy, and hybrid sharding (shard within a node, replicate across nodes) to keep the heavy traffic on fast links. Newer PyTorch exposes this as fully_shard (FSDP2), built on DTensor, which also composes with tensor parallelism.

NCCL communication primitives

NCCL is NVIDIA's library of GPU collectives. Learn what each does and what moves over the wire for a message of size S on N GPUs (ring algorithms):

all-reduce       everyone ends with the sum        sends ≈ 2(N−1)/N · S per GPU
reduce-scatter   everyone ends with 1/N of sum     sends ≈ (N−1)/N · S
all-gather       everyone ends with all shards     sends ≈ (N−1)/N · S
broadcast        one rank's tensor to all
all-to-all       each rank sends a slice to each   (MoE expert routing)
send / recv      point to point                    (pipeline stages)

The nccl-tests tools report bus bandwidth, which applies these factors so you can compare a measured number directly with the link's hardware speed. Small messages are latency-bound, big ones bandwidth-bound. That's why gradients are bucketed. Debug with NCCL_DEBUG=INFO to see which transport (NVLink, PCIe, InfiniBand, sockets) was actually chosen.

Gradient (activation) checkpointing

Chen, Xu, Zhang and Guestrin (2016): store activations only at some checkpoints and recompute the rest during the backward pass. With checkpoints every √n layers, activation memory falls to O(√n) for roughly one extra forward pass of compute. In practice people checkpoint each transformer block. Korthikanti et al. showed selective recomputation, of only the cheap-to-recompute but memory-hungry attention parts, keeps most of the saving with far less overhead.

Interactive: ZeRO memory per GPU

Model-state memory per GPU for mixed-precision Adam (16 bytes per parameter in total), using the ZeRO paper's formulas. Activations, temporary buffers and fragmentation are not included, so leave real headroom. Bars are green if they fit under the GPU memory line and red if not.

DP
–
ZeRO-1
–
ZeRO-2
–
ZeRO-3
–

Try this: 7B on 4 GPUs gives 112, 49, 38.5 and 28 GB. Only ZeRO-2 and ZeRO-3 leave room for activations on 80 GB cards. Switch to 24 GB cards: nothing fits, so you'd need 8+ GPUs, CPU offload, or LoRA instead of a full fine-tune. Then set 7.5B on 64 GPUs to reproduce the paper's 120 / 31.4 / 16.6 / 1.9 GB.

3. The practice project

(a) The laptop and free-GPU version

Two parts. First, build the primitives yourself on CPU. PyTorch's gloo backend runs collectives between processes on one machine, so torchrun --nproc_per_node=4 gives you four "GPUs" on a laptop. Write a column-then-row parallel MLP, check it matches the unsharded result, then time all-reduce across message sizes:

import time, torch, torch.distributed as dist
dist.init_process_group("gloo")
rk, ws = dist.get_rank(), dist.get_world_size()
torch.manual_seed(0)                      # same full weights on every rank
d, h = 1024, 4096
W1, W2 = torch.randn(d, h) / d**.5, torch.randn(h, d) / h**.5
x = torch.randn(8, d)
s = h // ws
y = torch.relu(x @ W1[:, rk*s:(rk+1)*s]) @ W2[rk*s:(rk+1)*s]  # partial sum
dist.all_reduce(y)                        # the one forward all-reduce
if rk == 0:
    print("max err", (y - torch.relu(x @ W1) @ W2).abs().max().item())

for n in [2**k for k in range(10, 25, 2)]:
    t = torch.ones(n)
    dist.barrier(); t0 = time.perf_counter()
    for _ in range(5):
        dist.all_reduce(t)
    dt = (time.perf_counter() - t0) / 5
    if rk == 0:
        bw = 2 * (ws - 1) / ws * n * 4 / dt / 1e9
        print(f"{n*4/1e6:8.2f} MB {dt*1e3:8.2f} ms  busbw {bw:.2f} GB/s")
dist.destroy_process_group()

You should see a tiny max error (float rounding), and bus bandwidth rising with message size until it levels off. That curve is why frameworks bucket small gradients together.

Second, get two real GPUs. Kaggle notebooks offer a free two-GPU T4 option. Fine-tune a model of about 0.5–1B parameters with DDP, then FSDP, then FSDP plus activation checkpointing. For each, record peak memory per GPU (torch.cuda.max_memory_allocated), tokens per second, and the share of step time in NCCL kernels from torch.profiler. T4s talk over PCIe, not NVLink, so communication shows up clearly.

(b) The full version from the post

Shard a 7B model across 4 GPUs, profile NCCL, and tune the tensor-parallel layout.

(c) Pitfalls

4. Reality check

The post's claim: single-GPU training is for tutorials, and production research needs sharding across clusters without bottlenecking the interconnect.

5. In this course

6. Why it matters for robotics

7. Self-check

Why can't you fully fine-tune a 7B model with Adam on one 80 GB GPU in plain mixed precision?

Model states alone need about 16 bytes per parameter: 2 for weights, 2 for gradients and 12 for fp32 master weights plus two Adam moments. That's about 112 GB before any activations.

Why do ZeRO-1 and ZeRO-2 add no communication over plain data parallelism, while ZeRO-3 adds about 50%?

An all-reduce already is a reduce-scatter plus an all-gather. ZeRO-1/2 keep only their shard of the reduced gradients and all-gather updated parameters, the same volume. ZeRO-3 must also all-gather parameters before the forward and again before the backward pass, which adds about another half.

Why is tensor parallelism usually kept inside one node?

It does all-reduces on activations twice per layer in the forward pass (and again backward), on the critical path. That needs NVLink-class bandwidth and latency. Across nodes it would stall every layer.

A pipeline has 8 stages and 8 micro-batches. Roughly what fraction of time is bubble, and how do you cut it?

About (8 − 1)/8, so the bubble is close to the useful work. Use many more micro-batches, or an interleaved schedule that gives each GPU several smaller layer chunks.

What does gradient checkpointing cost, and what does it save?

It saves activation memory, down to O(√n) for n layers with √n checkpoints, at the cost of recomputing the forward pass for the dropped segments, roughly one extra forward pass.

Your FSDP run is slow and the profile shows long all-gathers not overlapping compute. What do you try?

Enable forward and backward prefetching, check the wrapping granularity (one unit per transformer block is the usual start), use hybrid sharding if you're crossing nodes, and verify NCCL is using the fast transport.

8. Courses & content

ResourceTypeUse it for
Hugging Face · The Ultra-Scale PlaybookFree long-form guideData, tensor, pipeline, context and expert parallelism, ZeRO, with measured benchmarks
Google DeepMind · How To Scale Your ModelFree online bookRooflines, sharding and communication cost, worked examples
PyTorch · Getting started with FSDPOfficial tutorialSharded data-parallel training
DeepSpeed · ZeRO tutorialOfficial tutorialZeRO stages 1–3 and offload
Stanford CS336 · Assignment 2 (Systems)University course (free materials)Build memory-efficient distributed training yourself

Start with the Ultra-Scale Playbook: it covers every technique on this page in one place, with real measurements, and uses PyTorch like the project does.

Full course map for all 12 stages →

9. Papers & docs