AI / ML Theory
Chapter 2 · 22 min

2.11 — Automatic differentiation: forward vs reverse mode

Goal: Understand why reverse-mode AD is O(forward) for a single scalar output and is the only practical choice for deep learning.

1. Why this session exists

Backprop in 2.5–2.7 was derived for one specific architecture (the MLP). In real frameworks — PyTorch, JAX, TensorFlow — the same chain-rule computation is generated automatically for any computational graph you can write down. This is automatic differentiation (AD), and it comes in two flavours, forward-mode and reverse-mode. Knowing the cost asymmetry between them is the difference between a working training loop and one that's 106×10^6 \times slower than it should be.[1]


2. The computational graph

Every computation is a DAG of primitive operations: matmul, add, sigmoid, sum, log, …. Each node has nn inputs and mm outputs (usually m=1m = 1 for scalars, m∈{B,K,…}m \in \{B, K, \ldots\} for batched activations). The full forward pass is one such DAG; the loss is the unique sink with 11 output.

AD evaluates the Jacobian of the sink with respect to the leaves (parameters or inputs) by applying the chain rule along DAG edges. The two modes differ in the order of traversal.


3. Forward mode

Idea. Propagate Jacobian-times-vector products forward through the graph, alongside the values.

Suppose we want ∂L/∂θk\partial L / \partial \theta_k for one specific scalar parameter θk\theta_k. Define a "tangent" u˙:=∂u/∂θk\dot u := \partial u / \partial \theta_k for every intermediate uu. The tangents propagate by the chain rule:

if v=f(u1,…,un), then v˙=∑i∂f∂ui u˙i.\text{if } v = f(u_1, \dots, u_n), \text{ then } \dot v = \sum_i \frac{\partial f}{\partial u_i}\, \dot u_i.

Initialise θk˙=1\dot{\theta_k} = 1 and θj˙=0\dot{\theta_j} = 0 for j≠kj \ne k, sweep forward through the graph alongside the value computation, and read off L˙=∂L/∂θk\dot L = \partial L / \partial \theta_k at the sink.

Cost. One forward sweep per parameter gives one partial derivative. For NN parameters: Θ(N⋅forward cost)\Theta(N \cdot \text{forward cost}).

Good for. Few inputs, many outputs. Computing the Jacobian of an Rd→Rk\mathbb{R}^d \to \mathbb{R}^k function when d≪kd \ll k.

Bad for. Deep learning. We have N∼109N \sim 10^9 parameters and one scalar output. Forward-mode AD would cost 10910^9 forward passes per gradient step. Infeasible.


4. Reverse mode

Idea. Propagate Jacobian-vector products backwards through the graph, from the scalar sink to the leaves.

Define a "cotangent" uˉ:=∂L/∂u\bar u := \partial L / \partial u for every intermediate uu. The cotangents propagate by the chain rule applied to the reverse-mode adjoint:

if v=f(u1,…,un), then uˉi+=vˉ⋅∂f∂ui.\text{if } v = f(u_1, \dots, u_n), \text{ then } \bar u_i \mathrel{+}= \bar v \cdot \frac{\partial f}{\partial u_i}.

Initialise Lˉ=1\bar L = 1 and sweep backward through the graph (in reverse topological order from the sink). The cotangents at the leaves are exactly the partial derivatives of LL with respect to those leaves.

Cost. One backward sweep gives all parameter gradients at once: Θ(forward cost)\Theta(\text{forward cost}) — same order as the forward pass.

Good for. One output, many inputs (N∼109N \sim 10^9 parameters and a scalar loss). The only practical choice for deep learning.

The "Θ(forward)\Theta(\text{forward})" claim is essentially the Baur–Strassen theorem (1983)[2]: the cost of computing the gradient of a scalar-valued function is within a constant factor of the cost of evaluating the function. The constant is small (usually 22–55); for our MLP backprop derivation it was ≈2\approx 2 (one matmul backward ≈\approx one matmul forward).


5. Worked comparison

A toy: f(θ1,θ2,θ3)=(θ1+θ2)⋅θ3f(\theta_1, \theta_2, \theta_3) = (\theta_1 + \theta_2) \cdot \theta_3. Compute ∂f/∂θ1\partial f / \partial \theta_1, ∂f/∂θ2\partial f / \partial \theta_2, ∂f/∂θ3\partial f / \partial \theta_3.

Forward mode for ∂f/∂θ1\partial f / \partial \theta_1. Set θ˙1=1\dot \theta_1 = 1, θ˙2=θ˙3=0\dot \theta_2 = \dot \theta_3 = 0. Propagate:

  • u:=θ1+θ2u := \theta_1 + \theta_2, so u˙=θ˙1+θ˙2=1\dot u = \dot \theta_1 + \dot \theta_2 = 1.
  • f:=u⋅θ3f := u \cdot \theta_3, so f˙=u˙ θ3+u θ˙3=θ3\dot f = \dot u\, \theta_3 + u\, \dot \theta_3 = \theta_3.

Three operations to get one partial. Repeat for θ2\theta_2 (another sweep, also Θ(forward)\Theta(\text{forward})), then for θ3\theta_3. Total: 3×forward3 \times \text{forward}.

Reverse mode (all three at once). Forward pass: u=θ1+θ2u = \theta_1 + \theta_2, f=u⋅θ3f = u \cdot \theta_3. Backward: initialise fˉ=1\bar f = 1.

  • uˉ+=fˉ⋅θ3=θ3\bar u \mathrel{+}= \bar f \cdot \theta_3 = \theta_3 and θˉ3+=fˉ⋅u=u\bar \theta_3 \mathrel{+}= \bar f \cdot u = u.
  • θˉ1+=uˉ⋅1=θ3\bar \theta_1 \mathrel{+}= \bar u \cdot 1 = \theta_3 and θˉ2+=uˉ⋅1=θ3\bar \theta_2 \mathrel{+}= \bar u \cdot 1 = \theta_3.

All three partials in one sweep. Total: ≈1×forward\approx 1 \times \text{forward}.

Forward is 3×3 \times slower than reverse for three parameters. Scale this up to 10910^9 and the asymmetry is the difference between "feasible" and "the heat death of the universe".


6. How real frameworks implement it

PyTorch and JAX both implement reverse-mode AD with two main components.

  1. The forward pass builds a graph (PyTorch's Tensors carry a grad_fn attribute; JAX traces functions into a jaxpr) recording the operations performed.
  2. .backward() traverses the graph in reverse topological order, calling each node's pre-registered backward formula (a "vector–Jacobian product", or VJP).

Each primitive op (matmul, sigmoid, sum, log) ships with a forward + backward pair. Users don't write the backward by hand — they compose primitives. The chain-rule machinery from this chapter is implemented once, at the framework level, and called for every model.


7. Higher-order derivatives

Want a Hessian? Two ways:

  1. Forward over reverse. Run reverse-mode on LL to get ∇L\nabla L, then run forward-mode on the function θ↦∇L\theta \mapsto \nabla L to get ∇2L\nabla^2 L row-by-row. Cost Θ(N⋅forward)\Theta(N \cdot \text{forward}).
  2. Reverse over reverse. Run reverse-mode on θ↦∇L⋅v\theta \mapsto \nabla L \cdot v (a scalar) to get ∇2L⋅v\nabla^2 L \cdot v for an arbitrary direction vv in Θ(forward)\Theta(\text{forward}) — useful for Hessian-vector products without forming the matrix.

For deep learning, we almost never form the full Hessian (it has N2N^2 entries, 101810^{18} for our N=109N = 10^9 example). Hessian-vector products are the right primitive for second-order methods (K-FAC, Sophia, …; Chapter 3).


8. Takeaways

  • Forward-mode AD: Θ(#inputs⋅forward)\Theta(\#\text{inputs} \cdot \text{forward}). Good when inputs few, outputs many.
  • Reverse-mode AD: Θ(forward)\Theta(\text{forward}) for one scalar output. The standard mode for DL.
  • Backprop from 2.5–2.7 is reverse-mode AD specialised to the MLP graph.
  • Framework-level VJPs make this transparent for arbitrary user code.

Self-test

(a) A function f:R1000→R1000f : \mathbb{R}^{1000} \to \mathbb{R}^{1000} has Jacobian J∈R1000×1000J \in \mathbb{R}^{1000 \times 1000}. How many forward-mode sweeps to compute JJ? How many reverse-mode sweeps? Conclude they tie for square Jacobians.

(b) State the Baur–Strassen theorem in one sentence and explain why it justifies training 10910^9-parameter models with reverse-mode AD.

(c) A custom op my_op(x) = x * sin(x) should ship with what backward function? Write the VJP xˉ+=yˉ⋅(?)\bar x \mathrel{+}= \bar y \cdot (?) in terms of xx.

(d) Why don't deep-learning frameworks materialise the full Hessian? Answer in terms of NN, FLOPs, and memory. What is the right object to materialise instead?


References

  1. [1]

    Griewank, A., & Walther, A. (2008). Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation (2nd ed.). SIAM. — Canonical AD reference (2.11).

    ↩
  2. [2]

    Baur, W., & Strassen, V. (1983). "The complexity of partial derivatives". Theoretical Computer Science, 22(3), 317–330. — Cost-of-gradient = cost-of-function theorem (2.11).

    ↩