Link #
Link the executable runtime tape (Runtime.Autograd.Tape) to the shape-indexed SSA/DAG models in
Proofs.Autograd.Algebra.
GraphData stores executable forward, JVP, and VJP functions without derivative laws. Graph
extends that representation with the local adjointness law used by its global backpropagation
theorem. Lowering places the stored VJP into each runtime node's backward closure.
What is proved here #
- Forward-pass correspondence:
lowerGraphToTapeandlowerGraphDataToTapeproduce the same values asGraph.evalandGraphData.eval, respectively. Theirctx_eq_evalandvalues_eqlemmas show that the returned context and runtime tape store those values in the same order. - Backward-pass correspondence: running the runtime dense reverse loop
Tape.backwardDenseFromon a lowered tape matches the graph's stored reverse programbackpropAllCtx(backwardDenseFrom_lowerGraphToTape_eq_backpropAllCtxand itsGraphDatavariant). ForGraphData, this is an implementation-equivalence result. ForGraph, it can be combined withGraph.backprop_correctto obtain derivative correctness.
The core invariant making the runtime reverse loop well-founded is that lowered nodes only emit
contributions to earlier node ids (pid < id).
PyTorch correspondence / citations #
This is analogous to lowering a graph representation to an executable autograd tape whose nodes carry backward closures (PyTorch does this internally for the eager autograd engine). https://pytorch.org/docs/stable/autograd.html
Extend a tape with leaf nodes for every tensor in the input context Γ.
Each leaf has requiresGrad = true and an empty backward contribution array, so the runtime loop
treats them as gradient accumulation slots but never produces parent contributions from them.
Instances For
Turn a shape-erased tensor into a runtime leaf node.
This is the node-level counterpart of addLeaves: it has no parents and contributes nothing in
backward.
Instances For
Result equations #
Result is Except String. The runtime backward functions are written with do notation, so
proofs about them repeatedly need the unfolding equations for bind, pure, throw and
Except.map. They are stated once here and used with simp only instead of unfolding the
Monad instances by hand.
Binding a successful Result applies the continuation.
Binding a failed Result propagates the error.
throw in Result is Except.error.
Except.map on a successful Result.
Except.map on a failed Result.
Backward closures of t only point backwards: every contribution emitted by the node stored at
id targets a node id strictly smaller than id. This is the invariant that makes the dense
reverse loop well-founded; lowerGraphDataToTape_backward_pids_lt_id establishes it for lowered
tapes, including those of proved graphs, which lower through Graph.toData.
Instances For
Each entry of toShapeErasedArray carries the shape recorded at its position of ss.
addLeaves grows the tape by exactly Γ.length nodes.
addLeaves appends leafNodeOfSomeTensor nodes for each input tensor, in order.
Value projection of nodes_addLeaves: node.value agrees with toShapeErasedArray for added
leaves.
Runtime node produced by lowering one graph node.
The node stores the forward value and wraps the stored vjp in a runtime backward closure
that checks the shape of the upstream cotangent and emits one contribution per entry of the
node's input context, indexed from 0. The name only serves debugging output.
Instances For
The lowered node stores the node's forward value.
Lowered nodes always take part in gradient accumulation.
On an upstream cotangent of the right shape, the lowered backward runs the stored vjp.
On an upstream cotangent of the wrong shape, the lowered backward fails.
Lower an executable graph (GraphData) to a runtime tape by evaluating forward nodes and storing
each node's vjp program in its runtime backward closure.
PyTorch analogy: this corresponds to building a tape of autograd nodes during the forward pass, where each node stores enough information to compute parent contributions when given an upstream cotangent.
Instances For
Forward-pass correspondence #
The next lemmas show that lowerGraphDataToTape preserves executable forward semantics, and that
the resulting runtime tape contains exactly the evaluated context as shape-erased tensors in order.
The context returned by lowerGraphDataToTape agrees with GraphData.eval.
The lowered tape's .value array is GraphData.eval with shapes erased in the same order.
Size bookkeeping: the lowered tape contains one runtime node for each element of Γ ++ ss.
Lower a proved graph (Graph) to a runtime tape: the tape lowerGraphDataToTape builds for the
executable graph g.toData. Both interfaces share one lowering; the local adjointness proofs a
Graph carries are used only by the theorems about the tape.
Instances For
The context returned by lowerGraphToTape agrees with the proved Graph.eval.
The lowered tape's .value array is Graph.eval with shapes erased in the same order.
Size bookkeeping: lowerGraphToTape produces Γ.length + ss.length runtime nodes.
Full backpropagation (dense) for proofs and runtime #
The runtime engine computes a dense gradient array, accumulating cotangents for every node in the tape (inputs and intermediates). The following definitions connect that behavior to the proved backpropagation semantics.
"Full" backpropagation for GraphData: gradients for every value in Γ ++ ss, including inputs.
Peel off the last node, apply its VJP to the seed on that node, add the result into the previous
seed, and recurse. Only [Add α] is needed. Graph.backpropAllCtx runs this program on the
executable graph g.toData.
Instances For
"Full" backpropagation for a proof-carrying graph: gradients for every value in Γ ++ ss,
including inputs. It runs the executable reverse program of g.toData, exactly as Graph.eval
and Graph.backpropCtx run its forward and inputs-only reverse programs.