In earlier lessons, we followed the forward pass through a Transformer and looked closely at inference: KV caches, batching, memory bandwidth, and scheduling.
Today we'll move back to training and focus on the moment immediately after backpropagation.
Backpropagation gives us gradients:
But a gradient is not yet a parameter update.
It tells us:
If I nudge each parameter a tiny amount, which direction locally increases or decreases the loss?
The optimizer decides what to do with that information.
The central idea is:
Modern LLMs are usually trained with variants of Adam, commonly AdamW, rather than plain gradient descent.
Understanding why gives us a useful window into both the mathematics and the enormous memory cost of LLM training.
Start with the simplest possible optimizer¶
Suppose the model has parameters:
After a training batch, backpropagation gives us:
Plain stochastic gradient descent performs:
where:
is the learning rate.
The minus sign matters because the gradient points in the direction of increasing loss.
So SGD says:
measure downhill direction
↓
take a small step downhill
↓
repeat
This is conceptually beautiful.
But LLM optimization is much noisier than the smooth bowl often drawn in textbooks.
A one-parameter example¶
Imagine our entire model has one parameter:
Suppose the current gradient is:
With learning rate:
SGD gives:
If instead:
then:
The gradient determines direction and magnitude.
The learning rate globally scales the step.
Now imagine doing this not for one parameter, but for hundreds of billions.
The problem: gradients are noisy¶
An LLM does not compute the gradient over its entire training dataset before every update.
It trains on minibatches.
One batch might contain lots of:
Python
while another contains:
French prose
and another:
mathematics
So the gradient changes from step to step.
For one parameter we might see:
step 1: +0.8
step 2: +1.2
step 3: -0.4
step 4: +0.9
step 5: +0.3
The underlying useful signal might be positive, but individual batches are noisy.
It would be helpful to remember some recent history.
Momentum: a rolling direction of travel¶
A simple improvement is momentum.
Instead of using only the current gradient, maintain an exponentially weighted moving average:
If:
then roughly speaking, recent gradients matter most while older gradients fade away.
The update can use:
Suppose gradients repeatedly point in approximately the same direction.
Momentum accumulates that evidence.
If one batch produces an unusual gradient in the opposite direction, the historical average prevents the optimizer from instantly reversing course.
A good mental model is a heavy ball rolling downhill.
SGD behaves more like:
look at slope
take step
forget everything
Momentum behaves more like:
look at slope
combine with existing velocity
keep rolling
But different parameters have very different gradient scales¶
Now consider two parameters.
Suppose their gradients are:
and:
Using one global learning rate means B's raw update is:
larger than A's.
Sometimes that is appropriate.
But neural networks contain parameters with dramatically different scales and gradient statistics.
We would like the optimizer to adapt somewhat to each parameter.
This is where Adam's second moving average enters.
Adam tracks squared gradients too¶
Adam maintains:
and also:
The square is elementwise.
So for every parameter Adam tracks two pieces of history:
m → recent average gradient
v → recent average squared gradient
The first asks roughly:
Which direction have gradients consistently pointed?
The second asks:
How large have gradients for this parameter typically been?
Adam then forms an update approximately like:
This is the key equation.
Why divide by the square root of \(v\)?¶
Suppose one parameter consistently gets gradients around:
while another gets gradients around:
For the first:
so:
Its normalized gradient is roughly:
For the second:
so:
Its normalized gradient is also roughly:
Adam has partially normalized away the difference in gradient scale.
This does not mean every parameter gets exactly the same update.
The moving averages, signs, history, epsilon term, and changing gradients all matter.
But the basic effect is:
relative to their raw gradient magnitude.
A concrete two-parameter example¶
Suppose:
and:
Ignoring \(\epsilon\):
For B:
Despite a 100× difference in first-moment magnitude, their normalized Adam updates are equal.
This per-parameter adaptivity is one reason Adam became so useful for large neural networks.
The bias-correction wrinkle¶
At the beginning of training:
That means early moving averages are biased toward zero.
Adam corrects this:
and:
Then the update is:
After many steps:
and:
so the correction becomes negligible.
It mainly matters early in training.
Where AdamW comes in¶
Large models also commonly use weight decay.
The idea is to gently shrink parameters toward zero.
A naive way to combine this with Adam is to add an L2 penalty to the loss.
But because Adam rescales gradients parameter by parameter, that interacts with regularization in a somewhat unintuitive way.
AdamW instead decouples weight decay from the gradient update.
A simplified AdamW step is:
The first term is learning from the gradient.
The second is explicit weight decay.
Equivalently:
So AdamW conceptually separates:
learn from data
+
gently shrink weights
That decoupling is the "W" in AdamW.
Now comes the systems surprise¶
Adam is not free.
For every model parameter, training may need to store:
parameter
gradient
first moment m
second moment v
And some mixed-precision training setups also keep a higher-precision master copy of parameters.
Suppose we make a deliberately simplified estimate using FP32 optimizer states.
For one parameter:
BF16 parameter 2 bytes
BF16 gradient 2 bytes
FP32 m 4 bytes
FP32 v 4 bytes
--------------------------
12 bytes
Already:
A 70B-parameter model would therefore require roughly:
just for those states.
And that excludes activations and other training memory.
If an FP32 master weight copy is also stored, add another:
taking this simplified accounting to:
or:
for 70B parameters.
Exact implementations differ, but the lesson is important:
Compare training and inference¶
At inference, we may only need something like:
model weights
KV cache
temporary activations
Training needs substantially more:
model weights
forward activations
gradients
optimizer first moments
optimizer second moments
possibly master weights
temporary buffers
So a model that comfortably fits for inference may be nowhere close to fitting for training on the same hardware.
This is one reason distributed training techniques are essential.
Why optimizer sharding matters¶
Suppose we train across:
GPUs.
If every GPU stores a full copy of Adam's optimizer states, enormous amounts of memory are duplicated.
Instead, systems such as ZeRO-style training can shard states across devices.
Conceptually:
GPU 0 → optimizer states for parameters 0–12.5%
GPU 1 → optimizer states for parameters 12.5–25%
GPU 2 → optimizer states for parameters 25–37.5%
...
Rather than every device holding all optimizer state.
At different sharding stages, systems may distribute:
- optimizer states,
- gradients,
- parameters.
This trades memory duplication for communication.
Again we see a recurring theme in large-model systems:
The difficult engineering question becomes whether the interconnect is fast enough.
Why the learning rate still matters with Adam¶
Because Adam adapts each parameter's effective step, it is tempting to think the global learning rate no longer matters much.
It matters enormously.
The learning rate:
still scales the entire update.
Too large:
loss spikes
training destabilizes
parameters overshoot useful regions
Too small:
training progresses slowly
compute is wasted
model may undertrain
Large LLM runs therefore use learning-rate schedules.
A common shape is:
learning rate
^
| /\
| / \
| / \
| / \
|____/ \________
+------------------------> training steps
warmup decay
Why warmup exists¶
At the very beginning, the optimizer has little history.
The model is also far from a well-adapted region of parameter space.
Immediately applying the full learning rate can destabilize training.
So the learning rate often starts near zero and increases over some number of steps:
during warmup.
After warmup, it usually decays gradually.
One intuition is:
early training:
learn cautiously while optimizer statistics stabilize
middle training:
take larger productive steps
late training:
take increasingly fine steps
The exact schedule varies across training runs.
Gradient clipping is another safety mechanism¶
Occasionally a batch can produce unusually large gradients.
Define the global gradient norm:
If it exceeds some threshold:
we can rescale:
So if:
nothing changes.
If:
the direction stays the same but the magnitude is reduced.
Conceptually:
normal gradient → leave alone
huge gradient → cap its size
This helps prevent rare batches from producing catastrophic parameter jumps.
Put the whole training step together¶
A simplified LLM training iteration now looks like:
tokens
↓
forward pass
↓
loss
↓
backpropagation
↓
gradients
↓
gradient clipping
↓
AdamW
├─ update first moments
├─ update second moments
├─ normalize gradient update
└─ apply weight decay
↓
new parameters
Mathematically:
then perhaps clip \(g_t\), then:
bias-correct:
and finally:
Then the next batch begins.
This happens millions of times during a large training run.
The deeper mental model: hiking through fog¶
Imagine trying to descend a mountain in dense fog.
You cannot see the valley.
You can only measure the local slope under your feet.
That local slope is the gradient:
Plain SGD says:
Step downhill according to the slope you see right now.
Momentum says:
Also remember which direction we have consistently been traveling.
Adam adds:
And remember how volatile the slope has historically been in each direction, so don't overreact along dimensions that routinely produce huge gradients.
AdamW adds:
While doing all of that, gently keep the parameter vector from drifting unnecessarily large.
The learning-rate schedule controls how ambitious your steps are over the course of the journey.
Gradient clipping says:
If one measurement suddenly claims the mountain has become a cliff, don't take an enormous leap.
So training is not simply:
It is:
And this reveals an important difference between training and inference.
Inference mostly asks:
How efficiently can we execute the parameters we already have?
Training asks something harder:
How do we repeatedly estimate tiny useful changes to billions of parameters, maintain enough historical state to make those changes stable, and distribute all of that state across enormous machines?
The optimizer sits directly at that boundary between learning theory and systems engineering.
AdamW is only a handful of equations.
At LLM scale, those equations can require hundreds of gigabytes or even terabytes of state.