LLM Developer DocsLLM Docs

Loading the search index ...

↑ ↓ move, Enter open, Esc close

GitHub

Softmax & Cross-Entropy

Softmax with its maximum shift, the log-softmax loss, the combined gradient on the logits, and perplexity, as ops.py writes them.

ops.py

Softmax and the loss look like one operation, but in this code they are separate. softmax_forward serves attention and sampling. cross_entropy_forward computes its own log-probabilities from the logits, and cross_entropy_backward has its own gradient.

Softmax

What is it?

SoftmaxImplemented

Basic operations

Turns a vector of scores into probabilities along the last axis. The code subtracts the largest score first, so the exponentials cannot overflow.

Why does the model need it?

Attention uses it to turn scores into weights, and sampling uses it to turn logits into a distribution over tokens.

Mathematics

Shift by the maximum, then exponentiate #Link to equation: Shift by the maximum, then exponentiate
ei=exp⁡(xi−max⁡kxk)e_i = \exp\left(x_i - \max_k x_k\right)
Normalize #Link to equation: Normalize
pi=ei∑kekp_i = \frac{e_i}{\sum_k e_k}

Subtracting the maximum does not change the result, because the same factor appears above and below the line.

Why the shift is safe. The factor from any constant c cancels #Link to equation: Why the shift is safe. The factor from any constant c cancels
exi−c∑kexk−c=e−c exie−c∑kexk=exi∑kexk\frac{e^{x_i - c}}{\sum_k e^{x_k - c}} = \frac{e^{-c}\, e^{x_i}}{e^{-c} \sum_k e^{x_k}} = \frac{e^{x_i}}{\sum_k e^{x_k}}

So the code is free to choose c. It chooses the largest score, which makes the largest exponent exactly 0 and every other exponent negative. No exponential can then overflow.

What does each symbol mean?

SymbolMeaningIn codeShape
xxInput scoresxany; the last axis holds the scores
ccThe constant subtracted. The code uses the largest scorenp.max(x, axis=-1, keepdims=True)same as x, with 1 on the last axis
eeShifted exponentialsesame as x
ppProbabilities, which sum to 1 along the last axisnot named in the codesame as x

Tiny numerical example

Illustrative ExampleHand-picked input. Values computed by running ops.softmax_forward; they are not learned values.
Input x
[2.0000, 1.0000, 0.0000]
Shifted exponentials e
[1.0000, 0.3679, 0.1353]
Probabilities
[0.6652, 0.2447, 0.0900]
Moving all scores together changes nothing, and large scores stay finiteIllustrative ExampleHand-picked scores. The last column computes the exponential of the largest score with no shift: at 1002 it does not fit in a 64-bit float, so normalising would give NaN.
Scores xsoftmax_forward(x)exp(max x), with no shift
[2, 1, 0][0.6652, 0.2447, 0.0900]7.3891
[-3, -4, -5][0.6652, 0.2447, 0.0900]0.0498
[0, 1, 2][0.0900, 0.2447, 0.6652]7.3891
[1000, 1001, 1002][0.0900, 0.2447, 0.6652]Infinity

Tensor shapes

Attention weightstransformer.py:83
  1. S(H, T, T)(2, 16, 16)
  2. A(H, T, T)(2, 16, 16)

H = num_heads = 2. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt.

Samplinggenerate.py:11
  1. logits / temperature(V,)
  2. p(V,)

V depends on the corpus. It is not a value in config.py.

Actual implementation

ops.pysoftmax_forwardGitHub: ops.py
def softmax_forward(x):    e = np.exp(x - np.max(x, axis=-1, keepdims=True))    return e / e.sum(axis=-1, keepdims=True)

Code and math connection

EquationRoleLinesCode
Shift by the maximum, then exponentiateComputes itops.py:23e = np.exp(x - np.max(x, axis=-1, keepdims=True))
NormalizeComputes itops.py:24return e / e.sum(axis=-1, keepdims=True)
NormalizeCalls ittransformer.py:83A = softmax_forward(S)
NormalizeCalls itgenerate.py:11p = softmax_forward(logits / temperature)
Why the shift is safe. The factor from any constant c cancelsComputes itops.py:23e = np.exp(x - np.max(x, axis=-1, keepdims=True))

Where is it used?

Source

ops.py, lines 22-24, softmax_forward

PDF reference

Section 4.3, Eq. 23, p. 7

Used by

Called by: generate.sample_next, transformer.attention_forward

Tested by: tests.test_ops.test_softmax_backward_rows_sum_to_zero, tests.test_ops.test_softmax_values_and_stability

Written inline in ops.py, line 30. The same shift by the maximum, written inline before the log-softmax.

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
Shift by the maximum, then exponentiateComputes itops.pysoftmax_forward23GitHub: ops.py, line 23
NormalizeComputes itops.pysoftmax_forward24GitHub: ops.py, line 24
Why the shift is safe. The factor from any constant c cancelsComputes itops.pysoftmax_forward23GitHub: ops.py, line 23

Softmax backward

What is it?

Softmax, backwardImplemented

Basic operations

Given dY, the gradient on softmax's output y, this computes the gradient on its input scores. It is one line, along the last axis.

Why does the model need it?

Attention weights come from softmax, so the backward pass must cross it to reach the scores, and from there the query and key parameters.

Mathematics

Each output depends on every input, so the derivative is a matrix.

The derivative of softmax. The code never builds this matrix #Link to equation: The derivative of softmax. The code never builds this matrix
∂yi∂xj=yi(δij−yj)\frac{\partial y_i}{\partial x_j} = y_i\left(\delta_{ij} - y_j\right)

The code never forms that matrix. Multiplying dY by it, and writing the sum once, gives the single line that the function holds.

The backward line, which multiplies dY by that derivative #Link to equation: The backward line, which multiplies dY by that derivative
dxj=yj(dYj−∑iyi dYi)dx_j = y_j\left(dY_j - \sum_i y_i\, dY_i\right)

What does each symbol mean?

SymbolMeaningIn codeShape
yySoftmax output from the forward passysame as x; the last axis holds the probabilities
dYdYGradient of the loss on ydYsame as y
dxdxGradient on the input scores. The function returns it without naming itdS in attention_backwardsame as y
δij\delta_{ij}1 when i equals j, otherwise 0not named in the codescalar

Tiny numerical example

Softmax backwardIllustrative ExampleHand-picked numbers from test_softmax_backward_rows_sum_to_zero, with two rows. Each row is computed on its own, because the sum is along the last axis. dY is chosen by hand.
StepCodeValue
Softmax outputy[[0.6652, 0.2447, 0.0900], [0.0420, 0.8438, 0.1142]]
Gradient on the outputdY[[1.0000, 0.0000, 0.0000], [0.0000, 2.0000, -1.0000]]
Sum over each rownp.sum(y * dY, axis=-1, keepdims=True)[[0.6652], [1.5734]]
Gradient on the scoresy * (dY - ...)[[0.2227, -0.1628, -0.0599], [-0.0661, 0.3600, -0.2939]]

Tensor shapes

Attentiontransformer.py:100
  1. dA(H, T, T)(2, 16, 16)
  2. dS(H, T, T)(2, 16, 16)

H = num_heads = 2. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt.

Actual implementation

ops.pysoftmax_backwardGitHub: ops.py
def softmax_backward(y, dY):    return y * (dY - np.sum(y * dY, axis=-1, keepdims=True))

Code and math connection

EquationRoleLinesCode
The derivative of softmax. The code never builds this matrixComputes itops.py:27return y * (dY - np.sum(y * dY, axis=-1, keepdims=True))
The backward line, which multiplies dY by that derivativeComputes itops.py:27return y * (dY - np.sum(y * dY, axis=-1, keepdims=True))
The backward line, which multiplies dY by that derivativeCalls ittransformer.py:100dS = softmax_backward(c["A"], dA)
The backward line, which multiplies dY by that derivativeTests ittests/test_ops.py:40dz = softmax_backward(p, dp)

Where is it used?

Only attention calls it. The loss and sampling do not.

Source

ops.py, lines 26-27, softmax_backward

PDF reference

Section 4.3, Eq. 24, 25, p. 7

Used by

Called by: transformer.attention_backward

Tested by: tests.test_ops.test_softmax_backward_rows_sum_to_zero

  • Calls it: transformer.py, line 100 (attention_backward), for The backward line, which multiplies dY by that derivative
  • Tests it: tests/test_ops.py, line 40 (test_softmax_backward_rows_sum_to_zero), for The backward line, which multiplies dY by that derivative
Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
The derivative of softmax. The code never builds this matrixComputes itops.pysoftmax_backward27GitHub: ops.py, line 27
The backward line, which multiplies dY by that derivativeComputes itops.pysoftmax_backward27GitHub: ops.py, line 27

How this reaches a parameter update. Softmax has no parameters, so nothing is stored here. dS (transformer.py:100) goes on to dQh and dKh (transformer.py:101), and from those to the gradients of the query and key weights (transformer.py:106-108). batch_loss_and_grads averages them (train.py:61), clip_grad_norm may scale them (train.py:62), and opt.step uses them (train.py:66). Attention covers those steps.

Cross-entropy

What is it?

Cross-entropyImplemented

Loss

The loss. For each position it takes the log-probability the model gave to the correct next token, negates it, and averages over positions. It also returns the probabilities, which the backward pass reuses.

Why does the model need it?

Training needs one number to reduce. This loss falls when the model gives more probability to the correct token. The code works in log-probabilities, so it never takes the log of a number that has underflowed to 0.

Mathematics

The same shift as softmax, written inline #Link to equation: The same shift as softmax, written inline
ztv=logitstv−max⁡klogitstkz_{tv} = \text{logits}_{tv} - \max_{k} \text{logits}_{tk}
Log-softmax #Link to equation: Log-softmax
log⁡ptv=ztv−log⁡∑keztk\log p_{tv} = z_{tv} - \log \sum_{k} e^{z_{tk}}
Mean over positions #Link to equation: Mean over positions
L=−1T∑tlog⁡pt, ytL = -\frac{1}{T} \sum_{t} \log p_{t,\, y_t}
The probabilities it returns #Link to equation: The probabilities it returns
Ptv=elog⁡ptvP_{tv} = e^{\log p_{tv}}

The mean is over the positions of one sequence. batch_loss_and_grads then averages over the windows of a batch (train.py:17).

What does each symbol mean?

SymbolMeaningIn codeShape
logits\text{logits}The head's outputlogits(T, V)
yty_tThe correct next token id at position ttargets(T,)
zzLogits with each row's largest value subtractedz(T, V)
log⁡p\log pLog-probabilitieslogp(T, V)
LLThe losslossscalar
PPProbabilitiesprobs in the caller(T, V)
TTPositions in this sequenceTscalar

Tiny numerical example

Two positions, three tokensIllustrative ExampleHand-picked logits and targets. They are not model output.
StepCodeValue
Logitslogits[[2.0000, 1.0000, 0.0000], [0.5000, 0.5000, 3.0000]]
Correct token at each positiontargets[0.0000, 2.0000]
Shiftedlogits - logits.max(axis=-1, keepdims=True)[[0.0000, -1.0000, -2.0000], [-2.5000, -2.5000, 0.0000]]
Log-probabilitiesz - np.log(np.exp(z).sum(axis=-1, keepdims=True))[[-0.4076, -1.4076, -2.4076], [-2.6520, -2.6520, -0.1520]]
Picked at the targetslogp[np.arange(T), targets][-0.4076, -0.1520]
Loss-(...).mean()0.2798
Probabilities returnednp.exp(logp)[[0.6652, 0.2447, 0.0900], [0.0705, 0.0705, 0.8590]]
Why the code keeps log-probabilitiesIllustrative ExampleHand-picked logits from test_cross_entropy_extreme_logits_no_nan. The target's probability underflows to exactly 0, and the log of 0 is minus infinity. The code never takes that log, so its loss is finite.
StepCodeValue
Logitslogits[[1000.0000, 0.0000, -1000.0000]]
Correct tokentargets[2.0000]
Probabilitiesnp.exp(logp)[[1.0000, 0.0000, 0.0000]]
Lossloss2000.0000

Tensor shapes

targets has shape (T,), and the loss is one number.

From logits to probabilities. The loss is a single numbertransformer.py:173
  1. logits(T, V)(16, V)
  2. logp(T, V)(16, V)
  3. probs(T, V)(16, V)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. V depends on the corpus. It is not a value in config.py.

Actual implementation

ops.pycross_entropy_forwardGitHub: ops.py
def cross_entropy_forward(logits, targets):    z = logits - logits.max(axis=-1, keepdims=True)    logp = z - np.log(np.exp(z).sum(axis=-1, keepdims=True))    T = len(targets)    loss = -logp[np.arange(T), targets].mean()    return float(loss), np.exp(logp)

Code and math connection

EquationRoleLinesCode
The same shift as softmax, written inlineComputes itops.py:30z = logits - logits.max(axis=-1, keepdims=True)
Log-softmaxComputes itops.py:31logp = z - np.log(np.exp(z).sum(axis=-1, keepdims=True))
Mean over positionsComputes itops.py:33loss = -logp[np.arange(T), targets].mean()
Mean over positionsCalls ittransformer.py:173loss, probs = cross_entropy_forward(logits, targets)
Mean over positionsCalls ittrain.py:31loss, _ = cross_entropy_forward(logits, y)
Mean over positionsTests ittests/test_ops.py:48loss, probs = cross_entropy_forward(logits, targets)
The probabilities it returnsComputes itops.py:34return float(loss), np.exp(logp)

Where is it used?

loss_and_grads keeps both results. validation_loss keeps only the loss.

Source

ops.py, lines 29-34, cross_entropy_forward

PDF reference

Section 4.3, Eq. 26, 28, p. 7

Used by

Called by: train.validation_loss, transformer.loss_and_grads

Tested by: tests.test_ops.test_cross_entropy_extreme_logits_no_nan, tests.test_ops.test_cross_entropy_is_mean_over_positions, tests.test_ops.test_cross_entropy_worked_example

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
The same shift as softmax, written inlineComputes itops.pycross_entropy_forward30GitHub: ops.py, line 30
Log-softmaxComputes itops.pycross_entropy_forward31GitHub: ops.py, line 31
Mean over positionsComputes itops.pycross_entropy_forward33GitHub: ops.py, line 33
The probabilities it returnsComputes itops.pycross_entropy_forward34GitHub: ops.py, line 34

Cross-entropy backward

What is it?

Cross-entropy, backwardImplemented

Loss

Computes dZ, the gradient of the loss on the logits. It copies the probabilities, subtracts 1 at each position's correct token, and divides by T.

Why does the model need it?

This is where every gradient starts. dZ goes straight into the backward pass of the head, and the rest of the backward pass follows from there.

Mathematics

The derivative of the loss at one position #Link to equation: The derivative of the loss at one position
∂ℓt∂logitstv=Ptv−1[v=yt]\frac{\partial \ell_t}{\partial \text{logits}_{tv}} = P_{tv} - \mathbf{1}[v = y_t]

The loss is the mean over T positions, and each position depends only on its own row of logits. So each row is divided by T.

The combined backward, where E_y is one-hot at each correct token #Link to equation: The combined backward, where E_y is one-hot at each correct token
dZ=P−EyTdZ = \frac{P - E_y}{T}

What does each symbol mean?

SymbolMeaningIn codeShape
PPThe probabilities cross_entropy_forward returnedprobs(T, V)
EyE_yOne-hot rows, with 1 at each position's correct token. The code subtracts in place and never builds this arraynot named in the code(T, V)
ℓt\ell_tThe loss at one position, before the meannot named in the codescalar
dZdZGradient of the loss on the logitsdZ(T, V)
TTPositions in this sequenceTscalar

Tiny numerical example

A worked backwardIllustrative ExampleThe same hand-picked logits and targets as the cross-entropy example above.
StepCodeValue
ProbabilitiesdZ = probs.copy()[[0.6652, 0.2447, 0.0900], [0.0705, 0.0705, 0.8590]]
Subtract 1 at each correct tokendZ[np.arange(T), targets] -= 1.0[[-0.3348, 0.2447, 0.0900], [0.0705, 0.0705, -0.1410]]
Divide by Treturn dZ / T[[-0.1674, 0.1224, 0.0450], [0.0353, 0.0353, -0.0705]]

By hand, at position 0 and its correct token: (0.6652 - 1) / 2 = -0.1674. In this row the entry for the correct token is negative and the others are positive: raising the correct logit lowers the loss.

Checking this backward numericallyIllustrative ExampleA test-time idea, not part of the pipeline: gradcheck.numerical_grad is only called by tests. It nudges each number up and down by h = 1e-5 and measures how the loss changes. Tests compare the central differences here with that real function.
GradientFrom the backward formulaFrom central differencesLargest gap
dZ[-0.1674, 0.1224, 0.0450][0.0353, 0.0353, -0.0705][-0.1674, 0.1224, 0.0450][0.0353, 0.0353, -0.0705]2.0e-12

Tensor shapes

The start of the backward passtransformer.py:175
  1. probs(T, V)(16, V)
  2. dZ(T, V)(16, V)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. V depends on the corpus. It is not a value in config.py.

Actual implementation

ops.pycross_entropy_backwardGitHub: ops.py
def cross_entropy_backward(probs, targets):    T = len(targets)    dZ = probs.copy()    dZ[np.arange(T), targets] -= 1.0    return dZ / T

Code and math connection

Where is it used?

Source

ops.py, lines 36-40, cross_entropy_backward

PDF reference

Section 4.3, Eq. 27, 28, p. 7

Used by

Called by: transformer.loss_and_grads

Tested by: tests.test_ops.test_cross_entropy_is_mean_over_positions, tests.test_ops.test_cross_entropy_worked_example

  • Calls it: transformer.py, line 175 (loss_and_grads), for The combined backward, where E_y is one-hot at each correct token
  • Tests it: tests/test_ops.py, line 59 (test_cross_entropy_is_mean_over_positions), for The combined backward, where E_y is one-hot at each correct token
Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
The derivative of the loss at one positionComputes itops.pycross_entropy_backward39GitHub: ops.py, line 39
The combined backward, where E_y is one-hot at each correct tokenComputes itops.pycross_entropy_backward38GitHub: ops.py, line 38
The combined backward, where E_y is one-hot at each correct tokenComputes itops.pycross_entropy_backward39GitHub: ops.py, line 39
The combined backward, where E_y is one-hot at each correct tokenComputes itops.pycross_entropy_backward40GitHub: ops.py, line 40

How this reaches a parameter update. The loss has no parameters, so nothing is stored here. dZ (transformer.py:175) goes straight into linear_backward for the head (transformer.py:176), whose results fill head_w and head_b (transformer.py:178). The rest of the backward pass follows from dXf. batch_loss_and_grads averages the gradients (train.py:61), clip_grad_norm may scale them (train.py:62), and opt.step uses them (train.py:66).

Softmax lab

Edit the logits, or choose another correct token. Sampling covers temperature, which this lab leaves out.

Logits to probabilities, loss and gradientIllustrative ExampleHand-picked logits that you can edit. They are not model output. With one position, T is 1, so dZ is P minus the one-hot row.
logits
token 0token 1token 2
t0

Loss: 0.4076

P = exp(logp), shape (1, 3)
0.66520.24470.09
dZ = (P - one-hot) / T, shape (1, 3)
-0.33480.24470.09
Probabilities from these logitsIllustrative ExampleThe correct token is token 0. The loss falls as its bar grows.
Data table
LabelProbability
token 00.6652
token 10.2447
token 20.0900

Perplexity

What is it?

PerplexityImplemented

Loss

The exponential of the loss. train.py prints it once per epoch, next to the validation loss, and nothing else reads it.

Why does the model need it?

The model does not need it: it is not part of the loss or the update. It is a printed number that is easier to read than a log value. In general, it can be read as the number of equally likely tokens that would give the same loss.

Mathematics

Perplexity #Link to equation: Perplexity
perplexity=eL\text{perplexity} = e^{L}

What does each symbol mean?

SymbolMeaningIn codeShape
LLA loss. train.main passes the validation lossloss in perplexity, v in train.mainscalar

Tiny numerical example

Loss and perplexityIllustrative ExampleHand-picked losses, not losses from a training run.
LossWhere it comes fromPerplexity
0.2798The loss of the cross-entropy example above1.3229
1.6094ln 5, the loss of uniform probabilities over 5 tokens5.0000
5.7038ln 300, the value test_perplexity_of_uniform_is_vocab_size uses300.0000

Tensor shapes

It takes one number and returns one number, so there are no tensors.

Actual implementation

ops.pyperplexityGitHub: ops.py
def perplexity(loss):    return float(np.exp(loss))

Code and math connection

EquationRoleLinesCode
PerplexityComputes itops.py:60return float(np.exp(loss))
PerplexityCalls ittrain.py:71print(f"epoch {epoch + 1}: val loss {v:.3f} ppl {perplexity(v):.1f}")
PerplexityTests ittests/test_ops.py:88assert np.isclose(perplexity(np.log(300)), 300)

Where is it used?

train.main computes the validation loss after each epoch, and prints it with its perplexity:

train.pymainGitHub: train.py
        v = validation_loss(params, cfg, va_x, va_y)        print(f"epoch {epoch + 1}: val loss {v:.3f}  ppl {perplexity(v):.1f}")
Source

ops.py, lines 59-60, perplexity

PDF reference

Section 4.3, p. 7

Used by

Called by: train.main

Tested by: tests.test_ops.test_perplexity_of_uniform_is_vocab_size

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
PerplexityComputes itops.pyperplexity60GitHub: ops.py, line 60