How a model too big for any GPU is trained on thousands of them, with every byte of memory, every message on the network, and every idle second counted.
Chapter 10 · Section 10.1 · 50 min

Where the memory goes

Here is a puzzle that has ended many first attempts at training a large model. A 7-billion-parameter model stored in bfloat16 takes 13.5 GB. An H100 has 80 GB of memory, enough to hold that model almost six times over. So you load the model onto one GPU, run a forward pass, call loss.backward(), call optimizer.step(), and the program dies with CUDA out of memory. Where did the other 66 GB go?

The answer is that the weights are the smallest of four bills. Training also stores a gradient for every weight, an optimizer's private state for every weight, and the intermediate results of the forward pass that the backward pass needs, and each of these can be larger than the weights themselves. This section counts every byte. By the end you will be able to look at a model's shape and a batch size and say, to within a few percent, how much memory training it takes, which is the first question behind every decision in the rest of this chapter.

Four kinds of memory

Take a model with NN parameters, trained with Adam on micro-batches of bb sequences of TT tokens each. The GPU holds four things.

  1. Parameters, the weights themselves, NN numbers.
  2. Gradients, ∂L/∂θ\partial\mathcal{L}/\partial\theta, one number per parameter, produced by the backward pass and consumed by the optimizer.
  3. Optimizer state. Adam keeps a running mean m\mathbf{m} and a running mean of squares v\mathbf{v} of the gradients (§0.6), two more numbers per parameter. Mixed-precision training adds a full-precision copy of the weights, the master weights, which we will justify in a moment.
  4. Activations, the tensors computed in the forward pass and saved for the backward pass. These scale with bb and TT, not with NN.

Rajbhandari et al. (2020), the ZeRO paper we meet in §10.3, call the first three model states and everything else residual states (activations plus temporary buffers and fragmentation). The split is useful because the two halves scale differently. Model states grow with the size of the model and are the same for every batch. Activations grow with the batch and the sequence length and can be traded for compute.

Why must activations be saved at all? Recall from §0.3 that the backward pass of Y=XW\mathbf{Y} = \mathbf{X}\mathbf{W} needs ∂L/∂W=X⊤G\partial\mathcal{L}/\partial\mathbf{W} = \mathbf{X}^\top\mathbf{G}, where G=∂L/∂Y\mathbf{G} = \partial\mathcal{L}/\partial\mathbf{Y} is the upstream gradient. The input X\mathbf{X} was computed during the forward pass, long before G\mathbf{G} exists, so the framework must keep it until the backward pass reaches this layer. Every matrix multiply keeps its input, every nonlinearity keeps its input or output, and every dropout keeps its mask. For a deep network, that is a lot of tensors held at once, because the forward pass of the last layer finishes before the backward pass of the first layer begins.

Adam's bill, 16 bytes per parameter

Start with plain float32 training. Each number takes 4 bytes, and there are four numbers per parameter (weight, gradient, m\mathbf{m}, v\mathbf{v}), so

4⏟weights+4⏟gradients+4+4⏟Adam m, v=16 bytes per parameter.\underbrace{4}_{\text{weights}} + \underbrace{4}_{\text{gradients}} + \underbrace{4 + 4}_{\text{Adam } \mathbf{m},\, \mathbf{v}} = 16 \text{ bytes per parameter}.

Now mixed precision, the way every large model is trained. The forward and backward passes run in a 16-bit format (bfloat16 today, float16 in older recipes), because tensor cores are several times faster in 16 bits and the activations take half the space (§8.5 covers the formats in depth). So the working copy of the weights is 16-bit, 2 bytes, and so are the gradients the backward pass produces, another 2 bytes. The optimizer, however, keeps everything in float32. It holds m\mathbf{m} and v\mathbf{v} (4 bytes each) and a float32 master copy of the weights (4 bytes). Each step it updates the master weights in float32 and then casts them to 16 bits for the next forward pass. The total is

2⏟bf16 weights+2⏟bf16 gradients+4⏟fp32 master+4+4⏟fp32 m, v=16 bytes per parameter.\underbrace{2}_{\text{bf16 weights}} + \underbrace{2}_{\text{bf16 gradients}} + \underbrace{4}_{\text{fp32 master}} + \underbrace{4 + 4}_{\text{fp32 } \mathbf{m},\, \mathbf{v}} = 16 \text{ bytes per parameter}.

In plain words, mixed precision does not save any memory on model states. It moves 4 bytes from the weights and gradients into a master copy, and its real savings are in activations and speed.

ZeRO writes this as 2N+2N+KN2N + 2N + KN with K=12K = 12 bytes of optimizer state per parameter, and you will see "16N16N" or "16Ψ16\Psi" (the paper's symbol for the parameter count) all over the literature. For a 7B model that is 112 GB, which already exceeds the 80 GB of an H100 before a single activation is stored. That answers the puzzle.

Why keep a float32 master copy at all? Because bfloat16 cannot represent small changes to a number. It has 8 bits of significand precision (7 stored plus an implicit leading 1), so the gap between 1 and the next representable number is 2−7≈0.00782^{-7} \approx 0.0078, and an update smaller than half the local gap, about 0.0020.002 to 0.0040.004 for a weight near 1 (roughly 1/2561/256 of the weight), rounds away to nothing. Adam's update for one weight is roughly the learning rate times a number of order 1, and learning rates for large models are around 10−410^{-4}, so a weight kept only in bfloat16 would never move. A float32 master copy accumulates every one of those small steps, and its bfloat16 cast follows it. §8.5 works this out bit by bit, including the experiment where a thousand updates leave a pure bfloat16 weight exactly where it started, and the alternatives (stochastic rounding, compensated summation) that let some recipes drop the copy. For the memory bill, what matters is that the standard recipe needs the master weights, so it stays at 16 bytes.

Several variants are common, and an interviewer will expect you to know their costs.

recipeweightsgradientsmasteroptimizerbytes per parameter
float32 Adam44none816
mixed precision Adam224816
mixed, float32 gradient accumulation244818
mixed, 8-bit Adam states224210
pure bfloat16 Adam (no master)22none48
mixed, SGD with momentum224412

The third row is what Megatron-LM does for bfloat16 training, accumulating gradients over micro-batches in float32 so that summing many small contributions does not lose precision. The fourth uses the block-wise quantized optimizer states of Dettmers et al. (2022). The fifth only works with tricks such as stochastic rounding or compensated (Kahan) summation that recover the lost low bits, and it is used mostly for fine-tuning. Optimizers with less state, such as Adafactor's factored second moment or Muon, are the subject of §8.6.

Warm-up

Bytes per parameter for each recipe

Implement model_state_bytes(N, recipe) returning a dictionary with keys "weights", "grads", "master", "optimizer", and "total", each in bytes, for a model with N parameters trained with Adam under one of these recipes.

  • "fp32" keeps 4-byte weights and gradients and two 4-byte Adam moments, with no master copy.
  • "mixed" keeps 2-byte weights and gradients, a 4-byte master copy, and two 4-byte moments.
  • "mixed_fp32_grads" is "mixed" with 4-byte gradients.
  • "mixed_8bit" is "mixed" with two 1-byte moments.
  • "bf16_pure" keeps 2-byte weights, gradients, and moments, and no master copy.

Raise ValueError for any other recipe.

Hidden tests, hints, and AI help, from $1.99 a month.

Activations, counted one tensor at a time

Model states are the easy half. Activations are where people's estimates go wrong, because they depend on exactly which tensors each operation saves. The standard reference is Korthikanti et al. (2022), who counted them for a GPT-3-style layer. We will redo their count from scratch, then check it against PyTorch.

Set up the layer. The input is a tensor of shape (b,T,d)(b, T, d), a micro-batch of bb sequences of TT tokens of width dd, and the attention has hh heads. The layer is pre-norm GPT, with LayerNorm, attention, an output projection, dropout, a residual add, then LayerNorm, a d→4dd \to 4d linear, GELU, a 4d→d4d \to d linear, dropout, and another residual add (the block of §5.6, with dropout after the attention weights and after each sublayer). Activations are stored in 16 bits (2 bytes per number) and dropout masks in 1 byte per number. Write TbdTbd for the number of entries of one (b,T,d)(b, T, d) tensor. Here is every tensor the backward pass needs.

blocksaved tensorwhy it is neededbytes
LayerNorm 1its input X\mathbf{X}the norm's backward needs X\mathbf{X}2Tbd2Tbd
attentioninput to the Q\mathbf{Q}, K\mathbf{K}, V\mathbf{V} projectionsweight gradient of WQKV\mathbf{W}_{QKV}2Tbd2Tbd
Q\mathbf{Q} and K\mathbf{K}backward of QK⊤\mathbf{Q}\mathbf{K}^\top4Tbd4Tbd
softmax output A\mathbf{A}softmax backward2hT2b2hT^2b
dropout mask on A\mathbf{A}dropout backwardhT2bhT^2b
dropped-out A\mathbf{A} and V\mathbf{V}backward of AV\mathbf{A}\mathbf{V}2hT2b+2Tbd2hT^2b + 2Tbd
input to WO\mathbf{W}_Oweight gradient of WO\mathbf{W}_O2Tbd2Tbd
dropout mask after WO\mathbf{W}_Odropout backwardTbdTbd
LayerNorm 2its inputthe norm's backward2Tbd2Tbd
MLPinput to the first linearweight gradient2Tbd2Tbd
GELU input, width 4d4dGELU backward8Tbd8Tbd
input to the second linear, width 4d4dweight gradient8Tbd8Tbd
dropout maskdropout backwardTbdTbd

Add the columns. The attention block stores 2+4+2+2+1=112 + 4 + 2 + 2 + 1 = 11 units of TbdTbd plus 5hT2b5hT^2b, the MLP 2+8+8+1=192 + 8 + 8 + 1 = 19 units, and the two norms 4 units. The total for one layer is

activation bytes per layer=34 Tbd+5 hT2b=Tbd(34+5hTd).\text{activation bytes per layer} = 34\,Tbd + 5\,hT^2b = Tbd\left(34 + \frac{5hT}{d}\right).

In plain words, each token costs about 34d34d bytes per layer for everything linear, plus 5hT5hT bytes for its row of every head's attention matrix, and the second term grows with the sequence length.

This is Korthikanti et al.'s equation (1), written in our symbols (they write ss for the sequence length, hh for the width, and aa for the heads, so their sbh(34+5as/h)sbh(34 + 5as/h) is our formula). The LayerNorm also saves a mean and a variance per token, 2Tb2Tb numbers, which they drop because dd is in the thousands.

Notice how lopsided the two terms are. For GPT-3 (d=12,288d = 12{,}288, h=96h = 96, T=2048T = 2048), 5hT/d=805hT/d = 80, so the attention matrices are 70% of all activation memory. For a 7B model at T=4096T = 4096 (d=4096d = 4096, h=32h = 32), 5hT/d=1605hT/d = 160, more than four times the 34 of everything else. The quadratic term is the score matrix of §4.7, and it is exactly what FlashAttention never stores (§12.3). Korthikanti et al. reach the same place from the other direction with selective recomputation, which drops these tensors and recomputes them in the backward pass. Either way the per-layer cost falls to 34 Tbd34\,Tbd.

The bluntest version of the same trade is activation checkpointing, also called gradient checkpointing or full recomputation (Chen et al. 2016). Keep only each layer's input, 2Tbd2Tbd bytes, throw away everything the layer computed, and when the backward pass reaches that layer, rerun its forward pass from the saved input to rebuild the activations just in time. Memory per layer falls from 34 Tbd34\,Tbd (or more) to 2Tbd2Tbd, and the price is one extra forward pass per step, about a third more compute, which §10.7 counts exactly. The three modes, none, selective, and full, are the three settings of the exercises and the widget below.

PyTorch lets you watch the saving happen. torch.autograd.graph.saved_tensors_hooks calls a function every time an operation saves a tensor for backward. We record each saved storage once (several views can share one storage), skip the parameters, and charge 2 bytes per floating-point entry and 1 byte per boolean mask, as a bfloat16 run on a GPU would. Dropout is written out with an explicit boolean mask, which is what the fused GPU kernel stores.

🔥 In PyTorchfor a machine with PyTorch

The measured total is the formula plus 1,024 bytes, and those 1,024 bytes are exactly the LayerNorm statistics. Every other byte matches the table.

The whole model, and the modern layer

Multiply by the number of layers LL and you have nearly all the activation memory. Korthikanti et al. add the pieces outside the layers. The embedding dropout mask and the final norm are small, but the logits are not. The output layer produces bT×VbT \times V logits, and computing the loss in float32 keeps 4bTV4bTV bytes. For Llama 3's vocabulary of V=128,256V = 128{,}256 at T=8192T = 8192, that is 4.2 GB per sequence, as much as several layers. Fused cross-entropy kernels that process the vocabulary in chunks avoid ever materializing it.

The 34 is specific to the GPT-3 layer. A Llama-style layer has no dropout, RMSNorm instead of LayerNorm, grouped-query attention with nkvn_{kv} key-value heads of width dkd_k, a SwiGLU feed-forward network of width dffd_{\text{ff}} (§5.3), and FlashAttention. The same bookkeeping, done for plain PyTorch operations, gives per token and per layer the two norm inputs and two norm outputs (8d8d bytes), Q\mathbf{Q} and the attention output (4d4d), K\mathbf{K} and V\mathbf{V} (4nkvdk4n_{kv}d_k), FlashAttention's per-head log-sum-exp in float32 (4h4h), and four dffd_{\text{ff}}-wide tensors in the SwiGLU (the gate, the up projection, the activated gate, and their product, 8dff8d_{\text{ff}}).

Python · cell 1

The Llama layer comes to about 41d41d bytes per token, a bit more than GPT-3's 34d34d because its feed-forward network is wider. Fused kernels that recompute the activated gate shave a few dd off. The lesson is not the exact constant, which depends on your implementation, but the method. List the tensors each operation saves, multiply by their sizes, and check the result against the framework, as we did above.

Core

Activation memory of a layer

Implement layer_activation_bytes(T, b, d, h, recompute="none") returning the activation bytes of one GPT-3-style layer under Korthikanti et al.'s accounting, for a micro-batch of b sequences of length T, width d, and h heads. With recompute="none" return Tbd(34+5hT/d)Tbd(34 + 5hT/d). With recompute="selective" (the attention core is recomputed, or FlashAttention is used) return 34 Tbd34\,Tbd. With recompute="full" only the layer's input is kept, 2Tbd2Tbd. Return an integer, and raise ValueError for any other mode.

Hidden tests, hints, and AI help, from $1.99 a month.

Worked example, a 7B model on one GPU

Put the pieces together for Llama 2 7B (L=32L = 32, d=4096d = 4096, h=32h = 32, N=6.74N = 6.74 billion) at T=4096T = 4096, one sequence per micro-batch, and see how much each choice buys. We use the GPT-3 constant 34 for the layers, which is close for this model, and write GB for 10910^9 bytes.

Python · cell 2

Even with every activation trick, training this model with plain Adam needs about 109 GB, more than an 80 GB GPU holds, and nearly all of it is model states. No amount of recomputation helps, because the activations are already small. The fixes all attack the model states. You can shard them across several GPUs (ZeRO and FSDP, §10.3), shrink them (8-bit optimizer states, or freezing the weights and training a small adapter as in LoRA, which keeps only the frozen 16-bit weights plus a tiny optimizer), or move them to CPU memory.

Without FlashAttention or selective recomputation the picture is different. The activations alone are about 105 GB, and they grow by that much again with every extra sequence in the micro-batch. Which term dominates depends on TT, bb, and the model, so the habit to build is to compute both.

Worked example, a 70B model

Now Llama 3 70B (L=80L = 80, d=8192d = 8192, h=64h = 64, N=70.6N = 70.6 billion) at T=8192T = 8192. Model states are 16×70.6=1,12916 \times 70.6 = 1{,}129 GB, so even if the states were spread perfectly over 80 GB GPUs you would need 15 of them before storing any activations. One sequence's layer activations are 1.9 TB with naive attention, 182 GB with FlashAttention, and 10.7 GB with full recomputation, and the float32 logits over Llama 3's 128,256-token vocabulary add another 4.2 GB on top of each.

So no trick fits a 70B model on one GPU, and even a whole node of eight 80 GB GPUs, with 640 GB, holds only about half of its model states. Training it takes many nodes and a way to split the model states across them (short of offloading them to CPU memory). The rest of this chapter is about the ways to split, what each one costs in communication, and how to combine them. The widget below lets you see the bill for several models and the effect of each lever, including the ZeRO sharding of §10.3.

Where does the memory of a training step go, and which lever shrinks which part?
Bytes per GPU for one micro-batch, with activations counted by Korthikanti et al.'s formula. ZeRO shards the model states across the data-parallel GPUs; recomputation shrinks the activations.
Loading interactive figure…
Interview

The largest micro-batch that fits

An interviewer hands you a model and a GPU and asks for the largest micro-batch you can train with. Implement max_micro_batch(N, L, d, h, V, T, mem_bytes, recipe="mixed", recompute="none"). Memory is the model states (16 * N for "mixed", 18 * N for "mixed_fp32_grads", 16 * N for "fp32"), plus, per sequence, L layers of activations under Korthikanti et al.'s accounting with the given recompute mode, plus float32 logits of 4 * T * V bytes. Return the largest integer b >= 0 with total memory at most mem_bytes. Return 0 if not even one sequence fits.

Hidden tests, hints, and AI help, from $1.99 a month.

The memory you do not see

A calculation like the one above gets you within about 10% of the truth, and the remaining 10% has three sources worth knowing.

Temporary buffers. Some operations need workspace while they run, for example a matrix multiply's scratch space, the full-precision copy of a gradient during clipping, or the flattened buffers that communication libraries use to send many small tensors at once. They come and go within a step, but the peak is what matters.

Fragmentation. PyTorch's caching allocator keeps freed blocks to reuse them, and when tensors of many different sizes are allocated and freed, the free memory can be broken into pieces too small to serve the next large request. You can then see an out-of-memory error while torch.cuda.memory_reserved() is well above torch.cuda.memory_allocated(). Setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True lets the allocator grow segments instead of creating new fixed-size ones, which often helps.

The peak is not at the end of the forward pass. During the backward pass, activations are being freed while gradients are being created, and the optimizer step can create temporaries the size of the model states. When memory is tight, measure.

The snapshot shows every live tensor over time, colored by the line of code that allocated it, and it is the fastest way to find out which part of a step sets the peak.

Read more

Watch next

🎬 Training Models Too Big for One GPU | ZeRO & FSDP · Jia-Bin HuangA clear visual account of Adam's state, how GPUs store numbers, and the memory that training a large model needs.
🎬 Lecture 48: The Ultra Scale Playbook · GPU MODENouamane Tazi walks through the playbook, whose first part profiles the memory of a training step on one GPU.