LLM + RoboticsModule 12 · Video 01

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.

Video length23:05
Published13 October 2026
LevelIntermediate: you've trained a small model in PyTorch and know what a gradient is
Builds onModule 08 · Distributed training, Module 08 · Tensor maths & autograd

Video by Jia-Bin Huang · Watch on YouTube

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

  1. Numbers can be stored with fewer digits (16 bits instead of 32), which halves the space but makes them a bit less exact.
  2. 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.
  3. With plain data parallelism, every card holds all of this and works on different examples.
  4. ZeRO gives each card only a slice of the optimiser memory, then a slice of the gradients, then a slice of the model itself.
  5. 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

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.

TimeChapterWhat to look for
0:00How a model learnsThe 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:55Adam optimizerAdam keeps two running averages per weight. Count them: that's where most of the training memory goes.
1:56How a computer stores real numbersSign, exponent and mantissa bits. Exponent bits buy range, mantissa bits buy precision. Compare FP32, FP16 and BF16 with the demo below.
11:40The memory constraintAdd up bytes per parameter for weights, gradients and Adam states in mixed precision, and compare with one GPU's memory.
12:40Data parallelismEach GPU holds a full replica and sees a different slice of the batch. Notice what's duplicated.
13:21How GPUs sync their gradientsAll-reduce, and how it splits into a reduce-scatter followed by an all-gather. That split is the key to ZeRO.
16:07ZeRO stage 1 and 2: shard the optimizer states, then the gradientsWhy stages 1 and 2 cost no extra communication over plain data parallelism.
19:27ZeRO stage 3: shard the weightsWeights are gathered just in time for each layer and freed after. Look for the extra all-gather and what it costs.
21:28FSDP: the same schedule one unit at a timeFSDP 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:

FormatSign / exponent / mantissa bitsLargest finite valueGap after 1.0
FP321 / 8 / 23≈ 3.4 × 10³⁸2⁻²³ ≈ 1.2 × 10⁻⁷
BF161 / 8 / 7≈ 3.4 × 10³⁸2⁻⁷ ≈ 0.0078
FP161 / 5 / 1065,5042⁻¹⁰ ≈ 0.00098
FP8 E4M31 / 4 / 34482⁻³ = 0.125
FP8 E5M21 / 5 / 257,3442⁻² = 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:

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.

Examples:

BF16 stores
–
FP16 stores
–
FP8 E4M3 stores
–
More accurate 16-bit format
–

Try this: 0.1 is stored as 0.10009765625 in BF16, 0.0999755859375 in FP16 and 0.1015625 in E4M3, and none of them is exact. 70000 overflows FP16 (largest 65,504) but BF16 keeps it as 70,144. 0.000001 survives in FP16 only as a subnormal (about 1.01 × 10⁻⁶) and vanishes in both FP8 formats. 1.0001 rounds to exactly 1 in BF16 and FP16, which is why updates are applied to an FP32 master copy.

3. Where the course goes deeper

TopicCourse page
ZeRO stages, FSDP, NCCL collectives, checkpointing, with a ZeRO memory calculatorModule 08 · Distributed training
Pipeline parallelism and the GPipe schedule30u30 Day 24 · GPipe
Tensor, context, pipeline and expert parallelism togetherVideo 02 · 5D parallelism
Adam, AdamW and the optimiser familyVideo 07 · AdamW, Module 08 · Tensor maths & autograd
FP8, INT8, INT4 and NVFP4 for inferenceModule 09 · Quantisation & hardware
Training a trillion-parameter MoE at scale (Kimi K3)Module 05 · Trillion-scale architecture

4. Why it matters for robotics

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:

Primary papers cited in this guide: