AnyLearn
All lessons
Mathintermediate

Automatic Differentiation: How Gradients Are Actually Computed

Frameworks do not differentiate formulas symbolically or estimate derivatives numerically. They differentiate the program. This lesson builds forward mode from dual numbers and reverse mode from the backward sweep, shows why a full gradient costs about four function evaluations at any size, and where the answer is not what you meant.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 10

Three ways to get a derivative, and why two of them lose

ApproachInputFailure mode
Symbolica closed-form expressionexpression swell: the derivative formula grows far larger than the function, and there is no formula at all for a program with loops
Numericalff as a black boxabout half your digits, and n+1n+1 evaluations for a gradient in nn variables
Automaticthe program that computes ffexact to rounding, at a small constant times the cost of one evaluation

Symbolic differentiation is what a computer algebra system does, and it is genuinely useful right up to the point where the function is 400 lines of Python with a for loop in it. Numerical differencing, from the first lesson, survives that but cannot pay the per-parameter cost.

Automatic differentiation is the third option, and its trick is to refuse to work with formulas at all.

Full lesson text

All 10 steps on one page, for reading, reference, and search.

Show

1. Three ways to get a derivative, and why two of them lose

ApproachInputFailure mode
Symbolica closed-form expressionexpression swell: the derivative formula grows far larger than the function, and there is no formula at all for a program with loops
Numericalff as a black boxabout half your digits, and n+1n+1 evaluations for a gradient in nn variables
Automaticthe program that computes ffexact to rounding, at a small constant times the cost of one evaluation

Symbolic differentiation is what a computer algebra system does, and it is genuinely useful right up to the point where the function is 400 lines of Python with a for loop in it. Numerical differencing, from the first lesson, survives that but cannot pay the per-parameter cost.

Automatic differentiation is the third option, and its trick is to refuse to work with formulas at all.

2. Differentiate the program, not the formula

Every program, however long, is ultimately a sequence of primitive operations: add, multiply, exponentiate, index, matrix-multiply. There are perhaps a hundred of them in a framework, and the derivative of each one is a line of code somebody wrote once.

Automatic differentiation records the sequence of primitives a particular execution performed, then applies the chain rule along that recorded sequence. It never needs an expression for ff, only the trace of what ff did.

Key idea: Automatic differentiation is exact, like symbolic differentiation, and mechanical, like numerical differentiation. It gets both by changing the object it operates on: a straight-line list of primitive operations rather than a mathematical expression. Loops, branches and recursion disappear, because by the time you have run the program they have already resolved into a flat list.

3. The trace, made concrete

Take L=x1x2+sin(x1x2)L = x_1 x_2 + \sin(x_1 x_2) evaluated at x1=2x_1 = 2, x2=3x_2 = 3. Running it produces this trace:

StepOperationValue
v1v_1x1×x2x_1 \times x_26
v2v_2sin(v1)\sin(v_1)-0.2794
LLv1+v2v_1 + v_25.7206

Three primitives, each with a derivative rule known in advance: a product rule, the derivative of sine, and a sum rule. The chain rule now has somewhere to run. The only remaining question is which direction to run it in, and that choice is the whole difference between the two modes of automatic differentiation.

4. Forward mode: carry the derivative alongside the value

Attach a second number to every value, holding its derivative with respect to one chosen input. These pairs are dual numbers, obeying ε2=0\varepsilon^2 = 0:

(a+bε)(c+dε)=ac+(ad+bc)ε(a + b\varepsilon)(c + d\varepsilon) = ac + (ad + bc)\varepsilon

The product rule appears in the ε\varepsilon coefficient by itself. Forty lines of Python implement it:

import math

class Dual:
    def __init__(self, v, d=0.0):
        self.v, self.d = v, d          # value, derivative
    def __add__(self, o):
        return Dual(self.v + o.v, self.d + o.d)
    def __mul__(self, o):
        return Dual(self.v * o.v, self.v * o.d + self.d * o.v)

def sin(u):
    return Dual(math.sin(u.v), math.cos(u.v) * u.d)

x1 = Dual(2.0, 1.0)                    # seed: differentiate wrt x1
x2 = Dual(3.0, 0.0)
v1 = x1 * x2
L = v1 + sin(v1)
print(L.v, L.d)                        # 5.720584... 5.880510...

One sweep, forward, no tape. The catch is the seed: it differentiates with respect to x1x_1 only, so an nn-input gradient takes nn sweeps.

5. Reverse mode: run forwards, differentiate backwards

Reverse mode evaluates the function first, recording each intermediate value on a tape. Then it walks the tape backwards, carrying an adjoint: how much the final output changes per unit change in that intermediate. Seed the output with 1 and every adjoint falls out by the chain rule. For our trace, vˉ2=1\bar{v}_2 = 1, then vˉ1=1+cos(6)=1.9602\bar{v}_1 = 1 + \cos(6) = 1.9602, and finally xˉ1=1.9602×3=5.8805\bar{x}_1 = 1.9602 \times 3 = 5.8805 and xˉ2=1.9602×2=3.9203\bar{x}_2 = 1.9602 \times 2 = 3.9203. Both partial derivatives, from one backward sweep.

flowchart TD
  F1["forward: v1 = x1 * x2"] --> F2["forward: v2 = sin(v1)"]
  F2 --> F3["forward: L = v1 + v2"]
  F3 --> B3["backward: seed L-bar = 1, so v2-bar = 1 and v1-bar = 1"]
  B3 --> B2["backward: v1-bar += cos(v1) * v2-bar"]
  B2 --> B1["backward: x1-bar = v1-bar * x2 and x2-bar = v1-bar * x1"]

6. One backward pass, however many parameters

Forward mode costs one sweep per input. Reverse mode costs one sweep per output. Training has an extreme shape, many inputs and exactly one output, so reverse mode wins by whatever the parameter count happens to be.

Predict first

A language model has 10^9 parameters and produces a single scalar loss. How many backward passes does one full gradient take?

JAX's own documentation puts it plainly: for a function f:RnRf: \mathbb{R}^n \to \mathbb{R}, "we can do it in just one call. That's how grad is efficient for gradient-based optimization, even for objectives like neural network training loss functions on millions or billions of parameters."

7. The cheap gradient principle

The constant is small and, remarkably, it does not depend on the number of parameters. Baur and Strassen proved in 1983 that computing a full gradient costs at most about four times the cost of evaluating the function itself. JAX quotes a similar figure for its own transformations, "only about three times the cost of just evaluating the function".

Function evaluations for one full gradient
forward differencesreverse-mode AD
evaluations of f05001k1.5kn=10n=50n=100n=500n=1000
Source: Computed: n+1 for forward differences; reverse mode uses the Baur-Strassen bound of about 4x one evaluation, independent of n

The second bar never moves. That flat line is the reason a trillion-parameter model can be trained at all, and it is a theorem, not an engineering achievement.

8. What reverse mode charges instead: memory

The backward sweep needs the intermediate values the forward sweep produced, so they have to be kept. Memory therefore grows with the depth of the computation, which is exactly the cost forward mode does not pay. This is why activations, not weights, usually fill a training GPU.

The standard trade is gradient checkpointing: keep only some intermediates and recompute the rest on the way back. Chen and colleagues showed in 2016 that storing every n\sqrt{n}-th layer gives O(n)O(\sqrt{n}) memory "with only the computational cost of an extra forward pass per mini-batch", and reported reducing a 1,000-layer residual network from 48 GB to 7 GB with a 30 percent runtime increase.

In practice: Checkpointing is one flag in every major framework. Reach for it when you are memory-bound rather than compute-bound, which for large models is most of the time.

9. Where the answer is exact but not what you meant

Automatic differentiation returns the exact derivative of the program that ran. That is not always the derivative you wanted.

  • Control flow is resolved before differentiation, so you get the derivative of the branch taken. A max(a, b) routes the gradient entirely to the winner and reports nothing about the switch itself.
  • Non-differentiable points are still non-differentiable. Frameworks return a convention, ReLU at zero being the standard case, and that convention becomes part of your numerics.
  • In-place mutation overwrites values the backward sweep still needed. PyTorch raises "a variable needed for gradient computation has been modified by an inplace operation" rather than returning something wrong, which is the kinder failure.
  • Iterative solvers differentiate through every iteration you actually ran, not through the fixed point you were converging to. Implicit differentiation exists for precisely this case.

Gotcha: "The gradients are wrong" is nearly always a mismatch between the mathematical function you had in mind and the program you actually wrote.

10. Choosing a mode, and combining them

Forward mode (JVP)Reverse mode (VJP)
Computes in one sweepJvJ v, one column of JJvTJv^{\mathsf{T}} J, one row of JJ
Sweeps for a full Jacobiannnmm
Memoryindependent of depthgrows with depth
Best whenfew inputs, many outputsmany inputs, few outputs
Typical usedirectional derivatives, sensitivitytraining any scalar loss

They compose, which is where the second-order tools come from. A Hessian-vector product HvHv is the derivative of the gradient in the direction vv: build the gradient with reverse mode, then push one forward-mode sweep through it. Forward-over-reverse gives you HvHv exactly, at roughly the cost of a gradient, without ever forming the n×nn \times n matrix that made curvature look unaffordable in the previous lesson.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What object does automatic differentiation actually operate on?
    • A closed-form symbolic expression for the function
    • The trace of primitive operations that one execution performed
    • A finite-difference approximation refined to convergence
    • The source code, parsed but not run
  2. A function has 5 inputs and 2000 outputs, and you need the full Jacobian. Which mode is cheaper?
    • Reverse mode, since it is always cheaper than forward mode
    • Neither, since the two modes always cost the same
    • Reverse mode, because it needs only one sweep for any function
    • Forward mode, because it needs 5 sweeps while reverse mode needs 2000
  3. In the dual-number implementation, x1 is seeded as Dual(2.0, 1.0) and x2 as Dual(3.0, 0.0). What does the derivative component of the result represent?
    • The partial derivative of the output with respect to x1 only
    • The full gradient with respect to both x1 and x2
    • The second derivative with respect to x1
    • The numerical error in the computed value
  4. Why does reverse-mode AD use more memory than forward mode?
    • It stores the derivative rule for every primitive in the framework
    • It runs the function twice, doubling everything
    • The backward sweep needs the intermediate values the forward sweep produced, so they must be kept
    • It converts the program into a symbolic expression first
  5. You differentiate through an iterative solver that ran 50 steps before hitting its tolerance. What does AD give you?
    • The derivative at the exact fixed point the solver was converging to
    • The derivative of the 50-step computation you actually ran
    • An error, because iterative code cannot be differentiated
    • A finite-difference estimate, since no closed form exists

Related lessons