CNN VJP Proofs #
VJP correctness for the convolutional and pooling layers used in
historical/mlir_poc/hand_cnn_train_step.mlir. The architecture there is:
x(1,28,28) → Conv(1→32) → ReLU → Conv(32→32) → ReLU → MaxPool
→ Flatten → Dense(6272→512) → ReLU → Dense(512→512)
→ ReLU → Dense(512→10) → logits
The dense and ReLU layers are inherited from MLP.lean. This file
adds the new operations: conv2d, max-pool, and flatten.
The big idea: conv backward IS conv #
The most pedagogically valuable result is that the VJP of a convolution is itself expressible as convolutions — with appropriate kernel reversal and axis transposition. This is why conv layers train efficiently: there's no special backward operator. The same primitive runs in both directions.
Two specific tricks appear in the MLIR:
- Input-gradient via reversed kernel:
dx = conv(dy, reverse(Wᵀ)) - Weight-gradient via the transpose trick:
dW = conv(xᵀ, dyᵀ), where the spatial dims of the gradient become the "kernel".
We bundle the VJP formulas as HasVJP3 / HasVJP defs whose
.correct fields are proved (the proofs are standard matrix calculus
on cross-correlations), and the commentary explains why each formula
has the form it does.
Kernel4 oc ic kH kW and Vec (oc * ic * kH * kW) are in bijection
by row-major flattening — mirrors Mat.flatten / Tensor3.flatten.
We need this so that the weight-gradient VJP can be stated as a plain
HasVJP (Vec → Vec) on the flattened kernel, reusing the existing
framework instead of introducing a parallel 4D machinery.
Nat multiplication associates left, so `oc * ic * kH * kW` parses as
`((oc * ic) * kH) * kW` — three nested `finProdFinEquiv` calls.
Row-major unflatten: inverse of flatten.
Equations
- Proofs.Kernel4.unflatten v o c kh kw = v (finProdFinEquiv (finProdFinEquiv (finProdFinEquiv (o, c), kh), kw))
Instances For
Conv2d forward (SAME padding, stride 1).
y[o, h, w] = (Σ_{c, kh, kw} x[c, h+kh−p, w+kw−p] · W[o, c, kh, kw]) + b[o]
where p = (kH−1)/2 is the padding offset and out-of-bounds reads
return 0 (zero padding). The output spatial size equals the input.
Note: this is technically cross-correlation, not convolution in the strict signal-processing sense. ML literature uses "convolution" loosely; the difference (kernel flipping) only matters when comparing against classical signal-processing references.
MLIR (hand_cnn_train_step.mlir):
%cv0 = "stablehlo.convolution"(%x, %W0) {
padding = dense<[[1, 1], [1, 1]]>, ...
}
%h0pre = stablehlo.add %cv0, broadcast(%b0)
Equations
- One or more equations did not get rendered due to their size.
Instances For
A pad-guarded read is differentiable. The guard P does not depend on x, so
if hP : P then f hP x else 0 is one branch or the constant 0. Tagged for fun_prop, which
has no rule for a dependent if; every conv / depthwise pad-eval goes through it.
Conv2d is differentiable everywhere. Each output coordinate
conv2d W b x o hi wi is the affine map
b o + ∑_{c,kh,kw} W o c kh kw · (pad-eval x): a constant bias plus a
finite ℝ-linear combination of pad-guarded input reads
(differentiable_dite_zero).
Differentiability of an if hpad : P then v(σ hpad) else 0 term — the pointwise,
Vec-indexed form of differentiable_dite_zero.
Closed-form input gradient for conv2d — direct formula, written as
a sum over output positions (co, ho, wo) with reconstructed kernel
offsets kh_nat = hi + pH − ho, kw_nat = wi + pW − wo. The body is
nonzero only when the reconstructed (kh_nat, kw_nat) lies in
[0, kH) × [0, kW) — i.e., when the input position (hi, wi) is
actually reachable from output (ho, wo) via some valid kernel offset.
Equivalent (under the (ho, wo) ↔ (kh, kw) partial bijection
ho = hi+pH-kh) to the MLIR-aligned "reversed-kernel" formula
dx[c, h, w] = Σ_{o, kh, kw} W[o, c, kH−1−kh, kW−1−kw] · dy[o, h+kh−p, w+kw−p].
Equations
- One or more equations did not get rendered due to their size.
Instances For
The padded-tap indicator of a flat input index (ci, hi, wi): output (ho, wo) reads it
through tap (kh, kw) of channel c exactly when the channel matches and the tap offsets
land on it — the padding condition is then automatic.
Conv2d input-VJP — proved from foundation rules.
The function v ↦ flatten (conv2d W b (unflatten v)) is affine in
v: the bias-free conv (linear in v) plus the broadcast bias, so
pdiv_of_affine reads each per-(idx_in, idx_out) entry off the
bias-free conv of the basis vector — a sum over (c, kh, kw) of
W o(idx_out) c kh kw times the pad-guarded Kronecker. Reindex Fin (oc*h*w) ↔ Fin oc × Fin h × Fin w
on the sum-over-idx_out; padTap_indicator turns each Kronecker into
"c = ci and the tap lands on (hi, wi)", and sum_fin_ite_add_eq collapses
(c, kh, kw) to the closed-form input gradient conv2dInputGradFormula.
The backward function (accessed as (conv2dHasVJP3 W b).backward,
or via the conv2dInputGrad abbrev below) implements
conv2dInputGradFormula W dy ci hi wi — a direct sum over
(co, kh, kw) of W co ci kh kw * dy co ho_nat wo_nat for valid
(ho_nat, wo_nat). Equivalent (under kh ↔ kH−1−kh) to the
MLIR-aligned reversed-kernel formula
dx[c, h, w] = Σ_{o, kh, kw} W[o, c, kH−1−kh, kW−1−kw] · dy[o, h+kh−p, w+kw−p].
MLIR emits the reversed-kernel form directly: %W1_t = stablehlo.transpose %W1, dims = [1, 0, 2, 3] -- swap oc↔ic %W1_rev = stablehlo.reverse %W1_t, dims = [2, 3] -- flip spatial %d_h0 = "stablehlo.convolution"(%d_h1pre, %W1_rev) ...
Equations
- Proofs.conv2dHasVJP3 W b = { backward := fun (_x : Proofs.Tensor3 ic h w) (dy : Proofs.Tensor3 oc h w) => Proofs.conv2dInputGradFormula W dy, correct := ⋯ }
Instances For
Named accessor for the conv2d input backward — aligns with MLIR
codegen (stablehlo.convolution in the backward pass).
Equations
- Proofs.conv2dInputGrad W b x dy = (Proofs.conv2dHasVJP3 W b).backward x dy
Instances For
Uniform VJP-correctness wrapper for conv2d — a citable _correct
matching the convention of every other layer (just unfolds the
HasVJP3.correct field of conv2dHasVJP3).
Flat conv — conv2d bridged into flattened Vec → Vec space:
flatConv W b = flatten ∘ conv2d W b ∘ unflatten. Spatial dims are
preserved (ic h w → oc h w), so this is Vec (ic*h*w) → Vec (oc*h*w).
This is the form the ResNet/CNN VJP composition actually uses
(everything lives in flat Vec space).
Equations
- Proofs.flatConv W b v = (Proofs.conv2d W b (Proofs.Tensor3.unflatten v)).flatten
Instances For
flatConv is differentiable everywhere. Composition of the three
differentiable maps unflatten, conv2d, flatten. This is the
differentiability witness vjpCompAt needs to chain conv into the
block.
An act ∘ norm ∘ lin stage's VJP at a point — a conv (or depthwise, or batched) op, a
normalisation, then an activation. lin and norm are differentiable everywhere, so their
global VJPs lift through .toHasVJPAt; act needs a derivative and a VJP only at the stage's
pre-activation norm (lin v), which is what relu and relu6 have off their kinks. Two
vjpCompAts: norm ∘ lin, then act. Every conv-bn-act stage VJP in the repo is this at a
particular lin, norm and act.
Equations
- One or more equations did not get rendered due to their size.
Instances For
conv → bn → relu block VJP at a smooth point.
The workhorse for composing a ResNet VJP. In flattened Vec space,
the block is relu ∘ bnForward ∘ flatConv : Vec (ic*h*w) → Vec (oc*h*w)
(BatchNorm runs over the oc*h*w flattened activations with scalar
ε, γ, β). It is stageHasVJPAt at a point v:
- inner =
bnForward ∘ flatConv— both differentiable everywhere (flatConv_differentiable,bnForward_differentiable), so their bundled VJPs lift through.toHasVJPAt. The conv witness is theHasVJP3-bridgedHasVJP3.toHasVJP (conv2dHasVJP3 W b). - outer =
relu— needs the smoothness hypothesish_smooth(no post-BN activation hits the ReLU kink) for bothrelu_differentiableAt_of_smoothandreluHasVJPAt.
Mirrors mlpHasVJPAt (dense→relu→dense), with flatConv/bn in
place of the dense layers.
Equations
- One or more equations did not get rendered due to their size.
Instances For
conv → bn block VJP (no ReLU), everywhere. Just flatConv then
bnForward, both differentiable everywhere, so this is a global
HasVJP (no smoothness needed). This is the building block for the
second conv→bn of a residual body and for the 1×1 projection skip.
Equations
- Proofs.convBnHasVJP W b ε γ β hε = Proofs.vjpComp (Proofs.flatConv W b) (Proofs.bnForward (oc * h * w) ε γ β) ⋯ ⋯ (Proofs.conv2dHasVJP3 W b).toHasVJP (Proofs.bnHasVJP (oc * h * w) ε γ β hε)
Instances For
Basic-block body VJP at a smooth point.
F := convBn₂ ∘ convBnRelu₁ = bn₂ ∘ conv₂ ∘ relu ∘ bn₁ ∘ conv₁,
the body of a post-activation ResNet basic block (the outer ReLU and
skip-add are applied later). Channels go ic → mid → oc (generic;
set ic = mid = oc = c for the identity-skip block, ic ≠ oc for the
downsample/projection block). Spatial dims h w preserved.
Inner convBnRelu₁ needs the smoothness hyp h_smooth₁ (no post-bn₁
activation hits the ReLU kink); outer convBn₂ is everywhere
differentiable, lifted via .toHasVJPAt. Two vjpCompAt chain.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Basic-block body is DifferentiableAt at a smooth point. Needed as
the diff witness when feeding the body into the residual/projection
fan-in and the post-add ReLU.
Full basic residual block VJP (identity skip).
relu(x + F(x)) with F the conv→bn→relu→conv→bn body and an
identity skip (so ic = mid = oc = c, spatial preserved). Two
smoothness hyps: h_smooth₁ for the inner block ReLU, and
h_smooth_out for the post-add outer ReLU (F v + v avoids the
kink). Built as relu ∘ residual F via residualHasVJPAt then a
final vjpCompAt with relu.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Downsample/projection basic residual block VJP.
relu(proj(x) + F(x)) where the channel/stride change is folded into
the conv dims: body F maps ic → oc (first conv ic → oc, second
oc → oc), and the skip is a 1×1 convBn projection proj : ic → oc
(everywhere differentiable — no ReLU, so its diffAt is immediate).
Built with residualProjHasVJPAt, then vjpCompAt with the
post-add relu under h_smooth_out.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Weight gradient (now proved from foundation via unfold + fun_prop) #
The conv weight gradient implements the transpose trick:
`dW[o, c, kh, kw] = Σ_{h, w} x[c, h+kh−p, w+kw−p] · dy[o, h, w]`
Here's the slick observation: this is a convolution, with the input and gradient playing the roles of "input" and "kernel" respectively.
- View the input
x : (ic, H, W)as(ic, 1, H, W)(treat channels as batch). - View the gradient
dy : (oc, H, W)as(oc, 1, H, W)(same trick). - Now do a standard convolution: input shape
(ic, 1, H, W), kernel shape(oc, 1, H, W). The "spatial" dims of the kernel are H×W (the whole image), so the output is the small(ic, oc, kH, kW)weight gradient — produced by sliding the gradient as a giant kernel. - Transpose the output
(ic, oc, kH, kW) → (oc, ic, kH, kW)to match the kernel layout.
This avoids needing a separate "convolution-with-funny-dimension-numbers"
op; we use the same forward conv operator, just with shapes reinterpreted.
Critical for backends like IREE that don't accept non-standard
dimension_numbers (see iree-org/iree#21955).
MLIR (Conv 1 backward — exactly this trick): %x_t = stablehlo.transpose %x, dims = [1, 0, 2, 3] -- (1,128,28,28) %dh0p_t = stablehlo.transpose %d_h0pre, dims = [1, 0, 2, 3] -- (32,128,28,28) %d_W0_raw = "stablehlo.convolution"(%x_t, %dh0p_t) ... -- (1,32,3,3) %d_W0 = stablehlo.transpose %d_W0_raw, dims = [1, 0, 2, 3] -- (32,1,3,3)
Framework. HasVJP3 covered only input→output VJPs. For the
weight gradient we reuse the plain HasVJP on Vec by flattening
both the kernel (Kernel4.flatten : Kernel4 → Vec (oc*ic*kH*kW)) and
the output (Tensor3.flatten : Tensor3 → Vec (oc*h*w)). The bundled
HasVJP def packages a correct backward for the flattened function
together with its proof; the user-facing conv2dWeightGrad wrapper
does the flatten / unflatten housekeeping so callers see the natural
Kernel4 type.
Numerical validation: check_jacobians.py:test_conv2dWeightGrad
gradient-checks the transpose-trick formula against finite differences.
Conv2d weight-VJP — proved from foundation rules.
The function v ↦ flatten (conv2d (unflatten v) b x) is affine in
v: the broadcast bias b o(idx_out) plus the bias-free conv, which is
linear in v. pdiv_of_affine reads each Jacobian entry off the bias-free
conv of a basis vector — a Kronecker at (o', c', kh', kw') against the
padded input — and the collapsed sum is the transpose-trick backward
dW[o', c', kh', kw'] = Σ_{hi, wi} x_pad_term(...) · dy(flat(o', hi, wi)).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Named accessor for the conv2d weight backward — aligns with MLIR
codegen (the "transpose trick" stablehlo.convolution in the backward
pass). Unwraps the flattening so callers see Kernel4 → Kernel4.
Equations
- Proofs.conv2dWeightGrad W b x dy = Proofs.Kernel4.unflatten ((Proofs.conv2dWeightGradHasVJP b x).backward W.flatten dy.flatten)
Instances For
Conv2d bias-VJP — proved from foundation rules. The function
b ↦ flatten (conv2d W b x) decomposes
as (channel broadcast of b) + (bias-free conv, constant in b), so
pdiv_of_affine gives the channel Kronecker, collapsed over the
(c, hi, wi) decomposition of Fin (oc*h*w).
The backward is db[o] = Σ_{hi, wi} dy[o, hi, wi] (the same sum as
conv2dBiasGradFormula below, written on the flattened dy).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Named accessor for the conv2d bias backward via the VJP framework.
Equations
- Proofs.conv2dBiasGrad W b x dy = (Proofs.conv2dBiasGradHasVJP W x).backward b dy.flatten
Instances For
Conv2d bias gradient — closed-form formula.
db[o] = Σ_{h, w} dy[o, h, w]
This is the sum conv2dBiasGradHasVJP's backward is defined as, on
Tensor3 instead of the flattened dy. No theorem in this file states
the equation between the two.
Each output cell adds the same b[o], so its gradient accumulates
the contributions from every spatial position. MLIR emits this as
a stablehlo.reduce across the spatial (and batch) dims.
Equations
- Proofs.conv2dBiasGradFormula dy o = ∑ y : Fin h, ∑ x : Fin w, dy o y x
Instances For
MaxPool 2×2 stride 2 forward — concrete definition.
Each output cell is the maximum of a 2×2 window of input cells:
y[c, h, w] = max{ x[c, 2h+a, 2w+b] : a, b ∈ {0,1} }, written as
an explicit four-way max.
MLIR: %pool = "stablehlo.reduce_window"(%h1, %neginf) ({ ^bb0(%a, %b): stablehlo.return (stablehlo.maximum %a, %b) }) {window_dimensions = [1, 1, 2, 2], window_strides = [1, 1, 2, 2]}
Equations
Instances For
MaxPool2 input-VJP — gradient routes only to the argmax positions.
The backward function implements:
dx[c, 2h+a, 2w+b] = dy[c, h, w] · 𝟙[(a,b) is the argmax of the window]
Conceptually, max-pool is a piecewise selection: each output is one specific input. So the Jacobian is a sparse 0/1 matrix and the VJP just routes the gradient to the chosen input.
The verified renders (the maxPoolBack node, printed by Proofs.StableHLO.pretty) emit
stablehlo.select_and_scatter over the 2×2 stride-2 windows:
// select: keep the current cell unless a later one is strictly larger %sge = stablehlo.compare GE, %sa, %sb // scatter: add the routed cotangent into the chosen cell %ss = stablehlo.add %sc, %sd
The GE select keeps the first window cell attaining the max, so each window's
cotangent lands in ONE cell, as PyTorch's and JAX's max-pool backwards do.
Canonical (junk-at-tie) witness. HasVJP3.correct is
satisfied by the canonical pdiv3-derived backward via rfl. At
argmax-tie boundaries maxPool2 is not differentiable, so pdiv3
agrees with fderiv's junk default of 0 and the canonical
witness is also 0 there, where the render routes the cotangent to
the first tied cell. Off ties the two agree: see
maxPool2_codegen_matches_canonical below. The generic MlirCodegen
walk still emits the older tile-compare-select (compare EQ of the
input against the tiled window max, then select), which routes the
cotangent to every tied cell. See LeanMlir/Proofs/README.md's
trust-boundary section.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Named accessor for the maxPool2 input backward (the canonical witness of
maxPool2HasVJP3).
Equations
Instances For
Smooth-point codegen bridge for MaxPool2 #
Closes the smooth-point half of the codegen trust boundary at MaxPool2.
At points where every 2×2 window has a unique strict argmax, the
canonical pdiv-derived backward in maxPool2HasVJP3 collapses to
"route dy to the argmax position, zero elsewhere" — what the rendered
select_and_scatter computes when every window has one argmax. Mirrors relu_codegen_matches_canonical in MLP.lean, but
the local linearization is per-2×2-window rather than per-coordinate.
Smoothness: every 2×2 window of x attains its max at exactly one cell — a cell that
dominates its window is strictly above every other cell of it. The other cells may tie with
each other (a post-ReLU window [5, 0, 0, 0] qualifies); a window whose max is attained twice
(an all-zero post-ReLU window) does not, and there maxPool2 has no derivative.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Pairwise-distinct windows are smooth: a dominating cell is ≥ every other cell and differs
from it.
Input position (ci, hi_in, wi_in) attains the max of its window.
Equations
- Proofs.MaxPool2IsArgmax x ci hi_in wi_in = ∀ (a b : Fin 2), x ci (Proofs.winRowInv (Proofs.winRow hi_in) a) (Proofs.winColInv (Proofs.winCol wi_in) b) ≤ x ci hi_in wi_in
Instances For
A finite ∀ over the window of real ≤: decidable (classically, as every ℝ order is),
so an if MaxPool2IsArgmax … elaborates without open Classical.
Equations
- Proofs.MaxPool2IsArgmax.decidable x ci hi_in wi_in = id inferInstance
Offsets and positions coincide for 2×2 windows, so MaxPool2Smooth (over offsets) gives
the generic WindowSmooth (over positions).
A (not necessarily unique) argmax of the 2×2 window at output
position (co, ho, wo). Unique under MaxPool2Smooth.
Equations
- Proofs.maxPool2Argmax x co ho wo = Proofs.windowArgmax Proofs.winRowInv Proofs.winColInv x co ho wo
Instances For
For each output flat index k_out (decoded to (co, ho, wo)), the
flat index of the argmax's input position in Vec (c * (2*h) * (2*w)).
Used as the carrier of the local-linearization reindexCLM.
Equations
Instances For
Smooth-point local-linearization for max-pool: near flatten x the flattened max-pool is
the reindex y ↦ y ∘ σ, σ routing each output to its argmax's input position
(windowMax_flat_hasFDerivAt through maxPool2_eq_windowMax).
MaxPool2 smooth-point Jacobian. At a smooth point, pdiv3 of
maxPool2 is a sparse 0/1 indicator: 1 exactly when the output
(co, ho, wo) is the window of the input (ci, hi_in, wi_in) AND
that input is the argmax of its window.
Bridge: maxPool2HasVJP3's canonical backward matches the
codegen formula at smooth points.
At points where every 2×2 window has a unique strict argmax, the
canonical pdiv3-derived backward collapses to "dy at the
window's output position, but only at the argmax input cell" — what the
rendered select_and_scatter computes there. Closes the
smooth-point half of the codegen trust boundary; what remains is
the kink convention at argmax ties (the render routes the
cotangent to the first tied cell, the canonical witness gives 0).
MaxPool2 pointwise VJP — no canonical-witness escape.
HasVJPAt3 maxPool2 x under MaxPool2Smooth x. The backward is
the argmax routing directly (route dy to the argmax cell, zero
elsewhere); the correct field is
maxPool2_codegen_matches_canonical flipped, not rfl.
Companion of reluHasVJPAt in MLP.lean: mlpHasVJPAt chains
through reluHasVJPAt, and cnnHasVJPAt chains through both (this
one via maxPoolFlatHasVJPAt), so neither uses the global canonical
witness at a kinked operator.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Reshape (flatten / unflatten) #
Flatten is a permutation of indices, so its VJP is just the inverse permutation. No gradient computation needed.
MLIR: %flat = stablehlo.reshape %pool : (tensor<128x32x14x14xf32>) -> tensor<128x6272xf32>
The flatten / unflatten bijection is already defined in
Tensor.lean as Tensor3.flatten / Tensor3.unflatten (used by the
pdiv3 derivation there). We reuse those here rather than
duplicating — see Tensor3.flatten_unflatten / unflatten_flatten
for the mutual-inverse proofs.
Summary of derivations in this file #
conv2d,maxPool2— forward operations, concrete definitions.maxPool2HasVJP3— input-path VJP for maxPool2: the canonicalpdiv3-derived witness (zero at argmax ties);maxPool2HasVJPAt3is the argmax-routing form underMaxPool2Smooth.
Derived (not axioms):
conv2dHasVJP3— input-path VJP, proved withpdiv_of_affine(the conv is affine in its input; the Jacobian entry is the bias-free conv of a basis vector, a pad-guarded Kronecker). Backward function isconv2dInputGradFormula(sum over(co, ho, wo)with reconstructed kernel offsetskh = hi+pH-ho,kw = wi+pW-wo).conv2dWeightGradHasVJP— the weight-path VJP, bundled as a plainHasVJPon the Kernel4-flattened function. Numerically gradient-checked against the transpose-trick formula incheck_jacobians.py:test_conv2dWeightGrad.conv2dBiasGradHasVJP— the bias-path VJP, same bundledHasVJPpattern. The closed-form "sum output cotangent over spatial dims per channel" is expressed asconv2dBiasGradFormula; the namedconv2dBiasGradextracts the backward via the VJP.conv2dInputGrad,maxPool2InputGrad,conv2dWeightGrad,conv2dBiasGrad— named accessors, defined as.backward(plus flatten / unflatten housekeeping for the weight / bias variants) of the corresponding VJP.conv2dInputGradFormula,conv2dBiasGradFormula— the concrete closed-form formulas.conv2dInputGradFormulaisconv2dHasVJP3's backward by definition;conv2dBiasGradFormulais the same sum asconv2dBiasGradHasVJP's backward, with no theorem stating the equation.- 3D reshape (
Tensor3.flatten/Tensor3.unflatten) imported fromTensor.lean; 4D reshape (Kernel4.flatten/Kernel4.unflatten) defined here, both proved bijections.
Public correctness theorem for maxPool2HasVJP3: the
canonical-witness backward equals the pdiv3-contracted Jacobian
by definition. The codegen substitutes the standard argmax-routing
convention at non-smooth tiebreaks (see LeanMlir/Proofs/README.md's
Codegen Trust Boundary).
Public correctness theorem for maxPool2HasVJPAt3 — the
pointwise variant under MaxPool2Smooth. The underlying .correct
field is maxPool2_codegen_matches_canonical flipped (a real proof),
not rfl; this wrapper exposes it for comparator re-verification.
The capstone: a whole-network ResNet-style CNN VJP #
cnnHasVJPAt is the CNN analogue of vitFullHasVJP — a single
HasVJPAt for an end-to-end forward pass, chained entirely in flattened
Vec space via vjpCompAt. It first needs global average pooling,
which we define here:
globalAvgPool x ci = (∑ hi ∑ wi x ci hi wi) / (h*w) (mean over spatial
per channel), bridge it to flat Vec space (globalAvgPoolFlat), and
prove its linear VJP (globalAvgPoolFlatHasVJP, backward broadcasts
dy ci / (h*w) to every spatial cell of channel ci) and
differentiability.
Fixed structural choices (a concrete-but-representative pipeline, in
the spirit of hand_cnn_train_step.mlir; prioritising a complete
axiom-clean end-to-end witness over maximal generality):
input : Vec (ic * (2h) * (2w))
stem : convBnRelu ic → c (spatial 2h×2w preserved)
pool : maxPool2 c, 2h×2w → c, h×w
block1 : resblockHasVJPAt (identity skip, c → c, h×w)
block2 : resblockProjHasVJPAt (projection skip, c → oc, h×w)
gap : globalAvgPool oc, h×w → Vec oc
head : dense oc → nClasses
So: 1 stem conv, 1 max-pool, exactly two residual blocks (one of EACH
skip type — identity and 1×1 projection — to exercise both code paths),
global-average pool, one dense classifier. Channel/spatial dims stay
implicit Nat params; the block/stage counts are fixed. The bundled
smoothness hypotheses (h_stem, h_mp, h_rb1/h_rb1o, h_rb2/
h_rb2o) are the family of every ReLU + max-pool site's smooth-point
condition, exactly like mlpHasVJPAt's multiple h_smooth_*.
The differentiability obstacle (max-pool is non-smooth globally, so
vjpCompAt cannot get DifferentiableAt of a max-pool-containing
prefix from a global lemma) is discharged at the smooth point by
maxPool2_flat_hasFDerivAt (the local linearization already proved for
the max-pool Jacobian) via .differentiableAt.
Flat GAP: Vec (c*h*w) → Vec c.
Equations
Instances For
The pool is continuous (the fun_prop form of globalAvgPoolFlat_differentiable).
The channel of a flat index idx : Fin (c*h*w).
Equations
- Proofs.flatChannel c h w idx = (finProdFinEquiv.symm (finProdFinEquiv.symm idx).1).1
Instances For
Global average pool VJP (flattened). Linear map; backward
broadcasts dy ci / (h*w) to every spatial cell of channel ci.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Uniform VJP-correctness wrapper for globalAvgPoolFlat — a citable
_correct matching the convention of every other layer (just unfolds the
HasVJP.correct field of globalAvgPoolFlatHasVJP).
The 2×2/s2 max pool maxPool2 on the flattened Vec form (unflatten, pool, flatten).
Equations
- Proofs.maxPoolFlat c h w v = (Proofs.maxPool2 (Proofs.Tensor3.unflatten v)).flatten
Instances For
The VJP of maxPoolFlat at a flattened input whose 2×2 windows each have a unique max
(MaxPool2Smooth): maxPool2HasVJPAt3 moved to Vec form.
Equations
- Proofs.maxPoolFlatHasVJPAt x h_smooth = (Proofs.maxPool2HasVJPAt3 x h_smooth).toHasVJPAt
Instances For
maxPoolFlat is the 2×2 windowMaxFlat (maxPool2_eq_windowMax, flattened).
Equations
- Proofs.cbr W b ε γ β = Proofs.relu (oc * h * w) ∘ Proofs.bnForward (oc * h * w) ε γ β ∘ Proofs.flatConv W b
Instances For
Equations
- One or more equations did not get rendered due to their size.
Instances For
The forward CNN: stem(convBnRelu) → maxpool → resblock(id) → resblockProj → gap → dense.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The whole-net VJP of cnnForward at x, folded by vjpCompAt, given positive BN εs and six smoothness binders: h_stem (stem BN output
nonzero), h_mp (unique window maxima of the post-ReLU stem output the pool reads), h_rb1 /
h_rb1o (the identity block's inner BN output and pre-ReLU residual sum nonzero) and h_rb2 /
h_rb2o (the same two for the projection block).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Public correctness theorem for cnnHasVJPAt — exposes the
witness's .correct field as a top-level proposition: the full
ResNet-style CNN's backward equals the pdiv-contracted Jacobian
(Jacobian-transpose applied to the cotangent). CNN analogue of
vitFullHasVJP_correct.