TLDR

  1. A complex differentiable model is trainable because composition converts one global error into local sensitivity calculations that can be reused across the whole programme.
  2. The computation graph is the executable record of those dependencies: reverse mode walks it backwards, multiplying along paths and adding where paths meet.
  3. Design models as small typed transformations, include preprocessing and loss construction in the inspected graph, and test every custom derivative against an independent numerical check.
  4. A correct graph cannot rescue a hard threshold, hidden side effect, stale branch, saturated derivative or mathematically faithful gradient that is numerically useless.
  5. Choose reverse mode for many inputs and few outputs, forward mode for the opposite shape, and make graph breaks explicit architecture decisions.

One wrong number, four possible causes

0.068431502 the model's loss

A tiny model receives an input of 1.5, multiplies it by a weight of 0.8, adds a bias of −0.2, passes the result through a hyperbolic tangent, and compares the prediction with a target of 0.5. Its prediction is 0.761594. The final discrepancy is the number above.

That number tells us the model is wrong. It does not say how to change the weight, whether the bias matters more, whether the input is unusually influential, or where the correction becomes weak. A human can differentiate the expression by hand. A computer can rerun the whole model after nudging each input. Neither strategy is attractive when the expression has millions of parameters, reused intermediates and conditional branches.

The escape is surprisingly modest. Name the intermediate results. Record which result came from which operation. Give every primitive operation a local rule for how small changes pass through it. Then traverse those records in reverse.

Composition makes a global correction computable as a sequence of local contracts; the computation graph is the executable record of those contracts. The practical consequence is equally important: seeing a framework produce gradients is insufficient. A reviewer must establish that the graph represents the intended programme, that every local derivative is valid where the system operates, and that the resulting sensitivity is numerically useful.

The compositional bargain

A function is a rule that maps an admissible input to an output. The useful move is to resist treating a model as one enormous rule. Let f turn an input x into an intermediate z, and let g turn z into an output y. Their composition is written g ∘ f, meaning that f acts first and g receives its result.

z = f(x) and y = g(z) so y = (g ∘ f)(x) The intermediate z is a boundary. It lets us ask separately how x changes z, and how z changes y.

This naming seems cosmetic until a correction must travel backwards. For scalar quantities, the chain rule says that the sensitivity of y to x is the sensitivity of y to z, multiplied by the sensitivity of z to x.

dy/dx = (dy/dz) × (dz/dx) If either local term is near zero, the influence weakens. If either term is large, the influence grows. Repeated composition repeats this multiplication.

The equation is familiar. Its engineering meaning is easier to miss. Each primitive needs only a local derivative rule. Multiplication knows how its output changes with either input. Addition passes sensitivity unchanged. The hyperbolic tangent uses 1 − tanh²(z). The squared error uses twice the prediction error. None of these primitives needs to understand the model around it.

Automatic differentiation grew from precisely this decomposition. Wengert's 1964 programme evaluated derivatives by breaking an algebraic function into elementary expressions rather than expanding one symbolic derivative.[1] Modern systems differ in tracing, intermediate representations and compiler strategy, but the causal economy is the same: global sensitivity is assembled from local rules whose scope is small enough to implement, test and reuse.

A minimal worked example

Return to the opening model. Its forward computation is:

z = wx + b ; y = tanh(z) ; L = (y − t)² x is the input, w the weight, b the bias, t the target, y the prediction and L the loss.
Affine state z1.000000
Prediction y0.761594
Loss L0.068432
∂L/∂z0.219726
∂L/∂w0.329589
∂L/∂b0.219726

Start at the loss. The local derivative ∂L/∂y is 2(y − t) = 0.523188. The derivative ∂y/∂z is 1 − y² = 0.419974. Their product gives ∂L/∂z = 0.219726. Because ∂z/∂w = x = 1.5, the weight gradient is 0.329589. Because ∂z/∂b = 1, the bias gradient is 0.219726.

A small step opposite those positive gradients will lower the loss locally. Yet the gradients do not prove that the weight is the worldly cause of the error. They describe the slope of this chosen function at this chosen point. The distinction matters whenever inputs are correlated, features are proxies, or the objective omits a real consequence.

Loss landscape over weight and bias with a line of equally good solutions Diagonal contour bands surround a teal zero-loss valley. A coral point marks weight 0.8 and bias minus 0.2. A gradient arrow points uphill and a descent arrow points towards the valley. weight w bias b 00.71.4 −1.0−0.20.7 Local uphill sensitivity ∂L/∂w = 0.330, ∂L/∂b = 0.220 Descent heads towards the valley many (w, b) pairs make the same z zero-loss line: 1.5w + b = atanh(0.5)
Figure 1. The graph gives a direction, not a unique explanation. Values are calculated from the synthetic opening function. The diagonal zero-loss valley shows that weight and bias are not separately identifiable from one observation, even though their local gradients are exact.

Figure 1 adds a useful warning. The loss is zero along an entire line of weight and bias pairs. The graph can tell us a local descent direction from the current point. It cannot choose a unique explanation without more data, regularisation or constraints. Differentiability provides navigability, not identifiability.

What inspectability actually requires

Breaking a model into functions does not automatically make it inspectable. A useful boundary states the admissible input type and shape, the output it promises, the parameters it reads, any state it mutates, and the derivative rule it exposes. A function that silently reads a global variable or changes behaviour with hidden state may be easy to call while remaining difficult to reason about. The graph records declared data dependencies; it cannot reveal a dependency that the programme concealed from the representation.

For vector-valued functions, the local contract is a linear map rather than a table of every partial derivative. A primitive can say how an input tangent is pushed forwards, or how an output adjoint is pulled backwards, without materialising its full Jacobian. This is why matrix multiplication, convolution and attention can participate in large graphs: their derivative rules exploit structure rather than constructing vast dense matrices.

Inspectability means that a reviewer can reconstruct both the value path and the sensitivity path at the chosen boundary. That requires names for consequential intermediates, stable shape information, visible parameter reuse and declared graph breaks. It does not require exposing every compiler instruction. The right level is the smallest representation that preserves the claim being reviewed. For a custom normalisation layer, that may be one primitive plus a verified VJP. For a model-wide investigation, it may be a staged intermediate representation with selected activation and gradient probes.

Thought experiment: the sealed module

Imagine two modules with identical inputs and outputs. Module A exposes a sequence of multiply, add and tanh operations. Module B is a sealed binary that returns the same number for every tested input. Vary only the visibility of internal composition.

Finite differences can estimate how either complete module responds to a small input change. For one input, that may be enough. Only Module A can cheaply attribute sensitivity to internal parameters, reuse derivative work across shared intermediates, expose a suspect local rule, or permit a compiler to transform the backward computation.

The experiment isolates the value of the graph. It does not create the derivative. The derivative belongs to the mathematical function. The graph supplies an inspectable and reusable route for evaluating it.

The argument in one passChart · flow
flowchart TD
  A["One wrong number, four<br/>possible causes"] --> B["The compositional bargain"]
  B --> C["The graph carries two journeys"]
  C --> D["Where gradients fail without<br/>being false"]
  D -. "re-test" .-> A
  classDef first fill:#0a1424,stroke:#B96D00,stroke-width:2px,color:#e6f3ff
  classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff
  class A first
  class B,C,D step
A structural route through Functions, Composition and Computation Graphs, from One wrong number, four possible causes to Sources. Reading use: follow the relation, then test it against the surrounding evidence and limits.

The graph carries two journeys

A computation graph is a directed record of dependencies. Nodes hold inputs, intermediate values or primitive operations. An edge says that one result was needed to produce another. For a straight-line differentiable programme, the graph is usually acyclic. Loops are represented by unrolling, a loop primitive, a fixed-point operator or a compiler-level control-flow structure.

The forward journey computes values. The reverse journey computes sensitivities. During the forward pass, a tracing system records enough information to replay local derivative rules. PyTorch describes this conceptually as a directed acyclic graph of operations created while tensors execute.[9] JAX can instead stage a typed functional intermediate representation called a jaxpr, which can be inspected and transformed.[12] TensorFlow can turn Python functions into graph data structures that may be saved, optimised and executed away from Python.[14]

These are implementation choices, not rival definitions of the mathematics. An eager tape, a static graph and compiler intermediate representation can all support the same derivative calculation. The essential object is the executed dependency structure plus valid local linearisation rules.

Three techniques are often blurred. Finite differences perturb a complete programme and approximate a slope. Symbolic differentiation rewrites an expression into another expression, which can duplicate subexpressions and grow unwieldy. Automatic differentiation evaluates the original primitives while propagating exact chain-rule information to floating-point accuracy.[15] Backpropagation is the neural-network use of reverse-mode differentiation to move a loss signal through layered parameters; the optimiser that applies those gradients is a separate mechanism.[3] Keeping these objects distinct clarifies failures: an autodiff defect concerns the derivative programme, while a poor optimiser step can occur even when every derivative is correct.

Annotated computation graph for the minimal model Inputs x, w, b and target feed multiply, add, tanh, subtraction and square operations. Forward values are shown below nodes and local derivative rules above edges. wxbt 0.81.5−0.20.5 multiplyaddtanhsubtractsquare 1.21.00.7615940.2615940.068432 ∂/∂w = x∂/∂x = wpass 1 1−y²pass 12(y−t) Forward: values travel left to right Backward: adjoints travel right to left through the same dependencies
Figure 2. A function becomes operationally inspectable when its intermediates and local rules are named. All values are calculated from the synthetic example. The drawing is a semantic graph, not a claim about one framework's internal node layout.

The reverse recurrence

Suppose each node vj is produced by a primitive function of its parent nodes. Let the final scalar be L. Define the adjoint of a node as v̄i = ∂L/∂vi. The reverse rule is:

v̄ᵢ = Σj: i is a parent of j v̄ⱼ × (∂vⱼ/∂vᵢ) Multiply along each outgoing path, then add contributions from every child that used the node. The sum is essential when an intermediate is reused.

This recurrence is the whole reverse pass in compressed form. Seed the output with L̄ = 1, because ∂L/∂L = 1. Visit nodes in reverse topological order. Each node sends its incoming adjoint through its local derivative rule. If two downstream branches depend on the same node, their contributions accumulate.

Gradient contributions returning through two branches An input x splits into x squared and sine x, which join in a sum L. Reverse arrows carry one unit from L, then contributions 2x and cosine x merge at x. x u = x² local: 2x v = sin x local: cos x L = u + v L̄ = 1 ū = 1 v̄ = 1 2x cos x Contributions add at the shared node x̄ = 2x + cos x
Figure 3. Reverse mode is tributary accounting. The relationship is analytic, with no empirical data. Multiplication occurs along a path; addition occurs where independent downstream uses meet.

A common implementation error in hand-built engines is to overwrite a gradient at the shared node instead of adding to it. The resulting forward values are perfect. Only the backward semantics are broken. This is why the graph must be more than a visual diagram. Its fan-out and topological order carry executable meaning.

Forward mode, reverse mode and the shape of the question

For a vector function f: ℝⁿ → ℝᵐ, the Jacobian Jf has m rows and n columns. Forward mode computes a Jacobian-vector product, Jfv. It asks how one chosen input direction v changes all outputs. Reverse mode computes a vector-Jacobian product, uᵀJf. It asks how one chosen combination of outputs changes all inputs.

Training usually maps many parameters to one scalar loss. The Jacobian is very wide, so reverse mode obtains the full gradient in one backward sweep. JAX's official explanation makes the same dimensional argument and notes the associated memory cost: reverse mode stores or reconstructs information from the forward computation.[11] Forward mode is attractive when inputs are few and outputs are many, such as sensitivity of a large simulated field to a small number of control variables.

Choosing automatic differentiation mode from input and output dimensions Three panels show a wide Jacobian favouring reverse mode, a tall Jacobian favouring forward mode, and a square or structured Jacobian favouring mixed products or batching. Many inputs, few outputs Few inputs, many outputs Balanced or structured J ∈ ℝ¹ˣⁿ J ∈ ℝᵐˣ¹ J ∈ ℝᵐˣⁿ reverse mode forward mode JVP, VJP or mixed one scalar loss over many parameters one control direction, many responses exploit sparsity, batching or structure
Figure 4. Differentiation mode is a geometry decision. The regions are illustrative rather than benchmark thresholds. Actual performance also depends on primitive costs, sparsity, batching, compiler transformations and memory traffic.

The famous cheap-gradient idea gives useful intuition: for scalar-output arithmetic circuits, all first derivatives can be computed within a constant factor of evaluating the original function.[2] It does not promise that every real tensor programme is automatically cheap. Pathological transformations can create asymptotic slowdowns, and practical performance depends on sparsity, mutation, compiler optimisation and memory movement.[7]

Modern work sharpens the mechanism. Reverse mode can be decomposed into linearising primitives, separating residual values from the linear part, and transposing that linear programme.[6] This is more than a theoretical reformulation. It explains why local linear rules compose, why the backward pass runs in reverse order, and why checkpointing is a choice about which forward residuals to retain.

A 2025 result makes the compiler consequence concrete. Dangel and colleagues collapse Taylor-mode automatic differentiation for linear differential operators by pushing a sum through the computation graph. Their PyTorch experiments report lower time and memory for the studied Laplacian and biharmonic calculations than nested first-order differentiation.[16] The boundary matters: the rewrite uses linearity, and the implementation does not cover arbitrary operators. The broader lesson is exact. A computation graph is an optimisation surface only when a proposed rewrite preserves its mathematical contract.

Published evidence. Computation graphs support more than gradient calculation. Graph representations permit common-subexpression elimination, parallel scheduling, portability and memory analysis. These gains depend on the representation and compiler; they should not be attributed to the chain rule itself.[14]

Where gradients fail without being false

An autodiff engine can produce the exact derivative of the programme it saw and still mislead the system designer. Three questions must remain separate. Did the graph capture the executed computation? Were the local derivative rules mathematically valid? Does the derivative answer the decision question we care about?

A gradient can be computationally correct, numerically unusable and semantically irrelevant at the same time. Each failure requires a different remedy.

Thought experiment: replace one smooth gate

Take a model whose score passes through a sigmoid gate. Replace only that gate with a hard threshold that returns zero below a boundary and one above it. Far from the boundary, the forward outputs can look nearly identical. The causal feature varied is local smoothness.

The sigmoid supplies a graded derivative. The hard threshold is flat almost everywhere and undefined at the boundary. Reverse mode faithfully returns no useful local signal through the hard gate. More data, a more elaborate graph and a faster accelerator do not restore information that the chosen function removed.

This does not mean every decision must be differentiable. It means the hard decision should usually sit outside the optimised surrogate, with its own evaluation and authority logic.

Smooth sigmoid and hard threshold under an intervention The left plot shows sigmoid and step outputs. The right plot shows the sigmoid derivative peaking at the boundary while the hard threshold has zero derivative away from an undefined boundary. Forward output Local sensitivity sigmoidhard step sigmoid derivativezero away from boundary undefined at threshold scorescore
Figure 5. Similar forward behaviour can hide radically different learning behaviour. Curves are analytic and illustrative. The hard step's derivative is zero away from the threshold and undefined at the threshold; frameworks may adopt a convention, but the missing local signal remains.

Graph completeness

A graph is complete only relative to a claimed function. If feature normalisation occurs in untracked code, a parameter is detached, an external simulator is called without a derivative contract, or an in-place mutation changes a saved value, the backward graph may describe a different programme from the one the team imagines.

Dynamic graphs reduce some mismatches because they record operations that actually ran. They do not guarantee that every consequential effect is represented. File writes, network calls, database changes, random sampling and human decisions may sit outside the differentiable trace. A graph break is not automatically a defect. It is a boundary that needs a declared treatment: stop the gradient, supply a validated custom rule, use an estimator, redesign the objective, or evaluate the component separately.

Training and inference also diverge easily. Dropout is disabled, a branch changes, quantisation is introduced, a retrieval result enters, or a policy threshold is applied. The training gradient remains exact for the training programme. It says nothing direct about an inference-only branch unless that branch is included in evaluation.

Thought experiment: the invisible detach

Build two programmes that return exactly the same prediction and loss for every input. In Programme A, an intermediate z remains connected to its upstream parameter. In Programme B, z is copied into an untracked value before the rest of the forward computation continues. Vary only whether that one edge belongs to the derivative graph.

Every forward regression test passes. The displayed loss, latency and prediction are identical. In Programme A, the upstream parameter receives a gradient. In Programme B, it receives zero because the reverse path ends at the copied value. No amount of output comparison reveals the difference.

An intentional stop-gradient can be useful, for example to stabilise a target network or prevent one component from adapting to another. The experiment therefore does not prove that detaching is wrong. It proves that detaching is an optimisation assumption with causal force. A graph break must be reviewed like a changed equation, even when no forward value changes. Release evidence should name the boundary, its reason and a test showing which parameters are expected to stop receiving sensitivity.

Local-rule validity

Framework primitives come with derivative conventions. At non-differentiable points, systems may choose a subgradient, a continuity-based value, an arbitrary convention or an error. PyTorch documents such choices explicitly for non-differentiable functions.[9] Custom primitives are more dangerous because a user can return the correct forward value with a wrong backward rule.

Independent gradient checking is therefore a release control, not a classroom exercise. Central finite differences compare the autodiff result with two nearby function evaluations. PyTorch's gradcheck follows this analytical-versus-numerical pattern.[10] The comparison is imperfect near discontinuities and sensitive to precision, but it can expose a faulty local rule that ordinary forward tests will never see.

Finite-difference gradient error across step sizes A log-log plot of absolute error versus step size forms a U shape. Error falls as truncation improves, reaches a minimum near one millionth, then rises as floating-point cancellation dominates. 10⁻¹10⁻⁵10⁻⁹10⁻¹³10⁻¹⁶ 10⁻²10⁻⁶10⁻¹⁰ central-difference step h absolute gradient error Best region in this example h ≈ 10⁻⁶, error ≈ 1.7 × 10⁻¹¹ truncation dominates cancellation dominates
Figure 6. Numerical checking has a Goldilocks region. Points are measured in Python float64 for the opening function's weight derivative, using central differences from h = 10⁻¹ to 10⁻¹⁶. The optimum step is example-specific, not a universal threshold.

Large steps compare different parts of the curve and suffer truncation error. Tiny steps subtract nearly equal floating-point numbers and suffer cancellation. A failed check may reveal a wrong derivative, a non-smooth point, an unstable function or a poor step size. The diagnostic must inspect all four possibilities.

Numerical usefulness

A correct chain of derivatives can shrink towards zero or grow without bound. Repeated Jacobian multiplication explains vanishing and exploding gradients in long compositions. Bengio, Simard and Frasconi showed why learning long dependencies becomes increasingly difficult under gradient descent.[4] The graph has done its job. The architecture has created a poor signal path.

Saturation is the local version. In the opening example, tanh has derivative 0.419974 at z = 1. At z = 8, that derivative is effectively zero. The model can be far from the desired behaviour while an upstream parameter receives almost no correction. Residual connections, normalisation, activation choices and objective design address such problems in later layers of the canon. The present lesson is narrower: inspect gradient magnitude along the path, not only the final parameter gradient.

Semantic relevance

The gradient ∂L/∂amount in a fraud score is a property of the score function. It is not evidence that increasing a customer's transaction amount causes fraud in the world. Nor does ∂L/∂a protected attribute establish an admissible intervention. Gradients answer local counterfactual questions inside the model, holding the rest of the programme fixed. Causal claims require an intervention model and assumptions about the data-generating process.

The objective can also be wrong. A differentiable proxy may reward accuracy while ignoring calibration, recovery cost, fairness or a hard authority boundary. Automatic differentiation efficiently optimises whatever objective it is given; it does not validate the objective.

Explicit failure boundary. Computation-graph reasoning ceases to help when the consequential behaviour is not represented as a function with valid local sensitivity rules, or when the desired decision is inherently discrete and no defensible surrogate connects it to a smooth objective. At that boundary, use enumeration, simulation, policy evaluation, causal experiments, estimators or human judgement.

Negative control: two knobs and a cheap function

Suppose a programme has only two scalar inputs, takes microseconds to run and has no custom operations. Central differences need four extra evaluations to estimate both derivatives. They may be easier to implement and sufficiently accurate. A symbolic algebra system may produce a compact exact derivative.

This case weakens any claim that a computation graph is always necessary. It is not. The graph earns its cost when inputs are numerous, intermediates are shared, derivative transformations must compose, higher-order products are required, or the team needs inspectable boundaries and compiler optimisation. The correct baseline for a small function is often a simpler method.

Memory is part of the mechanism

Reverse mode needs forward values for local rules. Saving everything can make memory grow with depth. Checkpointing discards selected intermediates and recomputes them during the backward pass. JAX documents this directly as a memory-versus-computation trade.[13] Chen and colleagues demonstrated systematic sublinear-memory schedules for deep networks by analysing the computation graph.[5]

This yields an architectural rule: the graph is simultaneously a derivative plan and a lifetime plan for intermediate state. A model that fits in inference memory may fail in training because reverse mode retains activations. A checkpoint policy that saves memory may increase latency or repeat non-deterministic work. Those choices deserve explicit tests.

From toy function to reviewed model

The foundational mechanism becomes operational when the graph includes the whole optimised objective while preserving boundaries that should remain deterministic or accountable. Consider a synthetic transaction-risk model. It transforms amount, device novelty and counterparty novelty into a probability. A weighted log loss trains the score. A separate policy decides whether the transaction enters review.

The model uses:

a = log(1 + amount/100)
s = 0.7a + 1.1·device + 0.8·novelty − 1.5
p = sigmoid(s) ; L = −4 log(p) for a positive label The multiplier 4 is a synthetic miss-cost weight. It is illustrative, not a recommended fraud-loss parameter.

For amount £480, device novelty 0.7 and counterparty novelty 0.4, the transformed amount is 1.757858, the score is 0.820501, the probability is 0.694343 and the weighted loss is 1.459159. The local derivative ∂L/∂s is −1.222630. The amount sensitivity is −0.001476 loss units per pound, or approximately −0.1476 per £100 near this point, while all other inputs are held fixed.

That derivative helps optimise the score. It should not directly authorise a hold. The policy might require p ≥ 0.8 plus deterministic eligibility checks. The threshold is intentionally outside the training gradient. Its consequences should be selected through calibration, cost analysis, fairness tests, operational capacity and accountable decision rights.

Differentiable risk model separated from a deterministic decision boundary Feature transformations flow through a score, sigmoid and weighted loss inside a differentiable region. The probability also crosses an authority boundary into threshold, eligibility and review routing. Backward arrows stop before the policy boundary. Differentiable scoring objective Deterministic authority boundary amountdevice noveltycounterparty novelty log transformweighted sumlabel + miss weight sigmoid pweighted loss L threshold p ≥ 0.8eligibility checksreview route £4800.70.4 a = 1.758s = 0.8210.6941.459 falsetyped rulesno automatic hold gradient stops; evidence and policy continue reverse-mode update path
Figure 7. Optimisation and authority should share evidence without sharing a gradient. All values and weights are synthetic. The graph trains a probability model; the separate policy path governs an operational action.

This separation prevents two opposite errors. One is forcing a non-differentiable policy into the training path and then inventing a gradient convention that has no decision meaning. The other is allowing a smooth model score to bypass deterministic eligibility, capacity or authority controls merely because it is differentiable.

A release trace for the production-shaped case

A review begins by fixing the claimed function: the feature transformations, score, label convention, weighting and scalar objective for one version of the model. The team then captures the traced graph or intermediate representation for representative input shapes. Branches that depend on data need cases on both sides. Random operations need an explicit key or seed policy. Any custom operation receives a separate forward specification and derivative specification.

The next evidence concerns signal quality rather than mere existence. Sampled batches should show finite gradients at the objective, intermediate score and each parameter group. Histograms can expose a branch that never receives updates, a saturated activation or a scale difference that dominates the optimiser. Selected examples should be checked in float64 against finite differences or an analytic case. The deliberately wrong derivative from the executable artefact belongs in the same test suite, because it proves that the checker can reject a plausible forward implementation.

Finally, the team profiles the actual mode and residual plan. It records peak saved-state memory, recomputation cost and behaviour under checkpointing. It separately evaluates the threshold and eligibility policy against calibration, capacity and consequence criteria. The resulting release record contains two linked claims: the model can learn the stated smooth objective, and the deterministic policy can govern the resulting score. Neither claim silently substitutes for the other.

Practitioner synthesis. Treat the differentiable graph as a proposal-and-learning mechanism. Treat consequential action as a typed control problem. The score, uncertainty and evidence may cross the boundary; ambient authority should not.

Executable artefact: a scalar reverse-mode engine

The following Python programme implements a tiny tape-based autodiff engine with no dependencies. It records parents and local backward closures, topologically sorts the graph, seeds the loss with gradient one, and accumulates adjoints in reverse order.

Assumptions. Values are scalar float64 numbers; the graph is acyclic; primitives are pure during one run; and only the included operations are differentiable. Expected output is shown after the code. The positive case must agree with central differences. The negative case deliberately supplies a wrong local derivative and must be detected.

from __future__ import annotations
from math import tanh
from typing import Callable, Iterable

class Value:
 """A scalar reverse-mode automatic differentiation node."""

 def __init__(self, data: float,
 _children: Iterable["Value"] = (),
 op: str = "", label: str = "") -> None:
 self.data = float(data)
 self.grad = 0.0
 self._prev = tuple(_children)
 self._backward: Callable[[], None] = lambda: None
 self.op, self.label = op, label

 @staticmethod
 def _coerce(other: float | "Value") -> "Value":
 return other if isinstance(other, Value) else Value(float(other))

 def __add__(self, other: float | "Value") -> "Value":
 other = self._coerce(other)
 out = Value(self.data + other.data, (self, other), "+")
 def _backward() -> None:
 self.grad += out.grad
 other.grad += out.grad
 out._backward = _backward
 return out

 def __radd__(self, other: float | "Value") -> "Value":
 return self + other

 def __mul__(self, other: float | "Value") -> "Value":
 other = self._coerce(other)
 out = Value(self.data * other.data, (self, other), "*")
 def _backward() -> None:
 self.grad += other.data * out.grad
 other.grad += self.data * out.grad
 out._backward = _backward
 return out

 def __rmul__(self, other: float | "Value") -> "Value":
 return self * other

 def __neg__(self) -> "Value":
 return self * -1.0

 def __sub__(self, other: float | "Value") -> "Value":
 return self + (-self._coerce(other))

 def tanh(self) -> "Value":
 y = tanh(self.data)
 out = Value(y, (self,), "tanh")
 def _backward() -> None:
 self.grad += (1.0 - y * y) * out.grad
 out._backward = _backward
 return out

 def backward(self) -> None:
 order: list[Value] = []
 seen: set[int] = set()
 def visit(node: Value) -> None:
 if id(node) in seen:
 return
 seen.add(id(node))
 for parent in node._prev:
 visit(parent)
 order.append(node)
 visit(self)
 self.grad = 1.0
 for node in reversed(order):
 node._backward()

def central_difference(fn: Callable[[float], float],
 x: float, h: float = 1e-6) -> float:
 if h <= 0:
 raise ValueError("h must be positive")
 return (fn(x + h) - fn(x - h)) / (2.0 * h)

def bad_square(x: Value) -> Value:
 # Correct x² value, deliberately wrong derivative: 3x rather than 2x.
 out = Value(x.data * x.data, (x,), "bad_square")
 def _backward() -> None:
 x.grad += 3.0 * x.data * out.grad
 out._backward = _backward
 return out

def main() -> None:
 x, w = Value(1.5, label="x"), Value(0.8, label="w")
 b, target = Value(-0.2, label="b"), Value(0.5, label="target")
 prediction = (w * x + b).tanh()
 loss = (prediction - target) * (prediction - target)
 loss.backward()

 numeric_w = central_difference(
 lambda candidate: (tanh(candidate * 1.5 - 0.2) - 0.5) ** 2,
 0.8)
 good = abs(w.grad - numeric_w) < 1e-7

 probe = Value(2.0, label="probe")
 bad_square(probe).backward()
 numeric_bad = central_difference(lambda v: v * v, 2.0)
 caught = abs(probe.grad - numeric_bad) >= 1e-7

 print(f"loss={loss.data:.9f}")
 print(f"dL/dw autodiff={w.grad:.9f} finite_difference={numeric_w:.9f}")
 print(f"positive_case={'PASS' if good else 'FAIL'}")
 print(f"bad_square autodiff={probe.grad:.6f} finite_difference={numeric_bad:.6f}")
 print(f"negative_case={'PASS (fault detected)' if caught else 'FAIL'}")

if __name__ == "__main__":
 main()
loss=0.068431502
dL/dw autodiff=0.329588500 finite_difference=0.329588500
positive_case=PASS
bad_square autodiff=6.000000 finite_difference=4.000000
negative_case=PASS (fault detected)

.

The negative case is the important one. Forward testing sees bad_square(2) = 4 and passes. Only an independent sensitivity check notices that the custom rule returns six instead of four. The artefact therefore encodes the paper's release principle: inspect values and derivatives as separate contracts.

The gradient path audit

The following instrument is an original practitioner synthesis. It is meant for model reviews, custom-operation changes and architecture decisions. It does not replace statistical evaluation. It asks whether the sensitivity path itself deserves trust.

Six checkpoints in a gradient path audit A path moves through objective, graph coverage, local rules, numerical signal, mode and memory, and independent check. Red exits mark wrong objective, graph break, invalid derivative, unstable signal, unaffordable tape and failed check. objectivemeaning graphcoverage localrules signalquality mode +memory independentcheck wrong targethidden branchbad VJPzero, inf or NaNtape unaffordablemismatch release only when every checkpoint has evidence
Figure 8. A gradient is released as a chain of claims. The instrument is practitioner synthesis, not a measured industry standard. Each checkpoint can stop release independently.
CheckpointQuestionEvidenceRelease status
Objective meaningDoes the scalar objective represent the behaviour and consequences the optimisation should change?Objective specification, counterexample, stakeholder sign-offRed if a proxy can improve while the real outcome worsens
Graph coverageAre preprocessing, parameter reuse, branches, regularisers and loss construction represented as intended?Trace or IR inspection, train/inference comparisonRed for an undeclared graph break
Local rulesIs every primitive differentiable at operating points, or governed by an explicit custom rule?Primitive specification, boundary tests, custom VJP reviewAmber for a justified surrogate or subgradient
Signal qualityAre gradients finite, scaled and informative across representative paths?Layer and parameter histograms, saturation and perturbation testsRed for persistent zero, infinity or NaN
Mode and memoryDoes the input-output geometry justify forward, reverse or mixed mode, and can residuals fit?Dimension analysis, profile, checkpoint experimentAmber until the runtime budget is measured
Independent checkDo sampled gradients agree with finite differences, analytic cases or a second implementation?Positive and negative gradchecks in continuous integrationGreen only after faults are demonstrably caught

The strongest line in the table is the last one. A positive check shows agreement on selected points. A negative check proves the harness can fail when a derivative is wrong. Without the negative case, a permanently passing test may be ceremonial.

Open hypothesis. Requiring graph coverage, custom-rule checks and negative gradient tests as separate release evidence should reduce silent training defects. This article proposes the control; it does not claim a measured reduction across deployments.
Optional derivation: why fan-out requires addition

Let L depend on x through two children, u(x) and v(x): L = h(u, v). A small change dx changes both children. The total differential is dL = (∂L/∂u)(∂u/∂x)dx + (∂L/∂v)(∂v/∂x)dx. Factoring out dx gives the sum in the reverse recurrence. Overwriting either contribution changes the derivative of the represented function.

Optional implementation depth: what this tiny engine omits

Real systems handle tensors, broadcasting, devices, mixed precision, control flow, sparse operations, aliasing, in-place mutation, batching, higher-order transforms, distributed execution and compiler lowering. They also need lifecycle rules for saved tensors and graph retention. Enzyme demonstrates another route by differentiating optimised LLVM intermediate representation rather than requiring code to be rewritten inside one machine-learning framework.[8] The tiny engine is useful because its omissions are visible.

How the claim changes under pressureChart · state
stateDiagram-v2
  [*] --> Claim
  state "A minimal worked example" as Claim
  state "What inspectability actually<br/>requires" as Test
  state "Thought experiment, the sealed<br/>module" as Revision
  Claim --> Test: expose mechanism
  Test --> Decision: evidence survives
  Test --> Revision: rival survives
  Revision --> Claim: narrow or rebuild
  Decision --> [*]
The revision loop begins with A minimal worked example and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Compact glossary

Function
A rule mapping admissible inputs to outputs. Its derivative describes local sensitivity, not necessarily real-world causation.
Composition
Feeding one function's output into another. Composition creates the chain of local derivative contracts.
Primitive
An operation for which the system knows evaluation and transformation rules, such as addition, multiplication or matrix multiplication.
Computation graph
A representation of executed operations and dependencies. It may be an eager tape, static graph or compiler intermediate form.
Primal
The ordinary forward value of a computation, distinguished from tangent or adjoint information used for derivatives.
Adjoint
The accumulated sensitivity of a scalar output to an intermediate value, often written v̄ = ∂L/∂v.
JVP
A Jacobian-vector product that pushes one input direction forward through all outputs.
VJP
A vector-Jacobian product that pulls one output direction backwards to all inputs.
Graph break
A boundary where the tracked differentiable programme stops. It requires an explicit treatment rather than an assumed gradient.
Checkpointing
Recomputing selected forward intermediates during reverse mode to trade more computation for less stored state.

Review the path, not the presence of a gradient

The durable intuition is simple. A complex model does not need one gigantic derivative formula. It needs a decomposition into small transformations, each able to report how local changes pass through. The computation graph records the dependencies that make those reports composable. Reverse mode then multiplies along paths and adds where paths meet.

That mechanism explains the extraordinary reach of gradient-based learning. It also establishes a stricter review standard. The existence of a backward pass is evidence that software executed derivative rules; it is not evidence that the intended system was represented, the rules were valid, the signal was useful or the objective deserved optimisation.

The architecture decision changed by this argument is concrete. Build differentiable components as small, typed, side-effect-aware functions. Inspect the traced graph or intermediate representation. Keep preprocessing and objective construction inside the reviewed path. Put discrete authority outside the gradient unless a defensible surrogate and decision process connect them. Select forward, reverse or mixed mode from the shape of the derivative question. Profile saved state and checkpoint deliberately.

Finally, release the gradient as a chain of claims. The objective has meaning. The graph is complete. Local rules are valid. Signals are finite and informative. The mode and memory plan fit. An independent positive check agrees, and a negative test proves the check can fail.

The changed decision is to stop approving “autodiff enabled” as an implementation detail and start approving a gradient path as operating evidence. That is the point at which composition becomes more than convenient notation. It becomes a disciplined interface between mathematical intent, executable computation and model change.

The evidence conversationChart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: One wrong number, four possible causes
  M->>E: A minimal worked example
  E-->>M: compare rival explanations
  E->>D: Sources
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in Functions, Composition and Computation Graphs. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Sources

  1. Primary. R. E. Wengert, “A Simple Automatic Derivative Evaluation Program”, Communications of the ACM, 1964. Publisher record.
  2. Primary. Walter Baur and Volker Strassen, “The Complexity of Partial Derivatives”, Theoretical Computer Science, 1983. Publisher record.
  3. Primary. David E. Rumelhart, Geoffrey E. Hinton and Ronald J. Williams, “Learning Representations by Back-Propagating Errors”, Nature, 1986. Publisher record.
  4. Primary limitation result. Yoshua Bengio, Patrice Simard and Paolo Frasconi, “Learning Long-Term Dependencies with Gradient Descent Is Difficult”, IEEE Transactions on Neural Networks, 1994. Publisher record.
  5. Primary systems result. Tianqi Chen, Bing Xu, Chiyuan Zhang and Carlos Guestrin, “Training Deep Nets with Sublinear Memory Cost”, 2016. arXiv record.
  6. Primary mechanism result. Roy Frostig, Matthew J. Johnson, Dougal Maclaurin, Adam Paszke and Alexey Radul, “Decomposing Reverse-Mode Automatic Differentiation”, 2021; see also Radul et al., “You Only Linearize Once”, 2022. First paper; formal extension.
  7. Primary limitation and compiler result. Gilbert Bernstein, Michael Mara, Tzu-Mao Li, Dougal Maclaurin and Jonathan Ragan-Kelley, “Differentiating a Tensor Language”, 2020. arXiv record.
  8. Primary implementation result. William S. Moses and Valentin Churavy, “Instead of Rewriting Foreign Code for Machine Learning, Automatically Synthesize Fast Gradients”, NeurIPS 2020. Proceedings record.
  9. Official implementation source. PyTorch, “Autograd mechanics”. Documentation.
  10. Official evaluation source. PyTorch, torch.autograd.gradcheck. Documentation.
  11. Official mechanism source. JAX, “Forward- and Reverse-Mode Autodiff”. Documentation.
  12. Official representation source. JAX, “The jaxpr language”. Documentation.
  13. Official operating source. JAX, jax.checkpoint. Documentation.
  14. Official graph source. TensorFlow, “Introduction to graphs and tf.function”. Documentation.
  15. Authoritative synthesis. Atilim Gunes Baydin, Barak A. Pearlmutter, Alexey Andreyevich Radul and Jeffrey Mark Siskind, “Automatic Differentiation in Machine Learning: a Survey”, Journal of Machine Learning Research, 2018. Journal record.
  16. Recent primary systems result. Felix Dangel, Tim Siebert, Marius Zeinhofer and Andrea Walther, “Collapsing Taylor Mode Automatic Differentiation”, 2025, updated 2025. Used for the bounded claim that linear-operator structure can enable verified graph rewrites with measured time and memory gains.