Backpropagation #
Reverse-mode is implemented by traversing node ids in reverse order. Each node's backward
closure produces parent-gradient contributions, which we accumulate by elementwise summation.
Two traversal variants live here, and it matters which one a caller runs:
backwardDense(and its totalized formbackwardDenseAll) is what the eager trainer executes. It keeps anOptionper node and runs a node's VJP only when that node has received a cotangent, so disconnected nodes are never visited.backwardDenseAllthen fills the unvisited slots with explicit zero tensors. Skipping is deliberate: onFloat, feeding a synthetic zero cotangent through the VJP of a singular value can produceNaNvia0 * (1/0).backwardDenseFromstarts from a total gradient array and runs every node's VJP. It is the variant the proof layer reasons about directly (Proofs.Autograd.Algebra.Graph.backwardDenseFrom_lowerGraphToTape_eq_backpropAllCtx).
The two agree whenever every VJP on the tape sends a zero cotangent to zero contributions of the
parents' shapes; that is Proofs.Autograd.Algebra.Graph.ZeroPreserving in
NN.Proofs.Autograd.Runtime.Link.BackwardDense, where backwardDenseAll_eq_backwardDenseFrom
is proved, and NN.Proofs.Autograd.Runtime.Link.BackwardDenseGraph, where it is instantiated for
every tape produced by lowerGraphToTape.
Internal helper: add a single parent gradient contribution into the dense optional gradient array.
This is where we implement PyTorch-style accumulation for DAGs: if multiple children contribute to the same parent id, we sum the contributions.
The dense array entry is none until we first reach a node during reverse traversal.
Instances For
Reverse-mode backpropagation producing a dense array of optional gradients.
- The result array has length
t.nodes.size. - Entry
idissome gif the node was reached fromoutIdduring reverse traversal, otherwisenone. - When multiple paths contribute to the same node, we sum gradients via
SomeTensor.add. - A node's VJP runs only if the node was reached; see the section docstring for why. The proof
layer names this per-node step
backwardDenseStepand provesbackwardDenseis the reverse fold of it.
This is the variant the eager trainer executes (through backwardDenseAll). It is loosely
analogous to PyTorch's autograd engine walking the dynamic graph and accumulating .grad for
leaf tensors, but we keep gradients for every node id rather than leaves alone. That makes the
runtime easier to debug and gives proof-bridge code direct access to intermediate cotangents.
Reference (PyTorch): https://pytorch.org/docs/stable/notes/autograd.html
Instances For
Internal helper: like addGradDense, but assumes the gradient array is total (no Option).
This is used by the proof-friendly backwardDenseFrom* variants, which start from an explicit
gradient tensor for every node.
Instances For
One reverse-mode backprop step at a single node id, updating a total dense gradient array.
Precondition by convention: acc has one entry per tape node, and every entry has the matching
node shape. The function checks those conditions dynamically and returns an error if a caller
violates them. This makes it suitable as the small proof-friendly step used by
backwardDenseFromLoop.
Instances For
Reverse-mode accumulation over the first n nodes in reverse order.
The recursion visits node ids n-1, n-2, ..., 0. Passing n = t.nodes.size therefore traverses the
entire tape. This structurally recursive loop is also used by typed graph sessions after lowering.
Instances For
Reverse-mode accumulation starting from an explicit dense gradient array.
This is the variant the proofs reason about directly: it always runs every node's VJP (in
reverse order) and keeps a gradient tensor for every node id. The eager trainer does not call
it; it calls backwardDenseAll, which agrees with this function on zero-preserving tapes
(Proofs.Autograd.Algebra.Graph.backwardDenseAll_eq_backwardDenseFrom).
Instances For
Reverse-mode accumulation that returns a dense gradient array for every node id.
Propagation uses backwardDense, so local VJP closures run only for nodes reached from outId.
The optional result is then totalized with explicit zero tensors for disconnected nodes. This is
necessary at singular forward values: applying a disconnected VJP to a synthetic
zero cotangent can manufacture NaN through expressions such as 0 * (1 / 0), even though the
mathematical gradient of the selected output with respect to that node is zero.
This is the entry point the eager trainer executes. On zero-preserving tapes (in particular
every tape produced by lowerGraphToTape) it returns exactly what backwardDenseFrom returns
from the one-hot seed array; see NN.Proofs.Autograd.Runtime.Link.BackwardDense.
Instances For
Convert the optional dense gradient array returned by backwardDense into a sparse HashMap.
Only entries that are present (some (some g)) are kept. The result records exactly the nodes
reached by reverse-mode propagation.
Instances For
Reverse-mode backpropagation returning a HashMap of only the nodes that received gradients.
This is the sparse public form of backwardDense: it computes dense gradients first, then drops
nodes that did not receive a gradient.
Instances For
Backpropagate from a scalar output with seed gradient 1.
PyTorch analogy: loss.backward() when loss is a scalar.