When people talk about training a large language model, the exciting parts seem to be elsewhere: the trillions of tokens, the architecture, the thousands of GPUs. But all of that machinery serves one small operation, repeated a few hundred thousand times: take a batch of tokens, measure how wrong the model’s predictions are, and nudge the weights. Everything else, from data cleaning to distributed training to fine-tuning, is about feeding that operation or running it faster without changing what it means.
So in this post we follow one update from start to finish. On the way we will meet several quiet conventions that decide what the model actually learns: which positions count as targets, what each position may look at, how the losses are averaged, and how AdamW turns a gradient into a step. Get any of them wrong and training still runs. It just optimizes something you did not intend.
If backpropagation, cross-entropy or Adam are new to you, the Foundations posts on backpropagation, softmax and cross-entropy, optimisers and language models cover them.
What one update has to do
A language model reads text left to right and, at every position, guesses the next token. One training update does four things:
- Decide which positions are targets, and what the correct next token is at each.
- Run the model, letting each position look only at what it is allowed to see.
- Turn the guesses into one number, the loss, by averaging how surprised the model was at each target.
- Compute how each weight should change to lower that number, then let the optimizer take a step.
The first three steps define what the model is trained to do. The fourth decides how fast and in what shape it moves there. We take them in order.
Targets: each position predicts the next token
Take the sentence <bos> the cat sat <eos>, where <bos> and <eos> mark the beginning and end of a document. At every position the model predicts the next token:
| Position | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Sees up to | <bos> | the | cat | sat | <eos> |
| Should predict | the | cat | sat | <eos> | nothing |
Five tokens give four targets: the targets are the inputs shifted left by one, and the last position has nothing to predict.
In code, the targets usually start as a copy of the inputs, called labels, and the shift happens inside the loss. In Hugging Face Transformers 4.51.3, ForCausalLMLoss appends one extra ignored label and drops the first one, so position is scored against the token at . Any label set to -100 is ignored. Some codebases, such as nanoGPT, shift the targets in the data loader instead and use -1 as the ignore value. Either is fine; mixing the two conventions is not.
What does “ignored” mean for learning? A label of -100 removes the loss term at that output position, so that position’s prediction sends no direct gradient back. It does not freeze the token. Its input embedding and hidden states can still receive gradients through later positions that attend to it. A tiny example: if position 1 averages the inputs of positions 0 and 1, , and position 1 has a loss , then with and we get and
Position 0 has no target of its own, yet its input moves, because position 1’s prediction depends on it. This is exactly what happens to prompt tokens when fine-tuning masks their labels.
A batch with packing and padding
Real batches hold several rows of the same length. Suppose three short documents must fill two rows of six positions:
- document A:
<bos> the cat <eos> - document B:
<bos> cat, cut off at the end of the row - document C:
<bos> cat sat <eos>
Row 0 packs A and B together so no slot is wasted. Row 1 holds C and is padded with two <pad> tokens.
Everyday picture: packing is like two small groups sharing a minibus so no seat goes empty. The attention mask, coming next, is the partition that stops one group listening to the other’s conversation.
Three kinds of position get no target:
- the last position of each row, which has no next token;
- positions whose next token is padding;
- A’s last token, whose next token is B’s
<bos>. Training it to predict the start of an unrelated document teaches the packing format, not language, so many pipelines ignore this boundary target. Others keep it; the important thing is to know which you do.
That leaves 4 targets in row 0 and 3 in row 1: seven target positions out of twelve.
Attention masks: what each position may see
Labels say what each position must predict. The attention mask says what it may look at while predicting. They are separate decisions. In a decoder, position may attend to position only if:
- , so it never sees the future;
- is not padding;
- in a packed row, belongs to the same document.
The third rule is the one people skip, so look at a position where it matters. In row 0, position 4 is B’s <bos>, and it must predict cat. Give the five tokens it could see the illustrative values 1, 2, 3, 4 and 1, and let attention simply average whatever is allowed:
- causal mask only: position 4 sees positions 0 to 4, so it averages ;
- with the document mask: it sees only position 4, so it averages .
Same token, same target, different input to the prediction. Without the document mask, B’s prediction depends on an unrelated document A. Llama 3, §3.2: “We use an attention mask that prevents self-attention between different documents within the same sequence.” The paper reports that this had limited impact in standard pre-training but was important in continued pre-training on very long sequences, so it is worth measuring on your own data rather than assuming either way.
The padding at the end of row 1 never reaches a real position, even without rule 2: padding sits after every real token, so no real position satisfies for a padded . Rule 2 matters for left padding, used when generating from a batch of prompts of different lengths, where the padding comes first.
Position IDs are a third, separate choice: the position number each token gets for positional encoding. Isolating documents is the attention mask’s job and does not require restarting position IDs. With rotary position embeddings (RoPE), the attention score between two positions depends only on the distance between them, so shifting a whole document’s positions by a constant leaves its internal attention unchanged. Whether a codebase restarts positions at each document matters more for learned absolute positions and for how the model sees long contexts; codebases differ, so check yours.
cat, and with the document mask it can see only itself.The loss for one position
The model outputs a score, a logit, for every token in the vocabulary, and softmax turns the scores into probabilities. The loss at one position is how surprised the model is by the correct token: minus the log of the probability it gave that token.
Two quick cases with tiny vocabularies:
- Four tokens, all logits equal: each gets probability , so the loss is , whatever the target. This is a model that knows nothing.
- Three tokens with logits : the probabilities are . If the target is the first token, the loss is .
In general, for logits over the vocabulary and target token ,
- : the logit of token ; : the target token.
- The first term, the log-sum-exp, measures the total mass of all scores; the loss falls as rises relative to it.
Implementations compute the log-sum-exp after subtracting the largest logit, which changes nothing mathematically and prevents overflow.
Averaging: every token, or every row?
The batch has seven target positions, each with a loss. The update needs one number, and there are two natural ways to average. Use made-up losses to see the difference: row A has three targets with loss 1 each, and row B has one target with loss 3.
- Token mean: average over all targets, .
- Mean of row means: average each row, then the rows, .
They differ because they weight tokens differently. The token mean gives each of the four tokens weight . The row mean gives each of A’s tokens weight and B’s single token weight : three times as much as one of A’s.
Everyday picture: a school averaging exam marks. The token mean averages every paper in the school; the row mean averages each class first, so one paper in a class of 3 counts ten times as much as one paper in a class of 30.
Neither is wrong in itself, but they are different objectives, not two estimates of one. Pre-training normally intends the token mean, so that every token of text matters equally:
- : the model’s weights; indexes rows, positions.
- : the set of target positions, seven in our batch; .
- : the model’s probability for the true next token, computed under the attention mask.
Hugging Face’s ForCausalLMLoss uses the token mean by default: the cross-entropy is averaged over the labels that are not -100.
Gradient accumulation must keep the same objective
When a batch does not fit in memory, the step is split into microbatches: run each one, add up the gradients, and take one optimizer step at the end. Suppose each row of the example above is one microbatch.
The natural code averages each microbatch and divides by the number of microbatches:
which is the mean of row means, not the token mean. The fix is to divide each microbatch’s summed loss by the total number of targets in the whole step, :
Because gradients add up linearly, the summed gradients then equal the gradient of the token mean exactly. Dividing before backward needs in advance; equally valid is to backpropagate the plain sums and divide the accumulated gradients by once at the end, before clipping. Both are mathematically the same objective. They need not be bitwise identical, because floating-point additions happen in a different order.
How large can the distortion get? If one microbatch has 1,000 target tokens and another has 10, mean-then-divide gives each token of the small microbatch and each token of the large one : a 100-fold difference. Packing rows to a fixed length makes counts similar and hides this; variable-length batches expose it.
Two edge cases need explicit handling:
- A step with no targets. If every label in a step is ignored, and the token mean is . Skip the optimizer step and record that it happened, rather than dividing by zero. With several GPUs, must be the count over all of them, and all of them must make the same decision.
- Where comes from. In Transformers 4.51.3, the Trainer counts targets when the model accepts loss keyword arguments (as
LlamaForCausalLMdoes) or when you pass your own loss function. It fetches all of a step’s microbatches, countslabels.ne(-100).sum()and passes that asnum_items_in_batch, and the loss divides its sum by it. The count is taken on the unshifted labels, while the loss drops each row’s first label when shifting. With two rows of six tokens whose first label is<bos>, the count is 10 although only 8 positions are predicted. The ratio of true to counted targets depends on how many rows and targets each step has, so it is not a constant factor; it slightly reweights steps against each other. For pre-training rows of thousands of tokens it is negligible; for short fine-tuning sequences it is larger. Check the rule in the version you use.
The backward pass
Start with one position. Using the three-token example, the probabilities are and the target is the first token, written as the one-hot vector . The gradient of the loss with respect to the logits is
Read it as instructions: raise the target’s logit by , and lower each wrong token’s logit in proportion to the probability it was given. The entries sum to zero, as always does. Under the token mean each position’s loss is divided by , so with this position contributes . Ignored positions contribute nothing.
Now one layer of weights. If the logits come from a hidden vector through an output matrix , , the chain rule gives an outer product. With :
Each weight’s gradient is the size of the hidden feature it reads times the error of the logit it writes. Over a whole batch, the gradients from every target position are summed:
In a real Transformer the same signal enters the output layer and then flows back through every block.
Clipping the gradient
Before the optimizer step, most pre-training recipes cap the overall size of the gradient. The global norm treats every gradient entry of every parameter as one long vector. With just two entries, :
- the norm is ;
- with a cap of 1, every entry is multiplied by , giving , which has norm 1 and the same direction.
In general, with gradient vector and cap ,
PyTorch 2.6’s clip_grad_norm_ computes exactly this, so the result is up to the tiny in the denominator. Because the norm is global, one parameter with a huge gradient shrinks every other parameter’s gradient too.
Three practical points:
- Clip once per step, after all microbatches are accumulated and before the optimizer step. With FP16 loss scaling, unscale the gradients first, so that the cap applies to their true size.
- Clipping a gradient is not shrinking the update. Adam divides each step by a running estimate of the gradient’s size, so a smaller gradient does not mean a proportionally smaller step. On the very first step, as the next section shows, the update is the same whether or not the gradient was clipped.
- Clipping cannot repair a broken gradient. In PyTorch 2.6, one NaN entry makes the norm NaN and turns every gradient into NaN; one Inf entry makes the norm Inf, which zeroes the finite entries and leaves NaN where the Inf was. By default no error is raised. Check that the returned norm is finite, and skip the step if it is not.
One AdamW step on one number
Adam keeps two running averages for each parameter: , of the gradients, and , of the squared gradients, both starting at zero. Follow one parameter with learning rate , and , and ignore the tiny for now.
Step 1, gradient :
- and .
- Both started at zero, so early on they are too small. Bias correction divides by at step : and .
- The step is , so becomes .
Now repeat with a gradient a hundred times smaller, . Every quantity shrinks, but is again , and the step is again . On the first step, every parameter with a nonzero gradient moves by the learning rate, whatever the gradient’s size, in the direction of its sign.
Everyday picture: a crowd told “everyone take one step towards your exit”. On the first step only the direction each person faces matters, not how hard they were pushed. Adam learns how much to trust each push only as its averages fill up.
That is the special case. In general, at step with gradient :
- : learning rate; : how slowly the two averages forget; : a small constant for numerical safety; : the step number, starting at 1.
Step 2 shows that history matters. Continue from step 1 with gradient 0.2:
- If the second gradient is also 0.2, the step is again 0.1.
- If both gradients were halved, , the step is still 0.1: scaling the whole gradient history by the same positive factor leaves unchanged.
- If only the current gradient is halved, : and . After bias correction, and , so the step is about .
So Adam is insensitive to a consistent rescaling of all gradients, but not to rescaling one step relative to the others. That is why the Trainer’s varying count above is not harmless just because Adam is “scale invariant”.
Weight decay, in two conventions
AdamW adds weight decay: each step, every decayed weight shrinks slightly towards zero, separately from the gradient step. PyTorch 2.6 applies it first, multiplying the weight by , where is the weight_decay argument. Continuing the scalar example with :
The original paper writes it differently. Loshchilov and Hutter (2019), Algorithm 2 update , where is the base learning rate and a schedule multiplier. The decay is multiplied by the schedule, but not by the learning rate.
To convert, compare the shrink per step. In PyTorch the learning rate at step is , so the shrink is . In the paper it is . They match when
A recipe of “learning rate , weight decay 0.1” in PyTorch corresponds to . The same name, “weight decay”, differs by a factor of the learning rate between the two conventions, which matters whenever you copy a recipe from one codebase to another.
Which parameters decay is a separate convention. nanoGPT, for example, decays every parameter with two or more dimensions (the weight matrices and embeddings) and leaves biases and normalization weights alone. Recipes differ, so record the choice.
The same step in real code
A minimal step for a Hugging Face causal language model, written so that accumulation keeps the token mean and bad steps are skipped:
num_targets = sum((mb["labels"][:, 1:] != -100).sum() for mb in microbatches)
if num_targets == 0:
skip_step() # nothing to learn from; never divide by zero
else:
for mb in microbatches:
logits = model(input_ids=mb["input_ids"],
attention_mask=mb["attention_mask"]).logits
loss_sum = F.cross_entropy(
logits[:, :-1].reshape(-1, logits.size(-1)).float(), # predict t+1 from t
mb["labels"][:, 1:].reshape(-1),
ignore_index=-100, reduction="sum")
(loss_sum / num_targets).backward() # gradients add up to the token mean
norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
if torch.isfinite(norm):
optimizer.step()
scheduler.step()
optimizer.zero_grad(set_to_none=True)
The slicing logits[:, :-1] against labels[:, 1:] performs the shift; do not also pass pre-shifted labels. In Hugging Face, attention_mask marks padding, not document boundaries: isolating packed documents needs a separate mechanism, such as variable-length attention kernels driven by document lengths.
At realistic sizes the loss itself costs memory. ForCausalLMLoss upcasts the logits to 32-bit floats before the cross-entropy, a precision safeguard. With a vocabulary of 32,000 and rows of 4,096 tokens, that is MB per row, which is why large training codebases often compute the loss in chunks.
Failure modes
| Mistake | What happens | How it shows up | Check |
|---|---|---|---|
| Shifting twice, or not at all | The model learns to predict the token after next, or to copy its input | Loss unusually high, or near zero within a few hundred steps | Decode one row’s inputs and targets side by side before training |
| Padding not ignored | The model learns to predict <pad> | Training loss lower than it should be; worse text | Count targets per batch; padding must contribute none |
| Mean-then-divide accumulation with variable lengths | Short sequences over-weighted | Loss changes when you change the microbatch size at a fixed batch | Normalize by the step’s total target count |
| Non-finite gradients clipped silently | One NaN turns every gradient into NaN | Weights become NaN, loss becomes NaN | Check that the clip norm is finite; skip the step if not |
| Weight-decay convention mixed up | Effective decay off by a factor of the learning rate | A copied recipe does not reproduce | Compute the per-step shrink explicitly |
Where this update goes next
This one update is what the rest of the series keeps coming back to. Next we go upstream, to where the tokens come from: how a pre-training corpus is built, then how text becomes tokens. Later posts ask how to compute exactly this update when the batch, and then the model, no longer fit on one GPU. For a head start on that side, the paper reviews of ZeRO and Megatron-LM cover sharding the optimizer state and splitting the model across devices.
What to remember
- Targets are the inputs shifted by one. Positions labelled
-100contribute no loss and no direct gradient, but their inputs still receive gradients through later positions that attend to them. - Label masking, attention masking and position IDs are three separate decisions. The attention mask isolates packed documents; Llama 3 does this.
- Token mean and mean of row means are different objectives: 1.5 against 2 for losses and .
- Gradient accumulation keeps the token mean only if every microbatch’s sum is divided by the step’s total target count, before or after backward. A step with no targets must be skipped.
- The gradient with respect to the logits is , divided by under the token mean; a weight matrix’s gradient is the outer product of its input and that error.
- Clip once per step, after accumulation and any unscaling. Clipping preserves direction, does not proportionally shrink Adam’s updates, and cannot repair NaN or Inf.
- AdamW’s first step moves every parameter with a nonzero gradient by the learning rate. Adam is insensitive to rescaling the whole gradient history, not one step.
- PyTorch multiplies weight decay by the learning rate; the original paper does not: .
Try it yourself
- Figure 1. Step to the document mask: seven of twelve positions are targets, and position 4, which predicts
cat, can see only itself. - One loss. Two tokens with equal logits: each gets probability , and the loss is 0.693 whichever is the target.
- Two averages. Row A has losses and row B has . The token mean is 3; the mean of row means is 3.5.
- Accumulation. Same rows as two microbatches. Dividing each sum by the step’s 3 targets gives 3, the token mean; mean-then-divide gives 3.5.
- Clipping. Gradients with a cap of 2: the norm is 10, so multiply by to get (1.2, 1.6).
- First Adam step. Learning rate 0.1, gradient : the parameter increases by 0.1. With gradient it also increases by 0.1.
- Weight-decay conversion. PyTorch recipe with learning rate and weight decay 0.1: the paper’s is .
Sources
- Llama Team, AI @ Meta (2024). “The Llama 3 Herd of Models.” arXiv:2407.21783, v3. §3.2 on masking attention between documents.
- I. Loshchilov and F. Hutter (2019). “Decoupled Weight Decay Regularization.” ICLR 2019. arXiv:1711.05101. Algorithm 2, AdamW.
- D. P. Kingma and J. Ba (2015). “Adam: A Method for Stochastic Optimization.” ICLR 2015. arXiv:1412.6980.
- J. Su, Y. Lu, S. Pan, A. Murtadha, B. Wen and Y. Liu (2021). “RoFormer: Enhanced Transformer with Rotary Position Embedding.” arXiv:2104.09864.
- Hugging Face Transformers 4.51.3.
ForCausalLMLossinloss_utils.py;get_batch_samplesandcompute_lossintrainer.py. - PyTorch 2.6. The AdamW update in
optim/adam.py;clip_grad_norm_inclip_grad.py. - A. Karpathy. nanoGPT,
model.py(configure_optimizers).