foundations 2026-07-20 10 min read

Derivatives, gradients, and how backprop actually flows through attention

The chain rule as multiplied rates, gradients as vectors of partial derivatives, and why a linear layer's Jacobian is just its own weight matrix, transposed for the backward pass.

You already know how to find the slope of a line: rise over run, and it’s the same number everywhere on that line. A derivative asks the same question of a curve, where the honest answer is “it depends where you’re standing.” Zoom in far enough on any point of a smooth curve and it starts looking straight; the derivative at that point is the slope of the straight line it turns into. Every neural network learns by asking exactly this question, once per weight: if I nudge this weight slightly, does the error go up or down, and by how much? Gradient descent, the algorithm underneath essentially all of deep learning training, is nothing but “compute that for every weight, then step in the direction that decreases error.”

A curve doesn’t have a slope, it has infinitely many

Take f(x)=x2f(x) = x^2. Between x=1x=1 and x=2x=2 it rises from 11 to 44, slope 33. Between x=2x=2 and x=3x=3 it rises from 44 to 99, slope 55. Same curve, different interval, different slope: steepness here is a local property, not one number. To pin it down at a single point, pick a second point very close by, compute the ordinary slope between them, then ask what that slope approaches as the gap shrinks toward zero.

Concretely, at x=2x=2 with a small gap hh:

f(2+h)f(2)h=(2+h)24h=4h+h2h=4+h\frac{f(2+h)-f(2)}{h} = \frac{(2+h)^2 - 4}{h} = \frac{4h+h^2}{h} = 4+h

As h0h \to 0, this approaches 44 exactly, not approximately, because the algebra let you cancel hh from the denominator before ever setting it to zero. That cancellation is the one trick underneath every derivative you’ll ever compute, and doing it symbolically for any xx instead of one specific point gives the general definition:

f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

Run that same expansion-and-cancellation trick on f(x)=xnf(x) = x^n and every term of the binomial expansion except the first still carries a leftover hh, so every one of them vanishes in the limit, leaving f(x)=nxn1f'(x) = nx^{n-1}: the power rule, as a consequence rather than a rule to memorize.

Worked check. f(x)=(3x+1)2=9x2+6x+1f(x)=(3x+1)^2 = 9x^2+6x+1, so f(x)=18x+6f'(x) = 18x+6, and at x=1x=1 that’s 2424.

def numerical_derivative(f, x, h=1e-5):
    return (f(x + h) - f(x)) / h

f = lambda x: (3*x + 1)**2
print(numerical_derivative(f, x=1.0))   # ~24.00002, converging on the exact answer

The chain rule: rates multiply, they don’t add

If y=f(g(x))y = f(g(x)), a function of a function, how fast does yy change as xx changes? As xx moves a little, g(x)g(x) moves by some rate, g(x)g'(x); as g(x)g(x) moves a little, ff of it moves by a further rate, but that rate is ff' evaluated at g(x)g(x), not at xx, because ff‘s steepness genuinely differs depending on where you’re standing on ff, and you’re standing at g(x)g(x). The two rates compose by multiplication, the same way a 2x zoom followed by a 3x zoom gives 6x total, not 5x:

dydx=f(g(x))g(x)\frac{dy}{dx} = f'(g(x)) \cdot g'(x)

Checking (3x+1)2(3x+1)^2 at x=1x=1 the chain-rule way: inner g(x)=3x+1g(x)=3x+1, g(x)=3g'(x)=3, g(1)=4g(1)=4; outer f(u)=u2f(u)=u^2, f(u)=2uf'(u)=2u, so f(g(1))=2(4)=8f'(g(1)) = 2(4) = 8. Total: 8×3=248 \times 3 = 24. Matches the direct expansion exactly, which is the whole point: evaluating ff' at the wrong place is the single most common chain-rule mistake, and it’s worth checking against a case you can also verify directly until it stops being tempting.

Gradients: the same derivative, once per weight, packaged into a vector

A neural network doesn’t have one input, it has millions or billions of weights. The gradient asks the derivative question of every one of them simultaneously: nudge weight one, holding all the others fixed, how does the loss change? Then weight two. Then every other weight. Package all those answers into a single vector and you have f\nabla f. Nothing new happens mathematically here, it’s the ordinary single-variable derivative from above, computed one variable at a time while treating every other variable as a temporary constant, a move called a partial derivative.

For f(x,y)=x2y+3yf(x,y) = x^2y + 3y: fx=2xy\frac{\partial f}{\partial x} = 2xy (treat yy as a constant, power rule on x2x^2; the 3y3y term has no xx in it, contributes zero), and fy=x2+3\frac{\partial f}{\partial y} = x^2 + 3 (treat x2x^2 as a constant coefficient). At (2,1)(2,1): f=[4,7]\nabla f = [4, 7].

The gradient isn’t just a bookkeeping convenience, it has a specific geometric meaning worth deriving rather than accepting: it points in the direction of steepest increase. For a small unit step v\vec{v} away from a point, the change in ff is approximately fv\nabla f \cdot \vec{v}, a dot product. Recall from the linear algebra side of this that uv=uvcosθu \cdot v = \|u\|\|v\|\cos\theta; here that’s fcosθ\|\nabla f\| \cos\theta, maximized exactly when θ=0\theta = 0, meaning v\vec v points the same way f\nabla f does. Steepest ascent isn’t a separate fact about gradients, it’s a direct consequence of dot-product geometry.

import numpy as np

def f(x, y):
    return x**2 * y + 3*y

def numerical_gradient(f, x, y, h=1e-5):
    df_dx = (f(x + h, y) - f(x, y)) / h
    df_dy = (f(x, y + h) - f(x, y)) / h
    return np.array([df_dx, df_dy])

grad = numerical_gradient(f, 2.0, 1.0)          # ~[4, 7]
grad_dir = grad / np.linalg.norm(grad)

def directional_change(f, x, y, direction, step=1e-3):
    dx, dy = direction * step
    return f(x + dx, y + dy) - f(x, y)

# the gradient's own direction should beat every other direction tried
for d in [grad_dir, np.array([1.,0.]), np.array([0.,1.]), np.array([-1.,1.])/np.sqrt(2)]:
    print(directional_change(f, 2.0, 1.0, d))

Running this doesn’t just compute the gradient, it empirically confirms the steepest-ascent claim: the change measured along grad_dir comes out larger than along any of the other directions tried.

Jacobians: gradients for layers that output vectors

Most layers inside a network don’t output one number, they output a vector, so the gradient needs a further generalization. The Jacobian of f:RnRmf: \mathbb{R}^n \to \mathbb{R}^m is the m×nm \times n matrix whose row ii is fi\nabla f_i: how much every output responds to every input, all at once.

ObjectRate of change ofShapeWhere it shows up in a transformer
Derivativeone output, one inputscalarnever directly, nothing in an LLM has a single scalar input
Gradientone output, many inputsvectorL\nabla L with respect to all weights, what the optimizer consumes
Jacobianmany outputs, many inputsmatrixevery linear layer and attention block, what backprop chains through

Here’s the fact that makes backprop through a transformer tractable at all: for a linear layer y=Wxy = Wx, the Jacobian yx\frac{\partial y}{\partial x} is exactly WW itself. Entry yi=jWijxjy_i = \sum_j W_{ij}x_j, so yixk=Wik\frac{\partial y_i}{\partial x_k} = W_{ik} (every other term in the sum has no xkx_k in it and drops out), which is just WW‘s own entries, unchanged. A linear transformation’s “slope” is the same everywhere, because it’s linear, so the matrix that defines it and the matrix that describes its rate of change are the same object.

Backprop through a linear layer: the transpose is forced, not chosen

Suppose the loss LL depends on yy, and a later layer has already handed you Ly\frac{\partial L}{\partial y}, a vector with one entry per output. You need Lx\frac{\partial L}{\partial x}, a vector with one entry per input. The chain rule, generalized to vectors, says Lx=JTLy\frac{\partial L}{\partial x} = J^T \frac{\partial L}{\partial y}, and since J=WJ = W:

Lx=WTLy,LW=LyxT\frac{\partial L}{\partial x} = W^T \frac{\partial L}{\partial y}, \qquad \frac{\partial L}{\partial W} = \frac{\partial L}{\partial y}\, x^T

The transpose isn’t a memorized rule, it’s the only option the shapes allow: WW is m×nm \times n, Ly\frac{\partial L}{\partial y} has length mm, and multiplying an m×nm \times n matrix by a length-mm vector doesn’t typecheck under ordinary matmul, inner dimensions have to match. Transposing WW to n×mn \times m is the one arrangement that both typechecks and lands you back at length nn, matching xx.

Worked check. W=[2013]W = \begin{bmatrix}2&0\\1&3\end{bmatrix}, x=[11]x=\begin{bmatrix}1\\1\end{bmatrix}, forward: y=Wx=[24]y = Wx = \begin{bmatrix}2\\4\end{bmatrix}. Given Ly=[11]\frac{\partial L}{\partial y} = \begin{bmatrix}1\\1\end{bmatrix}: Lx=WT[11]=[2103][11]=[33]\frac{\partial L}{\partial x} = W^T\begin{bmatrix}1\\1\end{bmatrix} = \begin{bmatrix}2&1\\0&3\end{bmatrix}\begin{bmatrix}1\\1\end{bmatrix} = \begin{bmatrix}3\\3\end{bmatrix}.

def backward_linear(W, grad_output):
    return W.T @ grad_output

W = np.array([[2.,0.],[1.,3.]])
print(backward_linear(W, np.array([1.,1.])))   # [3, 3]

Attention’s backward pass: three chained rules, nothing new

Att(Q,K,V)=softmax(QKTdk)V\text{Att}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

is three operations chained together, a matmul, a softmax, then another matmul, which is exactly why backpropagating through attention gets asked in interviews: not because it’s new math, but because it’s three applications of the rule above, composed carefully with the right shapes at each step. The one genuinely new piece is softmax’s own Jacobian. For output αi=ezi/kezk\alpha_i = e^{z_i}/\sum_k e^{z_k}, the diagonal entries work out to αi(1αi)\alpha_i(1-\alpha_i) and the off-diagonal entries to αiαj-\alpha_i\alpha_j, which as a full matrix is simply diag(alpha) - outer(alpha, alpha).

def softmax(z):
    z = z - np.max(z, axis=-1, keepdims=True)   # more on this line below
    e = np.exp(z)
    return e / np.sum(e, axis=-1, keepdims=True)

def attention_forward(Q, K, V, d_k):
    z = (Q @ K.T) / np.sqrt(d_k)
    alpha = softmax(z)
    return alpha @ V, alpha, z

Chaining backward through this is: apply the transpose rule through alpha @ V to get L/α\partial L/\partial \alpha, apply the diag-minus-outer softmax Jacobian to get L/z\partial L/\partial z, then apply the transpose rule again through QKTQK^T to get L/Q\partial L/\partial Q and L/K\partial L/\partial K. Building that out fully and checking grad_Q against a finite-difference estimate (perturb every entry of QQ, measure the change, compare) matches to within about 3×1063\times10^{-6}, small enough to be floating-point noise rather than a sign error or a transpose in the wrong place, which is the actual failure mode this kind of derivation invites.

Two failure modes that only show up at this layer

The z - np.max(z, ...) line above isn’t decoration. Softmax involves eze^z, and raw attention scores qTkq^Tk can be large before the dk\sqrt{d_k} scaling fully tames them; exponentiating a large value overflows a floating-point format’s range, the exact same range-limit issue as FP32 versus BF16, showing up again three layers deeper in the stack. Subtracting the max before exponentiating doesn’t change softmax’s output, it’s a scale-invariance property of the formula, but it keeps every intermediate value inside a safe range. Skip it and training doesn’t slow down, it produces NaNs.

The second failure mode is readable directly off the Jacobian formula above: if one attention score saturates toward αi1\alpha_i \to 1, its diagonal gradient term αi(1αi)1×0=0\alpha_i(1-\alpha_i) \to 1 \times 0 = 0. If a score saturates toward 00, that term is 00 too. Either extreme kills the local gradient, not because of some separate instability, but because it’s the direct arithmetic consequence of the formula you already have.

Failure modeCauseFix
NaN loss during trainingSoftmax input overflows before the max-subtractionThe z_shifted trick above, standard in every framework
Vanishing attention gradientsAn attention score saturates near 1 or 0Better initialization, gradient clipping, architectural changes
Backward pass memory blowupMaterializing the full n×nn \times n attention matrixFused kernels (FlashAttention-style) that never store it in full

Common mistakes

Memorizing “bring down the exponent” without being able to reconstruct why: the binomial-expansion derivation above is what survives being combined with other rules later, memorized facts alone don’t.

Evaluating the chain rule’s outer derivative at xx instead of at g(x)g(x), the single most common chain-rule error, and the reason the worked check above exists: it’s cheap to verify against a direct expansion until the habit sticks.

Treating the gradient as a categorically new object instead of what it actually is: a vector of ordinary derivatives, one per input, each computed by freezing every other input.

Conflating gradient (defined for functions with one scalar output) with Jacobian (needed the moment a function outputs a vector): an LLM’s individual layers need Jacobians, only the final scalar loss has a gradient in the strict sense, and backprop’s whole job is chaining Jacobians together until you land back at that one gradient.

Try it yourself

Beginner. For f(x,y)=3x2+2xyy2f(x,y) = 3x^2+2xy-y^2, compute f\nabla f symbolically, then verify it at (1,2)(1,2) using numerical_gradient.

Intermediate. For W=[1231]W = \begin{bmatrix}1&2\\3&1\end{bmatrix}, x=[20]x=\begin{bmatrix}2\\0\end{bmatrix}, Ly=[12]\frac{\partial L}{\partial y}=\begin{bmatrix}1\\2\end{bmatrix}, hand-derive Lx\frac{\partial L}{\partial x} and check it against backward_linear.

Advanced. Derive the off-diagonal softmax Jacobian term αiαj-\alpha_i\alpha_j yourself using the quotient rule on αi=ezi/kezk\alpha_i = e^{z_i}/\sum_k e^{z_k}, differentiated with respect to zjz_j where jij \ne i. This is the one piece above stated rather than derived; closing that gap is what makes the rest of this note something you’ve actually verified rather than taken on trust.


The one-sentence version: a derivative is a slope you have to approach rather than compute directly, the chain rule is just those rates multiplying, a gradient is that same chain rule run once per weight and packed into a vector that provably points toward steepest increase, and backpropagation through attention is nothing but three applications of two rules you already have, chained through the exact matmuls and shapes from vectors and matrices and hitting the exact numerical range limits from bits, floats, and quantization along the way. That softmax Jacobian from attention’s backward pass turns out to have one more job: paired with cross-entropy loss, its messy terms cancel into the single cleanest gradient in this entire series.