Quant Memo
Advanced

Computational Graphs and Automatic Differentiation

A computer can compute the exact derivative of any function you can write as code, without you ever doing calculus by hand, by recording every elementary operation as a graph and applying the chain rule mechanically along it.

Prerequisites: Backpropagation, The Multilayer Perceptron

Every deep learning framework claims to compute the exact gradient of a loss with respect to millions of parameters, instantly, for any model you can write in code — not an approximation, not a numerical estimate, the exact derivative. That sounds like it requires either a superhuman calculus student or a shortcut. It is a shortcut, but a completely mechanical one: break any calculation, no matter how complicated, into a sequence of tiny operations simple enough that their individual derivatives are trivial — addition, multiplication, sine, exponent — and then apply the chain rule along that sequence automatically. No symbolic algebra is ever needed, and no human ever has to differentiate the whole function by hand.

The analogy: a spreadsheet of linked formulas

Think of a large spreadsheet where some cells hold raw numbers you typed in, and every other cell holds a formula referencing earlier cells — C3 = A1 * B2, D4 = SIN(C3), and so on, all the way down to one final cell holding the answer. You never wrote down the whole formula for that final cell as one giant expression; you built it as a chain of small, individually simple steps. If you wanted to know how the final answer changes when you nudge cell A1, you would not need to expand the entire chain into one messy equation and differentiate it by hand — you could just walk the chain of cell-to-cell dependencies, and at each link ask "how much does this cell move per unit move in the one it depends on," multiplying those local sensitivities together as you go.

That dependency graph of cells is a computational graph, and walking it to compute sensitivities is exactly what automatic differentiation does — mechanically, for graphs with millions of cells, every one of which has a known, simple local derivative.

The graph, and two ways to walk it

A computational graph is a directed graph where each node is one elementary operation, and edges show which nodes feed into which. Every node knows two things: how to compute its own value from its inputs (the forward pass), and its own local derivative with respect to each of its inputs (a simple, closed-form rule, since the node performs one primitive operation like +, ×, or sin).

There are two ways to combine these local derivatives into the derivative of the whole graph's output with respect to any input, and they differ in which direction they sweep:

Forward-mode starts at one input, seeds it with a derivative of 11 (and every other input with 00), and pushes that sensitivity forward through the graph alongside the values themselves, accumulating via the chain rule as it goes. One forward sweep gives the derivative of every output with respect to that one input.

Reverse-mode — what Backpropagation is, applied to any computational graph, not just neural networks — runs the forward pass once to get all the values, then walks backward from the final output with a seed derivative of 11, and at every node multiplies the incoming "adjoint" (gradient with respect to that node's output) by the node's local derivative to get the adjoint for its inputs. One backward sweep gives the derivative of one output with respect to every input.

That asymmetry is the whole reason deep learning uses reverse-mode almost exclusively: a training loss is a single number (one output) depending on millions of weights (many inputs), and reverse-mode gets every one of those millions of derivatives in one sweep, while forward-mode would need one sweep per weight.

Worked example 1: reverse-mode by hand on a tiny graph

Let f(x,y)=xy+sin(x)f(x, y) = xy + \sin(x), evaluated at x=2x = 2, y=3y = 3. Break it into elementary steps:

v1=xy,v2=sin(x),v3=v1+v2=fv_1 = xy, \qquad v_2 = \sin(x), \qquad v_3 = v_1 + v_2 = f

Forward pass: v1=2×3=6v_1 = 2 \times 3 = 6, v2=sin(2)=0.909v_2 = \sin(2) = 0.909, v3=6+0.909=6.909v_3 = 6 + 0.909 = 6.909.

Backward pass, seeding the output's own adjoint at 11 (the derivative of anything with respect to itself), and walking backward node by node:

  • v3=v1+v2v_3 = v_1 + v_2: the local derivative of a sum with respect to either input is 11, so the adjoint of v1v_1 is 1×1=11 \times 1 = 1 and the adjoint of v2v_2 is 1×1=11 \times 1 = 1.
  • v1=xyv_1 = xy: local derivatives are v1/x=y=3\partial v_1/\partial x = y = 3 and v1/y=x=2\partial v_1/\partial y = x = 2. Multiply by v1v_1's adjoint: contributes 1×3=31 \times 3 = 3 to xx's running total, and 1×2=21 \times 2 = 2 to yy's.
  • v2=sin(x)v_2 = \sin(x): local derivative is cos(x)=cos(2)=0.416\cos(x) = \cos(2) = -0.416. Multiply by v2v_2's adjoint: contributes 1×(0.416)=0.4161 \times (-0.416) = -0.416 to xx's running total.
fx=3+(0.416)=2.584,fy=2\frac{\partial f}{\partial x} = 3 + (-0.416) = 2.584, \qquad \frac{\partial f}{\partial y} = 2

Check against ordinary calculus: f/x=y+cos(x)=3+cos(2)=30.416=2.584\partial f/\partial x = y + \cos(x) = 3 + \cos(2) = 3 - 0.416 = 2.584, and f/y=x=2\partial f/\partial y = x = 2. Both match exactly — the graph walk produced the true derivative, using only elementary local rules, never symbolically expanding ff as a whole.

x=2 y=3 v₁=xy = 6 v₂=sin x = 0.909 v₃=f = 6.909 adjoint(x) = 3 − 0.416 = 2.584 adjoint(y) = 2
Values flow forward through the graph (top labels); adjoints — the accumulating derivative of the final output — flow backward, each node multiplying by its own simple local derivative.

Worked example 2: why reverse-mode wins when inputs outnumber outputs

Forward-mode can compute the same derivatives, but it needs one full sweep per input. Seed dx=1,dy=0dx = 1, dy = 0 (asking "how does everything move if only xx moves") and propagate a derivative alongside every value:

dv1=ydx+xdy=3(1)+2(0)=3,dv2=cos(x)dx=cos(2)(1)=0.416,dv3=dv1+dv2=2.584dv_1 = y\,dx + x\,dy = 3(1) + 2(0) = 3, \quad dv_2 = \cos(x)\,dx = \cos(2)(1) = -0.416, \quad dv_3 = dv_1 + dv_2 = 2.584

That one sweep recovers f/x=2.584\partial f/\partial x = 2.584. A second sweep, seeded dx=0,dy=1dx=0, dy=1, is required to get f/y\partial f/\partial y:

dv1=3(0)+2(1)=2,dv2=cos(2)(0)=0,dv3=2+0=2dv_1 = 3(0) + 2(1) = 2, \quad dv_2 = \cos(2)(0) = 0, \quad dv_3 = 2 + 0 = 2

Two inputs required two separate forward sweeps. Reverse-mode got both derivatives from a single backward sweep in worked example 1. Scale this up: a neural network with a million weights and one scalar loss would need one million forward-mode sweeps to get every weight's gradient, or exactly one reverse-mode sweep. That is not a minor efficiency difference — it is the difference between "trainable" and "impossible," and it is the entire reason backpropagation (reverse-mode automatic differentiation, applied to a graph whose final node is a scalar loss) is how every modern network is trained.

The cos(x)\cos(x) term above is one of the "elementary operations with a known local derivative" that autodiff leans on — every node in the graph is something this simple, chained together. Drag bb below and watch the tangent line update: that live slope is exactly the local derivative autodiff looks up at each node, before the chain rule stitches all of them together.

Function explorer
-221.1
x = 1.00f(x) = 0.731

Automatic differentiation never symbolically expands a function — it decomposes it into elementary operations with known local derivatives, then applies the chain rule mechanically along the graph. Reverse-mode gets the derivative of one output with respect to every input in a single backward sweep, which is why it, not forward-mode, powers neural network training.

What this means in practice

Every operation used inside a model — matrix multiplies, activation functions, even control flow like loops over a variable-length sequence — must have a known local derivative for automatic differentiation to work through it; frameworks like PyTorch and TensorFlow build the computational graph as the forward pass executes (or ahead of time), record every elementary operation, and then run reverse-mode automatically when .backward() or its equivalent is called. This is also precisely why a custom operation that is not differentiable — like a hard threshold, or code that reads a value out to plain Python and back — silently breaks gradient flow: the graph has a node with no known local derivative, or a gap where no node was recorded at all, and the chain rule cannot walk across it.

Practice

  1. For f(x,y)=x2yf(x,y) = x^2 y at x=3,y=2x=3, y=2, break it into elementary steps, run a forward pass, then a reverse-mode backward pass to find f/x\partial f/\partial x and f/y\partial f/\partial y, and check both against direct calculus.
  2. A model has 3 inputs and 1 output. How many forward-mode sweeps would be needed to get the full gradient, and how many reverse-mode sweeps?
  3. Explain in one sentence why a graph with 1 input and 1,000 outputs would actually prefer forward-mode over reverse-mode.

The common confusion is treating automatic differentiation as if it were the same thing as either symbolic differentiation (algebraically manipulating a formula, which can blow up in size for complex expressions) or numerical differentiation (approximating a derivative with finite differences, which is only ever approximate and can be badly inaccurate). Automatic differentiation is neither — it produces the exact derivative (up to floating-point rounding), using only the same elementary operations the forward computation used, at a cost that is a small constant multiple of the forward pass itself, regardless of how many inputs there are. Confusing "automatic" with "approximate" leads people to distrust gradients that are, in fact, exact.

Related concepts

Practice in interviews

Further reading

  • Baydin, Pearlmutter, Radul & Siskind, Automatic Differentiation in Machine Learning: A Survey (2018)
  • Griewank & Walther, Evaluating Derivatives (2008)
ShareTwitterLinkedIn