Type at least 2 letters. Enter opens the first result, Esc closes.

    ML for Science · Lesson 1 · Differentiable simulation

    Matrix calculus and reverse-mode automatic differentiation

    Intermediate 75 min 3 interactive figures 3 videos Colab notebook

    Before this lesson: linear algebra 15 (JVP and VJP, first view) and linear algebra 16 (matrix differentials, dlog⁡det⁡Xd\log\det X). 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 log⁡det⁡K\log\det K and K−1rK^{-1}r. 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 10910^9 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 y=tanh⁡(x)y = \tanh(x). A small change dxdx gives dy=(1−y2) dxdy = (1 - y^2)\,dx. So the derivative is the linear map "multiply by 1−y21 - y^2". Suppose a scalar loss LL depends on yy, and we know yˉ=∂L/∂y\bar y = \partial L/\partial y. Then

    dL=yˉ dy=yˉ (1−y2) dx,dL = \bar y\,dy = \bar y\,(1 - y^2)\,dx,

    so xˉ=(1−y2) yˉ\bar x = (1 - y^2)\,\bar y.

    Two facts appear already. First, the rule uses the output yy, so the forward pass must keep it. Second, the same factor 1−y21 - y^2 moves a small change forward and a sensitivity backward.

    Many variables. Let y=F(x)y = F(x) with x∈Rnx \in \R^n and y∈Rmy \in \R^m. The derivative is the linear map dy=J dxdy = J\,dx, where JJ is the m×nm\times n Jacobian. There are two ways to use it.

    • Push a direction vv forward: v↦Jvv \mapsto Jv. This is a Jacobian-vector product (JVP). It lives in the output space Rm\R^m.
    • Pull a sensitivity yˉ\bar y back: yˉ↦J⊤yˉ\bar y \mapsto J^\top \bar y. This is a vector-Jacobian product (VJP). It lives in the input space Rn\R^n.

    A small example. F(x1,x2)=(x1x2, x1+3x2)F(x_1, x_2) = (x_1 x_2,\ x_1 + 3x_2) at x=(1,2)x = (1, 2) has

    J=(x2x113)=(2113).J = \begin{pmatrix} x_2 & x_1 \\ 1 & 3 \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 1 & 3 \end{pmatrix}.

    The JVP with v=(1,−1)v = (1, -1) is Jv=(1,−2)Jv = (1, -2). The VJP with yˉ=(1,1)\bar y = (1, 1) is J⊤yˉ=(3,4)J^\top \bar y = (3, 4). The two are linked by one identity: ⟨yˉ,Jv⟩=1−2=−1\langle \bar y, Jv\rangle = 1 - 2 = -1 and ⟨J⊤yˉ,v⟩=3−4=−1\langle J^\top\bar y, v\rangle = 3 - 4 = -1. They are equal, as they must be for every vv and yˉ\bar y. This identity is the definition of the VJP in section 3.

    A program. A program is a chain of small operations x→v1→⋯→vN=Lx \to v_1 \to \dots \to v_N = L with Jacobians J1,…,JNJ_1, \dots, J_N. By the chain rule, its gradient is

    ∇xL=J1⊤J2⊤⋯JN⊤ 1.\nabla_x L = J_1^\top J_2^\top \cdots J_N^\top \, 1 .

    Multiply from the right: start with the number 11, then apply JN⊤J_N^\top, then JN−1⊤J_{N-1}^\top, and so on. Each step is one VJP, and the vector stays small. Nobody builds a Jacobian. For a softmax over nn classes the Jacobian has n2n^2 entries, but its VJP costs O(n)O(n).

    The figure runs this on a small network, L=∑itanh⁡(Wx+b)i2L = \sum_i \tanh(Wx + b)_i^2.

    Interactive figure: loading…

    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 yy: it is used twice by q=y⊙yq = y \odot y, so it receives two contributions (highlighted arrows), and they add up to yˉ=2y⊙qˉ\bar y = 2y \odot \bar q. At the end, a central difference checks one entry of Wˉ\bar W.

    Quick check

    In the figure, q=y⊙yq = y \odot y uses yy twice and qˉ=(1,1)\bar q = (1, 1). A programmer writes the backward pass so that the second contribution replaces the first. What does the code return for yˉ\bar y?

    3. Definitions and main results #

    Derivatives as linear maps #

    Arrays of any shape are vectors. For matrices we use the Frobenius inner product ⟨A,B⟩=∑ijAijBij=tr⁡(A⊤B)\langle A, B\rangle = \sum_{ij} A_{ij}B_{ij} = \operatorname{tr}(A^\top B).

    Definition (derivative, JVP, VJP). FF is differentiable at xx if

    F(x+dx)=F(x)+F′(x)[dx]+o(∥dx∥)F(x + dx) = F(x) + F'(x)[dx] + o(\lVert dx\rVert)

    with a linear map F′(x)F'(x). The JVP is the map v↦F′(x)[v]v \mapsto F'(x)[v]. The VJP is the adjoint map yˉ↦F′(x)∗[yˉ]\bar y \mapsto F'(x)^*[\bar y], defined by

    ⟨yˉ, F′(x)[v]⟩=⟨F′(x)∗[yˉ], v⟩for all v.\langle \bar y,\ F'(x)[v]\rangle = \langle F'(x)^*[\bar y],\ v\rangle \qquad \text{for all } v.

    If you flatten xx and yy into vectors, F′(x)F'(x) is the Jacobian JJ and F′(x)∗=J⊤F'(x)^* = J^\top. The inner-product form is better for matrices: it gives Aˉ\bar A directly as a matrix of the shape of AA, with no flattening.

    Notation. For a scalar loss LL, a bar marks the gradient of LL with respect to a variable: yˉ=∂L/∂y\bar y = \partial L/\partial y. It has the shape of yy. It is also called the cotangent or the adjoint of yy.

    Lemma (a gradient through a map is a VJP). If L=ℓ(F(x))L = \ell(F(x)) and yˉ=∇ℓ(F(x))\bar y = \nabla \ell(F(x)), then xˉ=F′(x)∗[yˉ]\bar x = F'(x)^*[\bar y].

    Proof.

    1. dL=⟨yˉ,dy⟩dL = \langle \bar y, dy\rangle, by the definition of the gradient of ℓ\ell.
    2. dy=F′(x)[dx]dy = F'(x)[dx], by the definition of the derivative.
    3. So dL=⟨yˉ,F′(x)[dx]⟩=⟨F′(x)∗[yˉ],dx⟩dL = \langle \bar y, F'(x)[dx]\rangle = \langle F'(x)^*[\bar y], dx\rangle for every dxdx. The gradient is the vector that represents dLdL in this way, so xˉ=F′(x)∗[yˉ]\bar x = F'(x)^*[\bar y]. ■\blacksquare

    Recipe for any VJP.

    1. Write the differential dydy in terms of dxdx.
    2. Write ⟨yˉ,dy⟩\langle \bar y, dy\rangle.
    3. Move everything away from dxdx with transposes and the cyclic rule tr⁡(ABC)=tr⁡(CAB)\operatorname{tr}(ABC) = \operatorname{tr}(CAB), until it reads ⟨G,dx⟩\langle G, dx\rangle. Then xˉ=G\bar x = G.

    GG must have the shape of xx. This is a free check of your algebra.

    The VJPs of the primitives #

    Here ⊙\odot is the elementwise product, 1\mathbf 1 is the all-ones vector and A−⊤=(A⊤)−1A^{-\top} = (A^\top)^{-1}.

    Primitive Differential VJP
    y=tanh⁡(x)y = \tanh(x), elementwise dy=(1−y2)⊙dxdy = (1 - y^2)\odot dx xˉ=(1−y2)⊙yˉ\bar x = (1 - y^2)\odot\bar y
    y=exy = e^x; y=log⁡xy = \log x dy=y⊙dxdy = y\odot dx; dy=dx/xdy = dx/x xˉ=y⊙yˉ\bar x = y\odot\bar y; xˉ=yˉ/x\bar x = \bar y/x
    y=sum⁡(x)y = \operatorname{sum}(x) over some axes dy=sum⁡(dx)dy = \operatorname{sum}(dx) xˉ=yˉ\bar x = \bar y broadcast back to the shape of xx
    Y=X+1b⊤Y = X + \mathbf 1 b^\top (bias bb added to every row) dY=dX+1 db⊤dY = dX + \mathbf 1\,db^\top Xˉ=Yˉ\bar X = \bar Y; bˉ\bar b = sum of the rows of Yˉ\bar Y
    s=softmax⁡(z)s = \operatorname{softmax}(z) ds=s⊙(dz−⟨s,dz⟩1)ds = s\odot(dz - \langle s, dz\rangle\mathbf 1) zˉ=s⊙(sˉ−⟨s,sˉ⟩1)\bar z = s\odot(\bar s - \langle s, \bar s\rangle\mathbf 1)
    C=ABC = AB dC=dA B+A dBdC = dA\,B + A\,dB Aˉ=CˉB⊤\bar A = \bar C B^\top, Bˉ=A⊤Cˉ\bar B = A^\top \bar C
    X=A−1BX = A^{-1}B (linear solve) A dX=dB−dA XA\,dX = dB - dA\,X Bˉ=A−⊤Xˉ\bar B = A^{-\top}\bar X, Aˉ=−BˉX⊤\bar A = -\bar B X^\top
    y=log⁡∣det⁡A∣y = \log\lvert\det A\rvert dy=tr⁡(A−1dA)dy = \operatorname{tr}(A^{-1}dA) Aˉ=yˉ A−⊤\bar A = \bar y\,A^{-\top}

    We derive the important rows now. Each derivation follows the recipe.

    Sum and broadcasting are adjoints of each other. ⟨yˉ,sum⁡(dx)⟩=⟨broadcast⁡(yˉ),dx⟩\langle \bar y, \operatorname{sum}(dx)\rangle = \langle \operatorname{broadcast}(\bar y), dx\rangle. This is why the gradient of a bias is a sum over the batch: the forward pass broadcasts bb to every row, so the backward pass sums the rows.

    Matrix product C=ABC = AB.

    1. dC=dA B+A dBdC = dA\,B + A\,dB (product rule).
    2. First term, with the cyclic rule:
    ⟨Cˉ,dA B⟩=tr⁡(Cˉ⊤dA B)=tr⁡(BCˉ⊤dA)=⟨CˉB⊤,dA⟩.\langle \bar C, dA\,B\rangle = \operatorname{tr}(\bar C^\top dA\,B) = \operatorname{tr}(B\bar C^\top dA) = \langle \bar C B^\top, dA\rangle .
    1. Second term:
    ⟨Cˉ,A dB⟩=tr⁡(Cˉ⊤A dB)=⟨A⊤Cˉ,dB⟩.\langle \bar C, A\,dB\rangle = \operatorname{tr}(\bar C^\top A\,dB) = \langle A^\top\bar C, dB\rangle .
    1. So Aˉ=CˉB⊤\bar A = \bar C B^\top and Bˉ=A⊤Cˉ\bar B = A^\top \bar C. ■\blacksquare

    Shape check: AA is m×nm\times n, BB is n×kn\times k, so CC and Cˉ\bar C are m×km\times k. Then CˉB⊤\bar C B^\top is (m×k)(k×n)=m×n(m\times k)(k\times n) = m\times n, the shape of AA. For a linear layer y=Wxy = Wx: Wˉ=yˉ x⊤\bar W = \bar y\, x^\top (an outer product, rank one) and xˉ=W⊤yˉ\bar x = W^\top \bar y. The backward pass is two matrix products of the forward size, so it costs about two forward passes.

    Quick check

    C=ABC = AB with AA of size 3×43 \times 4 and BB of size 4×54 \times 5. What is Aˉ\bar A, and what is its shape?

    Softmax si=ezi/∑jezjs_i = e^{z_i}/\sum_j e^{z_j} (row by row for a batch).

    1. log⁡si=zi−log⁡∑jezj\log s_i = z_i - \log\sum_j e^{z_j}, so dlog⁡si=dzi−⟨s,dz⟩d\log s_i = dz_i - \langle s, dz\rangle.
    2. dsi=si dlog⁡sids_i = s_i\,d\log s_i, so
    ds=s⊙(dz−⟨s,dz⟩1)=(diag⁡(s)−ss⊤) dz.ds = s\odot(dz - \langle s, dz\rangle\mathbf 1) = (\operatorname{diag}(s) - ss^\top)\,dz .
    1. The matrix diag⁡(s)−ss⊤\operatorname{diag}(s) - ss^\top is symmetric, so the VJP applies the same matrix: zˉ=s⊙sˉ−s ⟨s,sˉ⟩\bar z = s\odot\bar s - s\,\langle s, \bar s\rangle. The cost is O(n)O(n); the n×nn\times n matrix is never built. ■\blacksquare

    Linear solve X=A−1BX = A^{-1}B, where BB is a vector or a matrix.

    1. Differentiate AX=BAX = B: dA X+A dX=dBdA\,X + A\,dX = dB, so dX=A−1(dB−dA X)dX = A^{-1}(dB - dA\,X).
    2. Move A−1A^{-1} to the other side of the inner product as its transpose:
    ⟨Xˉ,dX⟩=⟨Xˉ,A−1(dB−dA X)⟩=⟨A−⊤Xˉ, dB−dA X⟩.\langle \bar X, dX\rangle = \langle \bar X, A^{-1}(dB - dA\,X)\rangle = \langle A^{-\top}\bar X,\ dB - dA\,X\rangle .
    1. Name Bˉ=A−⊤Xˉ\bar B = A^{-\top}\bar X: this is one solve with A⊤A^\top. The right side becomes
    ⟨Bˉ,dB⟩−⟨Bˉ,dA X⟩=⟨Bˉ,dB⟩−⟨BˉX⊤,dA⟩.\langle \bar B, dB\rangle - \langle \bar B, dA\,X\rangle = \langle \bar B, dB\rangle - \langle \bar B X^\top, dA\rangle .
    1. Read off Bˉ=A−⊤Xˉ\bar B = A^{-\top}\bar X and Aˉ=−BˉX⊤\bar A = -\bar B X^\top. ■\blacksquare

    The forward pass factors AA (for example PA=LUPA = LU). The same factors also solve with A⊤A^\top, because A⊤=U⊤L⊤PA^\top = U^\top L^\top P. So the backward solve is cheap, and no inverse is ever formed.

    Log-determinant (Jacobi's formula, from linear algebra 16).

    1. A+dA=A(I+E)A + dA = A(I + E) with E=A−1dAE = A^{-1}dA, so det⁡(A+dA)=det⁡A det⁡(I+E)\det(A + dA) = \det A\,\det(I + E).
    2. det⁡(I+E)=1+tr⁡E+O(∥E∥2)\det(I + E) = 1 + \operatorname{tr}E + O(\lVert E\rVert^2).
    3. So dlog⁡∣det⁡A∣=tr⁡(A−1dA)=⟨A−⊤,dA⟩d\log\lvert\det A\rvert = \operatorname{tr}(A^{-1}dA) = \langle A^{-\top}, dA\rangle, and Aˉ=yˉ A−⊤\bar A = \bar y\,A^{-\top}. ■\blacksquare

    The formula also holds when det⁡A<0\det A < 0. Compute the value from an LU or Cholesky factorization (slogdet in NumPy), never as log⁡(det⁡A)\log(\det A): det⁡A\det A overflows or underflows long before log⁡∣det⁡A∣\log\lvert\det A\rvert does. For a symmetric positive definite K=LL⊤K = LL^\top (Cholesky), log⁡det⁡K=2∑ilog⁡Lii\log\det K = 2\sum_i \log L_{ii} and Kˉ=yˉ K−1\bar K = \bar y\,K^{-1}.

    Reverse mode is back substitution #

    A program computes v0=xv_0 = x, then vk=φk(parents of k)v_k = \varphi_k(\text{parents of } k) for k=1,…,Nk = 1, \dots, N, in this order. L=vNL = v_N is a scalar. Each vkv_k 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 vˉN=1\bar v_N = 1, and for i=N−1,…,0i = N - 1, \dots, 0

    vˉi=∑k: i is a parent of k(∂iφk)∗[vˉk],\bar v_i = \sum_{k:\ i \text{ is a parent of } k} (\partial_i\varphi_k)^*[\bar v_k],

    where ∂iφk\partial_i\varphi_k is the derivative of φk\varphi_k with respect to its input viv_i. Then vˉ0=∇xL\bar v_0 = \nabla_x L.

    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
    1. Linearize every node: dvk=∑iMki dvidv_k = \sum_i M_{ki}\,dv_i, where Mki=∂iφkM_{ki} = \partial_i\varphi_k if ii is a parent of kk, and Mki=0M_{ki} = 0 otherwise. Parents come earlier, so Mki=0M_{ki} = 0 for i≥ki \ge k: MM is strictly lower triangular (block by block).
    2. With dv0=dxdv_0 = dx, all these equations together read (I−M) dv=E dx(I - M)\,dv = E\,dx, where EE puts dxdx into block 0. I−MI - M is unit lower triangular, so it is invertible.
    3. dL=dvN=eN⊤(I−M)−1E dxdL = dv_N = e_N^\top (I - M)^{-1}E\,dx, where eNe_N selects block NN. So ∇xL=E⊤w\nabla_x L = E^\top w with w=(I−M)−⊤eNw = (I - M)^{-\top}e_N.
    4. (I−M)⊤(I - M)^\top is unit upper triangular. Solve (I−M)⊤w=eN(I - M)^\top w = e_N by back substitution, last block first: wN=1w_N = 1 and wi=∑kMki⊤wkw_i = \sum_k M_{ki}^\top w_k. This is the sweep of the theorem with wi=vˉiw_i = \bar v_i, and E⊤w=w0=vˉ0E^\top w = w_0 = \bar v_0. ■\blacksquare

    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 (I−M) dv=E v(I - M)\,dv = E\,v for one input direction vv, in program order, together with the forward pass.

    Forward or reverse? #

    Let F:Rn→RmF : \R^n \to \R^m.

    forward mode (JVP) reverse mode (VJP)
    one pass gives JvJv (column jj for v=ejv = e_j) J⊤uJ^\top u (row ii for u=eiu = e_i)
    full Jacobian nn passes mm passes
    gradient of a scalar loss nn passes 1 pass
    time of one pass a small constant times cost⁡(F)\operatorname{cost}(F) a small constant times cost⁡(F)\operatorname{cost}(F)
    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.

    Interactive figure: loading…

    Choose a shape. Pick a preset or move the sliders for the number of inputs nn and outputs mm. The bars (log scale) count the passes needed for the full Jacobian: forward mode needs nn, reverse mode needs mm. Notice the memory line: only reverse mode stores a tape.

    • Reverse wins when m≪nm \ll n: 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 m=1m = 1.
    • Forward wins when n≪mn \ll m, 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).
    Quick check

    A loss L:R106→RL: \R^{10^6} \to \R is computed by a simulator. Which mode gives the full gradient most cheaply, and how many passes does it need?

    Checking a gradient #

    A gradient code needs a test. Pick a random direction vv and compare the directional derivative ⟨∇f(x),v⟩\langle\nabla f(x), v\rangle with the central difference

    Dh=f(x+hv)−f(x−hv)2h.D_h = \frac{f(x + hv) - f(x - hv)}{2h}.

    Two errors compete.

    • The truncation error comes from the Taylor series. For central differences the h2h^2 terms cancel, so it is O(h2)O(h^2). (For the forward difference (f(x+hv)−f(x))/h(f(x + hv) - f(x))/h it is only O(h)O(h).)
    • The rounding error comes from the subtraction of two nearly equal numbers. It is about ε∣f∣/h\varepsilon\lvert f\rvert/h, where ε≈2.2⋅10−16\varepsilon \approx 2.2\cdot 10^{-16} is the machine epsilon of float64.

    The sum Ch2+ε∣f∣/hC h^2 + \varepsilon\lvert f\rvert/h is smallest when both parts have similar size, at h≈ε1/3≈6⋅10−6h \approx \varepsilon^{1/3} \approx 6\cdot 10^{-6} (times the scale of xx). There the relative error is about 10−1010^{-10}. One random direction tests all entries of the gradient at once for 2 function calls; a full finite-difference gradient needs 2n2n calls.

    Interactive figure: loading…

    Test a gradient code. The function is f(X)=log⁡∣det⁡X∣f(X) = \log\lvert\det X\rvert for a 3×33\times 3 matrix. The curves show the relative error of the central (solid) and forward (dashed) differences against the step hh. Switch to the buggy gradient (X−1X^{-1} instead of X−⊤X^{-\top}): the curves stop at a floor that no choice of hh removes. Then move the asymmetry slider all the way to the left (s=0s = 0). The bug becomes invisible, because X−1=X−⊤X^{-1} = X^{-\top} for a symmetric matrix.

    Quick check

    You check a gradient in float64 with central differences. Which step hh gives the smallest error for a well-scaled problem (xx and ff of size about 1)?

    Answers to section 1.

    1. No inverse. The backward pass needs one solve with A⊤A^\top (bˉ=A−⊤xˉ\bar b = A^{-\top}\bar x), and it reuses the LU factors of the forward pass. Then Aˉ=−bˉ x⊤\bar A = -\bar b\,x^\top is an outer product.
    2. No difference. Since ∑isi=1\sum_i s_i = 1, ⟨s,sˉ+c1⟩=⟨s,sˉ⟩+c\langle s, \bar s + c\mathbf 1\rangle = \langle s, \bar s\rangle + c, and the shift cc cancels in sˉ−⟨s,sˉ⟩1\bar s - \langle s, \bar s\rangle\mathbf 1. In matrix form, (diag⁡(s)−ss⊤)1=0(\operatorname{diag}(s) - ss^\top)\mathbf 1 = 0: the softmax ignores a common shift of its inputs, so its VJP ignores a common shift of its cotangent.
    3. Add them. xˉ=sin⁡x+xcos⁡x\bar x = \sin x + x\cos x: the first term comes through the factor xx, the second through sin⁡x\sin x. 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

    L(K,b)=12 b⊤K−1b+12log⁡det⁡K,L(K, b) = \tfrac12\, b^\top K^{-1} b + \tfrac12 \log\det K,

    with a covariance matrix KK and a residual bb (the constant n2log⁡2π\tfrac n2 \log 2\pi is dropped). Take

    K=(2113),b=(12),det⁡K=5,K−1=15(3−1−12).K = \begin{pmatrix} 2 & 1 \\ 1 & 3 \end{pmatrix}, \quad b = \begin{pmatrix} 1 \\ 2 \end{pmatrix}, \quad \det K = 5, \quad K^{-1} = \frac15\begin{pmatrix} 3 & -1 \\ -1 & 2 \end{pmatrix}.

    Forward pass (the tape). The inputs are KK and bb.

    1. v1=α=solve⁡(K,b)=K−1bv_1 = \alpha = \operatorname{solve}(K, b) = K^{-1}b, so α=15(3−2, −1+4)=(0.2, 0.6)\alpha = \tfrac15(3 - 2,\ -1 + 4) = (0.2,\ 0.6).
    2. v2=q=b⊤α=1⋅0.2+2⋅0.6=1.4v_2 = q = b^\top\alpha = 1 \cdot 0.2 + 2 \cdot 0.6 = 1.4.
    3. v3=log⁡det⁡K=log⁡5≈1.6094v_3 = \log\det K = \log 5 \approx 1.6094.
    4. L=12v2+12v3=0.7+0.8047≈1.50472L = \tfrac12 v_2 + \tfrac12 v_3 = 0.7 + 0.8047 \approx 1.50472.

    Backward pass, from Lˉ=1\bar L = 1.

    1. L=12v2+12v3L = \tfrac12 v_2 + \tfrac12 v_3, so vˉ2=12\bar v_2 = \tfrac12 and vˉ3=12\bar v_3 = \tfrac12.
    2. Log-determinant: Kˉ+=vˉ3 K−⊤=12K−1=(0.3−0.1−0.10.2)\bar K \mathrel{+}= \bar v_3\,K^{-\top} = \tfrac12 K^{-1} = \begin{pmatrix} 0.3 & -0.1 \\ -0.1 & 0.2 \end{pmatrix}.
    3. Inner product q=b⊤αq = b^\top\alpha: bˉ+=vˉ2 α=(0.1, 0.3)\bar b \mathrel{+}= \bar v_2\,\alpha = (0.1,\ 0.3) and αˉ=vˉ2 b=(0.5, 1)\bar\alpha = \bar v_2\,b = (0.5,\ 1).
    4. Solve: bˉs=K−⊤αˉ\bar b_{\text{s}} = K^{-\top}\bar\alpha, so bˉs=15(1.5−1, −0.5+2)=(0.1, 0.3)\bar b_{\text{s}} = \tfrac15(1.5 - 1,\ -0.5 + 2) = (0.1,\ 0.3). Then bˉ+=bˉs\bar b \mathrel{+}= \bar b_{\text{s}}, and Kˉ+=−bˉsα⊤=−(0.020.060.060.18)\bar K \mathrel{+}= -\bar b_{\text{s}}\alpha^\top = -\begin{pmatrix} 0.02 & 0.06 \\ 0.06 & 0.18 \end{pmatrix}.

    Result.

    Kˉ=(0.28−0.16−0.160.02)=12(K−1−αα⊤),bˉ=(0.2, 0.6)=K−1b.\bar K = \begin{pmatrix} 0.28 & -0.16 \\ -0.16 & 0.02 \end{pmatrix} = \tfrac12\big(K^{-1} - \alpha\alpha^\top\big), \qquad \bar b = (0.2,\ 0.6) = K^{-1}b.

    bb is used twice (by the solve and by the inner product), so its cotangent has two terms: this is fan-out. The closed form 12(K−1−αα⊤)\tfrac12(K^{-1} - \alpha\alpha^\top) is the classic gradient of a Gaussian negative log-likelihood with respect to its covariance matrix.

    Quick check

    In the worked example, why does bˉ\bar b have two terms, (0.1,0.3)(0.1, 0.3) and (0.1,0.3)(0.1, 0.3)?

    (b) Softmax. z=(0,log⁡3)z = (0, \log 3) gives ez=(1,3)e^z = (1, 3) and s=(1/4, 3/4)s = (1/4,\ 3/4). Take L=s1L = s_1, so sˉ=(1,0)\bar s = (1, 0) and ⟨s,sˉ⟩=1/4\langle s, \bar s\rangle = 1/4. Then

    zˉ=s⊙(sˉ−141)=(14⋅34, 34⋅(−14))=(316, −316).\begin{aligned} \bar z &= s\odot(\bar s - \tfrac14\mathbf 1) = \big(\tfrac14\cdot\tfrac34,\ \tfrac34\cdot(-\tfrac14)\big)\\ &= \big(\tfrac{3}{16},\ -\tfrac{3}{16}\big). \end{aligned}

    Check with the known derivatives: ∂s1/∂z1=s1(1−s1)=3/16\partial s_1/\partial z_1 = s_1(1 - s_1) = 3/16 and ∂s1/∂z2=−s1s2=−3/16\partial s_1/\partial z_2 = -s_1 s_2 = -3/16. 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 θ\theta is ⟨Kˉ,∂K/∂θ⟩=12tr⁡((K−1−αα⊤) ∂K/∂θ)\langle \bar K, \partial K/\partial\theta\rangle = \tfrac12\operatorname{tr}\big((K^{-1} - \alpha\alpha^\top)\,\partial K/\partial\theta\big) with α=K−1b\alpha = K^{-1}b: section 4 plus one more inner product.
    • Normalizing flows and change of variables. The term log⁡∣det⁡A∣\log\lvert\det A\rvert with the Jacobian A=∂T/∂xA = \partial T/\partial x of the flow has the VJP yˉ A−⊤\bar y\,A^{-\top}, which needs a factorization of AA. 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 n×nn\times n softmax Jacobian, or A−1A^{-1} for a solve. Use the VJP formulas: O(n)O(n) for softmax, one transposed solve for solve.
    • A−1A^{-1} instead of A−⊤A^{-\top} for a non-symmetric AA, 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 Kˉ12+Kˉ21\bar K_{12} + \bar K_{21} (section 4).
    • A weak gradient check. Forward differences, float32, h=10−12h = 10^{-12}, or one coordinate only. Use float64, central differences, h≈10−6h \approx 10^{-6} to 10−510^{-5} and a random direction.
    • "Reverse mode is always better." For n≤mn \le m, or with a tight memory budget, forward mode can be the better choice.

    8. Check yourself #

    1. C=ABC = AB with A∈Rm×nA \in \R^{m\times n} and B∈Rn×kB \in \R^{n\times k}. Give Aˉ\bar A and Bˉ\bar B and check their shapes.
      AnswerAˉ=CˉB⊤\bar A = \bar C B^\top: (m×k)(k×n)=m×n(m\times k)(k\times n) = m\times n. Bˉ=A⊤Cˉ\bar B = A^\top\bar C: (n×m)(m×k)=n×k(n\times m)(m\times k) = n\times k. Both have the shapes of AA and BB.
    2. Scalar solve: ax=bax = b with a=2a = 2, b=4b = 4 and xˉ=1\bar x = 1. Compute bˉ\bar b and aˉ\bar a with the solve VJP, and check aˉ\bar a against ∂(b/a)/∂a\partial(b/a)/\partial a.
      Answerx=2x = 2. bˉ=xˉ/a=0.5\bar b = \bar x/a = 0.5 and aˉ=−bˉ x=−1\bar a = -\bar b\,x = -1. Directly: ∂(b/a)/∂a=−b/a2=−1\partial(b/a)/\partial a = -b/a^2 = -1.
    3. Why is the VJP of log⁡∣det⁡A∣\log\lvert\det A\rvert equal to yˉ A−⊤\bar y\,A^{-\top} and not yˉ A−1\bar y\,A^{-1}? When do the two agree?
      Answerdy=tr⁡(A−1dA)dy = \operatorname{tr}(A^{-1}dA), and ⟨G,dA⟩=tr⁡(G⊤dA)\langle G, dA\rangle = \operatorname{tr}(G^\top dA), so G⊤=A−1G^\top = A^{-1}, that is, G=A−⊤G = A^{-\top}. They agree when AA is symmetric (for example a covariance matrix).
    4. A climate emulator maps 4 forcing parameters to a field of 10610^6 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 10610^6 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.
    5. 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?
      AnswerThe sweep is back substitution with an upper triangular matrix: vˉi\bar v_i needs the final vˉk\bar v_k of every node kk that uses viv_i, and those come later on the tape. If node ii is processed too early, it passes on an incomplete cotangent, and the paths through the missing users are lost.
    6. Your gradient check in float64 with h=10−10h = 10^{-10} gives a relative error of 10−410^{-4}. Bug, or a bad check?
      AnswerProbably the check. The rounding error is about ε∣f∣/h≈2⋅10−6∣f∣\varepsilon\lvert f\rvert/h \approx 2\cdot10^{-6}\lvert f\rvert, which is large when ∣f∣\lvert f\rvert is much larger than the directional derivative. Repeat with h≈10−5h \approx 10^{-5} (scaled to xx). If the error stays above about 10−710^{-7} for every hh, look for a bug: a bug gives an error floor that does not move with hh.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. Define the vector-Jacobian product xˉ=F′(x)∗[yˉ]\bar x = F'(x)^*[\bar y] with inner products.

    2. C=ABC = AB. Give the VJPs Aˉ\bar A and Bˉ\bar B.

    3. X=A−1BX = A^{-1}B (a linear solve). Give the VJPs, and say what the backward pass computes.

    4. Give the VJP of y=log⁡∣det⁡A∣y = \log\lvert\det A\rvert, and the formula behind it.

    5. A value vv is used by two later nodes. How is vˉ\bar v computed in reverse mode?

    6. How many passes does each mode need for the full Jacobian of F:Rn→RmF: \R^n \to \R^m?

    7. What is the price of reverse mode, compared with forward mode?

    8. Why do central differences beat forward differences in a gradient check?

    9. What is the best step hh for central differences in float64, and why?

    10. Why is the softmax VJP O(n)O(n)?

    Open the review →

    9. Interview angle #

    • "Implement backprop for this small graph." — Record the nodes in execution order; seed Lˉ=1\bar L = 1; visit the nodes in reverse order, call each local VJP, and add into the parents' cotangents.
    • "Forward or reverse mode: costs, memory, when?" — nn JVPs or mm VJPs for an m×nm\times n Jacobian, each a few times the cost of FF; reverse mode stores the tape; reverse for losses, forward for few inputs or tight memory.
    • "What is the gradient of log⁡det⁡K\log\det K? Of b⊤K−1bb^\top K^{-1}b with respect to KK?" — K−⊤K^{-\top}; and −K−⊤bb⊤K−⊤-K^{-\top}bb^\top K^{-\top}, which is −αα⊤-\alpha\alpha^\top with α=K−1b\alpha = K^{-1}b for a symmetric KK.
    • "Why is the softmax backward pass O(n)O(n)?" — zˉ=s⊙(sˉ−⟨s,sˉ⟩1)\bar z = s\odot(\bar s - \langle s, \bar s\rangle\mathbf 1); the Jacobian is never formed.
    • "How do you add a sparse solver written in C++ to PyTorch?" — A torch.autograd.Function: forward solves and saves the factorization; backward does one transposed solve for bˉ\bar b and forms Aˉ=−bˉx⊤\bar A = -\bar b x^\top (only on the sparsity pattern of AA, if AA 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:

    1. Write the VJP of the matrix product and check it with central differences in a random direction.
    2. Write the VJP of a linear solve without forming A−1A^{-1}, and test it with a non-symmetric matrix.
    3. Check Jacobi's formula dlog⁡∣det⁡X∣=tr⁡(X−1dX)d\log\lvert\det X\rvert = \operatorname{tr}(X^{-1}dX) by finite differences, also for det⁡X<0\det X < 0.
    4. Build a 40-line tape with backward for add, mul, tanh and sum; get the gradient of a two-layer network and check it against finite differences.
    5. Plot the finite-difference error against hh (the V-curve) and find the best step for the forward and the central difference.

    11. Watch and read #

    Forward and reverse mode on a small computational graph, and why reverse mode wins for a scalar loss.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The derivative as a linear map, the view used in section 3 of this lesson.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    A scalar tape with a backward pass, built line by line; watch how the gradients of a reused value are added.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play