Automatic Differentiation
Frameworks do not solve derivatives symbolically or estimate them numerically. They apply the chain rule mechanically to the operations you actually ran, which is exact and cheap.
Why Does This Exist?
Nobody derives the gradient of a modern network by hand, and nobody needs to. You write a forward pass in Python and .backward() produces exact gradients for every parameter. That is not magic and it is not symbolic algebra. It is a specific technique with a specific cost model, and knowing the model tells you why some things are fast and others surprisingly are not.
Three concrete payoffs:
- Why training costs about two forward passes, regardless of parameter count. Reverse mode gets every gradient in one sweep. If it scaled per parameter, training a 7-billion-parameter model would be impossible rather than merely expensive.
- Why
jacrevandjacfwdboth exist, and how to pick. Choosing wrong can cost a thousandfold. - Why a custom backward is sometimes necessary, and what you promise when you write one.
Autodiff is also the reason research moves quickly: you change an architecture and get correct gradients without re-deriving anything. The chain rule it mechanises and the graphs it records live in computational graphs, while the object it produces is in gradients.
The one idea you need first
Derivatives compose by the chain rule: if depends on and depends on , then , where is the outer slope and is the inner slope. A gradient collects one such rate per parameter. The full rule with worked chains is in chain rule.
Think of It Like This
Costing a recipe forwards or backwards
A bakery runs flour into dough, dough into loaves, and loaves into a day's revenue.
Ask how a 1p flour rise moves everything downstream and you work forwards. Flour shifts dough cost, which shifts loaf cost, which shifts revenue. One sweep gives that one ingredient's effect on every quantity. Yeast needs another sweep.
Ask why revenue dropped and how much each ingredient contributed, and you work backwards. Start at revenue, attribute to loaves, then dough, then every ingredient at once. One sweep gives every ingredient.
Many inputs with one output, which is exactly a loss over parameters, means backwards wins by the number of inputs. The catch is that backwards needs the intermediates kept. You cannot attribute revenue to flour without knowing how much dough there was.
How It Actually Works
Autodiff is neither of the two things people assume.
It is not symbolic differentiation. Symbolic tools manipulate expressions and produce formulas, which suffer expression swell: the symbolic derivative of a deep composition is enormous and slow to evaluate.
It is not numerical differentiation. Finite differences approximate, need a step size, cost one evaluation per input, and lose precision to cancellation described in machine epsilon.
Autodiff is exact and cheap because it applies the chain rule to the sequence of operations that ran. Every primitive such as add, multiply, exp, and matmul has a known local derivative, and the framework composes them numerically as it goes. No formula is produced and no approximation is made: the result is exact to floating-point precision.
Two directions, one rule
Forward mode propagates derivatives alongside values, from inputs to outputs. Each variable carries a pair of its value and its derivative with respect to one chosen input. One sweep gives for a single , where is the seeded input.
The clean implementation is dual numbers: represent each quantity as where , with the value and the derivative. Then , and is exactly the product rule.
Reverse mode runs the function forward while recording a computational graph, where the graph is the tape of intermediates, then walks it backwards accumulating adjoints. One sweep gives .
The cost model that decides everything
For a function with inputs and outputs, in units of one function evaluation, where counts inputs and counts outputs:
- Forward mode costs about sweeps for the full Jacobian. Cheap when is small.
- Reverse mode costs about sweeps. Cheap when is small.
Training has in the billions and , because the loss is a scalar. Reverse mode gets the entire gradient in one sweep, while forward mode would need one per parameter. That asymmetry is the single most important fact about autodiff, and it is why backpropagation is just reverse mode applied to a network.
The trade is memory. Reverse mode must retain intermediate values for the backward pass, so activation memory scales with depth and batch size. Forward mode needs no tape at all, which is why it is used for Jacobian-vector products and for verifying gradients on small inputs.
Where the abstraction leaks
Autodiff is exact for the operations it sees, and three situations break that:
- Non-differentiable points such as ReLU at exactly zero,
abs,max, and rounding. Frameworks pick a subgradient by convention and carry on silently. - Control flow on data. A dynamic framework differentiates the branch that ran, which is correct for that input and means the gradient is only valid locally.
- Operations outside the framework such as a NumPy call, a C extension, a simulator, or a discrete sampling step. The chain silently stops and you get zero gradient rather than an error.
That last one is the expensive failure, and it is what custom backward implementations and reparameterisation tricks exist to repair.
Worked example
Take at , where and are inputs and is the scalar output.
Forward pass, recording each intermediate , where names a stored value:
Reverse pass, starting from and walking back, where means :
- Addition sends its incoming derivative to both inputs unchanged: and .
- has local derivative , contributing to , where .
- has local derivatives and , contributing to and to .
Note receives contributions from two paths, and they add:
Verify analytically: and .
The cost comparison is the point. Reverse mode produced both partials in one backward sweep. Forward mode would need two sweeps, one seeded on and one on . With two inputs that is a factor of , and with 7 billion it is the difference between possible and not.
Code
import math
class Dual: """Forward mode: value plus derivative, propagated by the arithmetic itself."""
def __init__(self, val: float, der: float = 0.0): self.val, self.der = val, der
def __mul__(self, o: "Dual") -> "Dual": return Dual(self.val * o.val, self.val * o.der + self.der * o.val)
def __add__(self, o: "Dual") -> "Dual": return Dual(self.val + o.val, self.der + o.der)
def sin_dual(d: Dual) -> Dual: return Dual(math.sin(d.val), math.cos(d.val) * d.der)
# Seed the derivative on x1 only: one forward sweep gives df/dx1.x1, x2 = Dual(2.0, 1.0), Dual(3.0, 0.0)out = x1 * x2 + sin_dual(x1)print(round(out.val, 4), round(out.der, 4)) # -> 6.9093 2.5839
# Seeding x2 instead needs a SECOND sweep -- that is the cost of forward mode.x1, x2 = Dual(2.0, 0.0), Dual(3.0, 1.0)print(round((x1 * x2 + sin_dual(x1)).der, 4)) # -> 2.0
print(round(3 + math.cos(2), 4), 2.0) # -> 2.5839 2.0 analytic checkand match the hand calculation, and the analytic check confirms them. The two separate seedings are the forward-mode cost made explicit: one sweep per input, where reverse mode would have taken one sweep total.
Watch Out For
Breaking the chain with an operation the framework cannot see
Autodiff only differentiates operations it recorded. Step outside the framework and the chain ends silently.
The routes are ordinary. Converting to NumPy and back with .numpy() or .detach(). Calling a C extension or an external simulator. Using .item() mid-computation. In every case the forward value stays correct while the gradient is zero or missing, so a parameter never updates.
Symptom is a subset of parameters staying at initial values while loss still falls, driven by connected parameters. Assert gradients exist and are nonzero after the first backward pass, and verify custom backwards with gradcheck in float64.
Choosing the wrong mode for the shape of your problem
Forward and reverse mode compute the same derivatives at wildly different costs, and cost depends only on input count and output count .
jacrev costs one sweep per output, while jacfwd costs one per input. For inputs and outputs, jacfwd is about times cheaper, yet people reach for jacrev by habit because reverse mode is what they associate with autodiff.
The habit is to count and before choosing. Fewer outputs means reverse, and fewer inputs means forward. jacfwd(jacrev(f)) is the standard Hessian because the gradient has many inputs and one output.
The Quick Version
- Autodiff is neither symbolic nor numerical. It applies the chain rule to the operations that actually ran, exactly.
- Every primitive has a known local derivative, and the framework composes them numerically.
- Forward mode carries derivatives with values, costs one sweep per input, and needs constant memory. Dual numbers are the clean implementation.
- Reverse mode records a graph forward, walks it back, costs one sweep per output, and needs the tape, so memory grows with depth.
- On at , forward values and sum to , and one reverse sweep gives and .
- Cost is sweeps forward and sweeps reverse. Training has in the billions and .
- Training therefore costs about two forward passes regardless of parameter count.
- Backpropagation is reverse-mode autodiff applied to a neural network.
- The chain breaks silently at NumPy conversions,
.detach(), external code, and discrete sampling. Assert nonzero gradients after the first step. - Non-differentiable points get a convention, not an error. Verify custom backwards with
gradcheckinfloat64.