Verified Deep Learning with Lean 4

9 Vision Transformer

Where Part 1 has been heading

Every previous chapter has introduced a new architectural primitive and proved its backward pass. Each one slotted into the VerifiedNetSpec type and earned its own hasVJP theorem, and that includes convolution, BatchNorm, residual, depthwise, SE, LayerNorm and GELU. This chapter does the same for attention, but it also does something larger: it composes every layer the framework has proved so far into one machine-checked backward pass over a complete state-of-the-art image classifier.

The destination is Theorem 138 at the end of the chapter. It says: the whole ViT-Tiny’s hand-written input-gradient chain — through the patch embedding, twelve transformer blocks with their own parameters, final LayerNorm and head, each slot the certified backward of its op at its own saved activation — equals the network’s Jacobian transpose at every input, assuming only \(\varepsilon {\gt} 0\) at the LayerNorms. The proof composes everything this chapter introduces, plus most of what the previous chapters introduced, into a tower of vjpMatComp applications with vjpComp at the joins. Read the proof’s dependency list and you see a tour of Part 1: dense, residual, layer norm, GELU, softmax, matrix multiply, plus the sixteen matrix-machinery lemmas this chapter sets up. None of them is new in its own right. What’s new is how high the composition rule can be pushed.

9.1 Run it first

Before any of the attention mathematics, train the network that needs it. Four commands, and the last network in Part 1 is also the quickest one in it:

lake exe cache get                         # Mathlib oleans, ~30 s
./scripts/datasets/download_imagenette.sh  # 330 MB dl, 2.4 GB unpacked
lake build vit-verified-adam
./.lake/build/bin/vit-verified-adam data

On one RTX 4060 Ti (CUDA 12.9), from runs/2026-08-12-vit-imagenette-xla-cuda/. XLA’s startup banner is removed, the network description line is wrapped, the per-step lines are dropped, and the middle of the run is elided:

[pjrt_ffi] XLA backend: PJRT 0.112, 1 device(s)
[pjrt_ffi] compiled verified_mlir/vit_adam_train_step.mlir
             (@vit_adam_train_step, 603 outputs, 1 replica)
             in 6104 ms
[pjrt_ffi] compiled verified_mlir/vit_fwd.mlir
             (@vit_fwd, 1 outputs, 1 replica) in 1644 ms
ViT-Tiny on Imagenette 224² (patch-16 → CLS+pos → 12 transformer
  blocks @ dim192/3heads/MLP768 → final LN → CLS-head 10)
  via the VERIFIED renderer → XLA/PJRT → GPU
  train 9469, val 3925; bs 32, ViT-Tiny adam
    (cosine+warmup 5ep, baseLR 0.000300), He init
[pjrt_ffi] RESIDENT: @vit_adam_train_step holds 600 parameter
             tensors (63.2 MB) on 1 device; they stop crossing PCIe
             from here
Epoch 1/80: loss=2.127541 lr=0.000060
  epoch 1: val_acc = 1381/3925 = 35.184713%  top5 = 3121/3925 = 79.515924%
Epoch 2/80: loss=1.869707 lr=0.000120
  epoch 2: val_acc = 1619/3925 = 41.248408%  top5 = 3350/3925 = 85.350318%
Epoch 3/80: loss=1.750333 lr=0.000180
  epoch 3: val_acc = 1591/3925 = 40.535032%  top5 = 3362/3925 = 85.656051%
Epoch 30/80: loss=1.004060 lr=0.000225
  epoch 30: val_acc = 2571/3925 = 65.503185%  top5 = 3698/3925 = 94.216561%
Epoch 79/80: loss=0.515797 lr=0.000000
  epoch 79: val_acc = 2698/3925 = 68.738854%  top5 = 3551/3925 = 90.471338%
Epoch 80/80: loss=0.515159 lr=0.000000
  epoch 80: val_acc = 2698/3925 = 68.738854%  top5 = 3549/3925 = 90.420382%
done (trained ViT-Tiny adam + cosine/warmup via packed threading).

Eighty epochs at about 18 seconds each, twenty-four minutes in total, and 68.74% top-1 with 90.42% top-5 on Imagenette’s 3,925-image validation split. The best epoch reached \(68.92\% \). That is the fastest network in this book’s Imagenette column by a wide margin and also the least accurate one in it (§9.4).

Two compile lines, and the second one is the whole eval story. Like ConvNeXt, and unlike the three chapters whose BatchNorm keeps running statistics, ViT emits no vit_fwd_eval. LayerNorm computes its statistics from the activation in front of it every time, so there is nothing to freeze and the training forward and the evaluation forward are the same function.

The top-5 column goes the wrong way, and that is the chapter’s result in miniature. Top-1 climbs the whole run, 35.2 to 68.7. Top-5 climbs to \(94.88\% \) at epoch 25 and then falls, finishing 4.5 points down at \(90.42\% \). On a ten-class problem top-5 is a weak question, so what that divergence measures is a network growing more confident about its top choice while its ranking of the remaining classes decays. A transformer with 5.5M parameters and 9,469 training images has enough capacity to memorize its way to a better top-1 and not enough signal to keep the tail honest, which is §9.4’s data-hunger argument showing up inside a single run rather than across the comparison table.

Attention is where you would expect this to break. What ran is pretty(provenGraph), the file verified_mlir/vit_adam_train_step.mlir, and every softmax in it backpropagates by Theorem 116, every scaled dot-product attention by Theorems 118–120, every LayerNorm by Theorem 126, every GELU by Theorem 97, and every residual skip by Theorem 104.

And the fold holds at every depth. vitForwardKVHasVJP chains the patch embedding, \(k\) transformer blocks with their own parameters, the final LayerNorm and the head through vjpComp for every \(k\), and vitTinyHasVJP_correct says the hand-written chain vitInputGradK is that fold’s Jacobian transpose at this network’s shape — twelve blocks, \(224/16\), any number of classes. Its only hypothesis is \(\varepsilon {\gt} 0\) at the LayerNorms. It is a single global HasVJP over the entire network for the same reason Chapter 8’s is — softmax, GELU and LayerNorm are all smooth — so ViT gets the strong sentence too: every operation in the graph that just trained carries a proved backward, and their composition into the whole network carries one too.

Attention in one equation

A transformer encoder block does two things to its input sequence \(X \in \mathbb {R}^{N \times d}\) (think of \(N\) image patches, each represented as a \(d\)-vector):

  1. Multi-head self-attention (MHSA): each output row is a weighted average of all input rows, where the weights are computed from the input itself.

  2. Per-token MLP: a small two-layer MLP applied independently to every row.

Both halves are wrapped in a LayerNorm + residual sandwich (exactly the same template as ConvNeXt’s blocks from Chapter 8, and the layout that transferred to convnets in 2022 actually came from transformers in 2017).

The interesting half is the first one. Scaled dot-product attention computes, for queries \(Q\), keys \(K\), and values \(V\) (all linear projections of \(X\)):

\[ \mathrm{Attention}(Q, K, V) \; =\; \mathrm{softmax}\! \left(\frac{Q K^\top }{\sqrt{d_h}}\right) V. \]

In words: every row of \(Q\) is compared to every row of \(K\) via inner product (the \(QK^\top \) matrix is \(N \times N\), one entry per query-key pair). Each row of that \(N \times N\) matrix is passed through softmax to get a probability distribution over the keys (which positions to attend to). Those probabilities then weight the rows of \(V\) to produce the output. The \(1 / \sqrt{d_h}\) scaling keeps the softmax from saturating at large \(d_h\). Multi-head attention just runs several copies of this in parallel on lower-dimensional projections, concatenating the outputs.

Each token, meaning each row of \(X\), ends up looking at every other token. That’s the qualitative break from CNNs: convolutions look only at a local neighborhood, attention looks everywhere. The receptive field is global from the first layer.

Why this chapter has thirty-seven theorems

The math of attention is matrix algebra. Chapter 1’s foundation rules (chain, sum, product, identity) were stated and proved for vector functions. Before we can prove backward passes through \(Q K^\top \) and the row-wise softmax, we need the matrix-level lifts of those rules: chain rule on matrices, additive fan-in on matrices, identity on matrices, plus matrix-specific moves like transpose, matmul-with-one-factor-fixed, and scalar scale. That’s the first section of theorems below (Matrix-level machinery, sixteen theorems). Every one of them is a mechanical lift of a vector-level theorem from Chapter 1, proved via the row-projection \(\mathrm{ContinuousLinearMap}\) and the existing chain rule. Pay the cost once, reuse forever.

The second section (Attention proofs, twenty-one theorems) applies the matrix machinery to the actual attention math. Row-wise softmax (a per-row softmax applied to the \(N \times N\) score matrix) has the well-known closed-form Jacobian \(p_j(\delta _{ij} - p_i)\), and we prove the row-wise lifting that says this holds across matrices. Scaled dot-product attention’s backward decomposes into three matmul-backward applications (one for each of \(Q\), \(K\), \(V\)) plus the row-wise softmax backward, and we prove each. Then multi-head, then the attention sublayer (with residual), then the MLP sublayer (with residual), then a single transformer block, then a \(k\)-deep transformer tower, then the ViT body, then the whole network at its real shape. Each step is one or two compositions of the previous step. The chain that started at vjpComp (Theorem 10) reaches all the way to a 5.5M-parameter image classifier here.

What’s actually proved

That’s the framework-side payoff of this chapter: every architectural piece used by any Part-1 network has a machine-checked backward. With Theorem 138 for the input gradient and Theorem 141 for the parameter gradients the trainer steps on, the sentence “the gradient computed by this trainer is the mathematically correct one” is no longer a hope or a folklore result. It is a theorem the Lean kernel verifies on every build.

The empirical-side payoff comes after the theorems, in the Example section: ViT-Tiny trained on Imagenette underperforms the ConvNets, fairly dramatically, because 9,469 training images is too small for a transformer’s lack of inductive bias. That’s informative on its own terms: the data-hungry-transformer story made concrete.

9.2 Matrix-level machinery

\begin{tikzpicture} [x=1pt,y=1pt, every node/.style={inner sep=1.50pt, font=\fontsize{7.00}{8.40}\selectfont\ttfamily}]
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=81.6pt, minimum height=21.5pt, double] at (40.8pt,10.7pt) {\hyperref[def:hasvjpmat]{hasvjpmat}\\{\color{gray!60!black}\fontsize{5.50}{6.60}\selectfont\ttfamily $\to$ 8 boxes here}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=73.2pt, minimum height=12.7pt] at (40.8pt,41.7pt) {\hyperref[ax:pdivMat_rowIndep]{pdivMat\_rowIndep}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=48.0pt, minimum height=12.7pt] at (40.8pt,68.7pt) {\hyperref[thm:vjpMatComp]{vjpMatComp}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=69.0pt, minimum height=12.7pt] at (40.8pt,95.7pt) {\hyperref[thm:biPathMatHasVJP]{biPathMatHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=77.4pt, minimum height=12.7pt] at (162.8pt,125.7pt) {\hyperref[thm:identityMatHasVJP]{identityMatHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=85.8pt, minimum height=12.7pt] at (162.8pt,98.7pt) {\hyperref[thm:pdivMat_scalarScale]{pdivMat\_scalarScale}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=77.4pt, minimum height=12.7pt] at (40.8pt,153.7pt) {\hyperref[thm:pdivMat_transpose]{pdivMat\_transpose}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=69.0pt, minimum height=21.5pt] at (40.8pt,184.7pt) {\hyperref[thm:pdivMat_matmul_left_const]{pdivMat\_matmul\_}\\\hyperref[thm:pdivMat_matmul_left_const]{left\_const}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=69.0pt, minimum height=21.5pt] at (40.8pt,220.7pt) {\hyperref[thm:pdivMat_matmul_right_const]{pdivMat\_matmul\_}\\\hyperref[thm:pdivMat_matmul_right_const]{right\_const}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=94.2pt, minimum height=12.7pt] at (162.8pt,184.7pt) {\hyperref[thm:matmulLeftConstHasVJP]{matmulLeftConstHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=98.4pt, minimum height=12.7pt] at (162.8pt,220.7pt) {\hyperref[thm:matmulRightConstHasVJP]{matmulRightConstHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=77.4pt, minimum height=12.7pt] at (282.7pt,98.7pt) {\hyperref[thm:scalarScaleHasVJP]{scalarScaleHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=69.0pt, minimum height=12.7pt] at (162.8pt,153.7pt) {\hyperref[thm:transposeHasVJP]{transposeHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=73.2pt, minimum height=12.7pt, double] at (162.8pt,41.7pt) {\hyperref[thm:rowwiseHasVJPMat]{rowwiseHasVJPMat}};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=29.1pt, minimum height=17.8pt, text=gray!60!black, align=center, font=\fontsize{5.50}{6.60}\selectfont\ttfamily] at (40.8pt,124.7pt) {\hyperref[ax:pdiv_id]{pdiv\_id}\\(ch 1)};
  \draw[->, >=stealth, black!70, line width=0.40pt] (77.8pt,41.7pt) .. controls (89.3pt,41.7pt) and (102.2pt,41.7pt) .. (114.4pt,41.7pt) -- (125.8pt,41.7pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (206.0pt,98.7pt) .. controls (214.6pt,98.7pt) and (223.6pt,98.7pt) .. (232.4pt,98.7pt) -- (243.7pt,98.7pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (79.8pt,153.7pt) .. controls (91.5pt,153.7pt) and (104.6pt,153.7pt) .. (116.8pt,153.7pt) -- (128.1pt,153.7pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (75.5pt,184.7pt) .. controls (84.4pt,184.7pt) and (94.2pt,184.7pt) .. (104.0pt,184.7pt) -- (115.3pt,184.7pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (75.5pt,220.7pt) .. controls (83.8pt,220.7pt) and (92.9pt,220.7pt) .. (102.1pt,220.7pt) -- (113.5pt,220.7pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.40pt] (55.6pt,124.9pt) .. controls (69.4pt,125.0pt) and (91.6pt,125.2pt) .. (112.2pt,125.3pt) -- (123.7pt,125.4pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.40pt] (55.6pt,121.8pt) .. controls (71.0pt,118.4pt) and (97.0pt,112.8pt) .. (119.3pt,108.0pt) -- (130.3pt,105.6pt);
\end{tikzpicture}

Attention is fundamentally matrix algebra: queries times keys transposed (\(QK^T\)), softmax over rows of the resulting matrix, matmul against values (\(\cdot V\)). Before we can prove backward passes through these operations, we need the matrix-level extensions of Ch 1’s foundation rules. Each theorem below is the matrix lift of a vector-level theorem from Ch 1, proved by composing the vector version with the row-projection \(\mathrm{ContinuousLinearMap}\). The Lean proofs all live in Proofs/Foundation/Tensor.lean alongside their vector counterparts. We collect them here, where the attention proofs need all of them at once.

Definition 101 Matrix VJP record
✓
#

The rank-2 analogue of Definition 9. Define \(\operatorname {pdivMat}f\, A\, (i,j)\, (k,l)\) as \(\operatorname {pdiv}\) of the row-major flattened map \(v \mapsto \mathrm{flatten}\bigl(f(\mathrm{unflatten}\, v)\bigr)\) with both index pairs encoded through the \((i, j) \leftrightarrow \text{flat}\) bijection. Then \(\mathsf{HasVJPMat}\, f\) bundles a backward function \(B\) with: for all \(A\), \(dY\), \((i, j)\),

\[ B(A, dY)_{ij} = \sum _{k, l} \operatorname {pdivMat}f\, A\, (i,j)\, (k,l) \cdot dY_{kl}. \]
Theorem 102 Row-independence for matrices
✓
#

assume:

  1. \(g : \mathbb {R}^{n} \to \mathbb {R}^{p}\) is differentiable everywhere [h_g_diff]

prove: applying \(g\) to each row of a matrix has block-diagonal Jacobian: \(\operatorname {pdivMat}(\text{rowwise } g)\, A\, (i,j)\, (k,l) = [i = k] \cdot \operatorname {pdiv}g\, (A_i)\, j\, l\).

Proof ▶
  1. Coordinate factoring: the \((k, l)\) output coordinate of the flattened rowwise map is \((y \mapsto g(y)_l) \circ \mathrm{rowProj}_k\), where \(\mathrm{rowProj}_k\) is the CLM extracting row \(k\) of the flat vector.
    proof: Unfold flatten/unflatten; \(\mathrm{rowProj}_k\) is a reindex, hence a CLM.

  2. Every output coordinate — hence the whole flat map — is differentiable, so \(\operatorname {fderiv}\) does not fall back to its junk default.
    proof: Assumption 1 composed with the CLM of step 1 (differentiableAt_pi).

  3. q.e.d.
    proof: Chain rule through the CLM: the derivative of \((y \mapsto g(y)_l) \circ \mathrm{rowProj}_k\) at \(\mathbf{e}_{(i,j)}\) sees \(\mathrm{rowProj}_k(\mathbf{e}_{(i,j)}) = [i = k]\, \mathbf{e}_j\), so by Definition 1 the entry is \([i = k] \cdot \operatorname {pdiv}g\, (A_i)\, j\, l\).

Theorem 103 Matrix-level chain rule
✓
#

assume:

  1. \(B_F\), \(B_G\) are correct backward functions (\(\mathsf{HasVJPMat}\, F\), \(\mathsf{HasVJPMat}\, G\)) [hF, hG]

  2. the flattened forms of \(F\) and \(G\) are differentiable everywhere [hF_diff, hG_diff]

prove: \(\mathsf{HasVJPMat}\, (G \circ F)\).

Proof ▶

Sketch: the VJP chain rule (Theorem 10) transcribed to rank-2 indices; every step is the same, with double sums where the rank-1 proof had single sums.

  1. Define \(B(A, dY) := B_F\bigl(A,\; B_G(F(A),\, dY)\bigr)\).

  2. suffices: \(B\) matches the \(\operatorname {pdivMat}\) contraction.
    proof: Definition 101.

  3. Chain rule for \(\operatorname {pdivMat}\): \(\operatorname {pdivMat}(G \circ F)\, A\, (i,j)\, (k,l) = \sum _{p,q} \operatorname {pdivMat}F\, A\, (i,j)\, (p,q) \cdot \operatorname {pdivMat}G\, (F A)\, (p,q)\, (k,l)\).
    proof: The flattened composite is the composite of the flattened maps (unflatten \(\circ \) flatten cancels), so the rank-1 chain rule (Theorem 2) applies at the flat level, applicable by assumption 2; re-index the flat middle sum to \((p, q)\) pairs.

  4. q.e.d.
    proof: Expand the two correct fields in 1, substitute 3, and swap the two double sums (pack indices into pairs, Finset.sum_comm, unpack); with 2 this is the goal — step for step the proof of Theorem 10.

Theorem 104 Matrix-level additive fan-in
✓

Rank-2 transcription of Theorem 11: given \(\mathsf{HasVJPMat}\) for \(F\) and \(G\) (with flat differentiability), \(\mathsf{HasVJPMat}\, (F + G)\) with backward \(B_F(A, dY) + B_G(A, dY)\).

Proof ▶
  1. suffices: the summed backward matches the \(\operatorname {pdivMat}\) contraction.
    proof: Definition 101.

  2. q.e.d.
    proof: Expand both correct fields, merge the double sums (Finset.sum_add_distrib twice); the sum rule for \(\operatorname {pdivMat}\) — Theorem 3 through the flatten bijection — rewrites \(\operatorname {pdivMat}F + \operatorname {pdivMat}G = \operatorname {pdivMat}(F + G)\).

Theorem 105 Matrix-level identity
✓

\(\mathsf{HasVJPMat}\, (\mathrm{id})\), backward \(B(A, dY) = dY\).

Proof ▶
  1. \(\operatorname {pdivMat}(\mathrm{id})\, A\, (i,j)\, (k,l) = [i = k \wedge j = l]\).
    proof: Flatten \(\circ \) unflatten is the identity on the flat vector, so the identity Jacobian (Theorem 5) applies; the flat-index equality decodes to \(i = k \wedge j = l\) by injectivity of the pairing.

  2. q.e.d.
    proof: Contract 1 with \(dY\): the two-dimensional Kronecker sum collapses (Finset.sum_eq_single twice) to \(dY_{ij}\); Definition 101 closes.

Theorem 106 Scalar-scale Jacobian
✓

prove: \(\operatorname {pdivMat}(M \mapsto s \cdot M)\, A\, (i,j)\, (k,l) = [i = k \wedge j = l] \cdot s\).

Proof ▶
  1. The flattened scalar-scale map reduces to \(v \mapsto s \cdot v\).
    proof: Flatten/unflatten round-trip, pointwise.

  2. \(\operatorname {pdiv}(v \mapsto s \cdot v) = s \cdot \delta \) at the flat indices.
    proof: Factor as \((\text{const } s) \cdot (\text{identity})\): product rule (Theorem 4), constant rule (Theorem 6), identity Jacobian (Theorem 5).

  3. q.e.d.
    proof: The flat-index equality decodes to \(i = k \wedge j = l\) by injectivity of the pairing.

Theorem 107 Transpose Jacobian
✓
#

prove: \(\operatorname {pdivMat}(\mathrm{transpose})\, A\, (i,j)\, (k,l) = [j = k \wedge i = l]\).

Proof ▶
  1. The flattened transpose is a pure gather: at output index \(\mathrm{idx}\), it reads \(v\) at the index with components swapped.
    proof: Unfold transpose/flatten/unflatten.

  2. q.e.d.
    proof: Reindex Jacobian (Theorem 7) with the swap map \(\sigma \): the indicator condition \(\mathrm{flat}(i,j) = \mathrm{flat}(l,k)\) decodes to \(j = k \wedge i = l\) by injectivity of the pairing.

Theorem 108 Matmul Jacobian, left factor fixed
✓
#

prove: \(\operatorname {pdivMat}(B' \mapsto C \cdot B')\, B\, (i,j)\, (k,l) = [l = j] \cdot C_{ki}\).

Proof ▶
  1. The flattened map at output index \(\mathrm{idx}\) is \(v \mapsto \sum _s C_{k(\mathrm{idx}), s} \cdot v_{\mathrm{flat}(s,\, l(\mathrm{idx}))}\) — a finite sum of (constant) \(\times \) (reindex) terms.
    proof: Unfold \(\mathrm{Mat.mul}\)/flatten/unflatten.

  2. Per-index Jacobian: finite-sum rule (Theorem 8) distributes over \(s\); product rule with the \(C\)-factor constant (Theorems 4, 6) and reindex Jacobian (Theorem 7) leave \(\sum _s C_{k, s} \cdot [\mathrm{flat}(i,j) = \mathrm{flat}(s, l)]\).

  3. q.e.d.
    proof: The indicator forces \(s = i\) and \(l = j\) (injectivity of the pairing), collapsing the sum to \([l = j] \cdot C_{ki}\).

Theorem 109 Matmul Jacobian, right factor fixed
✓
#

prove: \(\operatorname {pdivMat}(A' \mapsto A' \cdot D)\, A\, (i,j)\, (k,l) = [i = k] \cdot D_{jl}\).

Proof ▶

Mirror image of Theorem 108: the flattened map at output \(\mathrm{idx}\) is \(v \mapsto \sum _s v_{\mathrm{flat}(k(\mathrm{idx}),\, s)} \cdot D_{s,\, l(\mathrm{idx})}\), the same finite-sum-of-reindex-times-constant shape with the variable factor on the left; the same three steps (finite-sum, product + constant + reindex rules, indicator collapse at \(s = j\), \(k = i\)) give \([i = k] \cdot D_{jl}\).

Theorem 110 Matmul VJP, left factor fixed
✓

\(\mathsf{HasVJPMat}\, (B' \mapsto C \cdot B')\), backward \(dB = C^{T} \cdot dY\).

Proof ▶
  1. Define \(B(B', dY)_{ij} := \sum _k C_{ki}\, dY_{kj}\) (that is, \(C^T \cdot dY\)).

  2. suffices: \(B\) matches the \(\operatorname {pdivMat}\) contraction.
    proof: Definition 101.

  3. q.e.d.
    proof: Substitute the Jacobian (Theorem 108): \(\sum _{k,l} [l = j]\, C_{ki} \cdot dY_{kl}\) collapses at \(l = j\) (Finset.sum_ite_eq’) to \(\sum _k C_{ki}\, dY_{kj} = B\).

Theorem 111 Matmul VJP, right factor fixed
✓

\(\mathsf{HasVJPMat}\, (A' \mapsto A' \cdot D)\), backward \(dA = dY \cdot D^{T}\).

Proof ▶
  1. Define \(B(A', dY)_{ij} := \sum _l dY_{il}\, D_{jl}\) (that is, \(dY \cdot D^T\)).

  2. suffices: \(B\) matches the \(\operatorname {pdivMat}\) contraction.
    proof: Definition 101.

  3. q.e.d.
    proof: Substitute the Jacobian (Theorem 109): \(\sum _{k,l} [i = k]\, D_{jl} \cdot dY_{kl}\) collapses at \(k = i\) (Finset.sum_ite_eq) to \(\sum _l D_{jl}\, dY_{il} = B\).

Theorem 112 Scalar-scale VJP
✓

\(\mathsf{HasVJPMat}\, (M \mapsto s \cdot M)\), backward \(dA = s \cdot dY\).

Proof ▶

By Definition 101 it suffices to contract the Jacobian (Theorem 106) with \(dY\): \(\sum _{k,l} [i = k \wedge j = l]\, s \cdot dY_{kl}\) collapses at \((k, l) = (i, j)\) to \(s \cdot dY_{ij}\). q.e.d.

Theorem 113 Transpose VJP
✓

\(\mathsf{HasVJPMat}\, (\mathrm{transpose})\), backward \(dA = dY^{T}\).

Proof ▶

By Definition 101 it suffices to contract the Jacobian (Theorem 107) with \(dY\): \(\sum _{k,l} [j = k \wedge i = l] \cdot dY_{kl}\) collapses at \((k, l) = (j, i)\) to \(dY_{ji}\). q.e.d.

Theorem 114 Row-wise lifting of any \(\mathsf{HasVJP}\)
✓

assume:

  1. \(B_g\) is a correct backward for \(g : \mathbb {R}^{n} \to \mathbb {R}^{p}\) (\(\mathsf{HasVJP}\, g\)) [hg]

  2. \(g\) is differentiable everywhere [hg_diff]

prove: \(\mathsf{HasVJPMat}\) of the rowwise map on \(\mathbb {R}^{m \times n} \to \mathbb {R}^{m \times p}\), with backward applying \(B_g\) to each row: \(B(A, dY)_r = B_g(A_r, dY_r)\).

Proof ▶
  1. suffices: \(B\) matches the \(\operatorname {pdivMat}\) contraction.
    proof: Definition 101.

  2. The Jacobian is block-diagonal: \(\operatorname {pdivMat}= [i = k] \cdot \operatorname {pdiv}g\, (A_i)\, j\, l\).
    proof: Row-independence (Theorem 102), applicable by assumption 2.

  3. q.e.d.
    proof: Contract 2 with \(dY\): the row sum collapses at \(k = i\) (Finset.sum_ite_eq); what remains is precisely \(g\)’s own correctness equation at row \(i\) (assumption 1).

9.3 Attention proofs

\begin{tikzpicture} [x=1pt,y=1pt, every node/.style={inner sep=1.32pt, font=\fontsize{6.18}{7.42}\selectfont\ttfamily}]
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=49.8pt, minimum height=11.3pt] at (33.2pt,7.8pt) {\hyperref[ax:pdiv_softmax]{pdiv\_softmax}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=53.5pt, minimum height=11.3pt] at (138.1pt,14.9pt) {\hyperref[thm:softmaxHasVJP]{softmaxHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=75.7pt, minimum height=11.3pt] at (260.7pt,71.3pt) {\hyperref[thm:rowSoftmaxHasVJPMat]{rowSoftmaxHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=68.3pt, minimum height=11.3pt] at (399.9pt,83.7pt) {\hyperref[thm:sdpaBackQ_correct]{sdpaBackQ\_correct}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=68.3pt, minimum height=11.3pt] at (399.9pt,15.7pt) {\hyperref[thm:sdpaBackK_correct]{sdpaBackK\_correct}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=68.3pt, minimum height=11.3pt] at (138.1pt,45.7pt) {\hyperref[thm:sdpaBackV_correct]{sdpaBackV\_correct}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=60.9pt, minimum height=19.0pt] at (399.9pt,44.0pt) {\hyperref[ax:mhsaLayer_flat_differentiable]{mhsaLayer\_flat\_}\\\hyperref[ax:mhsaLayer_flat_differentiable]{differentiable}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=79.4pt, minimum height=11.3pt] at (138.1pt,119.0pt) {\hyperref[ax:patchEmbedFlatHasVJP]{patchEmbedFlatHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=60.9pt, minimum height=19.0pt] at (33.2pt,153.4pt) {\hyperref[ax:patchEmbedFlat_differentiable]{patchEmbedFlat\_}\\\hyperref[ax:patchEmbedFlat_differentiable]{differentiable}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=101.7pt, minimum height=11.3pt] at (260.7pt,174.6pt) {\hyperref[thm:layerNormPerTokenHasVJPMat]{layerNormPerTokenHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=72.0pt, minimum height=11.3pt] at (260.7pt,150.8pt) {\hyperref[thm:layerNormVecHasVJP]{layerNormVecHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=86.9pt, minimum height=11.3pt] at (138.1pt,216.1pt) {\hyperref[thm:densePerTokenHasVJPMat]{densePerTokenHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=83.2pt, minimum height=11.3pt] at (138.1pt,192.3pt) {\hyperref[thm:geluPerTokenHasVJPMat]{geluPerTokenHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=64.6pt, minimum height=19.0pt] at (260.7pt,44.0pt) {\hyperref[ax:rowSoftmax_flat_differentiable]{rowSoftmax\_flat\_}\\\hyperref[ax:rowSoftmax_flat_differentiable]{differentiable}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=90.6pt, minimum height=11.3pt] at (260.7pt,198.5pt) {\hyperref[thm:transformerMlpHasVJPMat]{transformerMlpHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=120.2pt, minimum height=11.3pt] at (399.9pt,186.1pt) {\hyperref[thm:transformerMlpSublayerHasVJPMat]{transformerMlpSublayerHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=64.6pt, minimum height=19.0pt] at (138.1pt,91.7pt) {\hyperref[thm:vitFwdGraphKMHV_faithful]{vitFwdGraphKMHV\_}\\\hyperref[thm:vitFwdGraphKMHV_faithful]{faithful}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=3.5pt, align=center, minimum width=49.8pt, minimum height=11.3pt] at (138.1pt,240.0pt) {\hyperref[thm:vit_step_tie]{vit\_step\_tie}};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=49.0pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (33.2pt,240.0pt) {\hyperref[ax:geluScalarDeriv]{geluScalarDeriv}\\(ch 8)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=34.4pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (33.2pt,192.3pt) {\hyperref[thm:geluHasVJP]{geluHasVJP}\\(ch 8)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=49.0pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (138.1pt,156.1pt) {\hyperref[thm:layerNormHasVJP]{layerNormHasVJP}\\(ch 8)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=66.5pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (33.2pt,45.7pt) {\hyperref[thm:matmulLeftConstHasVJP]{matmulLeftConstHasVJP}\\(above)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=69.4pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (260.7pt,125.2pt) {\hyperref[thm:matmulRightConstHasVJP]{matmulRightConstHasVJP}\\(above)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=54.8pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (260.7pt,97.0pt) {\hyperref[thm:scalarScaleHasVJP]{scalarScaleHasVJP}\\(above)};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=49.0pt, minimum height=15.7pt, text=gray!60!black, align=center, font=\fontsize{4.86}{5.83}\selectfont\ttfamily] at (33.2pt,104.9pt) {3 statements of\\ch 1, 8};
  \draw[->, >=stealth, black!70, line width=0.35pt] (58.6pt,9.5pt) .. controls (71.3pt,10.3pt) and (87.2pt,11.4pt) .. (101.3pt,12.4pt) -- (111.2pt,13.1pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (58.6pt,5.6pt) .. controls (69.6pt,4.7pt) and (82.8pt,3.8pt) .. (94.7pt,3.4pt) .. controls (187.5pt,0.0pt) and (296.3pt,7.1pt) .. (355.4pt,11.9pt) -- (365.5pt,12.7pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (155.0pt,20.8pt) .. controls (163.3pt,24.2pt) and (173.3pt,28.8pt) .. (181.6pt,34.3pt) .. controls (195.7pt,43.6pt) and (194.8pt,52.0pt) .. (209.8pt,59.9pt) .. controls (211.3pt,60.6pt) and (212.8pt,61.4pt) .. (214.4pt,62.0pt) -- (223.5pt,65.3pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (165.2pt,14.9pt) .. controls (209.8pt,15.1pt) and (301.2pt,15.4pt) .. (355.6pt,15.6pt) -- (365.6pt,15.6pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (298.7pt,74.7pt) .. controls (316.2pt,76.3pt) and (337.1pt,78.1pt) .. (355.2pt,79.8pt) -- (365.3pt,80.7pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (298.9pt,65.5pt) .. controls (303.3pt,64.0pt) and (307.6pt,62.2pt) .. (311.5pt,59.9pt) .. controls (327.8pt,50.2pt) and (323.4pt,37.6pt) .. (339.8pt,28.1pt) .. controls (344.7pt,25.2pt) and (350.2pt,23.0pt) .. (355.9pt,21.3pt) -- (365.4pt,18.9pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (312.0pt,178.8pt) .. controls (317.7pt,179.3pt) and (323.6pt,179.8pt) .. (329.6pt,180.3pt) -- (339.5pt,181.1pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (180.3pt,210.1pt) .. controls (189.3pt,208.8pt) and (199.0pt,207.4pt) .. (208.3pt,206.0pt) -- (218.3pt,204.5pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (179.9pt,194.4pt) .. controls (188.1pt,194.8pt) and (196.8pt,195.2pt) .. (205.3pt,195.7pt) -- (215.2pt,196.2pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (293.1pt,53.1pt) .. controls (315.8pt,59.7pt) and (346.2pt,68.4pt) .. (368.6pt,74.9pt) -- (378.3pt,77.7pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (293.1pt,37.5pt) .. controls (312.8pt,33.4pt) and (338.5pt,28.2pt) .. (359.6pt,23.8pt) -- (369.5pt,21.8pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (293.1pt,44.0pt) .. controls (312.7pt,44.0pt) and (338.1pt,44.0pt) .. (359.1pt,44.0pt) -- (369.0pt,44.0pt);
  \draw[->, >=stealth, black!70, line width=0.35pt] (306.2pt,194.5pt) .. controls (313.6pt,193.8pt) and (321.5pt,193.1pt) .. (329.4pt,192.4pt) -- (339.4pt,191.5pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (58.0pt,240.0pt) .. controls (71.5pt,240.0pt) and (88.5pt,240.0pt) .. (103.3pt,240.0pt) -- (113.1pt,240.0pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (50.7pt,192.3pt) .. controls (60.5pt,192.3pt) and (73.5pt,192.3pt) .. (86.4pt,192.3pt) -- (96.2pt,192.3pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (162.9pt,159.8pt) .. controls (176.4pt,161.8pt) and (193.9pt,164.5pt) .. (210.3pt,167.0pt) -- (220.3pt,168.6pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (162.9pt,155.0pt) .. controls (177.5pt,154.4pt) and (196.8pt,153.6pt) .. (214.3pt,152.8pt) -- (224.3pt,152.4pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (66.8pt,45.7pt) .. controls (75.3pt,45.7pt) and (84.7pt,45.7pt) .. (93.8pt,45.7pt) -- (103.8pt,45.7pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (291.2pt,117.0pt) .. controls (297.9pt,115.1pt) and (305.0pt,113.0pt) .. (311.5pt,111.1pt) .. controls (331.3pt,105.2pt) and (353.5pt,98.2pt) .. (370.6pt,92.8pt) -- (380.3pt,89.7pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (288.4pt,94.4pt) .. controls (307.4pt,92.5pt) and (333.3pt,90.0pt) .. (355.3pt,87.9pt) -- (365.4pt,86.9pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (58.0pt,108.2pt) .. controls (67.1pt,109.4pt) and (77.7pt,110.9pt) .. (88.2pt,112.3pt) -- (98.1pt,113.7pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.35pt] (58.0pt,101.8pt) .. controls (69.2pt,100.4pt) and (82.9pt,98.6pt) .. (95.6pt,97.0pt) -- (105.7pt,95.7pt);
\end{tikzpicture}
\begin{tikzpicture} [x=1pt,y=1pt, every node/.style={inner sep=1.50pt, font=\fontsize{7.00}{8.40}\selectfont\ttfamily}]
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=60.6pt, minimum height=12.7pt] at (230.2pt,194.1pt) {\hyperref[ax:mhsaHasVJPMat]{mhsaHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=140.4pt, minimum height=12.7pt] at (70.2pt,149.4pt) {\hyperref[thm:transformerAttnSublayerHasVJPMat]{transformerAttnSublayerHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=111.0pt, minimum height=12.7pt] at (57.2pt,104.6pt) {\hyperref[thm:transformerBlockHasVJPMat]{transformerBlockHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=111.0pt, minimum height=12.7pt] at (57.2pt,55.5pt) {\hyperref[thm:transformerTowerHasVJPMat]{transformerTowerHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=73.2pt, minimum height=12.7pt] at (57.2pt,6.4pt) {\hyperref[thm:vitBodyHasVJPMat]{vitBodyHasVJPMat}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=81.6pt, minimum height=12.7pt] at (167.2pt,104.6pt) {\hyperref[thm:vitForwardKVHasVJP]{vitForwardKVHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=64.8pt, minimum height=21.5pt] at (198.2pt,55.5pt) {\hyperref[thm:vitTinyHasVJP_correct]{vitTinyHasVJP\_}\\\hyperref[thm:vitTinyHasVJP_correct]{correct}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=64.8pt, minimum height=12.7pt] at (284.2pt,104.6pt) {\hyperref[thm:vit_whole_back]{vit\_whole\_back}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=4.0pt, align=center, minimum width=60.6pt, minimum height=12.7pt] at (324.2pt,149.4pt) {\hyperref[thm:vit_loss_grad]{vit\_loss\_grad}};
  \draw[->, >=stealth, black!70, line width=0.40pt] (208.3pt,187.3pt) .. controls (181.1pt,180.0pt) and (134.4pt,167.5pt) .. (102.9pt,159.1pt) -- (92.1pt,156.2pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (226.1pt,187.4pt) .. controls (216.4pt,173.9pt) and (191.9pt,139.9pt) .. (177.7pt,120.3pt) -- (171.1pt,111.0pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (229.8pt,187.5pt) .. controls (228.5pt,172.6pt) and (224.4pt,131.3pt) .. (215.2pt,98.2pt) .. controls (213.2pt,91.1pt) and (210.4pt,83.5pt) .. (207.6pt,76.8pt) -- (203.1pt,66.6pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (233.7pt,187.4pt) .. controls (242.0pt,174.0pt) and (262.6pt,140.5pt) .. (274.8pt,120.8pt) -- (280.9pt,111.0pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (242.9pt,187.4pt) .. controls (257.7pt,180.6pt) and (282.6pt,169.3pt) .. (301.0pt,160.9pt) -- (311.4pt,156.2pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (68.5pt,142.8pt) .. controls (66.9pt,137.6pt) and (64.5pt,129.5pt) .. (62.2pt,122.2pt) -- (58.9pt,111.3pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (57.2pt,97.8pt) .. controls (57.2pt,91.8pt) and (57.2pt,82.1pt) .. (57.2pt,73.7pt) -- (57.2pt,62.3pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (57.2pt,48.7pt) .. controls (57.2pt,42.7pt) and (57.2pt,33.0pt) .. (57.2pt,24.5pt) -- (57.2pt,13.2pt);
  \draw[->, >=stealth, black!70, line width=0.40pt] (171.0pt,97.8pt) .. controls (174.6pt,92.4pt) and (180.2pt,83.9pt) .. (185.4pt,76.0pt) -- (191.5pt,66.6pt);
\end{tikzpicture}
Theorem 115 Softmax Jacobian
✓
#

Writing \(p := \mathrm{softmax}(z)\): prove: \(\operatorname {pdiv}(\mathrm{softmax})\, z\, i\, j = p_j\, (\delta _{ij} - p_i)\).

Proof ▶
  1. \(\operatorname {pdiv}(\mathrm{softmax})\, z\, i\, j = \operatorname {fderiv}_{\mathbb {R}}\, \bigl(z' \mapsto e^{z'_j} \cdot (\textstyle \sum _k e^{z'_k})^{-1}\bigr)\, z\, (\mathbf{e}_i)\).
    proof: Definition 1; extract the \(j\)-th coordinate (fderiv_apply), differentiable because the denominator \(S := \sum _k e^{z_k}\) is a sum of positive exponentials, hence nonzero.

  2. Differentiate the product: numerator via HasFDerivAt.exp, denominator via (hasDerivAt_inv).comp_hasFDerivAt (licensed by \(S {\gt} 0\)).

  3. Evaluate at \(\mathbf{e}_i\): the numerator’s derivative contributes \(e^{z_j}\, \delta _{ji}\), and the denominator’s contributes \(-e^{z_j}\, S^{-2} \cdot e^{z_i}\), the inner sum collapsing by \(\sum _k e^{z_k}\, \delta _{ki} = e^{z_i}\).

  4. q.e.d.
    proof: Combine 2–3 and rewrite in terms of \(p = e^{z}/S\): \(S^{-1} e^{z_j} \delta _{ij} - e^{z_j} S^{-2} e^{z_i} = p_j(\delta _{ij} - p_i)\) (field_simp, ring).

Theorem 116 Standalone softmax VJP
✓

\(\mathsf{HasVJP}\, (\mathrm{softmax})\), with the closed-form \(O(c)\) backward

\[ B(z, dy)_i = p_i\, \bigl(dy_i - \langle p, dy \rangle \bigr), \]

where \(\langle p, dy \rangle = \sum _j p_j\, dy_j\) is one precomputed scalar — the rank-1 structure of the Jacobian is what turns the naive \(O(c^2)\) contraction into a reduction plus a broadcast, the same optimization pattern as BN and max-pool.

Proof ▶
  1. suffices: \(B(z, dy)_i = \sum _j \operatorname {pdiv}(\mathrm{softmax})\, z\, i\, j \cdot dy_j\).
    proof: Definition 9.

  2. \(\sum _j p_j (\delta _{ij} - p_i)\, dy_j = p_i\, dy_i - p_i \sum _j p_j\, dy_j\).
    proof: Substitute the softmax Jacobian (Theorem 115); split the sum, collapse the \(\delta \)-term (Finset.sum_ite), factor \(p_i\) out of the second (Finset.mul_sum).

  3. q.e.d.
    proof: The right side of 2 is \(p_i(dy_i - \langle p, dy\rangle ) = B(z, dy)_i\); with 1, done.

Theorem 117 Row-wise softmax VJP on a matrix
✓

\(\mathsf{HasVJPMat}\, (\mathrm{rowSoftmax})\): rows are independent, so the backward applies the softmax backward per row.

Proof ▶

The rowwise-lifting argument (Theorem 114) instantiated at \(g = \mathrm{softmax}\):

  1. The Jacobian is block-diagonal with the standalone softmax Jacobian in each block.
    proof: Row-independence (Theorem 102); softmax is differentiable (positive denominator).

  2. q.e.d.
    proof: Contract with \(dY\), collapse the row sum at \(k = i\); what remains is Theorem 116’s correctness equation at row \(i\); Definition 101 closes.

Theorem 118 SDPA backward wrt Q
✓

prove: for fixed \(K, V\): \(\mathrm{sdpaBackQ}\) matches the \(\operatorname {pdivMat}\) contraction of \(Q' \mapsto \mathrm{sdpa}(Q', K, V)\).

Proof ▶
  1. The forward is a four-link chain:

    \[ Q' \; \mapsto \; Q' K^T \; \mapsto \; \tfrac {1}{\sqrt{d}} \cdot {\mathord {@}} \; \mapsto \; \mathrm{rowSoftmax}\, {\mathord {@}} \; \mapsto \; {\mathord {@}} \cdot V. \]


    proof: Definitional (sdpaQChain_eq).

  2. Each link has a proved \(\mathsf{HasVJPMat}\): matmul-right-const at \(K^T\) (Theorem 111), scalar-scale (Theorem 112), row-wise softmax (Theorem 117), matmul-right-const at \(V\); glue with the matrix chain rule (Theorem 103) three times.
    proof: The flat-differentiability side conditions: the first two links are linear (fun_prop) and rowSoftmax is smooth (Theorem 129); compositions inherit.

  3. q.e.d.
    proof: The chain’s composed backward literally computes \(\mathrm{sdpaBackQ}\)’s nested formula (pure unfolding), so the chain’s correct field is the goal.

prove: for fixed \(Q, V\): \(\mathrm{sdpaBackK}\) matches the \(\operatorname {pdivMat}\) contraction of \(K' \mapsto \mathrm{sdpa}(Q, K', V)\).

Proof ▶

Same shape as Theorem 118, with a leading transpose link:

  1. The forward chain is \(K' \mapsto K'^T \mapsto Q \cdot {\mathord {@}} \mapsto \tfrac {1}{\sqrt{d}} \cdot {\mathord {@}} \mapsto \mathrm{rowSoftmax}\, {\mathord {@}} \mapsto {\mathord {@}} \cdot V\), the second link now matmul-left-const (Theorems 113, 110).

  2. Glue the five links with Theorem 103 (four applications), differentiabilities as before.

  3. q.e.d.
    proof: The chain’s backward computes \(\sum _k Q_{kj}\, d\mathrm{Scores}_{ki}\) while \(\mathrm{sdpaBackK} = (d\mathrm{Scores})^T Q\) expands to \(\sum _k d\mathrm{Scores}_{ki}\, Q_{kj}\) — equal by mul_comm at the summand.

Theorem 120 SDPA backward wrt V
✓

prove: for fixed \(Q, K\): \(\mathrm{sdpaBackV}\) matches the \(\operatorname {pdivMat}\) contraction of \(V' \mapsto \mathrm{sdpa}(Q, K, V')\).

Proof ▶
  1. \(V'\) enters only through the final matmul: \(\mathrm{sdpa}(Q, K, V') = W \cdot V'\) with \(W := \mathrm{sdpaWeights}(Q, K)\) fixed.
    proof: Definitional (sdpa_eq_mul_weights).

  2. q.e.d.
    proof: Theorem 110’s backward is \(W^T \cdot d\mathrm{Out}\), which unfolds to exactly \(\mathrm{sdpaBackV}\).

prove: \(\mathsf{HasVJPMat}\, (\mathrm{mhsaLayer})\).

Proof ▶

Sketch: column-stacking — each head touches only its own column slab, so the single-head SDPA backward lifts across the head axis the same way row-independence lifts vector VJPs across rows.

  1. \(\mathrm{mhsaLayer}\) factors as \((\text{per-token dense } W_o) \circ \mathrm{colSlabApply}(\text{single-head SDPA}) \circ (\text{per-token fused QKV dense})\).
    proof: Definitional (mhsaLayer_eq_compose).

  2. The middle factor’s Jacobian is block-diagonal across heads: pdivMat_colIndep, the column-axis analogue of row-independence (Theorem 102), lifts the proved single-head backward (Theorems 118–120) over the head axis (colSlabwiseHasVJPMat).

  3. Glue the three factors with the matrix chain rule (Theorem 103) twice; the dense factors are per-token lifts (Theorem 127).

  4. q.e.d.
    proof: The backward field is taken from the composed witness directly (kernel-cheap projection); the composition-equality transport of step 1 lives only in the correct field — a Prop the kernel never reduces.

Theorem 122 MHSA layer smoothness
✓
#

\(\mathsf{Differentiable}\) sibling of Theorem 121.

Proof ▶

Factor as in Theorem 121 step 1; each factor’s flattened form is differentiable (two per-token dense lifts, and \(\mathrm{colSlabApply}\) of the single-head function, whose smoothness comes from the same chain of exp/inv positivity as Theorem 129); compositions inherit differentiability. q.e.d.

Theorem 123 Patch-embedding VJP
✓
#

The concrete def unfolds to conv-with-stride + CLS prepend + positional embed. prove: \(\mathsf{HasVJP}\, (\mathrm{patchEmbedFlat})\), with backward the deconvolution formula: a sum over patches with reconstructed kernel offsets, the CLS row contributing nothing to the image gradient.

Proof ▶

Sketch: the conv2d input-VJP recipe (Theorem 34) reapplied to the strided patch convolution, with one new wrinkle at the collapse: the CLS row.

  1. Define \(B(\mathrm{img}, dy)\) at input position \((c, h, w)\) as \(\sum _{p, k_h, k_w} [\text{offset match}] \sum _d W^{\mathrm{conv}}_{d, c, k_h, k_w} \cdot dy_{\mathrm{flat}(p + 1,\, d)}\), where patch \(p\) decodes to grid position \((\lfloor p / W' \rfloor , p \bmod W')\).

  2. suffices: \(B\) matches the \(\operatorname {pdiv}\) contraction.
    proof: Definition 9.

  3. The forward decomposes as \((\text{constant: bias} + \text{CLS} + \text{pos-embed}) + (\text{img-linear part})\), where the pad guard absorbs the \(n = 0\) (CLS-row) test so the linear part is uniform in \(\mathrm{img}\).
    proof: Unfold; definitional.

  4. Per-index Jacobian by the conv2d recipe: sum + constant rules drop the constants (Theorems 3, 6); finite-sum rule (Theorem 8) \(\times 3\); product rule (Theorem 4) with constant \(W^{\mathrm{conv}}\) factor; guarded projection = reindex-or-zero (Theorems 7, 6).

  5. Contract with \(dy\) and collapse as in Theorem 34 step 5, with the new wrinkle: split the output-row sum over \(\mathrm{Fin}(N{+}1)\) into the CLS row (contributes \(0\)) plus \(\sum _{p : \mathrm{Fin}\, N}\) at rows \(n = p + 1\) (Fin.sum_univ_succ).

  6. q.e.d.
    proof: What remains is \(B\); with 2, done.

Theorem 124 Patch-embedding smoothness
✓
#

\(\mathsf{Differentiable}\) sibling of Theorem 123.

Proof ▶

The decomposition of Theorem 123 step 3 exhibits the flattened forward as constant + linear-in-img (each summand a constant times a guarded projection); both pieces are differentiable and the sum inherits it. q.e.d.

Theorem 125 Per-token LayerNorm lifted to a matrix
✓
Proof ▶

Instantiate the rowwise lift (Theorem 114) at \(g = \mathrm{layerNormForward}\), with Theorem 95 as the \(\mathsf{HasVJP}\) witness and its differentiability sibling (\(\varepsilon {\gt} 0\)) as the smoothness witness. The backward is block-diagonal: each token’s gradient is computed independently by the 1D three-term formula. q.e.d.

assume:

  1. \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]

prove: \(\mathsf{HasVJP}\, (\texttt{layerNormVec}\; D\; \varepsilon \; \gamma \; \beta )\) for per-feature \(\gamma , \beta \in \mathbb {R}^{D}\), and its row-wise lift to \(\mathsf{HasVJPMat}\) across the \(N\) tokens. This is the LayerNorm of the rendered block and of the depth-\(k\) tower (Theorem 136); the scalar-affine Theorem 125 is the LayerNorm of the weight-shared tower.

Proof ▶

\((+\beta ) \circ \mathrm{layerScale}\, \gamma \circ \mathrm{LN}(1, 0)\), three proven pieces glued by vjpComp, lifted row-wise by Theorem 114; see Proofs.layerNormVecHasVJP.

Theorem 127 Per-token dense lifted to a matrix
✓
Proof ▶

Instantiate the rowwise lift (Theorem 114) at \(g = \mathrm{dense}(W, b)\), with Theorem 18 as the \(\mathsf{HasVJP}\) witness; dense is affine, hence everywhere differentiable. This is \(Q = XW + b\) row-by-row with shared weights. q.e.d.

Theorem 128 Per-token GELU lifted to a matrix
✓
Proof ▶

Instantiate the rowwise lift (Theorem 114) at \(g = \mathrm{gelu}\), with Theorem 97 as the \(\mathsf{HasVJP}\) witness and the tanh-chain smoothness as the differentiability witness. Elementwise activation: the Jacobian is diagonal both across rows and within a row. q.e.d.

Theorem 129 Row-wise softmax smoothness
✓
#

prove: the flattened \(\mathrm{rowSoftmax}\) is \(\mathsf{Differentiable}\).

Proof ▶
  1. case \(n = 0\): the codomain is \(0\)-dimensional; the map is constant.
    proof: Trivial.

  2. case \(n \ge 1\): each output coordinate is \(v \mapsto e^{v_{(r, c)}} \cdot \bigl(\sum _j e^{v_{(r, j)}}\bigr)^{-1}\). The denominator is a positive finite sum (Real.exp_pos, Finset.sum_pos over the nonempty index set), so Differentiable.inv applies and the product of exponential chains closes.

  3. q.e.d.
    proof: Differentiability per coordinate gives differentiability of the Pi-valued map (differentiable_pi).

Theorem 130 Transformer MLP sublayer VJP
✓

\(\mathsf{HasVJPMat}\) of \(\mathrm{dense}_2 \circ \mathrm{gelu} \circ \mathrm{dense}_1\), all per-token.

Proof ▶
  1. Inner composition \(\mathrm{gelu} \circ \mathrm{dense}_1\): matrix chain rule (Theorem 103) on the two per-token lifts (Theorems 127, 128); both flat forms are differentiable (dense affine, GELU via the tanh chain), and the composition inherits it.

  2. q.e.d.
    proof: One more chain-rule application against the outer \(\mathrm{dense}_2\) lift.

Theorem 131 Transformer attention sublayer with residual VJP
✓

assume: \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]
prove: \(\mathsf{HasVJPMat}\) of \(X \mapsto X + \mathrm{MHSA}(\mathrm{LN}_1(X))\).

Proof ▶
  1. Inner arm \(\mathrm{MHSA} \circ \mathrm{LN}_1\): matrix chain rule (Theorem 103) on Theorems 125 and 121, differentiabilities from the \(\varepsilon {\gt} 0\) LN smoothness and Theorem 122.

  2. q.e.d.
    proof: Matrix additive fan-in (Theorem 104) of the identity arm (Theorem 105) and the inner arm — the same residual pattern as Theorem 58, one rank up.

Theorem 132 Transformer MLP sublayer with residual VJP
✓

assume: \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]
prove: \(\mathsf{HasVJPMat}\) of \(h \mapsto h + \mathrm{MLP}(\mathrm{LN}_2(h))\).

Proof ▶

Identical structure to Theorem 131: chain \(\mathrm{MLP} \circ \mathrm{LN}_2\) (Theorems 130, 125 via Theorem 103), then \(\operatorname {biPathMat}\) with the identity arm (Theorems 104, 105). q.e.d.

Theorem 133 Transformer block VJP
✓

prove: \(\mathsf{HasVJPMat}\) of one pre-norm encoder block \(z = x + \mathrm{MHSA}(\mathrm{LN}_1 x)\), \(y = z + \mathrm{MLP}(\mathrm{LN}_2 z)\).

Proof ▶

One matrix chain rule (Theorem 103) glueing the attention sublayer (Theorem 131) to the MLP sublayer (Theorem 132), with each sublayer’s flat differentiability discharged from its arms (both are \(\mathrm{id} + \text{smooth}\)). q.e.d.

Theorem 134 Transformer tower, any depth
✓

prove: \(\mathsf{HasVJPMat}\) of the \(k\)-block tower, for every \(k\) — ViT-Tiny/Base (\(k = 12\)) and Large (\(k = 24\)) are instances.

Proof ▶

Induction on \(k\):

  1. case \(k = 0\): the tower is the identity.
    proof: Theorem 105.

  2. case \(k + 1\): the tower is \(\mathrm{block} \circ \mathrm{tower}_k\).
    proof: Matrix chain rule (Theorem 103) on the induction hypothesis and one block (Theorem 133), with the tower’s flat differentiability carried through the same induction.

  3. q.e.d.
    proof: By induction, 1 and 2 cover every depth.

Theorem 135 ViT body: the grand finale
✓

prove: the full ViT transformer backbone \(\mathrm{finalLN} \circ \mathrm{transformerTower}\) is one \(\mathsf{HasVJPMat}\).

Proof ▶

One final matrix chain rule (Theorem 103) glueing the \(k\)-block tower (Theorem 134) to the final per-token LayerNorm (Theorem 125). Every step of the chain that began with vjpComp (Theorem 10) is a proved theorem; the network itself is Theorem 136, next. q.e.d.

The ladder for the transformer: the tower’s VJP at every depth, the typed graph that denotes it, ViT-Tiny’s whole chain, and then the batched rungs — the rendered backward, the emitted step, the loss gradient.

assume:

  1. \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]

prove: for the depth-\(k\) transformer tower with distinct parameters per block — patch embedding, CLS and position tables, \(k\) blocks of multi-head self-attention and MLP sublayers under vector LayerNorm, the classifier head — the backward composed from the patch embedding’s, the \(k\) blocks’, the final LayerNorm’s and the head’s backwards equals the \(\operatorname {pdiv}\)-contracted Jacobian of vitForwardKV at every input, cotangent and index, for every \(k\). The hand-written chain is tied to this backward in Theorem 138.

Proof ▶

Three applications of the VJP chain rule (Theorem 10): the patch embedding, the tower by induction on \(k\), the final LayerNorm and head; see Proofs.vitForwardKVHasVJP_correct.

Theorem 137 The depth-\(k\) graph denotes the tower
✓

prove: for every depth \(k\) the typed forward graph vitFwdGraphKMHV denotes vitForwardKV; and with stochastic depth on both residual branches of every block, the batched graph of the drop renders denotes, at either precision and at each example, this forward with that example’s two mask entries per block as its branch scales — at all-ones masks, vitForwardKV on every example. The emitted ViT text names each shared intermediate once and is not tied to this graph by a text check; the statement is about the typed graph.

Proof ▶

Op by op along the graph, block by block along the tower; see Proofs.StableHLO.vitFwdGraphKMHV_faithful.

assume:

  1. \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]

prove: the input-gradient chain vitInputGradK at ViT-Tiny’s shape — a \(3 \times 224 \times 224\) image, \(16 \times 16\) patches (\(N = 196\) plus the CLS token), \(d = 192 = 3 \times 64\), MLP \(768\), twelve blocks with distinct parameters, any number of classes — equals the \(\operatorname {pdiv}\)-contracted Jacobian of the flat forward vitForwardKV at every input, cotangent and index. The statement is generic in the weights.

Proof ▶
  1. vitForwardKVHasVJP: for every depth \(k\), the flat forward is \(\mathrm{head} \circ \mathrm{LN}_F \circ \mathrm{tower}_k \circ \mathrm{patchEmbed}\), and each factor carries a \(\mathsf{HasVJP}\) — the patch embedding (Theorem 123), the tower by induction on \(k\) with the vector-LN block (transformerBlockVHasVJPMat: Theorem 133’s two sublayers with Theorem 126 in both LayerNorm slots) bridged to the flat vector as the step, the per-token final LayerNorm (Theorem 126, bridged the same way), and the classifier (Theorem 18 on the CLS row).
    proof: Three applications of vjpComp (Theorem 10); every factor is smooth, so the differentiability side conditions need only assumption 1.

  2. vitForwardKVHasVJP_correct: the composed backward’s correct field, read out at any depth.

  3. vitInputGradK_eq_vitForwardKV_vjp: the chain is that backward — the tower fold vitTowerBackK_eq_vjp plus the head, final-LayerNorm and patch-embedding endpoint ties.

  4. q.e.d.
    proof: Rewrite by 3, then 2, at \(k = 12\) and the dimensions above.

assume:

  1. \(\varepsilon {\gt} 0\) [h\(\varepsilon \)]

prove: at ViT-Tiny’s dimensions the batched input-gradient chain vitInputGradKB equals the backward of the batched tower \(\mathrm{batchMap}\, B\; \texttt{vitForwardKV}\) for every \(B\), number of classes, cotangent and index. The same holds at every depth.

Proof ▶

Theorem 138 per example, lifted over the batch; see Proofs.vitTinyInputGradB_eq_vitTiny_vjp.

prove: each of the 200 parameter gradients of the batched ViT-Tiny step (three heads of 64, depth 12, \(16 \times 16\) patches) denotes the certified batch gradient at the chain cotangent of the label-smoothed cross-entropy, with the batch and the number of classes binders; the CLS gradient is summed over the batch inside the node. Stated at one replica, at either precision, with stochastic depth as an optional mask at each block’s two sites: vit_adam_train_step.mlir and the vitin_* steps emit these nodes, and at some the chain is the drop renders’, where a block’s out-projection and fc2 nodes read the cotangent scaled by its example’s mask and both skips read it raw.

Proof ▶

A fold over the 200 nodes, the chain’s cotangents the certified backwards of the lifted block and the head; see Proofs.ViTTieGB.vit_net_tiedGB.

assume:

  1. \(\varepsilon {\gt} 0\) at the LayerNorms [h\(\varepsilon \)]

  2. \(L\) is any loss of the logits, with gradient \(g\) there [hL]

prove: for each node of Theorem 140, the node at the chain cotangent from \(g\) is \(\partial L/\partial \theta \) of the batched ViT-Tiny forward at the same mask (vitNetB; at none, vitForwardKV applied per example) with that one parameter varied, for every batch and number of classes. Instantiated at the label-smoothed cross-entropy.

Proof ▶

The step tie’s node, with \(g\) pulled back through the certified block backwards (Theorem 15, once per stage); see Proofs.ViTTieGB.vit_net_lossGrad.

9.4 Example: ViT-Tiny on Imagenette

The architecture

ViT-Tiny is DeiT’s (Touvron et al. 2021, arXiv:2012.12877) addition to the family — the original paper’s smallest was ViT-Base — and the shortest spec in Part 1: .patchEmbed chops the image into \(16 \times 16\) patches and projects each to a 192-dim token (196 of them, plus a prepended CLS token), .transformerEncoder runs twelve pre-norm blocks, and a final LayerNorm feeds the CLS token to a dense head. The inset shows one of the twelve transformer blocks, which is two residual sub-blocks, attention then MLP, each pre-normed.

\begin{tikzpicture} [
  >={Stealth[length=1.8mm]},
  every node/.style={font=\sffamily\scriptsize},
  col/.style    = {align=center, rounded corners=2pt, inner sep=2pt, minimum height=0.56cm, minimum width=6.7cm},
  io/.style     = {col, draw=blue!55!black,   fill=blue!8},
  norm/.style   = {col, draw=teal!60!black,   fill=teal!10},
  blk/.style    = {col, draw=blue!45!violet,  fill=violet!8},
  head/.style   = {col, draw=red!60!black,    fill=red!8},
  logits/.style = {col, draw=green!50!black,  fill=green!14, very thick},
  arr/.style    = {->, thick, gray!60, shorten >=1pt, shorten <=1pt},
  stage/.style  = {font=\sffamily\scriptsize\itshape, gray!55!black, anchor=west},
]
  \node[io]                          (input){Input \;\; $224\times224\times3$};
  \node[norm, below=0.16cm of input] (pe)   {\textbf{PatchEmbed} $16\times16$ patches $\to 196\times192$};
  \node[norm, below=0.16cm of pe]    (cls)  {prepend CLS token $+$ pos embed \;\; $197\times192$};
  \node[blk, below=0.16cm of cls]    (enc)  {$12\times$ \textbf{Transformer encoder}\\dim 192, 3 heads, MLP 768};
  \node[norm, below=0.16cm of enc]   (ln)   {final LayerNorm};
  \node[head, below=0.16cm of ln]    (d)    {\textbf{Dense} $192\to10$ \;(identity), on CLS};
  \node[logits, below=0.16cm of d]   (out)  {Logits \;\; 10 classes, softmax-CE};
  \foreach \a/\b in {input/pe,pe/cls,cls/enc,enc/ln,ln/d,d/out}\draw[arr](\a)--(\b);
  \node[stage] at ($(enc.east)+(0.30,0)$){$\times 12$ blocks};
\end{tikzpicture}
\begin{tikzpicture} [
  >={Stealth[length=1.6mm]},
  every node/.style={font=\sffamily\scriptsize},
  proc/.style={align=center, rounded corners=2pt, inner sep=3pt, minimum height=0.58cm, draw=blue!45!violet, fill=violet!8},
  op/.style  ={circle, draw=blue!45!violet, fill=violet!14, inner sep=0pt, minimum size=0.46cm},
  term/.style={font=\sffamily\scriptsize\itshape, inner sep=1.5pt},
  flow/.style={->, thick, gray!60, shorten >=1.5pt, shorten <=1.5pt},
  skip/.style={->, thick, blue!45!violet, shorten >=1.5pt},
]
  \node[term] (x){$x$};
  \node[proc, right=0.5cm of x]   (ln1){LN};
  \node[proc, right=0.35cm of ln1](mh){MHSA};
  \node[op,   right=0.5cm of mh]  (s1){$+$};
  \node[proc, right=0.5cm of s1]  (ln2){LN};
  \node[proc, right=0.35cm of ln2](ml){MLP};
  \node[op,   right=0.5cm of ml]  (s2){$+$};
  \node[term, right=0.5cm of s2]  (y){$y$};
  \foreach \a/\b in {x/ln1,ln1/mh,mh/s1,s1/ln2,ln2/ml,ml/s2,s2/y}\draw[flow](\a)--(\b);
  \draw[skip] (x.north) .. controls +(0,0.72) and +(0,0.72) .. (s1.north)
        node[midway, above, term, blue!45!violet]{$+\,x$};
  \draw[skip] (s1.north) .. controls +(0,0.72) and +(0,0.72) .. (s2.north)
        node[midway, above, term, blue!45!violet]{$+\,z$};
  \node[term, below=0.18cm of ln2, gray!55!black]{one transformer block: $z = x + \mathrm{MHSA}(\mathrm{LN}\,x)$, \ $y = z + \mathrm{MLP}(\mathrm{LN}\,z)$};
\end{tikzpicture}

The verified trainer

The pattern is §5.3’s, minus bnChannels, as ConvNeXt’s was: there is no BatchNorm.

import LeanMlir.Verified.NetsCore
import LeanMlir.Verified.Train
-- 1. The network. `slug` names the committed render. No `bnChannels`:
--    LayerNorm keeps no running statistics, so nothing has to be
--    threaded from training into evaluation.
def vitVerified : VerifiedNetSpec where
  name     := "ViT-Tiny"
  slug     := "vit"
  inC      := 3
  imageH   := 224
  imageW   := 224
  data     := .imagenette
  layers   := [
    .conv 3 192 16 16,          -- patch embed 16x16/s16, 224 -> 14x14 = 196
    .param #[192] 2,            -- CLS token
    .param #[197, 192] 2,       -- positional embedding, 196 + 1
    .transformerBlock 192 768,  -- 12 pre-norm blocks @ dim 192, MLP 768
    .transformerBlock 192 768,  .transformerBlock 192 768,
    .transformerBlock 192 768,  .transformerBlock 192 768,
    .transformerBlock 192 768,  .transformerBlock 192 768,
    .transformerBlock 192 768,  .transformerBlock 192 768,
    .transformerBlock 192 768,  .transformerBlock 192 768,
    .transformerBlock 192 768,
    .layerNorm 192,             -- final LayerNorm, per-channel
    .dense 192 10 ]             -- CLS-head 192 -> 10
  -- Stochastic depth, read only by the `*drop` variants. TWENTY-FOUR
  -- sites for TWELVE blocks: ViT drops a block's two residual
  -- branches independently, at one keep shared by the pair,
  -- keep = 1 - 0.1 * (site/2) / 11.
  dropSites := (Array.range 24).map (· / 2)
  dropDenom := 11
  dropRate  := 0.1
-- 2. The schedule. Note it is NOT the other four nets' schedule.
def vitAdamConfig : VerifiedConfig where
  epochs    := 80
  batchSize := 32
-- 3. The entry point: AdamW, cosine with a 5-epoch warmup, baseLR 3e-4.
--    Batch, rate, EMA decay and drop rate are env knobs; the variant's
--    render must already exist.
def runViTAdam (argv : List String) : IO Unit := do
  let variant := (← IO.getEnv "LEAN_MLIR_VARIANT").getD "adam"
  let tsPath := s!"verified_mlir/vit_{variant}_train_step.mlir"
  if !(← System.FilePath.pathExists tsPath) then
    throw (IO.userError s!"{tsPath} missing — it is written by \
LeanMlir/Proofs/Codegen/ViTRender.lean; run `lake build LeanMlir.Proofs.Codegen.ViTRender` first")
  let bs := ((← IO.getEnv "LEAN_MLIR_BATCH").bind (·.toNat?)).getD vitAdamConfig.batchSize
  let emaDecay := match (← IO.getEnv "LEAN_MLIR_EMA_DECAY_U").bind (·.toNat?) with
    | some u => u.toFloat * 1e-6
    | none   => 0.99996
  let baseLR := match (← IO.getEnv "LEAN_MLIR_BASE_LR_U").bind (·.toNat?) with
    | some u => u.toFloat * 1e-6
    | none   => 0.0003
  let dropNet := match (← IO.getEnv "LEAN_MLIR_DROP_RATE_U").bind (·.toNat?) with
    | some u => { vitVerified.toNet with
                  dropKeeps := vitVerified.dropKeepsAt (u.toFloat * 1e-6) }
    | none   => vitVerified.toNet
  dropNet.trainAdamSched { vitAdamConfig with batchSize := bs }
    (argv.head?.getD "data") baseLR 0.9 0.999 5 variant 0.0 1.0 emaDecay
def main (argv : List String) : IO Unit := runViTAdam argv

The body is a patch embedding, twelve blocks and a head. The patch embed is spelled .conv 3 192 16 16, a \(16 \times 16\) kernel at stride 16, which chops the \(224 \times 224 \times 3\) image into \(14 \times 14 = 196\) non-overlapping patches and projects each to 192 dimensions. That it is a convolution rather than a distinct primitive is the point: a patch embedding is a strided convolution whose kernel equals its stride, so it reuses the conv2d backward (Theorem 34) and needs nothing new.

The CLS token and the positional embedding are .param entries. They are learned weights that no layer computes from an input, and the spec’s job is the parameter layout, so it names them rather than folding them into a block. The \(197\) is \(196\) patches plus the prepended CLS row.

.transformerBlock 192 768 is one block per line, twelve of them, each the pre-norm sandwich drawn in the inset above: LayerNorm, MHSA, residual, LayerNorm, MLP, residual.

The arm, and why sixteen tensors is the whole block.

  | param dims kind    => #[(dims, kind)]              -- Ch. 9: CLS, pos-embed
  | transformerBlock d m =>                            -- Ch. 9
    #[(#[d],1),(#[d],2),                               -- LN1
      (#[d,d],0),(#[d],2), (#[d,d],0),(#[d],2),        -- Wq, Wk
      (#[d,d],0),(#[d],2), (#[d,d],0),(#[d],2),        -- Wv, Wo
      (#[d],1),(#[d],2),                               -- LN2
      (#[d,m],0),(#[m],2), (#[m,d],0),(#[d],2)]        -- MLP d->m->d

.param is the escape hatch: a bare learned tensor with no layer around it, which is how the CLS token and the positional embedding get into the list at all. Everything else here is a dense layer wearing a different name — the four attention projections are [d,d] matrices with biases, indistinguishable in the parameter list from Chapter 1’s .dense. What makes them attention is the graph, not the shapes. Sixteen tensors a block, twelve blocks, plus the patch embed, the two .params, the final LayerNorm and the head: 200.

200 parameter tensors, 5,526,346 scalars. vitVerified.toSpecs is kernel-#guarded against ViTLayout.specs, an independently audited hand-list, and the ImageNet spec of §9.6 is pinned to this one at everything but its head. At 1000 classes the count is 5,717,416.

Twenty-four keeps for twelve blocks, and the pairing is the content. ViT drops each block’s attention branch and MLP branch independently, but at the same keep probability, so sites \(2i\) and \(2i+1\) share \(\mathrm{keep}_i = 1 - 0.1i/11\). The driver needs one entry per site because it draws one Bernoulli stream per mask input. Deriving twenty-four evenly spaced keeps from the site ordinal instead would silently unpair them, and no structural check would notice.

Note the learning rate is 0.0003 (a third of what the ConvNets used) and warmup is 5 epochs (longer than the ConvNets’ 3). ViTs need gentler optimization schedules, because the attention softmax is prone to collapse if gradients push logits too hard early, and warmup keeps the first few epochs slow enough to avoid that failure mode.

Results

§9.1 has the run: 80 epochs at about 18 seconds each, twenty-four minutes, \(\mathbf{68.74\% }\) top-1 and \(\mathbf{90.42\% }\) top-5. That completes the Part-1 column, and every row of it is now a measurement on the verified XLA path at the same dataset, the same eighty epochs, and one RTX 4060 Ti:

Model

Params

MLIR

Step time

Total

Top-1

Top-5

ResNet-34

21.29M

754 KB

120 ms

47 min

89.99%

98.46%

MobileNet V2

2.24M

1,071 KB

90 ms

35 min

89.25%

98.68%

EfficientNet-B0

4.02M

1,317 KB

103 ms

41 min

90.04%

98.29%

ConvNeXt-T

27.83M

1,019 KB

188 ms

1.2 h

82.27%

96.51%

ViT-Tiny

5.53M

1,282 KB

61 ms

24 min

68.74%

90.42%

Columns as in Chapter 7’s table; ResNet-34’s row is the five-seed mean of §5.6 (runs/2026-09-12-r34-ablation-fp32-seeds/) with one seed’s timing.

ViT-Tiny is the fastest per step by a factor of \(1.5\) over the next network and the lowest-accuracy by more than thirteen points. That gap is the data-hunger effect: 9,469 training images is tiny for a transformer. The ViT paper reached ImageNet-competitive accuracy only when trained on ImageNet-21K (14M images) or JFT-300M (300M images). At Imagenette scale the ConvNets’ inductive bias, locality and translation equivariance, actively helps, and the ViT has to learn those properties from data it doesn’t have. §9.6 is the control: the same architecture on 1.28M images reaches \(72.42\% \) over a thousand classes, which is the regime the design was for.

It reproduces exactly. This number was measured three times before being printed, and all three passes came back bit-identical on three different cards: the same \(68.738854\% \) and \(90.420382\% \) at epoch 80, and the same 160 epoch lines the whole way down. The reason is the graph’s shape. XLA selects convolution algorithms per process, which is what moves the convnet chapters’ numbers by a point or two between runs. This graph holds 435 dot_generals and two convolutions, so there is almost nothing for that selection to vary.

9.5 MLIR: Attention

What is already proven. Scaled dot-product attention is \(\mathrm{sdpa}(Q,K,V) = \mathrm{softmax}(QK^{\! \top }/\sqrt{d})\, V\). Its reverse-mode derivative produces three gradients (\(dQ\), \(dK\), \(dV\)), each proven separately (Theorems 118–120, sdpaBackQ/K/V), all built on the softmax backward, which couples every position to every other (Theorem 116). Because \(Q\), \(K\), and \(V\) are three linear projections of the same input \(a\), the input gradient is a three-way fan-in, \(da = dQ\, W_q^{\! \top } + dK\, W_k^{\! \top } + dV\, W_v^{\! \top }\). sdpaHasVJPMat3 bundles the three, and transformerAttnSublayerHasVJPMat composes the whole sublayer (LayerNorm, the QKV projections, attention, the output projection, the residual) through vjpMatComp.

The gap and how we close it. The softmax backward is non-elementwise and the three projections are coupled. The emitted graph denotes transformerAttnSublayerHasVJPMat’s backward. Here is the three-way fan-in at the input (two tokens, model dimension four, single head, with the SDPA backward that produces \(dQ,dK,dV\) elided to its comment):

// dQ,dK,dV = SDPA.back(dOut . Wo^T)  (softmax-weighted attention grads)
%daQ = stablehlo.dot_general %dQ, %Wq, contracting_dims = [1] x [1]
         : (tensor<2x4xf32>, tensor<4x4xf32>) -> tensor<2x4xf32>
%daK = stablehlo.dot_general %dK, %Wk, contracting_dims = [1] x [1]
         : (tensor<2x4xf32>, tensor<4x4xf32>) -> tensor<2x4xf32>
%daV = stablehlo.dot_general %dV, %Wv, contracting_dims = [1] x [1]
         : (tensor<2x4xf32>, tensor<4x4xf32>) -> tensor<2x4xf32>
%daQK = stablehlo.add %daQ, %daK : tensor<2x4xf32>
%da = stablehlo.add %daQK, %daV : tensor<2x4xf32>
// %dxa = LN.back(%da);  %dx = %dOut + %dxa  (residual skip)

Read it against the fan-in: each dot_general carries one of the attention gradients back through its projection (\(dQ\) through \(W_q\), and so on, contracting the model axis), and the two adds sum the three. This is the residual fan-in (Theorem 58) with three branches instead of one, because attention reads the input thrice, so the gradient returns thrice and adds. The softmax-weighted \(dQ,dK,dV\) that feed the dot_generals come from the proven sdpaBackQ/K/V, and the surrounding LayerNorm and output projection chain through their bridges.

One honesty note: the hard part of attention is in that first comment. This listing is the projection fan-in (three linear adjoints and two adds), while the softmax backward that couples every token to every other (softmaxHasVJP, § 9.3) is what // SDPA.back stands in for. We show the fan-in because it is the new structural move. The coupled core is carried by the proofs of § 9.3.

With attention pinned, the verified-codegen thread covers every operator in the modern vision stack, and that means dense, convolution, BatchNorm, residual, depthwise, squeeze-excite, layer scale and attention. Each is emitted as the rendering of a machine-checked derivative, from the same three pieces: denoted IR, bridge theorem, printer.

Caveats.

  • The attention bridge is unconditional. Softmax and the linear projections are smooth everywhere, with no smooth-point clause.

  • Representative scale (two tokens, \(d = 4\), one head).

9.6 ImageNet recipe

The Imagenette ViT-Tiny above trained on \(\sim \)9.5K images. The full 1000-class run is the same architecture with a 1000-class head, the .imagenet dataset, and the DeiT-Ti training recipe, so it is still patch16, embed 192, 12 transformer blocks and 3 heads, at 5,717,416 parameters. As in every ImageNet section, the recipe runs on both paths — the reference first, then the verified path, then the two side by side — and this is the chapter where the pair is cleanest, because a transformer has no BatchNorm to normalize differently.

The reference (JAX)

The reference trainer is jax/MainVitImagenet.lean, quoted as it stood for the run below (today it carries DeiT’s rows as the marker names them, for the rerun):

-- 1. Same ViT-Tiny backbone, 1000-class head.
def vitTinyImagenet : NetSpec where
  name   := "ViT-Tiny (ImageNet, bf16)"
  imageH := 224
  imageW := 224
  layers := [
    .patchEmbed 3 192 16 196,            -- (224/16)^2 = 196 patches
    .transformerEncoder 192 3 768 12,    -- 12 blocks, 3 heads, MLP 768
    .dense 192 1000 .identity            -- 1000-class head
  ]

-- 2. Full DeiT-Ti recipe: AdamW, cosine, the aug suite, stochastic depth, EMA.
def vitTinyImagenetConfig : TrainConfig where
  vitInit           := true          -- timm/DeiT trunc_normal(0.02) init, not Xavier
  learningRate      := 0.0005        -- proper DeiT batch-512 LR
  batchSize         := 512
  epochs            := 300           -- full DeiT-Ti schedule
  optimizer         := .adam         -- AdamW (decoupled weight decay)
  weightDecay       := 0.05
  wdExcludeNormBias := true          -- no_weight_decay: skip norm/bias/pos-embed/CLS
  cosineDecay       := true
  warmupEpochs      := 5
  minLR             := 0.00001       -- DeiT --min-lr 1e-5
  labelSmoothing    := 0.1
  gradClipNorm      := 1.0           -- the unlock for the 5e-4 LR (see below)
  useMixup          := true          -- Mixup alpha 0.8
  useCutmix         := true          -- CutMix alpha 1.0 (alternates with Mixup)
  useRandAugment    := true          -- rand-m9-mstd0.5-inc1 (color + geometric)
  randAugmentGeometric := true
  augBicubic        := true          -- PIL-bicubic geometry, as timm
  randomErasing     := true          -- Random-Erase p 0.25
  randomErasingProb := 0.25
  erasingPixel      := true          -- timm mode='pixel', N(0,1) fill
  dropPath          := 0.1           -- stochastic depth (linear 0 -> 0.1)
  useEMA            := true          -- model EMA (eval on shadow)
  emaDecay          := 0.99996       -- DeiT default
  bf16              := true          -- bf16 matmul compute
  lnEps             := 0.000001      -- DeiT's LayerNorm eps, 1e-6
  f32StemHead       := true          -- patch embed + head in fp32
  valEveryEpochs    := 5             -- ImageNet val is data-loading-bound
  repeatedAug       := 3             -- DeiT Repeated Augmentation (see below)

-- 3. Weight init. vitInit above selects trunc_normal(0.02) for every
--    transformer Linear and gives the patch-embed conv PyTorch's Conv2d
--    default for its weight and a zero bias; Xavier-uniform is the
--    emitter's generic path, and
--    at ViT-Ti's d=192 it is 3.6x wider than timm's fixed 0.02. The run
--    reported below is the default; xavier is the A/B arm.
def vitTinyImagenetConfigXavier : TrainConfig :=
  { vitTinyImagenetConfig with vitInit := false }

-- 4. Two named recipes, chosen by a positional CLI arg. This is the
--    reference driver; the verified path's entry point is trainAdamSched,
--    in the next section.
def vitTinyImagenetRecipes : List Recipe := [
  { name := "default", cfg := vitTinyImagenetConfig,       ... },
  { name := "xavier",  cfg := vitTinyImagenetConfigXavier, ... }
]

def main (args : List String) : IO Unit :=
  runRecipeMain "vit-tiny-imagenet" vitTinyImagenet .imagenet
    vitTinyImagenetRecipes args

The two recipes differ in exactly one field, and the trainers they generate differ in exactly one function: init_params, 59 initialization sites, with the schedule, augmentation, EMA and evaluation identical byte-for-byte.

Gradient clipping is not optional. At the DeiT learning rate (peak \(5\times 10^{-4}\) for batch 512) the model collapses to chance the moment warmup ramps past \({\sim }1.6\times 10^{-4}\), train loss pinned at \(\ln (1000)\approx 6.9\) and never recovering. gradClipNorm := 1.0 is the whole fix; the same LR then trains through warmup to convergence.

bf16. ViT is matmul-bound — patch embed, QKV, scores, output, MLP and head are all dense matmuls, no convolution — so bf16 is the only precision flag that applies and there is no bf16Conv to set. It buys \(\mathbf{1.47\times }\): \(205.1 \to 139.2\) ms/step, \(624 \to 919\) img/s, measured on one RTX 3060 at batch 128 (jax/scripts/jax_vit_bench.py tiny 128, synthetic). The multiple is below the convnets’ because ViT-Ti’s matmuls are narrow (\(d{=}192\)) and proportionally more time goes to the fp32 LayerNorm/softmax that bf16 leaves alone.

GPU

Precision

Per epoch

Epochs

Total

Val top-1

Val top-5

4\(\times \) 3060 (CUDA)

bf16

9.1 min

300

45.9 hr

\(\mathbf{72.07\% }\)

\(\mathbf{91.00\% }\)

Global batch 512 as \(4 \times 128\) replicas, the EMA weights scored over all \(50{,}000\) validation images; one attempt, no restarts. DeiT-Ti without distillation publishes \(72.2\% \) / \(91.1\% \), and publishes it for the live weights, not a shadow: this run’s live weights score \(71.86\% \) / \(90.88\% \), \(0.34\) and \(0.23\) under the paper, and the EMA is worth about \(+0.2\) on top of them (McNemar \(p = 0.002\) over the \(50{,}000\) images, so the shadow’s gain is real, if small). At \(n = 50{,}000\) the Wilson 95% interval is \(\pm 0.39\) points either way: on the paper’s number, not under it in any meaningful sense.

\begin{tikzpicture} 
\begin{axis}[
    width=0.92\linewidth, height=6.5cm,
    xlabel={Epoch}, ylabel={Validation accuracy (\%)},
    xmin=0, xmax=301, ymin=0, ymax=95,
    xtick={0,50,100,150,200,250,300},
    ytick={20,40,60,80},
    legend pos=south east,
    legend cell align={left},
    grid=major, grid style={gray!18},
    tick label style={font=\small},
    label style={font=\small},
    every axis plot/.append style={line width=1pt, mark size=0.8pt},
    title style={font=\small},
    title={DeiT-Ti recipe, 300 epochs --- reference},
]
\addplot[blue, mark=*, mark options={fill=blue}] coordinates {
(5,13.99) (10,30.31) (15,39.23) (20,44.50) (25,48.24) (30,50.67) (35,52.85) (40,54.45) (45,55.77) (50,57.05) (55,58.00) (60,58.74) (65,59.57) (70,60.22) (75,60.78) (80,61.54) (85,62.01) (90,62.41) (95,62.84) (100,63.15) (105,63.50) (110,63.93) (115,64.27) (120,64.60) (125,64.93) (130,65.23) (135,65.49) (140,65.80) (145,66.08) (150,66.31) (155,66.68) (160,66.95) (165,67.16) (170,67.50) (175,67.73) (180,67.93) (185,68.22) (190,68.42) (195,68.58) (200,68.75) (205,69.19) (210,69.33) (215,69.52) (220,69.79) (225,69.95) (230,70.20) (235,70.37) (240,70.58) (245,70.78) (250,71.02) (255,71.14) (260,71.22) (265,71.32) (270,71.52) (275,71.68) (280,71.81) (285,71.95) (290,71.95) (295,71.99) (300,72.07)
};
\addlegendentry{top-1}
\addplot[orange, mark=*, mark options={fill=orange}] coordinates {
(5,31.79) (10,54.41) (15,64.49) (20,70.01) (25,73.34) (30,75.82) (35,77.60) (40,78.82) (45,79.88) (50,80.79) (55,81.58) (60,82.23) (65,82.87) (70,83.46) (75,84.01) (80,84.36) (85,84.63) (90,84.91) (95,85.29) (100,85.59) (105,85.85) (110,86.08) (115,86.27) (120,86.51) (125,86.73) (130,86.93) (135,87.14) (140,87.27) (145,87.48) (150,87.63) (155,87.83) (160,87.95) (165,88.14) (170,88.32) (175,88.55) (180,88.65) (185,88.79) (190,88.98) (195,89.09) (200,89.25) (205,89.40) (210,89.48) (215,89.65) (220,89.73) (225,89.84) (230,89.99) (235,90.10) (240,90.19) (245,90.31) (250,90.40) (255,90.48) (260,90.53) (265,90.60) (270,90.66) (275,90.75) (280,90.82) (285,90.86) (290,90.94) (295,90.98) (300,91.00)
};
\addlegendentry{top-5}
\end{axis}
\end{tikzpicture}

The run, sampled every 5 epochs. The slow start is Mixup and CutMix blending the labels, compounded by the EMA shadow lagging the live weights; after that it climbs to the end of the schedule without an overfitting turn.

Where this still differs from the paper. The number matches. The recipe does not yet match exactly, and the honest list is short but not empty.

What matches. Architecture (patch16, \(d{=}192\), 12 blocks, 3 heads, 5,717,416 parameters, LayerNorm at \(\varepsilon = 10^{-6}\)); schedule (300 epochs, 5-epoch warmup, cosine to DeiT’s floor of \(10^{-5}\)); optimizer (AdamW, weight decay \(0.05\), no decay on norms, biases, CLS or positional embedding); the augmentation stack (Mixup \(0.8\) and CutMix \(1.0\) with label smoothing \(0.1\) folded into the mixed target, rand-m9-mstd0.5-inc1 with the SolarizeAdd\(+\)Invert op-set, per-op apply probability \(0.5\) and PIL-matched bicubic geometry, Random-Erase \(p{=}0.25\) in timm’s pixel mode, repeated augmentation \(3\times \)); stochastic depth \(0.1\); model EMA at \(0.99996\); and the patch embedding and classifier head in fp32 under the bf16 blocks. Weight initialization matches as well — every transformer Linear, the CLS token and the position embedding get \(\mathrm{trunc\_ normal}(\sigma {=}0.02)\), and the patch-embed convolution’s weight PyTorch’s Conv2d default — which is what vitInit in the listing above selects. It is worth saying why that one mattered: Xavier-uniform scales as \(1/\sqrt{d}\) against timm’s fixed \(0.02\), so the generic path was \(1.8\times \) too wide at ViT-B, \(2.6\times \) at ViT-S and \(3.6\times \) at ViT-Ti, while the patch embed came out \({\sim }6\times \) too narrow (it divided by the output fan \(d\cdot p^2\) rather than the input fan \(c\cdot p^2\)). The smallest model was the worst case.

What does not match is the marker after the side-by-side table; the verified path inherits every item, since it runs the same \(4\times 128\), clips at the same \(1.0\) inside the render, and streams the same tf.data pipeline. “Matches the paper’s number” and “matches the paper” are different claims, and only the first is established here, on either path.

The further \(+2.3\) points to DeiT-Ti’s \(74.5\% \) headline are a different thing entirely, the distillation variant (a second “distillation token” trained against a strong CNN teacher), which we do not implement.

The verified path (PJRT)

The verified trainer is apps/imagenette/MainViTImagenet.lean, built as lake build vit-imagenet-verified:

-- Identical backbone, 1000 classes instead of 10.
def vitImagenetVerified : VerifiedNetSpec where
  name       := "ViT-Tiny (ImageNet-1k)"
  slug       := "vitin"
  data       := .imagenet
  shimScript := "generated_vit_tiny_imagenet_shim.py"
  layers     := [ ... the Imagenette stack, 1000-class head ... ]

-- 300 epochs, because that is the schedule the reference number
-- above was measured on. Batch is 128 PER DEVICE, so four
-- replicas give the reference's global 512 exactly.
def vitImagenetConfig : VerifiedConfig where
  epochs    := 300
  batchSize := 128
  vitInit   := true  -- timm/DeiT init, as the reference's default recipe above

The head is the only parameter shape that moves, from \(192 \times 10\) to \(192 \times 1000\), which takes the count from 5,526,346 to 5,717,416. The same three kernel #guards that hold ConvNeXt’s two specs together hold these: equal toSpecs.size, equal toSpecs.pop.pop, and a back! of (#[1000], 2).

The collectives are gated one way here. tests/TestViTDpCheck.lean pins the data-parallel step against its single-device peer by handing both replicas the same rows, where the all-reduce mean is an identity. ViT has no BatchNorm, so that identity is exact rather than approximate. But a gate built on duplicated rows is structurally blind to a shard-offset bug, and the check that closes that hole works by giving the replicas genuinely different data. That one now covers the Imagenette vit spec, and it does not yet cover vitin. Two gates that sound alike are not one property, and this chapter has one and a half of them rather than ConvNeXt’s two.

The regularizer knobs are render variants, and here they compose. Weight-decay exclusion, grad clipping and stochastic depth are spelled wx, clip and drop, EMA is a variant too, and vitin_emadp128x4wxclipdropeps0000001bf16 is the render that carries all four at once on top of AdamW and the four-way all-reduce, at LayerNorm \(\varepsilon = 10^{-6}\) (the last field spells the \(\varepsilon \)). It is the artifact the run used: four regions, \([\theta \mid m \mid v \mid \mathrm{ema}]\), 831 operands and 200 all-reduces, with an fp32 evaluation forward at the same \(\varepsilon \) beside it. Theorem 140 reaches its bf16 gradient nodes at its twenty-four drop-path sites, with only the EMA tail outside it. Mixup, CutMix and repeated augmentation never enter the graph at all, because they are data-side and ride the shim. The job, which today launches the rerun’s render named in the marker below:

lake run vit-default-emabf16-4gpu

GPU

Precision

Per epoch

Epochs

Total

Val top-1

Val top-5

4\(\times \) 3060 (CUDA)

bf16

13.0 min

300

64.9 hr

\(\mathbf{72.42\% }\)

\(\mathbf{91.21\% }\)

Global batch 512 as \(4 \times 128\) replicas, the EMA weights scored over the same \(50{,}000\) images; one attempt, no restarts, every epoch scored. The live weights score \(72.15\% \) / \(91.11\% \), which is DeiT-Ti’s own \(72.2\) / \(91.1\) to within a twentieth of a point.

The two paths, side by side

 

reference (JAX)

verified path (PJRT)

lowering

JAX \(\to \) XLA

pretty(provenGraph) \(\to \) XLA

Python in the step

yes

none

ms/step

220

299

per epoch

9.1 min

13.0 min

total

45.9 hr

64.9 hr

Mixup / CutMix draw

one per global 512

one per 128-row shard

evaluation forward

bf16

fp32

precision

bf16 block matmuls

bf16 block matmuls

patch embed + head

fp32

fp32

global batch

512 (\(4\times 128\))

512 (\(4\times 128\))

steps/epoch

2,502

2,502

label smoothing

\(0.1\)

\(0.1\)

LayerNorm \(\varepsilon \) / LR floor

\(10^{-6}\) / \(10^{-5}\)

\(10^{-6}\) / \(10^{-5}\)

CLS + position init

\(\sigma = 0.02\)

\(\sigma = 0.02\)

EMA (scored)

0.99996

0.99996

val top-1

\(\mathbf{72.07\% }\)

\(\mathbf{72.42\% }\)

val top-5

\(\mathbf{91.00\% }\)

\(\mathbf{91.21\% }\)

live weights, top-1 / top-5

71.86 / 90.88

72.15 / 91.11

Both columns are ViT-Tiny on the DeiT-Ti recipe, 300 epochs, on the same four RTX 3060 cards, scored over the same \(50{,}000\) validation images. Rows above the first rule differ; rows between the rules are held equal by construction. [TODO: DeiT in five places, on both paths: batch \(1024\) (four replicas of \(256\), one shot), no gradient clip, the scheduler stepped once an epoch from \(10^{-6}\) over all \(300\) epochs, Mixup or CutMix drawn per batch, torchvision’s crop; plus the exact erf GELU, RandAugment’s fill at the image mean, the \(3\times \) repeats placed by an index-level sampler, the EMA decay constant from step \(0\), an fp32 evaluation forward, and the live weights as the headline where the EMA shadow is. In code since 2026-10-11 and not yet run; one rerun of both halves takes all of it — vit-default-jax-4gpu / vit-default-emabf16-4gpu, \({\sim }48 + 70\) h. Still out, disclosed: the patch-embedding bias at PyTorch’s default.]

The two paths agree within \(0.34\) points on top-1 and \(0.21\) on top-5, inside the 95% Wilson interval (\(\pm 0.39\) at \(72\% \) on \(50{,}000\) images), at one seed each. Above the rule sit the lowering, what it costs, and the two things the runs did not share: where the Mixup and CutMix draws are made — once over the global batch on the reference, once per replica’s \(128\) rows on the verified path, whose four producers each mix their own shard, which is also how timm mixes under DDP — and the precision of the evaluation forward, which a re-score of the reference in fp32 puts at \(+0.01\). Everything the first pair did not share — the label smoothing, the precision of the patch embedding and head, the CLS and position initialization, the cosine’s phase — is now held equal, and the batch is the reference’s own \(512\) with no BatchNorm to normalize over a different group. Against that first pair (\(72.31\) / \(72.35\)) the reference moved \(-0.24\) and the verified path \(+0.07\), each rerun also carrying the bicubic augmentation and DeiT’s \(\varepsilon \) and floor, so neither delta is a re-run of its predecessor.

Interval or no, these are not the same model. Both runs score the same \(50{,}000\) images, so the question can be put per image: the reference is right on \(2{,}257\) images the verified model misses and the verified model on \(2{,}438\) the reference misses, and McNemar’s exact test puts that at \(p = 0.009\) (\(0.043\) on the live weights). The verified checkpoint is ahead on this validation set. With one seed per arm that is a statement about two trained models, not about the two paths, and the per-shard Mixup draw is the first place to look for it; what the pair was rerun to settle is narrower, and settled: matched row for row, the verified path does not trail its reference.

The curves run together, and the verified path leads from early on. The two are tied through epoch 35 — \(13.89\) against \(13.99\) at epoch 5, \(48.27\) against \(48.24\) at 25 — and from epoch 40 the verified path sits between a tenth and half a point ahead, to the end, never reversing:

epoch

5

50

100

150

200

250

300

reference (JAX)

13.99

57.05

63.15

66.31

68.75

71.02

72.07

verified path (PJRT)

13.89

57.26

63.60

66.78

69.15

71.38

72.42

\(\Delta \)

\(-0.11\)

\(+0.21\)

\(+0.45\)

\(+0.46\)

\(+0.40\)

\(+0.36\)

\(+0.34\)

\begin{tikzpicture} 
\begin{axis}[
    width=0.92\linewidth, height=6.5cm,
    xlabel={Epoch}, ylabel={Validation top-1 (\%)},
    xmin=0, xmax=301, ymin=0, ymax=80,
    xtick={0,50,100,150,200,250,300},
    ytick={20,40,60,80},
    legend pos=south east,
    legend cell align={left},
    grid=major, grid style={gray!18},
    tick label style={font=\small},
    label style={font=\small},
    every axis plot/.append style={line width=1pt, mark size=0.8pt},
]
\addplot[blue, mark=*, mark options={fill=blue}] coordinates {
(5,13.99) (10,30.31) (15,39.23) (20,44.50) (25,48.24) (30,50.67) (35,52.85) (40,54.45) (45,55.77) (50,57.05) (55,58.00) (60,58.74) (65,59.57) (70,60.22) (75,60.78) (80,61.54) (85,62.01) (90,62.41) (95,62.84) (100,63.15) (105,63.50) (110,63.93) (115,64.27) (120,64.60) (125,64.93) (130,65.23) (135,65.49) (140,65.80) (145,66.08) (150,66.31) (155,66.68) (160,66.95) (165,67.16) (170,67.50) (175,67.73) (180,67.93) (185,68.22) (190,68.42) (195,68.58) (200,68.75) (205,69.19) (210,69.33) (215,69.52) (220,69.79) (225,69.95) (230,70.20) (235,70.37) (240,70.58) (245,70.78) (250,71.02) (255,71.14) (260,71.22) (265,71.32) (270,71.52) (275,71.68) (280,71.81) (285,71.95) (290,71.95) (295,71.99) (300,72.07)
};
\addlegendentry{reference (JAX)}
\addplot[red, dashed, mark=o] coordinates {
(5,13.89) (10,30.08) (15,39.11) (20,44.40) (25,48.27) (30,50.81) (35,52.95) (40,54.72) (45,56.07) (50,57.26) (55,58.22) (60,59.20) (65,59.98) (70,60.61) (75,61.34) (80,61.84) (85,62.38) (90,62.69) (95,63.17) (100,63.60) (105,63.93) (110,64.18) (115,64.47) (120,64.87) (125,65.29) (130,65.66) (135,65.91) (140,66.19) (145,66.46) (150,66.78) (155,66.98) (160,67.22) (165,67.54) (170,67.85) (175,68.02) (180,68.27) (185,68.54) (190,68.83) (195,69.07) (200,69.15) (205,69.30) (210,69.58) (215,69.81) (220,70.09) (225,70.40) (230,70.59) (235,70.79) (240,71.01) (245,71.17) (250,71.38) (255,71.54) (260,71.66) (265,71.83) (270,71.98) (275,72.08) (280,72.20) (285,72.32) (290,72.39) (295,72.41) (300,72.42)
};
\addlegendentry{verified path (PJRT)}
\end{axis}
\end{tikzpicture}

One recipe, two paths to XLA, sampled every 5 epochs and scored over all \(50{,}000\) validation images.

The remaining \(64.9\) against \(45.9\) hours is the data feed, not the renderer. With the shim removed the verified step is \(153\) ms — a \(38.5\)-hour schedule, under the reference’s own — and the fed step is \(299\); the graph has not changed since that was measured on the first pair’s render, which this one matches but for \(\varepsilon \). The gap is the shim: its producers run the same bicubic augmentation that cost the reference \(1.34\times \) over its pre-bicubic run, out of process, and then stream \(602\) KB per image of fp32 through a subprocess pipe, \(308\) MB per step at global \(512\), which is host work the JAX trainer does in-process. Two knobs close most of the pipe’s share and both are throughput-only: producer count, and tf.data op determinism, which serializes the augmentation map and costs \(5.3\times \) on the producers; it moves the order of random draws and nothing else, and the reference has never paid it either. The same accounting holds for every verified ImageNet run in this book, and the convnets’ gaps are read against it. [TODO: re-measure the gap on the shim as it stands, which mixes in place and writes from its own thread; the uint8 wire does not apply to this recipe, and PIL rotate is not built. At DeiT’s \(1024\) the 2026-10-11 smoke of vit-default-emabf16-4gpu read a \(330\) ms floor under a \(638\) ms fed step with four producers; the rerun measures it end to end.]

A note on the training loss, which the first pair could not compare: its curves sat \({\sim }0.47\) apart at epoch 50 while top-1 agreed to a tenth of a point, and that was the smoothing, because label-smoothed cross-entropy sums \(\log p\) over all \(1{,}000\) classes and is hypersensitive to the small-probability tail, which top-1 cannot see. With the smoothing matched the two curves run within \(0.08\) of each other for the whole schedule, the verified path’s lower on average from the third 25-epoch window on and by \(0.03\) at the end — so the model that generalizes better here also fits its training stream better, a stream that is not quite the reference’s, since each producer mixes its own \(128\) rows. The validation accuracies remain the comparison.

9.7 Side quest: scaling to ViT-S and B

The axis. ViT scales on one axis where ConvNeXt scaled on two. Tiny, Small and Base are the same twelve blocks at a wider model dimension, with the MLP at \(4D\) throughout. Depth never moves.

model

shape

params

graph, 4 cards \(128\)/dev: fp32 \(\to \) bf16

ViT-Ti

\(12\) blocks, \(D{=}192\) / 3 heads

\(5.7\) M

\(171.6 \to 96.4\) ms  (\(1.78\times \))

ViT-S

\(12\) blocks, \(D{=}384\) / 6 heads

\(22{,}050{,}664\)

\(464.0 \to 245.5\) ms  (\(\mathbf{1.89\times }\))

ViT-B

\(12\) blocks, \(D{=}768\) / 12 heads

\(86{,}567{,}656\)

\(1295.6 \to 745.3\) ms  (\(1.74\times \))

Counts #guard against DeiT-S’s \(22.05\) M and DeiT-B’s \(86.57\) M. These are the graph’s times, all-reduce in and no trainer around them, at four replicas because S and B have no single-device render at all, on the first renders at \(128\) per device. The jobs below run DeiT’s \(1024\) instead — S as four replicas of \(256\) in one shot, B as two accumulated micro-batches of \(4 \times 128\) — at the \(10^{-3}\) that batch sets, which took one renderer feature, the accumulating optimizer tail, beyond the #eval lines.

A full schedule, four cards, \(300\) epochs at the jobs’ own shape — DeiT’s \(1024\), \(1{,}251\) optimizer steps an epoch — with each path’s own trainer timed on real ImageNet (the 2026-10-11 smokes, on the 4060 Ti box):

 

reference (JAX)

verified path (PJRT)

4 cards, bf16

ms/step

300 epochs

ms/step

300 epochs

ViT-S

\(648\)

\(72\) h  (\(3.0\) d)

\(650\)

\(72\) h  (\(3.0\) d)

ViT-B

\(1{,}700\)

\(180\) h  (\(7.5\) d)

\(1{,}718\)

\(187\) h  (\(7.8\) d)

Steps are \(1{,}024\) rows; the JAX side accumulates two of \(512\), and ViT-B’s verified render two micro-steps of \(4 \times 128\) at \(859\) ms each. Totals include the per-epoch evaluation. These are fed steps, not graph times, and both verified steps are compute-bound, \(9\) and \(5\) ms of them waiting on the shim.

What is queued. Both pairs exist. The verified jobs are vits-default-emabf16-4gpu on vitsin_emadp256x4wxdropeps0000001erfbf16, DeiT’s \(1024\) as four replicas of \(256\) in one shot, and vitb-default-emabf16-4gpu on vitbin_emaaccdp2x128x4wxdropeps0000001erfbf16, the same \(1024\) as two accumulated micro-batches of \(4 \times 128\), exact under LayerNorm — Tiny’s recipe as its marker has it: vitInit, LayerNorm \(\varepsilon = 10^{-6}\), the \(10^{-5}\) learning-rate floor, EMA at \(0.99996\) constant from step \(0\), no clip, the exact erf GELU, bf16 under an fp32 patch embedding and head — and their references are vits-accum-jax-4gpu and vitb-accum-jax-4gpu, both accumulating \(2\times 512\) to the same \(1024\) (the JAX ViT-S step does not fit the arena in one shot where the verified render does). ViT-S’s render peaks at \(10.75\) GiB of the \(11.68\) GiB default arena; ViT-B’s at \(13.83\) GiB, so its job raises the allocator fraction to \(0.97\) (\(15.11\) GiB).

[TODO: as Tiny’s; neither has run —
vits-accum-jax-4gpu / vits-default-emabf16-4gpu,
vitb-accum-jax-4gpu / vitb-default-emabf16-4gpu.]

What Part 1 has established

Every layer primitive shipped in Part 1’s trainers (MLP, CNN, ResNet, MobileNet, EfficientNet, ConvNeXt, ViT) has a machine-checked backward pass. The same VerifiedNetSpec / VerifiedConfig / trainAdamSched pipeline trains all of them with at most config-level changes. The MLIR each emits is between 7 KB (MLP) and 1,317 KB (EfficientNet-B0), and the lowerer compiles all of them the same way.

The bestiary in Part 2 then shows that every other architecture you’ve seen in modern deep learning composes from the same small set of primitives plus a handful of architecture-specific bundled layers. That list runs UNet, YOLO, DETR, Mask R-CNN, DCGAN, CycleGAN, Pix2Pix, DDPM, Stable Diffusion, VAE, AlphaGo, AlphaZero, MuZero, Mamba, BERT, GPT, Whisper, CLIP, LLaVA, SAM, SegFormer, DeepLab v3+, Nyström-former, QANet and Evoformer. Three rules (chain, additive fan-in, multiplicative fan-in). Five Jacobian tricks (diagonal, sparse Toeplitz, binary selection, rank-1 correction, outer product).

That’s the whole framework. Part 2’s bestiary takes the same set of primitives and tours how they compose into every well-known modern architecture, a survey of the field expressed in the proven kit, with a small library of bundled idioms for the patterns that recur.