Training models too big for one GPU: ZeRO & FSDP
The video starts with how a computer stores a real number and ends with a model whose weights live in pieces across many GPUs. This guide follows the same path: number formats, mixed precision, the memory bill of Adam, and how ZeRO and FSDP remove the copies that plain data parallelism keeps on every GPU.
Start here (no maths needed)
A big AI model is a giant list of numbers. Training it needs several extra lists of the same size, and together they don't fit in one graphics card. The trick in this video is to stop every card keeping its own full copy, and give each card just one slice.
Think of it like this
Eight friends are each solving the same huge jigsaw puzzle at home, from the same picture, to compare notes. Every one of them has bought the full box, the picture on the lid, and a notebook of tips. That's eight copies of everything. A smarter group splits it: each friend keeps one eighth of the pieces and one eighth of the notebook. When someone needs a piece they don't own, they borrow it from the right friend for a moment and hand it back. Nobody's table is crowded, and the puzzle still gets solved.
How it works
- Numbers can be stored with fewer digits (16 bits instead of 32), which halves the space but makes them a bit less exact.
- Training keeps the model's numbers, the corrections for each number (gradients), and the optimiser's memory of past corrections. The optimiser part is the biggest.
- With plain data parallelism, every card holds all of this and works on different examples.
- ZeRO gives each card only a slice of the optimiser memory, then a slice of the gradients, then a slice of the model itself.
- When a card needs a slice it doesn't own, the cards pass it around over the network, use it, and throw it away again.
Why robots care
Robot foundation models (vision-language-action models) start from big vision-language models. Fine-tuning one of them, even a "small" one with a few billion numbers, already needs more memory than a single card has. Sharding is what lets a lab, or a student with a few rented GPUs, train these models at all.
Try it
- Write the number 3.14159 on a card with room for only 3 digits, then 2, then 1. How wrong is it each time? Now try 70,000 with only room for 4 digits and no "×10" allowed. That's the difference between precision and range.
- In the demo further down, type a value into Value to store and read how each format stores it. Try 0.1, 70000 and 0.000001, or click the Examples buttons.
Words to know
- GPU memory
- The fast memory on a graphics card. Everything the card works on must fit there.
- Gradient
- For each number in the model, which way and how much to nudge it to reduce the error.
- Optimiser state
- Extra numbers the optimiser (like Adam) keeps for each weight, such as running averages of past gradients.
- Data parallelism
- Many GPUs each run the full model on different examples, then agree on one shared update.
- Sharding
- Cutting something into slices and giving each GPU one slice.
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. Watch
Chapter guide
Timestamps come from the video's description. The right-hand column is this course's reading guide, written from the chapter titles; it doesn't describe what the video shows frame by frame.
| Time | Chapter | What to look for |
|---|---|---|
| 0:00 | How a model learns | The loop of forward pass, loss, backward pass and update. Every later memory cost is one of the things this loop needs to keep around. |
| 0:55 | Adam optimizer | Adam keeps two running averages per weight. Count them: that's where most of the training memory goes. |
| 1:56 | How a computer stores real numbers | Sign, exponent and mantissa bits. Exponent bits buy range, mantissa bits buy precision. Compare FP32, FP16 and BF16 with the demo below. |
| 11:40 | The memory constraint | Add up bytes per parameter for weights, gradients and Adam states in mixed precision, and compare with one GPU's memory. |
| 12:40 | Data parallelism | Each GPU holds a full replica and sees a different slice of the batch. Notice what's duplicated. |
| 13:21 | How GPUs sync their gradients | All-reduce, and how it splits into a reduce-scatter followed by an all-gather. That split is the key to ZeRO. |
| 16:07 | ZeRO stage 1 and 2: shard the optimizer states, then the gradients | Why stages 1 and 2 cost no extra communication over plain data parallelism. |
| 19:27 | ZeRO stage 3: shard the weights | Weights are gathered just in time for each layer and freed after. Look for the extra all-gather and what it costs. |
| 21:28 | FSDP: the same schedule one unit at a time | FSDP groups layers into units and gathers one unit at a time, overlapping communication with compute. |
2. The ideas, explained
Adam, and why it's expensive
Adam (Kingma and Ba, 2014) keeps an exponential moving average of the gradient (first moment) and of the squared gradient (second moment) for every parameter, and divides one by the square root of the other:
m_t = β1·m_(t−1) + (1 − β1)·g_t
v_t = β2·v_(t−1) + (1 − β2)·g_t²
m̂ = m_t / (1 − β1^t), v̂ = v_t / (1 − β2^t)
θ_t = θ_(t−1) − η · m̂ / (√v̂ + ε)
So Adam stores two extra numbers per parameter, on top of the parameter and its gradient. The optimiser itself, its variants and AdamW's decoupled weight decay are the subject of Video 07; here only the memory matters.
How a computer stores real numbers
A floating-point number is (−1)^sign × 1.mantissa × 2^(exponent − bias). The exponent field sets the range (how big and how small a number can be); the mantissa field sets the precision (how close together neighbouring numbers are). The formats that matter for training:
| Format | Sign / exponent / mantissa bits | Largest finite value | Gap after 1.0 |
|---|---|---|---|
| FP32 | 1 / 8 / 23 | ≈ 3.4 × 10³⁸ | 2⁻²³ ≈ 1.2 × 10⁻⁷ |
| BF16 | 1 / 8 / 7 | ≈ 3.4 × 10³⁸ | 2⁻⁷ ≈ 0.0078 |
| FP16 | 1 / 5 / 10 | 65,504 | 2⁻¹⁰ ≈ 0.00098 |
| FP8 E4M3 | 1 / 4 / 3 | 448 | 2⁻³ = 0.125 |
| FP8 E5M2 | 1 / 5 / 2 | 57,344 | 2⁻² = 0.25 |
BF16 ("brain float", from Google Brain's TPU work) is FP32 with the mantissa cut short: same range, much less precision. FP16 keeps more precision but its range is tiny, so small gradients underflow to zero and large activations overflow to infinity. The two FP8 encodings were proposed together by Micikevicius et al. (2022): E5M2 follows IEEE conventions, while E4M3 drops infinities and keeps a single NaN pattern to stretch its range to 448. FP8 training and inference on newer GPUs is covered in Module 09 · Quantisation & hardware.
Mixed-precision training
Micikevicius et al. (2017, "Mixed Precision Training") showed you can store weights, activations and gradients in FP16 and still match FP32 accuracy, with two tricks:
- An FP32 master copy of the weights. Updates are often much smaller than the weight itself. Added in FP16, they round away to nothing (with a gap of about 0.001 after 1.0, adding 0.0001 changes nothing). So the optimiser updates an FP32 copy, which is rounded to half precision for the next forward pass.
- Loss scaling. Multiply the loss by a factor S before the backward pass so small gradients stay above FP16's underflow threshold, then divide the gradients by S before the update. Modern frameworks pick S dynamically and skip a step if an overflow appears.
BF16 has FP32's range, so it usually needs no loss scaling, which is why it became the default for large-model training. The FP32 master weights stay either way.
The memory bill: 16 bytes per parameter
The ZeRO paper (Rajbhandari, Rasley, Ruwase and He, 2019) counts the "model states" for mixed-precision Adam with Ψ parameters:
fp16 weights 2Ψ bytes
fp16 gradients 2Ψ
fp32 master weights 4Ψ ┐
fp32 Adam m 4Ψ ├ optimiser states: K·Ψ with K = 12
fp32 Adam v 4Ψ ┘
total 16Ψ bytes
7.5B parameters → 16 × 7.5e9 = 120 GB, before any activations
Activations, temporary buffers and fragmentation come on top. Activation memory is reduced separately, mainly with checkpointing (recomputing activations in the backward pass), which Module 08 · Distributed training covers.
Data parallelism and the all-reduce
In data parallelism each of N GPUs runs the full model on its own micro-batch, and the gradients are averaged with an all-reduce so every replica applies the same update. A ring all-reduce has two halves:
reduce-scatter: after it, GPU i holds the SUM of chunk i over all GPUs
all-gather: every GPU sends its finished chunk to all the others
data each GPU sends ≈ 2·(N − 1)/N × gradient size (nearly independent of N)
The catch: all N GPUs hold the same 16Ψ bytes of model states. Adding GPUs adds throughput but never lets you fit a bigger model.
ZeRO stages 1, 2 and 3
ZeRO ("Zero Redundancy Optimizer") removes the duplication in three steps, and the per-GPU memory for model states becomes:
baseline DP (2 + 2 + K)·Ψ = 16Ψ
ZeRO-1 P_os (2 + 2)·Ψ + K·Ψ/N
ZeRO-2 P_os+g 2Ψ + (2 + K)·Ψ/N
ZeRO-3 P_os+g+p (2 + 2 + K)·Ψ/N = 16Ψ/N
7.5B params, N = 64: 120 GB → 31.4 GB → 16.6 GB → 1.9 GB
Stage 1 shards the optimiser states: GPU i updates only its 1/N of the parameters. Stage 2 also shards gradients: instead of an all-reduce, a reduce-scatter leaves each GPU with the summed gradient for exactly the slice it updates, and the rest is freed. After the update, an all-gather shares the new weights. Reduce-scatter plus all-gather is what an all-reduce already does, so the paper reports no extra communication for stages 1 and 2. Stage 3 shards the weights too. Before each layer's forward pass, its weights are all-gathered, used and freed; the same happens again in the backward pass, followed by a reduce-scatter of the gradients. That's about 1.5× the communication volume of plain data parallelism, in exchange for memory that falls linearly with N.
The ZeRO memory calculator in Module 08 · Distributed training lets you vary Ψ, N and K and see each stage's bar; this guide won't repeat it.
FSDP: the same idea, one unit at a time
PyTorch's Fully Sharded Data Parallel (Zhao et al., 2023) is the native implementation of the ZeRO-3 schedule. You wrap the model into units, typically one transformer block each. Each unit's parameters are flattened and sharded across GPUs. During the forward pass, FSDP all-gathers one unit, runs it, frees the full parameters, and moves on, prefetching the next unit's all-gather while the current one computes. The backward pass gathers each unit again, computes its gradients and reduce-scatters them. Peak memory is roughly the sharded states plus one or two fully gathered units, instead of the whole model.
for unit in model: # forward
full = all_gather(unit.shard) # overlapped with the previous unit's compute
x = unit(x, full); free(full)
for unit in reversed(model): # backward
full = all_gather(unit.shard)
grads = backward(unit, full); free(full)
unit.grad_shard = reduce_scatter(grads)
optimizer.step() # each GPU updates its own shard only
The original FSDP API exposes sharding strategies that map onto ZeRO: FULL_SHARD is stage 3, SHARD_GRAD_OP is close to stage 2, NO_SHARD is ordinary DDP, and HYBRID_SHARD shards inside a node and replicates across nodes to keep the chatty all-gathers on fast links. The paper reports throughput comparable to DDP on models that fit, with near-linear scaling on models that don't.
Interactive: how each format stores your number
Each row is one number format. Grey cell: sign bit. Accent cells: exponent bits. Outlined cells: mantissa bits. Filled cells are 1s. On the right: the value actually stored and its relative error. Values beyond a format's range overflow (to infinity, or NaN for E4M3); values too small underflow to zero.
3. Where the course goes deeper
| Topic | Course page |
|---|---|
| ZeRO stages, FSDP, NCCL collectives, checkpointing, with a ZeRO memory calculator | Module 08 · Distributed training |
| Pipeline parallelism and the GPipe schedule | 30u30 Day 24 · GPipe |
| Tensor, context, pipeline and expert parallelism together | Video 02 · 5D parallelism |
| Adam, AdamW and the optimiser family | Video 07 · AdamW, Module 08 · Tensor maths & autograd |
| FP8, INT8, INT4 and NVFP4 for inference | Module 09 · Quantisation & hardware |
| Training a trillion-parameter MoE at scale (Kimi K3) | Module 05 · Trillion-scale architecture |
4. Why it matters for robotics
- Fine-tuning a VLA is already a multi-GPU job. At 16 bytes per parameter, full fine-tuning of a 3B-parameter vision-language-action model needs about 48 GB for model states alone, before activations from long image-token sequences. FSDP or ZeRO-2/3 across a handful of GPUs is the usual way to fit it; LoRA (Video 03) is the other.
- Robot data is image-heavy. Multi-camera inputs make activations large, so sharding the model states frees memory for bigger batches of trajectories.
- Precision choices carry over to the robot. A model trained in BF16 is usually served in BF16 or quantised further. Knowing what each format can and can't represent helps when an on-robot model starts producing NaNs.
- Weak link: sharding is a training-time technique. It does nothing for latency or memory on the robot's own computer, where the model runs on a single device.
Self-check
Why does mixed-precision Adam need 16 bytes per parameter?
2 bytes of FP16 weights, 2 of FP16 gradients, and 12 of FP32 optimiser states: a master copy of the weights plus Adam's first and second moments, 4 bytes each.
BF16 and FP16 both use 16 bits. Why is BF16 preferred for training large models?
BF16 has 8 exponent bits like FP32, so it has the same range and rarely overflows or underflows, and usually needs no loss scaling. FP16 has more mantissa bits but a maximum of 65,504 and a much higher underflow threshold.
Why do ZeRO stages 1 and 2 add no communication over plain data parallelism?
An all-reduce is a reduce-scatter followed by an all-gather. ZeRO-2 reduce-scatters gradients so each GPU gets the summed slice it owns, updates that slice, then all-gathers the updated weights. The total volume matches the all-reduce.
A 7.5B model trained with ZeRO-3 on 64 GPUs: how much model-state memory per GPU?
16 × 7.5 × 10⁹ / 64 ≈ 1.9 GB, compared with 120 GB per GPU for plain data parallelism.
What does FSDP keep fully materialised at any moment, and why does that bound peak memory?
Only the unit (for example one transformer block) currently being computed, plus the next one being prefetched. Everything else stays sharded, so peak memory is the shards plus one or two gathered units rather than the whole model.
Why can't you apply an update of 0.0001 to a weight of 1.0 stored in FP16?
The gap between 1.0 and the next FP16 number is about 0.00098, so 1.0001 rounds back to 1.0 and the update is lost. Mixed precision applies updates to an FP32 master copy for this reason.
References
From the video's description:
- Paper: ZeRO: Memory Optimizations Toward Training Trillion Parameter Models (Rajbhandari, Rasley, Ruwase, He, 2019)
- Code: DeepSpeed
- Docs: PyTorch FSDP
Primary papers cited in this guide:
- Adam: A Method for Stochastic Optimization (Kingma, Ba, 2014)
- Mixed Precision Training (Micikevicius et al., 2017)
- FP8 Formats for Deep Learning (Micikevicius et al., 2022)
- PyTorch FSDP: Experiences on Scaling Fully Sharded Data Parallel (Zhao et al., 2023)