skoro v0.0.0

Automatic differentiation over frames

How a straight-line program is differentiated by composing each step's derivative maps over its computation graph, forward with frames of tangents, backwards over its backpropagation graph with frames of cotangents, and forward-over-reverse for Hessian products.

Updated 2026-10-12 · 4 min read

Automatic differentiation computes the derivatives of a program exactly, up to rounding, from the derivatives of the steps it is made of. This note states the three ways of composing them that a library needs (pushing tangents forward, pulling cotangents back, and differentiating the pull-back once more for second order), each over a frame of several directions at once.

Programs and their computation graphs#

A straight-line program has slots s0,s1,…s_0, s_1, \dots, each holding a vector, and runs a sequence of steps

sok←Fk(sik,1,…,sik,mk),(1) s_{o_k} \leftarrow \map_k\bigl(s_{i_{k,1}}, \dots, s_{i_{k,m_k}}\bigr), \tag{1}
(1)

each writing one slot from slots written earlier. Each step’s map Fk\map_k is smooth, with partial Jacobians ∂jFk\partial_j \map_k, one per input, of shape (output dimension) by (dimension of input jj). When an input is not differentiated, its partial Jacobian is simply never used.

A program is pictured by its computation graph. Its nodes are the slots, and it has an edge from each input of a step to that step’s output, so a node with incoming edges is computed by the step that writes it, from the nodes those edges come from. Because each step reads only slots written before it, the computation graph has no cycles, and the order of the steps is one of its topological orders. Its roots, the nodes with no incoming edge and so no ancestor, are the slots no step writes: the program’s inputs, the variables x\point we differentiate with respect to. Some nodes are targets, the values to differentiate, f(x)f(\point). A node with several outgoing edges is a slot read by several steps.

A computation graph
x1\point_1 x2\point_2 s3s_3 F1=⊙\map_1 = {\odot}s4s_4 F2=exp⁡\map_2 = \expff F3=+\map_3 = {+}
  • Root (an input)
  • Slot written by a step
  • input of a step
A computation graph. The program s3←x1⊙x2s_3 \leftarrow \point_1 \odot \point_2, s4←exp⁡s3s_4 \leftarrow \exp s_3, f←s3+s4f \leftarrow s_3 + s_4. Each node is a slot; each edge runs from an input of a step to its output. The roots x1\point_1 and x2\point_2 are the program's inputs, and ff is its target.

A frame is a matrix whose columns are directions: a tangent frame at a slot of dimension d\dimension is a d×n\dimension \times n matrix v\tangent of nn tangent vectors, and a cotangent frame a d×n\dimension \times n matrix α\alpha whose columns are linear functionals, written as columns. Below, vs\tangent_s and αs\alpha_s are the frames held at slot ss.

Forward: pushing tangent frames#

Seed each root with a tangent frame vx\tangent_{\point} (missing means zero), and run the steps in order, each pushing its inputs’ frames forward along the computation graph’s edges by its Jacobian–vector product:

vsok=∑j∂jFk  vsik,j.(2) \tangent_{s_{o_k}} = \sum_j \partial_j \map_k \; \tangent_{s_{i_{k,j}}} . \tag{2}
(2)

By the chain rule, the frame reaching a target is ∑l∂f/∂xl⋅vxl\sum_l \partial f / \partial \point_l \cdot \tangent_{\point_l}: one pass with a frame of nn columns gives nn directional derivatives, and seeding the columns of an identity gives the whole Jacobian.

Reverse: pulling cotangent frames over the backpropagation graph#

The reverse pass runs on the backpropagation graph: the same nodes as the computation graph, with every edge reversed and labelled by the pull-back ∂jFk⊤\partial_j \map_k\transpose of the step it came from. Since reversing the edges swaps incoming for outgoing, a node is a root of the computation graph if and only if it is a leaf of the backpropagation graph, a node with no outgoing edge. Cotangents enter at the targets, flow along its edges, and end at those leaves, the program’s inputs. A node with several incoming edges, a slot read by several steps, receives a cotangent along each, and holds their sum.

Its backpropagation graph
∂1F1⊤\partial_1 \map_1\transpose∂2F1⊤\partial_2 \map_1\transpose∂F2⊤\partial \map_2\transpose∂1F3⊤\partial_1 \map_3\transpose∂2F3⊤\partial_2 \map_3\transpose x1\point_1 x2\point_2 s3s_3 F1=⊙\map_1 = {\odot}s4s_4 F2=exp⁡\map_2 = \expff F3=+\map_3 = {+}
  • Root (an input)
  • Slot written by a step
  • pull-back
Its backpropagation graph. The same slots with every edge reversed and labelled by the pull-back it applies. Cotangents enter at the target ff and end at the leaves x1\point_1 and x2\point_2, the computation graph's roots; s3s_3 has two incoming edges, and its cotangent is their sum.

Seed each target with a cotangent frame αf\alpha_f, every other slot with zero, and run the steps backwards, each adding its vector–Jacobian product, the pull-back of its output’s cotangent, into each input:

αsik,j+=∂jFk⊤ αsok.(3) \alpha_{s_{i_{k,j}}} \mathrel{+}= \partial_j \map_k\transpose \, \alpha_{s_{o_k}} . \tag{3}
(3)

The accumulation, which sums the cotangents arriving along every incoming edge of the backpropagation graph, is the chain rule for a slot with several consumers. When the steps are done, each root holds αxl=∑f(∂f/∂xl)⊤αf\alpha_{\point_l} = \sum_f (\partial f / \partial \point_l)\transpose \alpha_f. For a scalar target seeded with αf=1\alpha_f = 1, that is the gradient ∇xlf\nabla_{\point_l} f, whatever the number of roots.

Each column of a frame costs a small multiple of one evaluation of the program, in either direction. A Jacobian with few rows is cheaper in reverse, one with few columns forward.

Forward-over-reverse: Hessian products#

For second derivatives, differentiate the reverse pass itself along a path x+tv\point + t\tangent. Run the forward pass of (2) with velocities v\tangent seeded at the roots, so that every slot holds its value ss and its velocity vs\tangent_s, then run the reverse pass over the backpropagation graph carrying two cotangents per slot: αs\alpha_s, as in (3), and βs\beta_s, its derivative along the path. One column of v\tangent is one path; a frame of nn columns runs nn paths at once, and then βs\beta_s is a frame of nn columns while αs\alpha_s is the same for all of them. (A cotangent frame of several columns together with a velocity frame of several columns would make β\beta a three-way array; the passes here take one cotangent column.)

Differentiating (3) along the path by the product rule gives

βsik,j+=hvp⁡k,j(αsok)+∂jFk⊤ βsok,hvp⁡k,j(α)≔ddt∣t=0∂jFk(s+tvs)⊤α=∑l(∂l∂jFk [vsik,l])⊤α.(4) \beta_{s_{i_{k,j}}} \mathrel{+}= \hvp_{k,j}(\alpha_{s_{o_k}}) + \partial_j \map_k\transpose \, \beta_{s_{o_k}}, \qquad \hvp_{k,j}(\alpha) \defeq \frac{\dd}{\dd t}\Big|_{t=0} \partial_j \map_k(s + t\tangent_s)\transpose \alpha = \sum_l \bigl(\partial_l \partial_j \map_k\,[\tangent_{s_{i_{k,l}}}]\bigr)\transpose \alpha . \tag{4}
(4)

The first term is the step’s own second-order map, which depends on the velocities of its inputs; the second is the step’s ordinary pull-back, applied to β\beta. A library writes the first for each step and the program applies the rule. Two consequences are useful when writing hvp⁡\hvp for a step. If a step is linear, its partial Jacobians are constant and its hvp⁡\hvp is zero. If the pull-back into input jj does not involve input ll, then the velocity of input ll contributes nothing to hvp⁡k,j\hvp_{k,j}.

Proposition 5(Forward-over-reverse computes Hessian products).

Let ff be a scalar target, seed its cotangent with αf=1\alpha_f = 1 and βf=0\beta_f = 0, and seed the roots with velocities v\tangent. Then the reverse recurrences deliver, at every root xi\point_i,

αxi=∇xif(x),βxi=∑l∇xi∇xlf(x) vxl. \alpha_{\point_i} = \nabla_{\point_i} f(\point), \qquad \beta_{\point_i} = \sum_l \nabla_{\point_i}\nabla_{\point_l} f(\point)\,\tangent_{\point_l} .
Proof.

The cotangents αs\alpha_s are functions of the roots: the reverse recurrence holds as an identity in x\point, with every ∂jFk\partial_j \map_k evaluated at the slot values the program computes from x\point. Differentiate that identity along x+tv\point + t\tangent for one column v\tangent. By the product rule, βs≔ddtαs(x+tv)∣t=0\beta_s \defeq \frac{\dd}{\dd t}\alpha_s(\point + t\tangent)\big|_{t=0} satisfies (4), because the slot values move with the velocities the forward pass computed, and the seeds αf=1\alpha_f = 1 and βf=0\beta_f = 0 are the value and the derivative of a constant. At a root, αxi(x)=∇xif(x)\alpha_{\point_i}(\point) = \nabla_{\point_i} f(\point) identically in x\point, so its derivative along the path is ∑l∇xi∇xlf(x) vxl\sum_l \nabla_{\point_i} \nabla_{\point_l} f(\point)\,\tangent_{\point_l}.

∎

With all roots stacked into one vector, βx=∇2f(x) v\beta_\point = \nabla^2 f(\point)\,\tangent: a frame of nn velocities gives nn columns of the Hessian for about nn times the cost of a gradient, and the identity frame gives all of it.

Built on by