Residual Connections — Gradient Accumulation #
The first chapter where backprop has to accumulate gradients from multiple paths into the same input. So far every layer has been a straight-line composition (chain rule), but residual blocks introduce fan-out: one input feeds two paths whose outputs are added.
The math is trivial — it's the pattern that matters. Once you see "two backwards add", you'll see it everywhere: residuals, attention, SE blocks, multi-head outputs, anywhere a tensor is consumed by more than one downstream op.
This file builds on the proved foundations in Tensor.lean:
biPath f gandbiPathHasVJP(additive fan-in, proved)identityHasVJP(identity VJP, proved)pdiv_addandpdiv_id(calculus facts, proved from Mathlib'sfderiv)
With those in hand, the residual definitions are one-liners — no sorry's.
- Defines
residual f x = f x + xviabiPath f idand its VJP. - Defines
residualProj proj f x = proj x + f xviabiPath proj fand its VJP. - Comments on how this matches the ResNet skip connection in the
MLIR (
MlirCodegen.leanresidual block emission).
References #
- He, Zhang, Ren, Sun 2016, Deep Residual Learning for Image Recognition. https://arxiv.org/abs/1512.03385
A basic residual block: output = sub-network output + identity.
residual f x = f(x) + x
The "skip connection" lets gradients flow directly from output back
to input without going through f. This is why ResNets train: even
if f has near-zero gradients (vanishing), the identity path keeps
the signal alive.
Equations
- Proofs.residual f = Proofs.biPath f fun (x : Proofs.Vec n) => x
Instances For
Residual VJP: dx = f.back(x, dy) + dy.
The skip's contribution is just dy (identity backward). The block's
contribution is f.back(x, dy). They add. The skip passes dy to the
input unchanged, so the gradient does not have to go through f's
Jacobian to reach x (the two terms can still cancel; no lower bound
on dx is claimed).
MLIR (MlirCodegen.lean, the residual-block case of the backward walk):
The "skip grad" is added to the first convBn of the block — exactly
f.back(x, dy) + dy_skip, where dy_skip = dy here.
Proof: immediate from biPathHasVJP and identityHasVJP,
both proved in Tensor.lean.
Equations
- Proofs.residualHasVJP f hf_diff hf = Proofs.biPathHasVJP f (fun (x : Proofs.Vec n) => x) hf_diff ⋯ hf (Proofs.identityHasVJP n)
Instances For
Projected residual block: when input and output shapes don't match (e.g. when stride > 1 downsamples), the skip is not identity but a 1×1 projection conv.
residualProj proj f x = proj(x) + f(x)
Both paths now have nontrivial backwards. The gradient still adds at the input — neither path is privileged.
Equations
- Proofs.residualProj proj f = Proofs.biPath proj f
Instances For
Projected residual VJP: dx = proj.back(x, dy) + f.back(x, dy).
Both backwards run on the same dy and their results sum at x.
This is the truly general "fan-out → backward fan-in" pattern.
MLIR: ResNets with stride > 1 use this — see emitConvBnBackward
where the projection's VJP is emitted alongside the main block's,
and both gradients accumulate into the same incoming-grad SSA.
Proof: immediate from biPathHasVJP, proved in Tensor.lean.
Equations
- Proofs.residualProjHasVJP proj f hproj_diff hf_diff hproj hf = Proofs.biPathHasVJP proj f hproj_diff hf_diff hproj hf
Instances For
Why this matters beyond ResNets #
The fan-out/backward-add pattern is the structural building block for every modern architecture:
• ResNets — y = f(x) + x (this file).
• DenseNets — y = concat(f(x), x). The concat splits dy and each
half goes back through its respective path. Same pattern, different
glue (split instead of add).
• Squeeze-and-Excitation — y = x · gate(x). The product rule
introduces a different kind of bi-path: the gate's gradient gets
x ⊙ dy and the main path's gradient gets gate(x) ⊙ dy. See
SE.lean for that derivation.
• Multi-head attention — concatenated heads. Same structure.
• Two-tower models — independent encoders → joint loss. Even more
extreme fan-out.
If you understand biPathHasVJP, you understand backprop through any
DAG. Composition (chain rule) handles the "linear" part; bi-path handles
the joins. Together they're enough for any computation graph.
Public correctness theorem for residualHasVJP: skip-connection
backward equals the pdiv-contracted Jacobian of f + id.
Public correctness theorem for residualProjHasVJP: same as
residualHasVJP_correct but for the projected variant where the skip
isn't identity.
ResNet residual bodies contain ReLU, which is only DifferentiableAt
at smooth points (see MLP.lean), not Differentiable globally. So the
end-to-end CNN VJP (cnnHasVJPAt, in CNN.lean) chains through the
pointwise HasVJPAt framework — vjpCompAt + the witnesses below —
exactly as mlpHasVJPAt does for the MLP. These mirror the everywhere
versions above, at a fixed x; pdiv_add is already stated at-point so
the proof is the everywhere one with intro x dropped.
Additive fan-in at a point — smooth-point analog of biPathHasVJP.
Equations
- Proofs.biPathHasVJPAt f g x hf_diff hg_diff hf hg = { backward := fun (dy : Proofs.Vec n) (i : Fin m) => hf.backward dy i + hg.backward dy i, correct := ⋯ }
Instances For
Residual VJP at a point: dx = f.back(dy) + dy. The skip
(identity) is differentiable everywhere, so only f needs the
smooth-point hypothesis.
Equations
- Proofs.residualHasVJPAt f x hf_diff hf = Proofs.biPathHasVJPAt f (fun (x : Proofs.Vec n) => x) x hf_diff ⋯ hf ((Proofs.identityHasVJP n).toHasVJPAt x)
Instances For
Projected residual VJP at a point: both paths carry smooth-point hypotheses (the 1×1 stride-2 projection is linear, but stated at-point for uniformity with the composition).
Equations
- Proofs.residualProjHasVJPAt proj f x hproj_diff hf_diff hproj hf = Proofs.biPathHasVJPAt proj f x hproj_diff hf_diff hproj hf
Instances For
A residual branch is continuous when its body is.
A projected residual is continuous when both branches are.