Documentation

LeanMlir.Proofs.Architectures.Residual

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:

With those in hand, the residual definitions are one-liners — no sorry's.

  1. Defines residual f x = f x + x via biPath f id and its VJP.
  2. Defines residualProj proj f x = proj x + f x via biPath proj f and its VJP.
  3. Comments on how this matches the ResNet skip connection in the MLIR (MlirCodegen.lean residual block emission).
noncomputable def Proofs.residual {n : } (f : Vec nVec n) :
Vec nVec n

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
Instances For
    noncomputable def Proofs.residual_has_vjp {n : } (f : Vec nVec n) (hf_diff : Differentiable f) (hf : HasVJP f) :

    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. This is why ResNets are easier to train: the gradient floor is dy itself, so it can never get smaller than the loss gradient at this layer.

    MLIR (MlirCodegen.lean residual block backward, around line 1107): 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 biPath_has_vjp and identity_has_vjp, both proved in Tensor.lean.

    Equations
    Instances For
      noncomputable def Proofs.residualProj {m n : } (proj f : Vec mVec n) :
      Vec mVec n

      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
      Instances For
        noncomputable def Proofs.residualProj_has_vjp {m n : } (proj f : Vec mVec n) (hproj_diff : Differentiable proj) (hf_diff : Differentiable f) (hproj : HasVJP proj) (hf : HasVJP f) :

        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 biPath_has_vjp, proved in Tensor.lean.

        Equations
        Instances For

          Why this matters beyond ResNets #

          The fan-out/backward-add pattern is the structural building block for every modern architecture:

          ResNetsy = f(x) + x (this file). • DenseNetsy = 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-Excitationy = 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 biPath_has_vjp, 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.

          theorem Proofs.residual_has_vjp_correct {n : } (f : Vec nVec n) (hf_diff : Differentiable f) (hf : HasVJP f) (x dy : Vec n) (i : Fin n) :
          (residual_has_vjp f hf_diff hf).backward x dy i = j : Fin n, pdiv (residual f) x i j * dy j

          Public correctness theorem for residual_has_vjp: skip-connection backward equals the pdiv-contracted Jacobian of f + id.

          theorem Proofs.residualProj_has_vjp_correct {m n : } (proj f : Vec mVec n) (hproj_diff : Differentiable proj) (hf_diff : Differentiable f) (hproj : HasVJP proj) (hf : HasVJP f) (x : Vec m) (dy : Vec n) (i : Fin m) :
          (residualProj_has_vjp proj f hproj_diff hf_diff hproj hf).backward x dy i = j : Fin n, pdiv (residualProj proj f) x i j * dy j

          Public correctness theorem for residualProj_has_vjp: same as residual_has_vjp_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 (cnn_has_vjp_at, future) must chain through the pointwise HasVJPAt framework — vjp_comp_at + the witnesses below — exactly as mlp_has_vjp_at 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.

          noncomputable def Proofs.biPath_has_vjp_at {m n : } (f g : Vec mVec n) (x : Vec m) (hf_diff : DifferentiableAt f x) (hg_diff : DifferentiableAt g x) (hf : HasVJPAt f x) (hg : HasVJPAt g x) :

          Additive fan-in at a point — smooth-point analog of biPath_has_vjp.

          Equations
          Instances For
            noncomputable def Proofs.residual_has_vjp_at {n : } (f : Vec nVec n) (x : Vec n) (hf_diff : DifferentiableAt f x) (hf : HasVJPAt f x) :

            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
            Instances For
              noncomputable def Proofs.residualProj_has_vjp_at {m n : } (proj f : Vec mVec n) (x : Vec m) (hproj_diff : DifferentiableAt proj x) (hf_diff : DifferentiableAt f x) (hproj : HasVJPAt proj x) (hf : HasVJPAt f x) :

              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
              Instances For
                theorem Proofs.residual_has_vjp_at_correct {n : } (f : Vec nVec n) (x : Vec n) (hf_diff : DifferentiableAt f x) (hf : HasVJPAt f x) (dy : Vec n) (i : Fin n) :
                (residual_has_vjp_at f x hf_diff hf).backward dy i = j : Fin n, pdiv (residual f) x i j * dy j

                Public correctness theorem for residual_has_vjp_at.

                theorem Proofs.residualProj_has_vjp_at_correct {m n : } (proj f : Vec mVec n) (x : Vec m) (hproj_diff : DifferentiableAt proj x) (hf_diff : DifferentiableAt f x) (hproj : HasVJPAt proj x) (hf : HasVJPAt f x) (dy : Vec n) (i : Fin m) :
                (residualProj_has_vjp_at proj f x hproj_diff hf_diff hproj hf).backward dy i = j : Fin n, pdiv (residualProj proj f) x i j * dy j

                Public correctness theorem for residualProj_has_vjp_at.