Matrix calculus and reverse-mode automatic differentiation
Before this lesson: linear algebra 15 (JVP and VJP, first view) and linear algebra 16 (matrix differentials, ). From the calculus track: the chain rule and computational graphs, automatic differentiation and numerical differentiation. This is the first lesson of the track, so it repeats the key facts briefly. Time: about 75 minutes, plus the notebook.
Why this matters #
In ML for science you differentiate more than neural networks.
- A Gaussian process or a Kalman filter has a likelihood with and . Fitting its hyperparameters needs their gradients (lesson 07).
- An implicit time step contains a linear solve (lesson 02).
- Adjoint methods for ODEs, 4D-Var in weather forecasting and full-waveform inversion in seismology are reverse mode applied to a simulator (lessons 03, 04 and 06).
- A hybrid climate model puts a network inside a physics code.
PyTorch and JAX know the VJPs of their built-in operations. A Fortran physics code, a sparse direct solver or a custom GPU kernel has none: you must write its VJP yourself, or you cannot train through it. The same rules also decide the cost. Reverse mode gives the gradient with respect to parameters for a few times the cost of the loss, but it must keep (or recompute) the intermediate values. This memory cost is the central topic of lesson 03.
1. Predict first #
2. The picture #
One variable. Let . A small change gives . So the derivative is the linear map "multiply by ". Suppose a scalar loss depends on , and we know . Then
so .
Two facts appear already. First, the rule uses the output , so the forward pass must keep it. Second, the same factor moves a small change forward and a sensitivity backward.
Many variables. Let with and . The derivative is the linear map , where is the Jacobian. There are two ways to use it.
- Push a direction forward: . This is a Jacobian-vector product (JVP). It lives in the output space .
- Pull a sensitivity back: . This is a vector-Jacobian product (VJP). It lives in the input space .
A small example. at has
The JVP with is . The VJP with is . The two are linked by one identity: and . They are equal, as they must be for every and . This identity is the definition of the VJP in section 3.
A program. A program is a chain of small operations with Jacobians . By the chain rule, its gradient is
Multiply from the right: start with the number , then apply , then , and so on. Each step is one VJP, and the vector stays small. Nobody builds a Jacobian. For a softmax over classes the Jacobian has entries, but its VJP costs .
The figure runs this on a small network, .
Step through the tape. Press Next step. The forward pass fills the values (blue), one node at a time. Then the backward pass fills the cotangents (violet), in the reverse order. Watch node : it is used twice by , so it receives two contributions (highlighted arrows), and they add up to . At the end, a central difference checks one entry of .
In the figure, uses twice and . A programmer writes the backward pass so that the second contribution replaces the first. What does the code return for ?
Each use of contributes . The correct cotangent is the sum of the contributions, . If the second write replaces the first, the result is : every gradient below this node is then too small by a factor of 2. Cotangents of a value that is used more than once must be added.
3. Definitions and main results #
Derivatives as linear maps #
Arrays of any shape are vectors. For matrices we use the Frobenius inner product .
Definition (derivative, JVP, VJP). is differentiable at if
with a linear map . The JVP is the map . The VJP is the adjoint map , defined by
If you flatten and into vectors, is the Jacobian and . The inner-product form is better for matrices: it gives directly as a matrix of the shape of , with no flattening.
Notation. For a scalar loss , a bar marks the gradient of with respect to a variable: . It has the shape of . It is also called the cotangent or the adjoint of .
Lemma (a gradient through a map is a VJP). If and , then .
Proof.
- , by the definition of the gradient of .
- , by the definition of the derivative.
- So for every . The gradient is the vector that represents in this way, so .
Recipe for any VJP.
- Write the differential in terms of .
- Write .
- Move everything away from with transposes and the cyclic rule , until it reads . Then .
must have the shape of . This is a free check of your algebra.
The VJPs of the primitives #
Here is the elementwise product, is the all-ones vector and .
| Primitive | Differential | VJP |
|---|---|---|
| , elementwise | ||
| ; | ; | ; |
| over some axes | broadcast back to the shape of | |
| (bias added to every row) | ; = sum of the rows of | |
| , | ||
| (linear solve) | , | |
We derive the important rows now. Each derivation follows the recipe.
Sum and broadcasting are adjoints of each other. . This is why the gradient of a bias is a sum over the batch: the forward pass broadcasts to every row, so the backward pass sums the rows.
Matrix product .
- (product rule).
- First term, with the cyclic rule:
- Second term:
- So and .
Shape check: is , is , so and are . Then is , the shape of . For a linear layer : (an outer product, rank one) and . The backward pass is two matrix products of the forward size, so it costs about two forward passes.
with of size and of size . What is , and what is its shape?
has the shape of , . The only product of and with the shape of is : . The option is , the cotangent of the other factor. A cotangent always has the shape of its variable; use this to check your formulas.
Softmax (row by row for a batch).
- , so .
- , so
- The matrix is symmetric, so the VJP applies the same matrix: . The cost is ; the matrix is never built.
Linear solve , where is a vector or a matrix.
- Differentiate : , so .
- Move to the other side of the inner product as its transpose:
- Name : this is one solve with . The right side becomes
- Read off and .
The forward pass factors (for example ). The same factors also solve with , because . So the backward solve is cheap, and no inverse is ever formed.
Log-determinant (Jacobi's formula, from linear algebra 16).
- with , so .
- .
- So , and .
The formula also holds when . Compute the value from an LU or Cholesky factorization
(slogdet in NumPy), never as : overflows or underflows long before
does. For a symmetric positive definite (Cholesky),
and .
Reverse mode is back substitution #
A program computes , then for , in this order. is a scalar. Each can be an array. The tape (also called the Wengert list) stores the nodes in this order, with their parents and the values their VJPs need.
Theorem (the reverse sweep gives the gradient). Set , and for
where is the derivative of with respect to its input . Then .
Why this is true, in one picture. Write all the linearized steps as one system of equations. Each step depends only on earlier steps, so the system is triangular. The gradient is the solution of the transposed system, and a transposed lower-triangular system is solved from the last unknown to the first: that is the backward sweep.
Proof of the theorem
- Linearize every node: , where if is a parent of , and otherwise. Parents come earlier, so for : is strictly lower triangular (block by block).
- With , all these equations together read , where puts into block 0. is unit lower triangular, so it is invertible.
- , where selects block . So with .
- is unit upper triangular. Solve by back substitution, last block first: and . This is the sweep of the theorem with , and .
Three consequences:
- Order. The sweep visits the nodes in reverse tape order. A node is processed only after all its users have added their terms.
- Fan-out adds. A value used by several nodes collects one term per use. A parameter shared by many layers, or by many time steps of a simulation, gets the sum of all terms.
- Forward mode solves the same system the other way: forward substitution of for one input direction , in program order, together with the forward pass.
Forward or reverse? #
Let .
| forward mode (JVP) | reverse mode (VJP) | |
|---|---|---|
| one pass gives | (column for ) | (row for ) |
| full Jacobian | passes | passes |
| gradient of a scalar loss | passes | 1 pass |
| time of one pass | a small constant times | a small constant times |
| extra memory | one tangent per live value; nothing is kept | the tape: every value a VJP needs, for the whole program |
The small constant is the cheap gradient principle. For programs made of arithmetic operations, the Baur–Strassen theorem bounds the cost of the gradient by 5 times the cost of the function. For a network dominated by matrix products, the backward pass costs about two forward passes.
Choose a shape. Pick a preset or move the sliders for the number of inputs and outputs . The bars (log scale) count the passes needed for the full Jacobian: forward mode needs , reverse mode needs . Notice the memory line: only reverse mode stores a tape.
- Reverse wins when : a loss, a data misfit, a log-likelihood. Training, 4D-Var, seismic full-waveform inversion and fitting the hyperparameters of a Gaussian process all have .
- Forward wins when , or when memory is the limit. Examples: how a whole simulated field changes with three source parameters; the Jacobian of a small ODE right-hand side for an implicit solver. A sparse Jacobian (a PDE stencil) needs only a few JVPs with grouped ("colored") seeds.
- Memory is the price of reverse mode. For a long time loop the tape grows with the number of steps. Checkpointing stores only some values and recomputes the rest (lesson 03).
A loss is computed by a simulator. Which mode gives the full gradient most cheaply, and how many passes does it need?
One reverse pass gives one row of the Jacobian; with this row is the whole gradient. Forward mode gives one column per pass, so it needs passes, and each pass costs about the same as a reverse pass. The price of reverse mode is memory, not time.
Checking a gradient #
A gradient code needs a test. Pick a random direction and compare the directional derivative with the central difference
Two errors compete.
- The truncation error comes from the Taylor series. For central differences the terms cancel, so it is . (For the forward difference it is only .)
- The rounding error comes from the subtraction of two nearly equal numbers. It is about , where is the machine epsilon of float64.
The sum is smallest when both parts have similar size, at (times the scale of ). There the relative error is about . One random direction tests all entries of the gradient at once for 2 function calls; a full finite-difference gradient needs calls.
Test a gradient code. The function is for a matrix. The curves show the relative error of the central (solid) and forward (dashed) differences against the step . Switch to the buggy gradient ( instead of ): the curves stop at a floor that no choice of removes. Then move the asymmetry slider all the way to the left (). The bug becomes invisible, because for a symmetric matrix.
You check a gradient in float64 with central differences. Which step gives the smallest error for a well-scaled problem ( and of size about 1)?
The total error is about . Its minimum is at . At the rounding part is about : far worse. The value is the best step for the forward difference, whose truncation error is .
Answers to section 1.
- No inverse. The backward pass needs one solve with (), and it reuses the LU factors of the forward pass. Then is an outer product.
- No difference. Since , , and the shift cancels in . In matrix form, : the softmax ignores a common shift of its inputs, so its VJP ignores a common shift of its cotangent.
- Add them. : the first term comes through the factor , the second through . This is the product rule, and it is fan-out in the theorem above.
4. Worked example #
(a) A Gaussian negative log-likelihood. Gaussian process regression, Kalman filters and 4D-Var all contain the function
with a covariance matrix and a residual (the constant is dropped). Take
Forward pass (the tape). The inputs are and .
- , so .
- .
- .
- .
Backward pass, from .
- , so and .
- Log-determinant: .
- Inner product : and .
- Solve: , so . Then , and .
Result.
is used twice (by the solve and by the inner product), so its cotangent has two terms: this is fan-out. The closed form is the classic gradient of a Gaussian negative log-likelihood with respect to its covariance matrix.
In the worked example, why does have two terms, and ?
Each use of sends back one cotangent: through the inner product and through the solve. They add up (fan-out). The term does not depend on at all. The two terms are equal here only because is symmetric and .
(b) Softmax. gives and . Take , so and . Then
Check with the known derivatives: and . The entries sum to zero, as the shift invariance requires.
5. In code #
NumPy checks the worked example. The backward pass below follows the four backward steps of section 4 line by line; a random-direction central difference tests it.
import numpy as np
# L(K, b) = 1/2 b^T K^{-1} b + 1/2 log det K
def loss(K, b):
return 0.5 * b @ np.linalg.solve(K, b) + 0.5 * np.linalg.slogdet(K)[1]
def loss_vjp(K, b):
alpha = np.linalg.solve(K, b) # forward: one solve
Kbar = 0.5 * np.linalg.inv(K).T # logdet VJP (fine for 2x2; use a factorization at scale)
bbar = 0.5 * alpha # q = b^T alpha, first use of b
abar = 0.5 * b # q = b^T alpha, cotangent of alpha
bs = np.linalg.solve(K.T, abar) # solve VJP: one solve with K^T
return Kbar - np.outer(bs, alpha), bbar + bs # fan-out: b gets two terms
K = np.array([[2.0, 1.0], [1.0, 3.0]])
b = np.array([1.0, 2.0])
Kbar, bbar = loss_vjp(K, b)
print(loss(K, b)) # 1.5047189562170504
print(Kbar) # [[ 0.28 -0.16] [-0.16 0.02]]
print(bbar) # [0.2 0.6]
# Gradient check in one random direction (central differences, float64)
rng = np.random.default_rng(0)
dK, db = rng.standard_normal((2, 2)), rng.standard_normal(2)
h = 1e-5
fd = (loss(K + h * dK, b + h * db) - loss(K - h * dK, b - h * db)) / (2 * h)
exact = np.sum(Kbar * dK) + bbar @ db
print(abs(fd - exact) / abs(exact) < 1e-8) # True
# Jacobi's formula d log|det X| = tr(X^{-1} dX), also when det X < 0
X = np.array([[1.0, 2.0, 0.0], [3.0, 1.0, 1.0], [0.0, 1.0, 2.0]])
dX = rng.standard_normal((3, 3))
fd = (np.linalg.slogdet(X + h * dX)[1] - np.linalg.slogdet(X - h * dX)[1]) / (2 * h)
exact = np.trace(np.linalg.solve(X, dX))
print(round(np.linalg.det(X), 6), abs(fd - exact) / abs(exact) < 1e-8) # -11.0 True
In PyTorch the same check is torch.autograd.gradcheck, and a new primitive with a hand-written VJP
is a torch.autograd.Function (in JAX: jax.custom_vjp). Its backward method is one row of the
table in section 3. The notebook builds a small tape in NumPy and checks every rule of the table.
6. Where it shows up #
- Every autodiff framework.
loss.backward()is the sweep of section 3. To add your own primitive (a fused kernel, a call into a C++ or Fortran physics library, a sparse solver), you register its VJP. - Gaussian likelihoods. Gaussian process hyperparameters, Kalman-filter likelihoods and the background term of 4D-Var (lessons 06 and 07) need solves and log-determinants. By the chain rule, the gradient with respect to a kernel parameter is with : section 4 plus one more inner product.
- Normalizing flows and change of variables. The term with the Jacobian of the flow has the VJP , which needs a factorization of . This is one reason why flows use triangular or coupling layers, whose determinants are cheap.
- Seismic imaging. Full-waveform inversion fits a velocity model with millions of cells to recorded seismograms. The misfit is one number, so reverse mode (there called the adjoint-state method) gives the whole gradient for about the cost of one more wave simulation (lesson 04).
- Weather forecasting. 4D-Var gets the gradient of its cost function from an adjoint model of the forecast model (lesson 06). In the other direction, a JVP tells how a whole simulated field moves with each of a few physical parameters.
- Training at scale. Activation memory is the tape: it grows with depth, batch size and sequence length. Activation checkpointing trades it for recomputation.
7. Common mistakes #
- Building Jacobians or inverses. An softmax Jacobian, or for a solve. Use
the VJP formulas: for softmax, one transposed solve for
solve. - instead of for a non-symmetric , in the solve and log-det VJPs. The shapes match, so only a test with a non-symmetric matrix catches this bug.
- Overwriting instead of adding cotangents at fan-out. A related bug is aliasing: if two
cotangents are the same array object, an in-place
+=on one changes the other. - Forgetting to unbroadcast. The cotangent of a bias is summed over the batch. A shape check catches this.
- Symmetric parameters. One off-diagonal parameter gets (section 4).
- A weak gradient check. Forward differences, float32, , or one coordinate only. Use float64, central differences, to and a random direction.
- "Reverse mode is always better." For , or with a tight memory budget, forward mode can be the better choice.
8. Check yourself #
- with and . Give and and
check their shapes.
Answer
: . : . Both have the shapes of and . - Scalar solve: with , and . Compute and with
the solve VJP, and check against .
Answer
. and . Directly: . - Why is the VJP of equal to and not ?
When do the two agree?
Answer
, and , so , that is, . They agree when is symmetric (for example a covariance matrix). - A climate emulator maps 4 forcing parameters to a field of values, and a scalar loss
compares the field with observations. (a) How do you get the full Jacobian of the emulator, and
with how many passes? (b) And the gradient of the loss with respect to the 4 parameters?
Answer
(a) Forward mode, 4 JVPs (reverse mode would need VJPs). (b) One reverse pass, or 4 forward passes: both are cheap. Forward mode keeps no tape, so it wins if the emulator's intermediate values do not fit in memory; otherwise one reverse pass is the cheapest. - In the sweep, why must the nodes be visited in reverse tape order? What goes wrong if a node is
processed before all its users?
Answer
The sweep is back substitution with an upper triangular matrix: needs the final of every node that uses , and those come later on the tape. If node is processed too early, it passes on an incomplete cotangent, and the paths through the missing users are lost. - Your gradient check in float64 with gives a relative error of . Bug, or a
bad check?
Answer
Probably the check. The rounding error is about , which is large when is much larger than the directional derivative. Repeat with (scaled to ). If the error stays above about for every , look for a bug: a bug gives an error floor that does not move with .
Define the vector-Jacobian product with inner products.
It is the map with for all . In flattened form, .
. Give the VJPs and .
and . Each has the shape of its variable.
(a linear solve). Give the VJPs, and say what the backward pass computes.
(one solve with , reusing the LU factors) and . No inverse is formed.
Give the VJP of , and the formula behind it.
, from Jacobi's formula .
A value is used by two later nodes. How is computed in reverse mode?
As the sum of the two contributions, one per use (fan-out adds). It is passed on only after both users are processed.
How many passes does each mode need for the full Jacobian of ?
Forward mode: JVPs (one column each). Reverse mode: VJPs (one row each). Each pass costs a small constant times .
What is the price of reverse mode, compared with forward mode?
Memory. It must store the tape (every intermediate value a VJP needs) for the whole program, or recompute parts of it (checkpointing).
Why do central differences beat forward differences in a gradient check?
Their truncation error is instead of , because the terms of the Taylor series cancel. So they reach about relative error instead of about .
What is the best step for central differences in float64, and why?
About (times the scale of ). It balances the truncation error against the rounding error .
Why is the softmax VJP ?
needs one inner product and elementwise operations; the Jacobian is never built.
9. Interview angle #
- "Implement backprop for this small graph." — Record the nodes in execution order; seed ; visit the nodes in reverse order, call each local VJP, and add into the parents' cotangents.
- "Forward or reverse mode: costs, memory, when?" — JVPs or VJPs for an Jacobian, each a few times the cost of ; reverse mode stores the tape; reverse for losses, forward for few inputs or tight memory.
- "What is the gradient of ? Of with respect to ?" — ; and , which is with for a symmetric .
- "Why is the softmax backward pass ?" — ; the Jacobian is never formed.
- "How do you add a sparse solver written in C++ to PyTorch?" — A
torch.autograd.Function:forwardsolves and saves the factorization;backwarddoes one transposed solve for and forms (only on the sparsity pattern of , if is sparse). - "What is the VJP of a VJP?" — The JVP. A VJP is linear in its cotangent, and the adjoint of an adjoint is the original map (Blondel and Roulet, §8.4).
10. Practice #
The notebook for this lesson runs in Colab or in the browser. It has five exercises, each with a check cell and a folded solution:
- Write the VJP of the matrix product and check it with central differences in a random direction.
- Write the VJP of a linear solve without forming , and test it with a non-symmetric matrix.
- Check Jacobi's formula by finite differences, also for .
- Build a 40-line tape with
backwardfor add, mul, tanh and sum; get the gradient of a two-layer network and check it against finite differences. - Plot the finite-difference error against (the V-curve) and find the best step for the forward and the central difference.
11. Watch and read #
- Bright, Edelman, Johnson, Matrix Calculus (for Machine Learning and Beyond): ch. 2 (derivatives as linear operators), §5.1 (the matrix inner product), §6.3 (reverse-mode "adjoint" differentiation), ch. 7 (derivatives of the determinant and the inverse), §8.3–8.4 (forward and reverse mode on computational graphs). The lectures are on MIT OpenCourseWare 18.S096.
- Blondel and Roulet, The Elements of Differentiable Programming: §2.3 (linear maps, adjoints, JVPs and VJPs), §8.1–8.3 (forward and reverse mode, their costs, the Baur–Strassen theorem), §8.4 (VJPs of primitives).
- Giles, An extended collection of matrix derivative results for forward and reverse mode algorithmic differentiation (2008): §2.2–2.3 (product, inverse, determinant, solve), §2.4 (the Gaussian likelihood).
- autodidact: a small, readable version of the HIPS autograd library. Read its tape and VJP table after you finish exercise 4.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.