Automatic Differentiation

Suppose you write a function with a thousand lines of code: convolutions, attention layers, custom losses, control flow with loops and conditionals. You need its exact gradient with respect to millions of parameters. It must be computed fast enough to run once per training step. Writing that gradient by hand is infeasible and numerically fragile. Automatic differentiation (autodiff, AD) solves this by mechanically transforming the program itself. It becomes a new program that computes derivatives exactly, up to floating-point rounding. The cost is comparable to the original computation. Autodiff is the machinery underneath every modern deep learning framework. When you call `.backward()` in PyTorch or trace a function in JAX, you invoke a reverse-mode autodiff engine. It walks a computational graph built from elementary operations, each with a known local derivative rule. This article generalizes and formalizes what Backpropagation does for neural networks specifically. Backpropagation is reverse-mode autodiff applied to the layered computation graph of a neural network's loss function. The big picture is that differentiation is compositional. Any program, no matter how complex its control flow, is ultimately a sequence of elementary operations (add, multiply, sine, exponential, matrix multiply). We know how to differentiate each of these in isolation. The chain rule glues these local derivatives into a global derivative. Autodiff systems exploit this by recording the sequence of operations executed (the trace). They then apply the chain rule mechanically, either forward through the trace (forward mode) or backward through it (reverse mode). They never resort to symbolic manipulation of closed-form expressions or to numerical finite differences. This article delivers a precise definition of forward- and reverse-mode AD. It covers the dual-number and adjoint formulations, and full pseudocode for building and traversing a computational graph. It includes a complexity derivation explaining why reverse mode is the only tractable choice for training deep networks. It also gives a worked numerical trace through both modes on the same function. Finally, it discusses the practical failure modes engineers hit when implementing or debugging AD systems.

Key takeaways
  • Automatic differentiation computes exact derivatives of programs by applying the chain rule mechanically over a graph of elementary operations, avoiding both symbolic bloat and finite-difference error.
  • Forward mode propagates directional derivatives alongside primal values; it costs one pass per input dimension.
  • Reverse mode propagates adjoints backward from the output; it costs one pass per output dimension, regardless of the number of inputs.
  • Neural network training needs gradients of one scalar loss with respect to millions of parameters — reverse mode gives this in a single backward pass, which is exactly what Backpropagation is.
  • AD is implemented by building a computational graph (dynamically or statically) that records every operation and its local Jacobian-vector product rule.
  • AD is exact up to floating-point precision, unlike numerical differentiation, and unlike symbolic differentiation it does not suffer expression-swell.
The core insight

Automatic differentiation applies the chain rule mechanically to a graph of elementary operations, and choosing forward vs. reverse mode is a choice about which direction through that graph is cheaper.

The idea

Automatic differentiation is a family of techniques for evaluating the derivative of a numerically-specified function by decomposing it into elementary operations — addition, multiplication, exponentiation, trigonometric functions, and so on — each with a known derivative, and combining these via the chain rule. It stands apart from two other approaches. Numerical differentiation approximates derivatives via finite differences, e.g. f(x+h)−f(x)h\frac{f(x+h)-f(x)}{h}, which is cheap to implement but suffers from truncation error (bias from finite hh) and cancellation error (loss of precision as h→0h \to 0), and requires a separate function evaluation per input dimension. Symbolic differentiation manipulates expression trees to produce closed-form derivative expressions, as a computer algebra system would; it is exact but the resulting expressions can grow exponentially in size (expression swell) when applied to programs with shared subexpressions, loops, or branching. Autodiff sidesteps both problems: it evaluates derivatives numerically at a specific point, using the exact symbolic rule for each elementary step, and reuses shared intermediate values instead of re-expanding them. The formal object underlying AD is the computational graph (or Wengert list, or tape): a directed acyclic graph whose nodes are variables (inputs, intermediate values, outputs) and whose edges represent elementary operations. Every differentiable program, however it is written — with loops, conditionals, recursion — can be unrolled at a specific input into such a graph, because AD only needs to differentiate through the operations actually executed, not through all possible executions. This is the crucial insight: AD operates on the execution trace, not on the static source code, which is why it handles arbitrary control flow correctly as long as that control flow doesn't itself depend discontinuously on the differentiated variables in a way that breaks differentiability. Two dual computational strategies exploit this graph. Forward-mode AD computes, alongside every intermediate value viv_i, its directional derivative v˙i=∂vi∂xj\dot v_i = \frac{\partial v_i}{\partial x_j} with respect to one designated input xjx_j, propagating from inputs to outputs in the same order as the original computation. Reverse-mode AD computes, for one designated output yky_k, the adjoint vˉi=∂yk∂vi\bar v_i = \frac{\partial y_k}{\partial v_i} for every intermediate variable, propagating from outputs back to inputs — the opposite order from the original computation, which is precisely what "backpropagation" refers to. Both modes compute the exact same Jacobian matrix J=∂y∂xJ = \frac{\partial y}{\partial x}; they differ only in whether they compute it column-by-column (forward) or row-by-row (reverse).

How it works

Forward mode. Extend every real number vv in the computation to a dual number pair (v,v˙)(v, \dot v), where v˙\dot v tracks the derivative of vv with respect to a chosen input direction. Elementary operations are overloaded to propagate both components simultaneously using the sum, product, and chain rules:

(u,u˙)+(v,v˙)=(u+v, u˙+v˙),(u,u˙)⋅(v,v˙)=(uv, u˙v+uv˙),f(u,u˙)=(f(u), f′(u) u˙).(u,\dot u) + (v,\dot v) = (u+v,\ \dot u + \dot v), \qquad (u,\dot u)\cdot(v,\dot v) = (uv,\ \dot u v + u\dot v), \qquad f(u,\dot u) = (f(u),\ f'(u)\,\dot u).

Seed the input whose derivative you want with x˙j=1\dot x_j = 1 (and every other input with x˙=0\dot x = 0), run the program once, and every intermediate and output variable now carries its derivative with respect to xjx_j for free, in the same pass as the value itself. To get the full Jacobian for nn inputs, repeat this nn times, once per seed direction (or propagate all nn seed directions simultaneously as vectors, at n×n\times the per-variable memory).

Reverse mode. Run the ordinary computation forward once, recording every elementary operation and its inputs on a tape (the computational graph). Then traverse the tape backward, maintaining for each variable an adjoint vˉ=∂y/∂v\bar v = \partial y / \partial v initialized to yˉ=1\bar y = 1 at the single output of interest. At each node, propagate the adjoint to its parents (the graph inputs to that operation) using the local Jacobian-vector product (a "vector-Jacobian product", VJP) of that elementary operation, and accumulate contributions when a variable feeds into multiple downstream nodes (multivariate chain rule = sum over all paths). After one backward traversal, every input variable holds ∂y/∂xi\partial y/\partial x_i — the entire gradient, for the cost of roughly one extra forward-equivalent pass, regardless of the number of inputs. ```

# ---------- Shared representation ----------

# A computational graph node stores:

# value : the primal numeric value

# op : the elementary operation that produced it (or None for a leaf/input)

# parents : the input nodes to that operation

# local_fn : function(parents_values) -> value (forward rule)

# vjp_fn : function(grad_output) -> tuple(grad_wrt_each_parent) (reverse rule)

# jvp_fn : function(parents_tangents) -> tangent (forward rule)

# ---------- FORWARD MODE (dual numbers) ---------- function FORWARD_MODE_AD(f, x, seed_index):

# x: list of n input values; seed_index: which input's derivative we want n = length(x) v = copy(x) # primal values vdot = zeros(n) vdot[seed_index] = 1.0 # seed direction: d/dx_seed for each elementary step (op, inputs) in TRACE(f, v): # trace built by executing f primal_out = op.local_fn(values(inputs)) tangent_out = op.jvp_fn(tangents(inputs)) # chain rule, one direction record(primal_out, tangent_out) return primal_out, tangent_out # = f(x), df/dx_seed evaluated at x

# To get the full gradient of a SCALAR loss w.r.t. n inputs: call this n times -> O(n) passes.

# ---------- REVERSE MODE (tape + adjoints) ---------- function REVERSE_MODE_AD(f, x): tape = [] # ordered list of (node, op, parents)

# ---- Forward pass: build the graph, remember every op ---- for each elementary step (op, parents) executed while computing f(x): out_node = Node(value = op.local_fn(values(parents)), op = op, parents = parents) tape.append(out_node) y = tape[-1].value # scalar output (the loss)

# ---- Backward pass: propagate adjoints ---- adjoint = { node: 0 for node in tape } adjoint[tape[-1]] = 1.0 # dy/dy = 1 for node in reversed(tape): # topological order guarantees parents processed after children if adjoint[node] == 0: continue grads_wrt_parents = node.op.vjp_fn(adjoint[node], node.parents) for (parent, g) in zip(node.parents, grads_wrt_parents): adjoint[parent] += g # accumulate: multiple paths sum (multivariate chain rule) return y, { x_i: adjoint[x_i] for x_i in inputs(x) } # one pass gives ALL n partial derivatives ``` The key structural difference is visible in the pseudocode: forward mode's outer loop is over input dimensions (repeat the whole pass per seed), while reverse mode's outer loop is over nothing — one forward pass builds the tape, one backward pass yields every partial derivative simultaneously, because the adjoint accumulation rule naturally fans out to all parents at once.

Figure
Reverse-mode AD: forward pass builds the tape (solid), backward pass propagates adjoints (dashed)
x y a=xy b=sin a c=a² z=b+c forward pass (build tape) — solid backward pass (adjoints) — dashed z̄ = 1

Complexity & correctness

Forward mode complexity. Let the primal computation cost TT (time) and SS (space, dominated by intermediate values), for a function f:Rn→Rmf:\mathbb R^n \to \mathbb R^m. Propagating one tangent direction costs a constant factor more than the primal (each elementary op does one extra local multiply/add for the tangent), so one directional derivative costs O(T)O(T) time and O(S)O(S) space — no tape is needed, since tangents are consumed as the pass proceeds. To get the full Jacobian (nn columns), repeat nn times: total O(nT)O(nT) time. Forward mode is therefore efficient when nn is small (few inputs, e.g. sensitivity of an output to one or two physical parameters) or when m≫nm \gg n (many outputs, few inputs — Jacobian is "wide"), since you'd need O(nT)O(nT) regardless of mm.

Reverse mode complexity. The forward pass costs O(T)O(T) and must be stored in full — every intermediate value on the tape is kept because the backward pass needs it to evaluate local derivative rules — so space is O(S)O(S) where SS scales with the number of operations executed (not just the primal's own working memory), a real and often binding cost for very deep networks. The backward pass processes each tape entry exactly once, and each entry's `vjp_fn` costs a constant factor more than its forward counterpart (Baur–Strassen-type argument: differentiating one elementary op costs at most a small constant times evaluating it), so the backward pass is also O(T)O(T). One backward pass yields the entire row of the Jacobian for a chosen output — i.e., the gradient of a scalar loss with respect to all nn inputs simultaneously — at total cost O(T)O(T), independent of nn. This is why training a network with 10810^8 parameters is feasible: reverse mode gives all 10810^8 partial derivatives for about 22–3×3\times the cost of one forward evaluation, whereas forward mode or finite differences would need 10810^8 separate passes.

Why reverse mode wins for deep learning. A training step computes one scalar loss LL (so m=1m=1) as a function of millions of parameters (so n≫1n \gg 1). Forward mode costs O(nT)O(nT) — intractable. Reverse mode costs O(T)O(T) — one backward pass. This asymmetry, m=1≪nm=1 \ll n, is exactly the regime reverse mode is built for, and it is precisely the mathematical content of Backpropagation: applying reverse-mode AD to the graph of a layered network's loss.

Correctness argument. The correctness of reverse mode follows from the multivariate chain rule applied inductively over a topological order of the DAG. Define the adjoint of node vv as vˉ=∂y/∂v\bar v = \partial y/\partial v for the single scalar output yy. If vv feeds into successors w1,…,wkw_1,\dots,w_k, the chain rule gives

vˉ=∂y∂v=∑i=1k∂y∂wi⋅∂wi∂v=∑i=1kwˉi⋅∂wi∂v.\bar v = \frac{\partial y}{\partial v} = \sum_{i=1}^k \frac{\partial y}{\partial w_i}\cdot\frac{\partial w_i}{\partial v} = \sum_{i=1}^k \bar w_i \cdot \frac{\partial w_i}{\partial v}.

Processing nodes in reverse topological order guarantees that when we reach vv, every successor wiw_i has already had its adjoint wˉi\bar w_i fully accumulated (all of its successors were processed earlier, since reverse order visits sinks before sources). Thus the accumulation loop in the pseudocode — `adjoint[parent] += g` executed for every node in reverse order — computes exactly this sum, term by term, and by induction on the topological order, every node's adjoint equals its true partial derivative once all of its successors have been visited. The base case is the output itself, yˉ=1\bar y = 1. This is a finite, exact sequence of arithmetic operations — no truncation, no approximation — so reverse-mode AD is exact up to the floating-point precision of the arithmetic used, unlike numerical differentiation which has an unavoidable O(h)O(h) or O(h2)O(h^2) truncation bias.

Honest tradeoffs. Reverse mode trades time for memory: the tape must retain every intermediate activation for the backward pass, which is the dominant memory cost of training large networks (addressed in practice by graph-level tricks like gradient checkpointing/activation recomputation, trading recomputation time for reduced memory). Forward mode needs no tape and has flat memory, but its cost scales linearly in input dimension, making it a poor fit for gradient descent over huge parameter vectors, though it remains the tool of choice for Jacobian-vector products, Hessian-vector products (forward-over-reverse), and sensitivity analysis with few parameters.

Worked example

Let f(x,y)=sin⁡(xy)+(xy)2f(x,y) = \sin(xy) + (xy)^2, evaluated at x=2,y=3x=2, y=3. Let a=xya = xy, b=sin⁡ab=\sin a, c=a2c = a^2, z=b+cz = b + c. We want ∂z/∂x\partial z/\partial x and ∂z/∂y\partial z/\partial y. Analytically: z=sin⁡(xy)+(xy)2z = \sin(xy) + (xy)^2, so ∂z/∂x=ycos⁡(xy)+2xy2\partial z/\partial x = y\cos(xy) + 2xy^2 and ∂z/∂y=xcos⁡(xy)+2x2y\partial z/\partial y = x\cos(xy) + 2x^2y. At (x,y)=(2,3)(x,y)=(2,3): xy=6xy = 6, cos⁡6≈0.9602\cos 6 \approx 0.9602, so ∂z/∂x=3(0.9602)+2(2)(9)=2.8806+36=38.8806\partial z/\partial x = 3(0.9602) + 2(2)(9) = 2.8806 + 36 = 38.8806, and ∂z/∂y=2(0.9602)+2(4)(3)=1.9204+24=25.9204\partial z/\partial y = 2(0.9602) + 2(4)(3) = 1.9204 + 24 = 25.9204.

Reverse mode trace (one forward pass, one backward pass gives both derivatives):

stepforward computationvalue
1a=x⋅ya = x\cdot y2⋅3=62\cdot 3 = 6
2b=sin⁡ab = \sin asin⁡6≈−0.2794\sin 6 \approx -0.2794
3c=a2c = a^23636
4z=b+cz = b + c35.720635.7206
stepbackward computation (adjoints)value
5zˉ=1\bar z = 111
6bˉ=zˉ⋅1\bar b = \bar z \cdot 1 (from z=b+cz=b+c)11
7cˉ=zˉ⋅1\bar c = \bar z \cdot 111
8aˉ+=bˉ⋅cos⁡a\bar a \mathrel{+}= \bar b\cdot\cos a (from b=sin⁡ab=\sin a)cos⁡6≈0.9602\cos 6 \approx 0.9602
9aˉ+=cˉ⋅2a\bar a \mathrel{+}= \bar c\cdot 2a (from c=a2c=a^2)0.9602+12=12.96020.9602 + 12 = 12.9602
10xˉ=aˉ⋅y\bar x = \bar a \cdot y (from a=xya=xy)12.9602⋅3=38.880612.9602\cdot 3 = 38.8806
11yˉ=aˉ⋅x\bar y = \bar a \cdot x12.9602⋅2=25.920412.9602\cdot 2 = 25.9204

Both partial derivatives match the analytic values exactly, and step 9 shows the accumulation rule in action: aa feeds two successors (bb and cc), so its adjoint is the sum of contributions from both — the multivariate chain rule enforced automatically by the `+=` accumulation, exactly as the correctness argument requires.

Forward mode, for comparison, needs two separate passes (one per input) to get the same two numbers. Seeding x˙=1,y˙=0\dot x = 1,\dot y=0: a˙=x˙y+xy˙=3\dot a = \dot x y + x\dot y = 3; b˙=cos⁡(a)a˙=0.9602⋅3=2.8806\dot b = \cos(a)\dot a = 0.9602\cdot 3=2.8806; c˙=2aa˙=12⋅3=36\dot c = 2a\dot a = 12\cdot 3=36; z˙=b˙+c˙=38.8806\dot z = \dot b+\dot c = 38.8806 — matches ∂z/∂x\partial z/\partial x. Re-run seeding x˙=0,y˙=1\dot x=0,\dot y=1 to get z˙=25.9204=∂z/∂y\dot z = 25.9204 = \partial z/\partial y. Two full passes for two derivatives versus one backward pass for both — the gap only widens as the number of inputs grows, which is the entire complexity argument made concrete.

Common pitfalls & traps

PitfallWhy it happensConsequence
Confusing forward and reverse mode's cost regimesAssuming one mode is "just better"Using forward mode for nn-parameter training makes cost O(nT)O(nT) instead of O(T)O(T)
Forgetting adjoint accumulation over shared subexpressionsTreating each use of a variable independently instead of summingSilently wrong gradients when a value is reused (e.g. tied weights, residual connections)
In-place mutation of tensors needed for the backward passOverwriting a value before its adjoint rule needs itBackward pass reads corrupted/overwritten data, producing wrong or `NaN` gradients
Non-differentiable ops treated as smooth (e.g. `argmax`, hard threshold)AD differentiates whatever code ran, even if the true function has a kink or discontinuityGradient exists numerically but is mathematically meaningless at non-differentiable points
Retaining the entire tape unnecessarilyNot freeing intermediate activations no longer neededMemory blow-up on deep networks / long sequences
Detached/no-grad boundaries misusedAccidentally cutting the graph (`.detach()`, `stop_gradient`) where a gradient path was neededSome parameters silently receive zero gradient and never train

Pioneers & history

Automatic differentiation's reverse mode was independently discovered several times before becoming central to deep learning: in optimal control theory, in numerical computing, and finally as the engine of neural network training under the name backpropagation. Seppo Linnainmaa gave the first complete reverse-mode AD algorithm for arbitrary differentiable programs in his 1970 thesis, describing it as an error-propagation method for numerical rounding analysis, without yet connecting it to machine learning. The idea was reinvented and popularized for training multilayer neural networks over the following decade, culminating in the 1986 paper by Rumelhart, Hinton, and Williams that demonstrated backpropagation could train hidden layers to learn useful internal representations, catalyzing the connectionist revival.

Origins · 1 minSeppo Linnainmaa and how this was discovered
SL
Seppo Linnainmaa
1945– · Finnish computer scientist, originator of reverse-mode automatic differentiation

Linnainmaa's 1970 University of Helsinki master's thesis described what is now recognized as the first full formulation of reverse-mode automatic differentiation, developed for tracking rounding error propagation through numerical computations rather than for machine learning, decades before the technique became the computational backbone of deep learning training.

History
1964
— Henry J. Kelley and Arthur Bryson develop reverse-mode-like methods in optimal control theory.
1970
— Seppo Linnainmaa formalizes reverse-mode automatic differentiation for arbitrary computational graphs.
1986
— Rumelhart, Hinton, and Williams popularize backpropagation for training multilayer neural networks.
2000
s — General-purpose AD libraries (ADOL-C, Theano) bring reverse-mode AD to scientific computing and deep learning.
2015–2018
— TensorFlow, PyTorch, and JAX make dynamic, dataflow-graph-based reverse-mode AD a default tool for every deep learning researcher.

Where this matters

Every major deep learning framework — PyTorch's `autograd`, TensorFlow's `GradientTape`, JAX's `grad` — is a reverse-mode AD engine operating over a computational graph, whether built dynamically at runtime (define-by-run) or compiled ahead of time (define-and-run). Training any neural network, from a small multilayer perceptron to a trillion-parameter transformer, relies on reverse-mode AD to compute the gradient of the loss with respect to every weight in a single backward pass, which is the concrete mechanism behind Backpropagation. Beyond neural networks, forward-mode AD is standard in engineering simulation and sensitivity analysis (computing how a small number of design parameters affect many outputs, e.g. in computational fluid dynamics or circuit design), and mixed forward-over-reverse AD computes Hessian-vector products used in second-order optimization and in curvature-aware training diagnostics. Physics-informed machine learning, differentiable rendering, and probabilistic programming languages all depend on AD to differentiate through complex, control-flow-heavy programs exactly. Once a framework can compute exact gradients cheaply, the next practical question becomes how to initialize the parameters those gradients will update, which leads directly into Weight Initialization Strategies.

Ask the tutor: How does gradient checkpointing trade recomputation time for reduced memory in reverse-mode AD, and how do you choose which activations to checkpoint?

Is automatic differentiation the same thing as backpropagation?

Not quite — backpropagation is reverse-mode automatic differentiation specialized to the computational graph of a layered neural network's loss function. Automatic differentiation is the general theory and mechanism (forward mode, reverse mode, mixed modes) that applies to any differentiable program, including ones with loops, branches, or no neural network structure at all. When people say "backprop," they specifically mean running reverse-mode AD over the layer-by-layer graph that defines a network's forward computation, propagating gradients from the scalar loss back to every weight. So backpropagation is a special case and a historical name for applying reverse-mode AD to neural networks, not a distinct algorithm.

Why not just use numerical differentiation (finite differences) instead of AD?

Finite differences approximate f′(x)≈f(x+h)−f(x)hf'(x) \approx \frac{f(x+h)-f(x)}{h}, which introduces truncation error that shrinks as h→0h\to 0, but simultaneously introduces catastrophic cancellation error from subtracting two nearly-equal floating-point numbers, which grows as h→0h\to 0. There is no value of hh that eliminates both, so finite differences are fundamentally approximate, typically accurate to only about half the machine precision's significant digits. They also require one extra function evaluation per input dimension, making them O(nT)O(nT) like forward-mode AD but with dramatically worse accuracy. Finite differences remain useful only as a debugging tool to sanity-check an AD implementation, not as the production gradient computation method.

Can automatic differentiation handle control flow like loops and if-statements?

Yes, because AD operates on the trace of operations actually executed for a specific input, not on the static source code. A loop that runs five times simply gets unrolled into five sets of recorded operations on the tape; an if-statement contributes only the branch that was actually taken. This means the gradient is correct for that specific input's execution path, but it also means AD is silent about discontinuities: if a branch condition itself depends on the differentiated variable in a way that changes behavior nearby, the true function may not be differentiable there, even though AD will still return some numeric derivative for the executed path.

What is a Jacobian-vector product (JVP) and a vector-Jacobian product (VJP), and how do they relate to forward and reverse mode?

For a function f:Rn→Rmf:\mathbb R^n\to\mathbb R^m with Jacobian J∈Rm×nJ\in\mathbb R^{m\times n}, a JVP computes JvJv for a given direction vector v∈Rnv\in\mathbb R^n — this is exactly what forward-mode AD computes at each elementary step, propagating a tangent vector forward. A VJP computes u⊤Ju^\top J for a given vector u∈Rmu\in\mathbb R^m — this is exactly what reverse-mode AD computes, propagating an adjoint (cotangent) backward. Every elementary operation in an AD system must implement both a `jvp_fn` and a `vjp_fn` rule; frameworks like JAX expose these primitives directly, letting users compose forward-over-reverse or reverse-over-forward differentiation for Hessian-vector products.

Why does reverse-mode AD need so much memory, and how do practitioners deal with it?

Reverse mode must retain every intermediate value produced during the forward pass, because the backward pass's local derivative rules typically depend on those primal values (e.g., d(sin⁡a)/da=cos⁡ad(\sin a)/da = \cos a needs aa itself). For a deep network with many layers processed on large batches, this tape of activations can dominate GPU memory usage, often more than the parameters themselves. The standard mitigation is gradient checkpointing (activation recomputation): store only a subset of activations, and recompute the rest on demand during the backward pass by re-running short forward segments, trading extra compute time for reduced peak memory.

Does automatic differentiation give the mathematically exact gradient, or just an approximation?

AD gives the exact gradient of the function as computed by the program, up to the floating-point rounding error inherent in any numerical computation — it does not introduce any additional approximation of its own, unlike finite differences. Each elementary step's derivative rule (e.g. d(sin⁡x)/dx=cos⁡xd(\sin x)/dx=\cos x) is applied in exact symbolic form, evaluated numerically at the current point, so the composition via the chain rule is mathematically exact. The only caveat is that if the underlying function itself is not truly differentiable at the given input (e.g. at a kink in a ReLU, or across a discrete branch), AD still returns some well-defined numeric value by convention (e.g. a subgradient), which may not correspond to a classical derivative.

How does forward mode relate to computing an entire Jacobian efficiently when there are many outputs but few inputs?

When f:Rn→Rmf:\mathbb R^n\to\mathbb R^m has nn small and mm large — for instance, a simulation with two tunable physical parameters producing a large output field — forward mode needs only nn passes (one tangent seed per input) to recover the full m×nm\times n Jacobian, each pass costing O(T)O(T), for total O(nT)O(nT). Reverse mode, by contrast, would need mm backward passes (one adjoint seed per output row), costing O(mT)O(mT), which is far worse when m≫nm\gg n. This is the mirror image of the deep learning case, and it is why forward mode remains the preferred tool in fields like sensitivity analysis and simulation with few design parameters.

Before this

Where this leads