Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

activation
A rule applied to each number in a layer that lets the network bend, rather than only scale and add.
autograd
Software that records operations during a forward pass and later computes their gradients automatically.
backpropagation
Working backwards through a network to find how each weight affects the loss, by multiplying local slopes.
batch
A group of examples processed together in one step, averaged into a single update.
epoch
One full pass over the training examples.
forward pass
Running inputs through a network to produce outputs, before any error is measured.
gradient
The direction and steepness in which a value rises fastest; training moves weights the opposite way.
learning rate
How far each update moves the weights; too large overshoots, too small crawls.
loss
One number saying how wrong the current predictions are, which training tries to reduce.
optimiser
The procedure that uses gradients to decide how each weight changes.
overfitting
When a network memorises accidental details of its training examples and does worse on new ones.
regularisation
Any penalty or limit that discourages a network from chasing noise in its training examples. The aim is better performance on new data.
tensor
A nested list of numbers with a shape that says what each axis means.

Deep learning from the ground up

GPUs and mixed precision

A GPU is a throughput machine, and a float is a budget of range and resolution. Training in mixed precision is the art of spending that budget where it does not change the answer.

Part 9 of 10

Advanced17 min readBack to Courses

Why the GPU wins

A CPU has a handful of cores optimised for latency, which is the delay before one task finishes. A GPU has thousands of lanes optimised for throughput, which is the number of tasks finished per second. A matrix multiply is the same small operation repeated over independent elements. That shape is why the GPU is the default for training. It is also why a model built mostly from small elementwise operations does not benefit as much as its FLOP count suggests. A FLOP is one floating-point operation.

Arithmetic intensity is the number of floating-point operations per byte moved. A large matrix multiply loads each value once and reuses it many times, so it is compute-bound. An elementwise operation reads and writes memory for every operation, so it is bandwidth-bound. A kernel is the compiled function that runs on the GPU. The ratio decides which limit a kernel hits, and therefore what to optimise.

Kernel fusion and batch size raise reuse. Fusing two elementwise operations avoids a round trip to memory. A larger batch reuses the same weights across more rows. Launch overhead matters for the same reason. A kernel that runs for a few microseconds can spend more time being scheduled than doing arithmetic.

A float is range and resolution

A floating-point number has a sign, an exponent and a mantissa. The exponent sets the range, and the mantissa, the significant digits, sets the resolution. fp32 spends 8 bits on the exponent and 23 on the mantissa. fp16 spends 5 and 10, bf16 spends 8 and 7, and tf32 spends 8 and 10.

fp16 has fine resolution but a small range. Its largest finite value is 65,504. A gradient smaller than about 6 × 10⁻⁸ becomes zero. bf16 has the range of fp32 and less resolution. That is usually the better trade for deep learning, because the values that matter span many orders of magnitude.

Rounding is not free. Summing many small values in a low-precision accumulator loses the small ones once the running total is large. An accumulator is the running total. The standard pattern is therefore to multiply in low precision and accumulate in fp32. A dot product that accumulates in fp16 can be visibly wrong after a few thousand terms. The error is largest exactly where the sum is dominated by a few big values.

Mixed precision in a training loop

import torch
from torch.amp import GradScaler, autocast

scaler = GradScaler("cuda")
for inputs, targets in loader:
    optimizer.zero_grad()
    with autocast("cuda", dtype=torch.float16):
        loss = criterion(model(inputs), targets)
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

Round a dot product

Compare the exact sum with half-precision inputs and with a half-precision accumulator, then change the size and watch the accumulation error grow.

Output
Click "Run" to execute your code

The mixed-precision recipe

Keep a master copy of the weights in fp32. Cast them to fp16 or bf16 for the forward and backward matrix multiplies, and accumulate the results in fp32. The optimiser updates the master weights. A change smaller than the low-precision resolution is then not lost between steps.

Autocast chooses the precision per operation. Matrix multiplies and convolutions run in low precision. Reductions, losses and anything numerically delicate stay in fp32. It is a policy rather than a global switch. That policy is why mixed precision usually matches a full-precision run without hand-editing every layer.

With fp16 the gradients can underflow, which means they become zero because the value is too small. Loss scaling multiplies the loss by a large constant before backward, then divides the gradients afterwards. The scaler raises the constant when it sees an overflow and lowers it when the gradients are healthy. bf16 has enough range that scaling is usually unnecessary. That is one reason it is the default on hardware that supports it.

A loss that becomes nan after a few hundred steps is often an overflow in fp16, not a bad learning rate. Check the scaler’s scale factor and the gradient norms before changing the optimiser.

Memory is the real budget

Training memory has four parts: weights, gradients, optimiser state and activations. Adam keeps two fp32 moments per parameter, and mixed precision keeps an fp32 master copy. A 7B model therefore needs about 84 GB for the optimiser state alone, before the low-precision weights and the activations. That number, not the parameter count, is what decides how many devices a run needs.

Activations scale with batch, sequence length, width and depth. At long context they are often the largest term. Gradient checkpointing stores a few activations and recomputes the rest during backward. It trades compute for memory, and it is the standard way to fit a longer sequence without shrinking the model.

Measure before tuning. Four numbers tell you whether a change helped: peak allocated memory, tokens per second, time per step, and a loss curve against a full-precision baseline. A run that is twice as fast and quietly diverging is not faster. It is a shorter run to the same failure.

Budget a training step

Compare the three precisions across both activation scenarios, then change PARAMS to 70 billion and see how the device count changes.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does the exponent of a floating-point format control?