Attention — the Capstone #
The fanciest architectural primitive in modern vision and language models, formalized in one file. If you're reading the book straight through, this is the chapter where everything you've learned clicks together and you realize there's nothing left to learn.
The cast of characters #
Scaled dot-product attention:
out = softmax((Q * K^T) / sqrt(d)) * V
where Q = X Wq, K = X Wk, V = X Wv — three dense projections of
the same input X. Every piece is something we already have:
| Piece | Chapter | VJP move |
|---|---|---|
Q = X Wq | MLP.lean | dense backward |
K = X Wk | MLP.lean | dense backward |
V = X Wv | MLP.lean | dense backward |
Q * K^T | (matmul = dense) | chain rule |
/ sqrt(d) | (scalar) | chain rule + scale |
softmax(...) | Softmax.lean | closed-form collapse |
... * V | (matmul = dense) | chain rule |
| three-way fan-in at X | Residual.lean | biPathHasVJP |
So the only genuinely new ingredient in attention is the standalone
softmax VJP (softmaxHasVJP, separate from the CE-loss gradient). Once
that's in hand, everything else is composition via tools we built in
earlier chapters.
Structure of this file #
- The multi-head matrix kit — column-slab independence (
pdivMat_colIndep,colSlabwiseHasVJPMat) and the ternary matrix VJPHasVJPMat3; then the differentiability helpers. - The standalone softmax VJP is in
Softmax.lean(imported here). - Scaled dot-product attention — SDPA as a composition.
- Multi-head wrapper — reshape/transpose boilerplate, no new math.
- Transformer block — LN -> MHSA -> + -> LN -> MLP -> +, pure composition.
- Final commentary — why the taxonomy is complete.
- Bridging ranks — patch embedding, classifier head, and the weight-tied
whole-ViT witness
vitFullHasVJP(vitFullHasVJP_correct).
References #
- Vaswani et al. 2017, Attention Is All You Need. https://arxiv.org/abs/1706.03762
- Dosovitskiy et al. 2021, An Image is Worth 16x16 Words (ViT). https://arxiv.org/abs/2010.11929
Per-head / per-slab column independence #
Multi-head attention applies the same per-head function to each of heads
column slabs of width d_in from a Mat n (heads * d_in) input. The
column-slab analog of rowwiseHasVJPMat factors that vmap-over-heads
structure: each head's output depends only on its own slab of the input,
so the matrix Jacobian is block-diagonal across the head axis. The general form lets each slab
have its own map (colSlabApplyH); the shared-map colSlabApply is its constant-family case.
colSlabApply with its own map on each head's slab: output column (h, j) is column j of
g h applied to input slab h. The attention core in one of Q, K, V is one, since head
h reads the other two projections' slab h.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The Jacobian stays block-diagonal across heads — pdivMat_colIndep with a per-slab map:
zero unless the input and output slabs agree, and g h's own Jacobian on slab h if they do.
Apply g : Mat n d_in → Mat n d_out to each of the heads column
slabs of width d_in in a Mat n (heads * d_in) input, producing
a Mat n (heads * d_out) output. Output column (h, j_out) is
column j_out of g (slab h M), where slab h M extracts the
d_in-wide column block at head index h. Definitionally
colSlabApplyH (fun _ => g).
Equations
- Proofs.colSlabApply g M r hj = g (fun (r' : Fin n) (j_in : Fin d_in) => M r' (finProdFinEquiv ((finProdFinEquiv.symm hj).1, j_in))) r (finProdFinEquiv.symm hj).2
Instances For
Column-slab independence Jacobian — column-axis analog of
pdivMat_rowIndep. For a slab-applied function colSlabApply g,
the Jacobian is block-diagonal across the heads axis: zero unless
the input slab h_j matches the output slab h_l, otherwise equal
to pdivMat g on that slab.
Requires Differentiable ℝ (flat g) for the same reason as
pdivMat_rowIndep: the Pi-valued flat form must be differentiable
everywhere so fderiv doesn't fall back to junk-default 0. The
constant-family case of pdivMat_colIndepH.
Lift per-slab VJPs to colSlabApplyH — colSlabwiseHasVJPMat with a per-slab map: the
backward runs slab h's own backward on slab h.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Lift HasVJPMat g to column-slab vmap — column-axis analog of
rowwiseHasVJPMat. Given g : Mat n d_in → Mat n d_out with a
matrix VJP, applying g independently to each of heads-many column
slabs gives a HasVJPMat for colSlabApply g. The backward applies
g.backward per slab; its correctness is colSlabwiseHasVJPMatH's at
a constant family.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Ternary matrix VJP #
For ternary-input functions like SDPA (Q, K, V) ↦ out, package the
three per-input VJPs as a single structure analogous to HasVJPMat.
The backward returns the triple of per-input gradients; correctness
holds independently for each input (with the others fixed).
VJP structure for Mat × Mat × Mat → Mat functions where all
three inputs share the same shape Mat n d_in and the output is
Mat n d_out. Backward returns the triple of per-input gradients;
correct_{1,2,3} ensure each gradient matches the partial derivative
treating the other two inputs as constants.
Instances For
Differentiability of the flattened per-token dense map.
fun X => fun n => dense W b (X n) is linear in X, so the
flattened version is Differentiable everywhere.
The per-token dense is differentiable in its weight, flattened (input fixed).
…in its bias.
Differentiability of the flattened per-token GELU map, at either form of the GELU.
GeluForm.scalar_differentiable is tagged for fun_prop, so the proof discharges
automatically.
Differentiability of layerNormForward D ε γ β — it is bnForward (definitionally),
differentiable when ε > 0. Tagged for fun_prop.
Differentiability of the flattened per-token LayerNorm map: each output coordinate is a
coordinate of layerNormForward (layerNorm_differentiable) applied to one row of the input.
Differentiability of the flattened identity matrix map.
Mat.flatten ∘ id ∘ Mat.unflatten = id on Vec (a*b).
Differentiability of a flattened composition G ∘ F from those of F and G: the
Mat.unflatten (Mat.flatten _) round trip in the middle is the identity.
Attention as a composition #
For a single sequence of n tokens, each with feature dim d, let
X : Mat n d be the input. Attention produces out : Mat n d via:
Q = X * Wq -- (n x d), dense projection
K = X * Wk -- (n x d)
V = X * Wv -- (n x d)
scores = Q * K^T -- (n x n)
scaled = scores / sqrt(d)
weights = softmax_row(scaled) -- softmax applied per row
out = weights * V -- (n x d)
Because the input X is a matrix, we need matrix-level types. We work
with Mat n d throughout this section (already defined in Tensor.lean).
Row-wise softmax is just "apply the 1D softmax to each row independently." Its VJP is just "apply the 1D softmax VJP to each row independently." No new derivation; the fan-out structure is trivially parallel.
Smoothness of rowSoftmax: each output coordinate of the flattened map is a
coordinate of softmax n (softmax_differentiable) applied to one row of the input.
Row-wise softmax VJP — proved, no sorry.
Rows are independent, so the Jacobian is block-diagonal with the
standalone softmax Jacobian in each block. The backward just
applies softmaxHasVJP per row; correctness is rowwiseHasVJPMat's.
Equations
- Proofs.rowSoftmaxHasVJPMat = { backward := fun (A dY : Proofs.Mat m n) (r : Fin m) (c : Fin n) => (Proofs.softmaxHasVJP n).backward (A r) (dY r) c, correct := ⋯ }
Instances For
Alias so rowSoftmaxHasVJPMat types against the actual rowSoftmax
definition (definitionally equal, but lets Lean unify on the name).
Equations
Instances For
Scaled dot-product attention, for a single sequence and a
single head. Q K V : Mat n d.
sdpa Q K V = softmax_row(Q * K^T / sqrt(d)) * V
MLIR (MlirCodegen.emitMHSAForward):
%mh_sc = dot_general %mh_q, %mh_k, contracting_dims = [3] x [3]
%mh_ss = multiply %mh_sc, broadcast(1/sqrt(d))
%mh_sm = softmax(%mh_ss) -- via reduce max, shift, exp, reduce sum, divide
%mh_av = dot_general %mh_sm, %mh_v, contracting_dims = [3] x [2]
Equations
- Proofs.sdpa n d Q K V = (Proofs.rowSoftmax fun (i j : Fin n) => 1 / √↑d * Q.mul K.transpose i j).mul V
Instances For
The backward pass through SDPA (by hand, then compositionally) #
Working backward from d_out : Mat n d, four steps:
Step 1. Through the final matmul out = weights * V. By the dense
layer VJP generalized to matrices (same derivation as denseHasVJP,
just with a batch dimension):
d_V = weights^T * d_out -- (n x d)
d_weights = d_out * V^T -- (n x n)
Step 2. Through the per-row softmax. Each row is independent, so
we apply softmaxHasVJP row-by-row:
d_scaled_i = weights_i * (d_weights_i - <weights_i, d_weights_i> * 1)
Step 3. Through the scalar scale scaled = scores / sqrt(d). Just
divide the incoming gradient by sqrt(d):
d_scores = d_scaled / sqrt(d)
Step 4. Through scores = Q * K^T. Same matrix-matmul VJP as
step 1, but now Q and K both flow back:
d_Q = d_scores * K -- (n x d)
d_K = d_scores^T * Q -- (n x d)
Step 5. Three parallel dense backwards from Q, K, V back to X.
Each uses denseHasVJP:
d_X_via_Q = d_Q * Wq^T
d_X_via_K = d_K * Wk^T
d_X_via_V = d_V * Wv^T
Step 6. Fan-in at X — the three paths add:
d_X = d_X_via_Q + d_X_via_K + d_X_via_V
This is biPathHasVJP from Residual.lean, applied twice (to
combine three paths). The three-way fan-in is the attention
backward pass at the input. Q, K, V are parallel branches reading
from X, so their gradients accumulate at X.
And the parameter gradients (for W_q, W_k, W_v, W_o) are collected at each dense layer along the way — exactly as with any other dense layer in the book.
There is no novel structural move in attention. It's three dense layers, two matmuls, one row-softmax, one scale, and a three-way fan-in. Every piece has been proved. The composition is mechanical.
The backward, concretely #
This section gives:
- Concrete definitions of
sdpaBackQ,sdpaBackK,sdpaBackVtranscribed from the step-by-step derivation above. - Correctness theorems
sdpaBackQ_correct,sdpaBackK_correct,sdpaBackV_correct, stated in terms ofpdivMat(the matrix-level partial derivative primitive fromTensor.lean), each proved compositionally as avjpMatCompchain (four steps for Q).
The concrete formulas are also numerically gradient-checked in
check_jacobians.py (test_sdpaBackQ/K/V) for cross-validation.
Softmax-weights under the SDPA scale, reused by all three backwards.
Equations
- Proofs.sdpaWeights n d Q K = Proofs.rowSoftmax fun (i j : Fin n) => Proofs.sdpaScale d * Q.mul K.transpose i j
Instances For
Gradient flowing into weights from the final matmul out = weights · V.
Equations
- Proofs.sdpaDWeights V dOut = dOut.mul V.transpose
Instances For
Per-row softmax VJP: p_i * (dw_i - <p_i, dw_i>).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Gradient w.r.t. the pre-softmax scores, after undoing the / sqrt(d) scale.
Equations
- Proofs.sdpaDScores n d Q K V dOut i j = Proofs.sdpaScale d * Proofs.sdpaDScaled n d Q K V dOut i j
Instances For
Backward w.r.t. Q: dQ = dScores · K.
Equations
- Proofs.sdpaBackQ n d Q K V dOut = (Proofs.sdpaDScores n d Q K V dOut).mul K
Instances For
Backward w.r.t. K: dK = dScores^T · Q.
Equations
- Proofs.sdpaBackK n d Q K V dOut = (Proofs.sdpaDScores n d Q K V dOut).transpose.mul Q
Instances For
Backward w.r.t. V: dV = weights^T · dOut. (V does not appear on the
RHS: V's gradient flows only through the final matmul, not through
weights.)
Equations
- Proofs.sdpaBackV n d Q K _V dOut = (Proofs.sdpaWeights n d Q K).transpose.mul dOut
Instances For
Q and K correctness via compositional SDPA forward chain #
For Q (with K, V fixed), sdpa n d · K V is the composition:
Q ↦ Q · K^T ↦ scale * _ ↦ rowSoftmax _ ↦ _ · V
Four steps, four already-proved HasVJPMat building blocks:
matmulRightConstHasVJP (Mat.transpose K)— ∂(Q · K^T)/∂QscalarScaleHasVJP (sdpaScale d)— ∂(scale · scores)/∂scoresrowSoftmaxHasVJPMat— ∂(rowSoftmax scaled)/∂scaledmatmulRightConstHasVJP V— ∂(weights · V)/∂weights
Chain them with vjpMatComp thrice → a HasVJPMat for the full
Q-path. Then show the chain's backward function equals sdpaBackQ
pointwise (trivial — the chain's backward literally computes the same
nested formula) and invoke its .correct to discharge the goal.
HasVJPMat for the chain — built by nesting vjpMatComp thrice.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Correctness of sdpaBackQ — proved, no sorry.
Two moves: (1) replace fun Q' => sdpa n d Q' K V by the chain via
sdpaQChain_eq; (2) apply the chain's .correct and verify that
the chain's backward reduces to sdpaBackQ (pure unfolding).
K case #
K enters through a transpose before the first matmul. One extra step in the chain: K ↦ K^T, then follow the Q chain (but with the matmul being "left factor constant" this time because Q is fixed and K^T is on the right).
The VJP of sdpaKChain, vjpMatComp down its five steps.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Correctness of sdpaBackK — proved, no sorry.
Same shape as Q, but the chain goes through a leading transpose
step. The resulting backward computes ∑ k, Q k j * dScores k i
whereas sdpaBackK is Mat.mul (Mat.transpose dScores) Q, which
expands to ∑ k, dScores k i * Q k j. Equal by mul_comm at the
summand level.
The final matmul in SDPA: for fixed Q, K, the function V' ↦ sdpa Q K V'
is V' ↦ W · V' where W = sdpaWeights Q K. Pure rewrite; definitional.
Correctness of sdpaBackV — proved, no sorry.
The V-path is the simplest case: V' only enters through the final
matmul out = weights · V'. So fun V' => sdpa n d Q K V' is just
fun V' => Mat.mul W V' where W is fixed (= sdpaWeights n d Q K),
and the VJP comes directly from matmulLeftConstHasVJP.
Bundled SDPA ternary VJP. Packages sdpaBackQ_correct,
sdpaBackK_correct and sdpaBackV_correct into a single HasVJPMat3 instance. The backward triple
(sdpaBackQ, sdpaBackK, sdpaBackV) gives per-input
gradients; correctness is the three existing per-input theorems
in one structure.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Multi-head: parallelism over a partition #
Multi-head attention is:
- Project
X : Mat N Dthree ways:Q = X·Wq + bq,K = X·Wk + bk,V = X·Wv + bv. - Reshape each projection
(N, D) → (N, heads, d_head)by slicing the feature axis. - Run SDPA independently on each of the
headsslices. - Concatenate the head outputs back to
(N, D). - Apply the output projection
Y = concat · Wo + bo.
In the MLIR (emitMHSAForward):
reshape (B, N, D) -> (B, N, H, D_h)
transpose -> (B, H, N, D_h)
[SDPA per head, using batching_dims = [0, 1]]
transpose -> (B, N, H, D_h)
reshape -> (B, N, D)
dense projection (the "output projection" Wo)
This section proves mhsaHasVJPMat end-to-end: we define
mhsaLayer concretely in Lean (Q/K/V projections → per-head slice
→ sdpa-per-head → concat → Wo projection), then prove its HasVJPMat
via the pdivMat_colIndep + colSlabwiseHasVJPMat framework,
which lifts the per-head SDPA backward over the
head axis. Formula remains numerically gradient-checked in
check_jacobians.py for cross-validation.
Multi-head SDPA on a single sequence: Mat N (heads·d_head) → Mat N (heads·d_head).
Concretely defined (not opaque):
- Q, K, V projections (each a per-token dense with its own Wq/Wk/Wv).
- For each head
h : Fin heads, extract the(N, d_head)slice of Q/K/V by indexingfinProdFinEquiv (h, k)in the combined axis. - Run
sdpaon each slice. - Concatenate the head outputs back along the feature axis.
- Output projection Wo · concat + bo (per-token dense).
The bundled VJP theorem below packages the correctness of this
whole thing — composing dense Jacobians, the per-head SDPA
jacobians (sdpaBackQ_correct, sdpaBackK_correct, sdpaBackV_correct), and
the reshape/unreshape pdiv_reindex facts, with the per-head
independence handled by the column-stacking framework
(pdivMat_colIndep + colSlabwiseHasVJPMat).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Column-stacked SDPA — the bridge from HasVJPMat3 to multi-head. #
Two facts are needed for multi-head attention:
(1) joint differentiability of `(Q, K, V) ↦ sdpa Q K V`, which doesn't
follow from the per-input `_flat_diff` lemmas; (2) the per-head
"vmap" structure, which `colSlabwiseHasVJPMat` handles for
*unary* per-slab functions but SDPA is naturally ternary.
The fix: column-stack `(Q | K | V)` into a single `Mat n (3 * d_head)`
"qkv slab", define `mhsaG : Mat n (3 * d_head) → Mat n d_head` as the
unary view of SDPA on this slab, and lift via the existing framework.
Both `mhsaG_flat_differentiable` and `mhsaGHasVJPMat` are then mechanical
composition of existing pieces: the joint `_flat_diff` factors through
`rowSoftmax_flat_differentiable` after stage-by-stage chaining, and the VJP comes
from `sdpaHasVJPMat3` plus a "column-third projection" argument that
matches the `(c : Fin 3)` index of the slab to the Q/K/V partial.
Column-stacked SDPA: takes a slab Mat n (3 * d_head) whose columns
encode (c : Fin 3, j : Fin d_head) via finProdFinEquiv, with c = 0
being the Q-third, c = 1 the K-third, c = 2 the V-third. Returns
sdpa applied to those three thirds.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Pre-softmax matrix in mhsaG: scale · Q · K^T as a function of slab.
Each entry is a polynomial in the slab's coords (linear projections
times each other), so fun_prop discharges flat-diff after unfolding.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Post-softmax weights in mhsaG: rowSoftmax(scale · Q · K^T).
Equations
- Proofs.mhsaWeights n d slab = Proofs.rowSoftmax (Proofs.mhsaPreWeights n d slab)
Instances For
Joint flat-diff of column-stacked SDPA.
Joint diff in (Q, K, V) doesn't follow from the existing per-input _flat_diff lemmas. Here
we prove it by treating the qkv-slab as the variable, factoring SDPA
as Mat.mul ∘ rowSoftmax ∘ scaled-matmul: the weights are differentiable
(mhsaWeights_flat_differentiable), and the final matmul with the V-third of the
slab is polynomial in the weights and the slab coordinates.
Column-stacked SDPA VJP #
`HasVJPMat (mhsaG n d)`: the backward column-stacks
`(sdpaBackQ, sdpaBackK, sdpaBackV)` according to the c-third
of the slab column index. Correctness reduces to `sdpaHasVJPMat3`
after observing that perturbing the c-th third of the slab only
perturbs the c-th input of SDPA.
Column projection slab ↦ slab^[c] for a fixed c : Fin 3.
Linear, so its flat form is a reindexCLM.
Equations
- Proofs.mhsaProjC c slab r j = slab r (finProdFinEquiv (c, j))
Instances For
"Lift to slab third c": embeds Vec (n * d) into Vec (n * (3 * d)) by
placing u in the c-th column third and zero elsewhere. Linear, hence
a CLM. Constructed from per-coord CLMs
via ContinuousLinearMap.pi: each output coord is either a projection
(if the index is in the c-third) or zero.
Equations
- One or more equations did not get rendered due to their size.
Instances For
"Embed Q' into slab at the c-th third, keep other thirds at slab's values."
Affine function: mhsaLiftCCLM c · u + (slab with c-th third zeroed).
Equations
- One or more equations did not get rendered due to their size.
Instances For
pdivMat of mhsaG splits per-c into the corresponding pdivMat of
SDPA against its c-th argument. The freezing lemma: changes in the
c-th column third of the slab only perturb the c-th input of SDPA.
Proved via the chain rule mhsaG ∘ mhsaEmbedC = freeze_c.
HasVJPMat for column-stacked SDPA. Backward column-stacks the three
sdpaBackQ/sdpaBackK/sdpaBackV outputs by their c : Fin 3 slot. Correctness comes from
pdivMat_mhsaG_split (case-splits on c into the corresponding
one-input SDPA pdivMat) and sdpaHasVJPMat3.correct_*.
Equations
- One or more equations did not get rendered due to their size.
Instances For
colSlabApplyH is differentiable, flattened, when every slab's map is: each output coordinate
factors through the linear slab projection and g h by flat_differentiable_comp.
Single-head attention is differentiable in Q, flattened (K, V fixed).
…in K.
…in V (the softmax weights are a constant).
Flat-diff for colSlabApply g: the constant-family case of
colSlabApplyH_flat_differentiable.
Combined Q/K/V weight matrix: stack Wq | Wk | Wv with the per-head
interleave layout. Output column (h, c, j) ↦ (Wq | Wk | Wv)[k, fPF(h, j)]
based on c : Fin 3. Used to express the three Q/K/V projections as a
single per-token dense, enabling clean composition with colSlabApply mhsaG.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The mhsaLayer factorization: it equals
output_dense ∘ colSlabApply mhsaG ∘ qkv_stack_dense.
All three pieces have HasVJPMat and flat-diff:
qkv_stack_denseusesmhsaQkvW,mhsaQkvBas a single per-token dense.colSlabApply mhsaGliftsmhsaGHasVJPMatper-head.output_denseis the standard per-token dense for Wo, bo.
The composed MHSA VJP — Wo-dense ∘ colSlabApply mhsaG ∘ qkv-dense,
stated on the explicit composition (no mhsaLayer_eq_compose transport).
This is the substantive witness; mhsaHasVJPMat below re-types it to
mhsaLayer with the cast confined to the correct field, so
(mhsaHasVJPMat …).backward reduces in one projection.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Multi-head SDPA VJP, composed from mhsaGHasVJPMat, colSlabwiseHasVJPMat, and
the per-token dense framework. backward is the composed witness's
field DIRECTLY (kernel-cheap projection); the mhsaLayer_eq_compose
transport lives only in correct — a Prop the kernel never reduces.
See mhsaComposedHasVJPMat's docstring for why.
Equations
- Proofs.mhsaHasVJPMat N heads d_head Wq Wk Wv Wo bq bk bv bo = { backward := (Proofs.mhsaComposedHasVJPMat N heads d_head Wq Wk Wv Wo bq bk bv bo).backward, correct := ⋯ }
Instances For
Differentiability of the flattened multi-head SDPA layer —
composition of three _flat_diff lemmas.
Per-token liftings (theorems) #
Every per-token operation in a transformer (LN, dense, GELU) lifts from
HasVJP on Vec D to HasVJPMat on Mat N D via the single helper
rowwiseHasVJPMat (Tensor.lean).
Per-token layer norm across a sequence. Applies layerNormForward
to each row of the (N, D) input; the backward is block-diagonal.
Equations
- Proofs.layerNormPerTokenHasVJPMat N D ε γ β hε = Proofs.rowwiseHasVJPMat (Proofs.layerNormHasVJP D ε γ β hε) ⋯
Instances For
Per-token dense projection across a sequence.
Q = X · W + b, row-by-row dense with shared weights.
Equations
- Proofs.densePerTokenHasVJPMat N inD outD W b = Proofs.rowwiseHasVJPMat (Proofs.denseHasVJP W b) ⋯
Instances For
Per-token GELU across a sequence. Elementwise activation, so diagonal Jacobian both across rows and within a row.
Equations
- Proofs.geluPerTokenHasVJPMat gf N D = Proofs.rowwiseHasVJPMat (gf.hasVJP D) ⋯
Instances For
Per-token GELU input-VJP: the flat gf.hasVJP (N*D) backward IS the
flatten of the rowwise geluPerTokenHasVJPMat.backward at the saved
pre-GELU activation A (GELU is elementwise, so flat and rowwise agree).
A transformer encoder block #
From MlirCodegen.emitTransformerBlockForward:
block(x) = h1 + MLP(LN2(h1)) where h1 = x + MHSA(LN1(x))
Expanding:
h1 = x + MHSA(LN1(x)) -- attention sub-layer with residual
out = h1 + MLP(LN2(h1)) -- MLP sub-layer with residual
where MLP(z) = dense(Wfc2, bfc2, gelu(dense(Wfc1, bfc1, z))).
Every piece is now a HasVJPMat on Mat N D:
LN1,LN2—layerNormPerTokenHasVJPMat(theorem viarowwiseHasVJPMat)MHSA—mhsaHasVJPMat(bundledHasVJPMatdef)MLP— twodensePerTokenHasVJPMat+ onegeluPerTokenHasVJPMat, glued withvjpMatComp+residuals —biPathMatHasVJP(theorem, Tensor.lean) with identity
The transformer block theorem below glues these with vjpMatComp and
biPathMatHasVJP.
MLP sublayer of a transformer block: dense ∘ GELU ∘ dense applied per-token.
Concretely: MLP(z) = Wfc2 · gelu(Wfc1 · z + bfc1) + bfc2, applied row-wise. gf is the form
of the GELU — the tanh approximation or the exact x · Φ(x) (GeluForm) — and every
definition and theorem built on this one carries it and holds for both.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened transformerMlp — dense ∘ gelu ∘ dense per token,
all smooth at either GELU (GeluForm.scalar_differentiable is tagged for fun_prop).
HasVJPMat for the MLP sublayer — chain of two vjpMatComp
steps over per-token liftings (dense ∘ gelu ∘ dense). Every Diff
hypothesis is discharged by the per-token-flat helpers above.
Equations
- One or more equations did not get rendered due to their size.
Instances For
VJP of a pre-norm residual sublayer X ↦ X + F (L X) (norm L, then body F):
biPathMatHasVJP of the identity skip and the vjpMatComp chain L-back ∘
F-back. Every transformer sublayer (scalar- and vector-LN, attention and MLP) is
an instance.
Equations
- Proofs.preLNResHasVJPMat L F hL hF vL vF = Proofs.biPathMatHasVJP (fun (X : Proofs.Mat N D) => X) (F ∘ L) ⋯ ⋯ (Proofs.identityMatHasVJP N D) (Proofs.vjpMatComp L F hL hF vL vF)
Instances For
Attention sublayer: X ↦ X + MHSA(LN1(X)). Top-level composition;
the biPathMat skip-adds identity to the MHSA∘LN1 branch.
Equations
- One or more equations did not get rendered due to their size.
Instances For
MLP sublayer: h ↦ h + MLP(LN2(h)). Same biPathMat structure.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Transformer encoder block forward: MLP-sublayer ∘ attention-sublayer.
Signature matches the codegen: Mat N (heads·d_head) → Mat N (heads·d_head).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened attention sublayer.
biPathMat (id) (mhsa ∘ LN1) flattens to a sum, both arms Differentiable.
Attention sublayer VJP: preLNResHasVJPMat at L = LN1, F = mhsa.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened MLP sublayer.
biPathMat (id) (transformerMlp ∘ LN2) flattens to a sum, both arms Differentiable.
MLP sublayer VJP: preLNResHasVJPMat at L = LN2, F = transformerMlp.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened transformer block.
MlpSublayer ∘ AttnSublayer; both sublayers' flat Diff are theorems above.
Transformer block VJP — composition of attn + mlp sublayers:
a single vjpMatComp of the two sublayer witnesses with their Diff
helpers. Both LayerNorms are scalar-affine (γ1 β1 γ2 β2 : ℝ).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Stacking transformer blocks #
ViT-Tiny has 12 transformer blocks; ViT-Base has 12, ViT-Large has 24.
The stack is just k-fold composition of individual blocks. By
vjpMatComp and induction on k, if each block has a HasVJPMat
then so does the stack — for any depth.
The formal tower transformerTower uses a single shared parameter
tuple across all k blocks (Nat.rec over one tuple), with
scalar-affine LayerNorms; in practice every block has its own weights
and a per-feature LN. Per-block parameters are not formalized in this
file.
k-fold iterated transformer block, sharing parameters across all
k layers. Defined by Nat.rec so the HasVJPMat proof is a
straightforward induction on k.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened k-fold transformer tower.
Induction on k: zero case is identity, successor case is
block ∘ tower(k) composed via Differentiable.comp.
Transformer tower VJP — k-fold composition of one shared block
(transformerTower): induction on k via vjpMatComp and
transformerBlockHasVJPMat.
Equations
- One or more equations did not get rendered due to their size.
Instances For
ViT body: tower + final LN #
vitBody is the ViT backbone operating on a single (N, D) sequence,
after the patch embedding produced a Mat N D input and before the
classifier head slices the CLS token and runs dense+softmax CE.
patch_embed(X : Tensor3 ic h w) : Mat N D ← outside Mat-land
vitBody(M : Mat N D) : Mat N D ← the backbone (this file)
classifier(M) = dense(W_cls, b_cls, M[0]) ← Mat → Vec, then softmax CE loss
The backbone is finalLN ∘ transformerTower. Both sides are Mat N D,
so vjpMatComp glues the two VJPs.
The patch-embedding and classifier-head steps exit Mat-land (they
change type to/from Tensor3 and Vec respectively), so they don't fit
in the uniform HasVJPMat frame. They are bridged below (§ Bridging
ranks): the body is flattened with HasVJPMat.toHasVJP and composed with
patchEmbedFlatHasVJP and classifierFlatHasVJP in vitFullHasVJP.
ViT body — transformer tower followed by final per-token LayerNorm.
Composition is finalLN ∘ transformerTower; matches the codegen's
emitForwardBody ordering for a .transformerEncoder followed by
the implicit final LN block.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the flattened ViT body.
finalLN ∘ transformerTower — both have flat Diff theorems above.
The ViT body VJP — finalLN ∘ transformerTower: a single
vjpMatComp of the tower + final LN with their Diff helpers.
A depth-k weight-tied backbone (one parameter tuple for every block,
scalar-affine LayerNorms) has a correct VJP, composed entirely from
proved building blocks; mhsaHasVJPMat and
mhsaLayer_flat_differentiable are proved through the column-stacking
framework, so the chain uses no project axioms.
Equations
- One or more equations did not get rendered due to their size.
Instances For
What we've proved (and what's left) #
Proved (zero sorry's, machine-checked):
- Dense, ReLU (
MLP.lean) - Softmax cross-entropy loss gradient (
softmaxCE_grad,Softmax.lean) - Conv2d, MaxPool, Flatten (
CNN.lean) - BatchNorm closed-form backward (
BatchNorm.lean) - Residual / biPath fan-in (
Residual.lean) - Depthwise conv (
Depthwise.lean) - Squeeze-and-Excitation / elementwise product VJP (
SE.lean) - LayerNorm (
LayerNorm.lean); GELU (Activations.lean,GeluErf.lean,GeluForm.lean) - Standalone softmax VJP (
softmaxHasVJP,Softmax.lean) - Scaled dot-product attention backwards
sdpaBackQ/sdpaBackK/sdpaBackV(sdpaBackQ_correctviavjpMatCompcomposition of four matrix-level VJP building blocks: matmul, scalarScale, rowSoftmax, matmul). Formulas are also numerically gradient-checked incheck_jacobians.py. - Multi-head attention (
mhsaHasVJPMat), the transformer block (transformerBlockHasVJPMat), the weight-tied tower and body (transformerTowerHasVJPMat,vitBodyHasVJPMat) andvitFullHasVJP(this file).
Three calculus rules do all the structural work (theorems
proved from Mathlib's fderiv):
pdiv_comp (chain rule — functions compose, derivatives compose)
pdiv_add (linearity — derivatives of sums are sums of derivatives)
pdiv_mul (product rule — derivatives of elementwise products)
Five closed-form "Jacobian-structure tricks" handle the layers whose Jacobians are dense but exploitable:
- Diagonal (activations) — collapse the sum_j to one term.
- Sparse toeplitz (conv, depthwise) — reversed/transposed kernels.
- Binary selection (max-pool) — route gradients to argmax cells.
- Rank-1 correction to diagonal (softmax, BN, LN) — one extra scalar reduction, everything else is pointwise.
- Outer product + reductions (dense, matmul) — rank-1 update accumulation.
That is the complete taxonomy. I've thought hard about this and cannot find a sixth trick or a fourth calculus rule anywhere in the modern architecture zoo. Every paper, every block, every optimization is a rearrangement of these eight things.
What this means for the reader #
If you've read this far, you have a complete decoder for the architecture-of-the-month. Pick any paper — Swin, ConvNeXt, CLIP, Mamba, anything — and walk through the forward pass. For each operation, ask:
- Is it composition of known ops? -> chain rule.
- Is it a sum of branches? -> fan-in add.
- Is it an elementwise / scalar product of branches? -> fan-in mul.
- Is it an activation? -> diagonal Jacobian template.
- Is it a normalization? -> closed-form three-term formula.
- Is it a convolution or linear map? -> the structured-matmul machinery.
- Is it an attention or softmax-based selection? -> the closed-form rank-1 collapse.
If the answer is "none of the above" — which it won't be — then you've found the first genuinely new layer of the decade, and you get to write the next chapter of this book.
Until then, welcome to the end of the road.
Bridging ranks: from Mat-land back to Vec-land #
vitBodyHasVJPMat lives in HasVJPMat territory. The pieces at the
boundaries — patch embedding (image → tokens) and classifier head (tokens
→ logits) — change tensor rank. Rather than invent new mixed-rank VJP
frameworks, we flatten everything to Vec at the interfaces and compose
via plain HasVJP, glued by vjpComp.
Two ingredients needed:
HasVJPMat.toHasVJP(Tensor.lean) — bridges anyHasVJPMattoHasVJPon the flattened endpoints. One theorem, no new axioms.clsTokenFlatHasVJP— gathers row 0 of a flattenedMat (N+1) D. Derivable frompdiv_reindex.
CLS token extraction, stated on the flattened matrix. Row 0 of a
Mat (N+1) D is a Vec D; on the flattened Vec ((N+1)*D) this is
the gather v ↦ fun k => v (fPF (0, k)).
Equations
- Proofs.clsTokenFlat N D v k = v (finProdFinEquiv (0, k))
Instances For
CLS slice VJP — gather-style; backward scatters dy to row 0.
Derived from pdiv_reindex.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Classifier head: flattened CLS slice + dense projection to Vec nClasses.
fun v : Vec ((N+1)*D) => dense W_cls b_cls (clsTokenFlat v)
Equations
- Proofs.classifierFlat N D nClasses Wcls bcls = Proofs.dense Wcls bcls ∘ Proofs.clsTokenFlat N D
Instances For
Differentiability of clsTokenFlat — linear reindex.
Classifier head VJP — composition via vjpComp. clsTokenFlat
and dense are both linear, so their Diff hypotheses discharge by
fun_prop.
Equations
- Proofs.classifierFlatHasVJP N D nClasses Wcls bcls = Proofs.vjpComp (Proofs.clsTokenFlat N D) (Proofs.dense Wcls bcls) ⋯ ⋯ (Proofs.clsTokenFlatHasVJP N D) (Proofs.denseHasVJP Wcls bcls)
Instances For
Patch embedding #
The patch embedding takes a flattened image Vec (ic*H*W) and produces
a flattened Vec ((N+1)*D) interpreted as Mat (N+1) D:
- Conv projection with stride = patchSize: per-patch dense projection
W_conv : Kernel4 D ic patchSize patchSize+ biasb_conv : Vec D. - Reshape spatial
(D, H', W')to tokens(N, D)— pure permutation. - Prepend learnable CLS token at row 0 →
(N+1, D). - Add learnable positional embedding matrix →
(N+1, D).
The forward patchEmbedFlat is a concrete def; its VJP
patchEmbedFlatHasVJP and patchEmbedFlat_differentiable are proved
from foundation rules.
The N parameter is independent of (H, W, patchSize) — out-of-range
patches contribute zero (via a hpad guard on the image read), so the
API does not require N = (H/patchSize) * (W/patchSize).
Patch embedding forward on flattened endpoints.
Output at flat-index idx_out = finProdFinEquiv (n, d):
n = 0:cls_token d + pos_embed 0 d.n > 0(letp := n - 1,h' := p / (W/patchSize),w' := p % (W/patchSize)): `b_conv d + Σ c kh kw, W_conv d c kh kw * img(c, h'*P+kh, w'*P+kw)- pos_embed n d
, where the image read is guarded byh'*P+kh < H ∧ w'*P+kw < W` (returns 0 if out of range).
- pos_embed n d
This handles arbitrary N cleanly: for n whose decoded patch
(h', w') falls outside the image grid, the inner sum is identically
zero.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Patch embedding differentiability — proved from foundation rules.
patchEmbedFlat is linear in img plus constants
(pos_embed, cls_token, b_conv); the only non-trivial part is
the dependent if hpad : ... then img(σ hpad) else 0 pattern handled
by differentiableAt_pad_eval.
Closed-form input gradient for patchEmbedFlat — direct formula,
written as a sum over patches p : Fin N with reconstructed kernel
offsets (kh, kw) matching the input position (hh, ww) decoded from
idx_in. Equivalent (under the patch-row decomposition h' := p/(W/P),
w' := p%(W/P)) to the standard "deconvolution" formula for patchEmbed.
The CLS row (n = 0) does not appear here — idx_in only flows through
the conv-projection branch (n > 0), so the gradient sums over
p : Fin N (corresponding to output rows n = p+1).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Patch embedding VJP — proved from foundation rules.
The forward is affine in img (pdiv_of_affine): the pad-guarded conv read, identically
zero on the CLS row, plus the constant pos_embed + (cls_token | b_conv). Closing collapse
mirrors conv2dHasVJP3, with one new wrinkle: split Σ n : Fin (N+1) into
n = 0 (CLS row, contributes 0 to img-grad) + Σ p : Fin N (n = p+1)
via Fin.sum_univ_succ.
Backward: patchEmbedInputGradFormula W_conv dy.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The full ViT theorem #
Compose patch embed + ViT body (via the Mat→Vec bridge) + classifier.
All three are HasVJPs on Vec, so vjpComp chains them directly.
vitFull — a weight-tied ViT forward from flattened image pixels to logits:
all kBlocks transformer blocks share one parameter tuple, and every
LayerNorm (γ1 β1, γ2 β2, final γF βF) is scalar-affine.
Vec (ic*H*W) → Vec nClasses
Composition: patchEmbed → (flatten ∘ vitBody ∘ unflatten) → classifier.
Uses D := heads * d_head directly (no separate D parameter) so the
type-level reinterpretation at the body is a no-op.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Differentiability of the classifier head — composition of linear ops.
vitFull VJP — the VJP of the weight-tied vitFull: three
vjpComp steps glueing patchEmbedFlatHasVJP,
HasVJPMat.toHasVJP (vitBodyHasVJPMat ...), and
classifierFlatHasVJP. Each vjpComp's Diff hypotheses are
discharged by the per-stage Diff theorems above.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Public correctness theorems for the attention defs #
The HasVJP / HasVJPMat defs above bundle a backward function
with a .correct field; these _correct theorems expose that field
as a top-level proposition so consumers can refer to the contract
directly without reaching into record internals.
Public correctness theorem for mhsaHasVJPMat: multi-head
SDPA's backward equals the pdivMat-contracted Jacobian. The
column-stacking proof (mhsaGHasVJPMat, colSlabwiseHasVJPMat) uses no
project axiom.
Public correctness theorem for transformerBlockHasVJPMat:
the full transformer block backward (attention sublayer + MLP sublayer
glued by vjpMatComp) equals the pdivMat-contracted Jacobian.
Public correctness theorem for vitFullHasVJP: the backward of
vitFull (weight-tied blocks, scalar-affine LayerNorms) equals the pdiv-contracted Jacobian (Jacobian-transpose applied to
the cotangent). Exposes the witness's .correct field as a top-level
proposition so consumers (and #print axioms audits) can cite the apex
contract directly instead of reaching into the record. The long signature is
vitFull's parameter set; the proof is the witness field.