Verified Deep Learning with Lean 4

8 ConvNeXt

ConvNets strike back

By 2022 the conventional read on image recognition had flipped. ResNet had been the default backbone for six years. Then in late 2020 ViT (Dosovitskiy et al., arXiv:2010.11929) showed that a pure transformer could match or beat ResNet on ImageNet at sufficient scale, and a year later Swin (Liu et al. 2021, arXiv:2103.14030) layered in hierarchical attention to win COCO/ADE20K on top of that. The research community’s working assumption became “transformers beat convnets for vision now.”

ConvNeXt (Liu et al. 2022, arXiv:2201.03545) was a deliberate stress test of that assumption. The authors started from a standard ResNet-50 and asked: which of the design differences between ResNet and Swin is actually doing the work? They proceeded by applying transformer-era choices to the ResNet one at a time and measuring each contribution in isolation. The full list:

  1. Training recipe modernization (300 epochs, AdamW, RandAug, Mixup, CutMix, label smoothing, stochastic depth, layer scale)

  2. Stage compute ratio change (from (3, 4, 6, 3) to (3, 3, 9, 3), matching Swin)

  3. “Patchify” stem (replace the 7\(\times \)7 stride-2 conv with a 4\(\times \)4 stride-4 conv, matching ViT’s patch embedding)

  4. Depthwise convolutions (split spatial and channel mixing, as in MobileNetV2)

  5. Inverted bottleneck (1\(\times \)1 expand 4\(\times \), depthwise, 1\(\times \)1 project, MobileNetV2-style)

  6. Larger kernel (depthwise 7\(\times \)7 instead of 3\(\times \)3, to approximate Swin’s window attention receptive field)

  7. Fewer activations and norms (one per block instead of one per layer, again matching transformer blocks)

  8. LayerNorm instead of BatchNorm

  9. GELU instead of ReLU

The result: a model that’s pure convolution top to bottom, doesn’t contain a single attention operation, and matches or beats Swin-T on ImageNet at the same compute budget. The conclusion of the paper isn’t “convolutions are back.” It’s “the architectural gap between ResNet and Swin was mostly the training recipe, not the receptive field.” Most of what looked like transformer superiority was modernization that hadn’t been back-ported to the convnet baseline.

8.1 Run it first

Before the two new primitives, train the network that needs them. Four commands, and the heaviest per-epoch net in the book still finishes inside an hour and a quarter:

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

On one RTX 4060 Ti (CUDA 12.9), from runs/2026-09-13-convnext-imagenette-ls1e-6-resident/. XLA’s startup banner and the resident-parameter notices are removed, the network description line is wrapped and its trailing LayerNorm note dropped, each interval is wrapped onto an indented continuation, and epochs 5 through 78 are elided:

[pjrt_ffi] XLA backend: PJRT 0.114, 1 device(s)
[pjrt_ffi] command buffers: enabled (CUDA default)
[pjrt_ffi] compiled verified_mlir/convnext_adam_train_step.mlir
             (@convnext_adam_train_step, 549 outputs, 1 replica)
             in 6361 ms
[pjrt_ffi] compiled verified_mlir/convnext_fwd.mlir
             (@convnext_fwd, 1 outputs, 1 replica) in 588 ms
ConvNeXt-T on Imagenette 224² (patchify /4 → stem channel-LN →
  [3,3,9,3] blocks @ [96,192,384,768] depthwise-7×7 + channel-LN +
  GELU + layerScale + 3 downsamples 56→7 → GAP → LN → dense)
  via the VERIFIED renderer → XLA/PJRT → GPU
  xla/pjrt verified_mlir/convnext_adam_train_step.mlir
  xla/pjrt verified_mlir/convnext_fwd.mlir
  train 9469, val 3925; bs 32, ConvNeXt-T adam
    (cosine+warmup 3ep, baseLR 0.001000), He init
Epoch 1/80: loss=2.321469 lr=0.000333
  epoch 1: val_acc = 1106/3925 = 28.178344%  top5 = 2873/3925 = 73.197452%
    [95% CI 26.79–29.61]
Epoch 2/80: loss=2.000190 lr=0.000667
  epoch 2: val_acc = 1566/3925 = 39.898089%  top5 = 3378/3925 = 86.063694%
    [95% CI 38.38–41.44]
Epoch 3/80: loss=1.675131 lr=0.001000
  epoch 3: val_acc = 2057/3925 = 52.407643%  top5 = 3601/3925 = 91.745223%
    [95% CI 50.84–53.97]
Epoch 4/80: loss=1.506907 lr=0.001000
  epoch 4: val_acc = 2226/3925 = 56.713376%  top5 = 3640/3925 = 92.738854%
    [95% CI 55.16–58.26]
Epoch 79/80: loss=0.500732 lr=0.000000
  epoch 79: val_acc = 3230/3925 = 82.292994%  top5 = 3789/3925 = 96.535032%
    [95% CI 81.07–83.46]
Epoch 80/80: loss=0.500793 lr=0.000000
  epoch 80: val_acc = 3229/3925 = 82.267516%  top5 = 3788/3925 = 96.509554%
    [95% CI 81.04–83.43]
done (trained ConvNeXt-T adam + cosine/warmup via packed threading).

Eighty epochs at about 55 seconds each, one hour and fourteen minutes in total, and 82.27% top-1 with 96.51% top-5 on Imagenette’s 3,925-image validation split. The best epoch reached \(82.32\% \). That is the lowest top-1 of the convnets in this book’s Imagenette column, from much the largest network among them, and §8-2 is about why that is the expected answer rather than a defect.

Two compile lines, where the previous three chapters each had three. ResNet-34, MobileNetV2 and EfficientNet-B0 all emit a third artifact, <slug>_fwd_eval, because BatchNorm behaves differently in training and in evaluation and the evaluation forward has to read frozen running statistics. ConvNeXt has no BatchNorm. 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 chapter’s central swap pays for itself in the build before it pays for itself in the mathematics.

Two new primitives

ConvNeXt requires two activations / normalizations that the chapters so far haven’t needed:

LayerNorm (Ba, Kiros, Hinton 2016, arXiv:1607.06450). BatchNorm normalizes per-feature across a batch: it relies on the batch having enough samples for the mean and variance to be stable. That fails for small batches (memory-constrained training) and for variable-length sequences (where “the batch axis” isn’t a clean concept). LayerNorm solves both by flipping the axis: normalize per-sample across the feature dimension instead. Same three-term Jacobian cancellation as BN (Chapter 4), same closed-form backward. Just a different reduction axis. Theorem 109 states this formally for one vector, and Theorem 110 lifts it to the channel axis of a feature map.

GELU (Hendrycks & Gimpel 2016, arXiv:1606.08415). A smooth approximation of ReLU: \(\mathrm{GELU}(x) = x \cdot \Phi (x)\), where \(\Phi \) is the standard-normal CDF. ReLU has a corner at zero, where the proven backward and the emitted mask part company (Chapter 2). GELU is differentiable everywhere, with a soft-gate behavior near zero that empirically helps transformers (and, it turns out, modernized CNNs). Diagonal activation Jacobian, same family as ReLU’s diagonal, just with a smooth scalar derivative instead of an indicator. It comes in two forms: the tanh approximation (Definition 102), which the Imagenette tier below trained on both paths, and the exact \(x \cdot \Phi (x)\) (Definition 105), which the paper’s code and the ImageNet pairs compute. Definition 108 names the choice, Theorem 111 is the VJP at either, and Theorem 112 is the exact form in the \(\mathrm{erfc}\) spelling the emitter prints.

Both primitives are used by the upcoming Vision Transformer chapter (Chapter 9) without modification. ConvNeXt introduces them here so they’re available before ViT needs them.

The ConvNeXt block

Each ConvNeXt block stacks the modernization items into a single residual unit:

\[ \text{DW } 7 \times 7 \; \to \; \text{LN} \; \to \; 1 \times 1\ \text{expand } 4\times \; \to \; \text{GELU} \; \to \; 1 \times 1\ \text{project} \; \to \; \text{LayerScale} \; \to \; +\, \text{residual}. \]

Two pieces: the inverted bottleneck (\(1 \times 1\) expand 4\(\times \), depthwise spatial conv at the wide point, \(1 \times 1\) project) is exactly the MobileNetV2 inverted residual from Chapter 6. ConvNeXt is paying the MobileNet idea forward. LayerScale (Touvron et al. 2021) is a learnable per-channel scalar \(\gamma \) initialized to \(10^{-6}\) that multiplies the block’s contribution before the residual add, keeping the block near-identity at init in the same spirit as ResNet’s residual. The spec’s .convNextStage primitive emits \(N\) of these blocks at fixed channel width. The .convNextDownsample primitive emits the LN + \(2 \times 2\) stride-2 conv that transitions between stages.

ConvNeXt-T is four stages, ResNet-style

The full ConvNeXt-T spec is the same stem–stages–head template as ResNet-34. A \(4 \times 4\) stride-4 “patchify” stem replaces the \(7 \times 7\) stride-2 stem. Four stages with block counts \((3, 3, 9, 3)\) at channel widths \((96, 192, 384, 768)\) replace the ResNet stages. A final global-average-pool plus 768-to-10 dense replaces the head. Total: 27.8M parameters, comparable to ResNet-50. Every architectural primitive in the spec is now codegen-backed and has a proved VJP. None of the math in this chapter is genuinely new beyond the LayerNorm-axis rotation and the GELU smoothness.

With a modernized convnet backbone, the dominant axis of variation between runs becomes the training recipe, not the architecture. The example section below makes that concrete. The chapter closes by putting the full recipe side-by-side with the 2018 ResNet-34 recipe of Chapter 5, knob by knob: the four-year jump in how convnets are trained, on one page.

8.2 The theorems

Figure 8-1 is the dependency graph of this section.

\begin{tikzpicture} [x=1pt,y=1pt, every node/.style={inner sep=1.10pt, font=\fontsize{5.14}{6.17}\selectfont\ttfamily}]
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=59.9pt, minimum height=15.8pt] at (159.4pt,172.1pt) {\hyperref[ax:geluScalar]{geluScalar}\\{\color{gray!60!black}\fontsize{4.04}{4.84}\selectfont\ttfamily $\to$ 8 boxes here}};
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=50.6pt, minimum height=9.4pt] at (224.8pt,172.1pt) {\hyperref[ax:geluScalarDeriv]{geluScalarDeriv}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=32.1pt, minimum height=9.4pt] at (212.3pt,136.1pt) {\hyperref[ax:pdiv_gelu]{pdiv\_gelu}};
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=59.9pt, minimum height=15.8pt] at (373.8pt,172.1pt) {\hyperref[ax:geluErfScalar]{geluErfScalar}\\{\color{gray!60!black}\fontsize{4.04}{4.84}\selectfont\ttfamily $\to$ 9 boxes here}};
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=59.9pt, minimum height=9.4pt] at (303.3pt,172.1pt) {\hyperref[ax:geluErfScalarDeriv]{geluErfScalarDeriv}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=41.4pt, minimum height=9.4pt] at (303.3pt,136.1pt) {\hyperref[ax:pdiv_geluErf]{pdiv\_geluErf}};
  \node[draw=green!50!black, fill=green!15, rectangle, align=center, minimum width=29.1pt, minimum height=9.4pt] at (135.9pt,136.1pt) {\hyperref[def:geluForm]{geluForm}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=50.6pt, minimum height=9.4pt] at (434.7pt,136.1pt) {\hyperref[thm:layerNormHasVJP]{layerNormHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=63.0pt, minimum height=9.4pt] at (87.5pt,103.2pt) {\hyperref[thm:chanLNTensor3HasVJP]{chanLNTensor3HasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=35.2pt, minimum height=9.4pt] at (244.6pt,103.2pt) {\hyperref[thm:geluHasVJP]{geluHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=63.0pt, minimum height=9.4pt] at (314.3pt,70.4pt) {\hyperref[thm:geluErf_as_computed]{geluErf\_as\_computed}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=59.9pt, minimum height=9.4pt] at (210.1pt,37.5pt) {\hyperref[thm:convnextFullHasVJP]{convnextFullHasVJP}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=63.0pt, minimum height=9.4pt] at (152.8pt,70.4pt) {\hyperref[thm:convnext_whole_back]{convnext\_whole\_back}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=56.8pt, minimum height=9.4pt] at (39.1pt,37.5pt) {\hyperref[thm:convnext_step_tie]{convnext\_step\_tie}};
  \node[draw=green!50!black, fill=green!15, rectangle, rounded corners=2.9pt, align=center, minimum width=59.9pt, minimum height=9.4pt] at (83.8pt,4.7pt) {\hyperref[thm:convnext_loss_grad]{convnext\_loss\_grad}};
  \node[draw=gray!70, dashed, rounded corners=2pt, minimum width=40.7pt, minimum height=13.0pt, text=gray!60!black, align=center, font=\fontsize{4.04}{4.84}\selectfont\ttfamily] at (434.7pt,172.1pt) {2 statements of\\ch 1, 4};
  \draw[->, >=stealth, black!70, line width=0.29pt] (223.2pt,167.1pt) .. controls (221.6pt,162.6pt) and (218.9pt,155.3pt) .. (216.7pt,149.0pt) -- (213.8pt,141.1pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (226.0pt,167.2pt) .. controls (228.9pt,157.1pt) and (236.5pt,131.6pt) .. (241.0pt,116.2pt) -- (243.3pt,108.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (216.5pt,131.2pt) .. controls (221.0pt,126.9pt) and (228.2pt,119.9pt) .. (234.2pt,114.0pt) -- (240.2pt,108.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (303.3pt,167.1pt) .. controls (303.3pt,162.7pt) and (303.3pt,155.6pt) .. (303.3pt,149.4pt) -- (303.3pt,141.1pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (299.6pt,167.2pt) .. controls (290.6pt,156.8pt) and (266.9pt,129.6pt) .. (253.8pt,114.5pt) -- (248.2pt,108.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (308.7pt,167.1pt) .. controls (314.9pt,161.7pt) and (324.9pt,151.8pt) .. (329.0pt,140.7pt) .. controls (336.2pt,121.0pt) and (327.4pt,96.8pt) .. (320.6pt,82.8pt) -- (316.8pt,75.3pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (310.5pt,167.2pt) .. controls (318.7pt,162.1pt) and (331.7pt,152.6pt) .. (338.5pt,140.7pt) .. controls (355.4pt,111.4pt) and (372.5pt,91.8pt) .. (351.0pt,65.7pt) .. controls (338.2pt,50.2pt) and (285.8pt,43.4pt) .. (248.6pt,40.4pt) -- (240.3pt,39.8pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (279.1pt,167.1pt) .. controls (250.4pt,161.6pt) and (204.6pt,151.5pt) .. (191.0pt,140.7pt) .. controls (172.1pt,125.8pt) and (161.2pt,98.8pt) .. (156.2pt,83.2pt) -- (153.9pt,75.3pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (274.4pt,167.1pt) .. controls (225.7pt,159.7pt) and (131.0pt,145.2pt) .. (116.1pt,140.7pt) .. controls (81.9pt,130.5pt) and (65.3pt,135.6pt) .. (42.7pt,107.9pt) .. controls (29.5pt,91.7pt) and (32.5pt,65.6pt) .. (35.8pt,50.3pt) -- (37.8pt,42.3pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (316.6pt,167.2pt) .. controls (339.6pt,159.0pt) and (384.0pt,138.7pt) .. (384.0pt,103.9pt) .. controls (384.0pt,103.9pt) and (384.0pt,103.9pt) .. (384.0pt,69.6pt) .. controls (384.0pt,16.7pt) and (203.1pt,7.2pt) .. (122.2pt,5.7pt) -- (114.0pt,5.5pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (295.4pt,131.1pt) .. controls (286.3pt,126.2pt) and (271.1pt,118.1pt) .. (259.7pt,112.0pt) -- (252.6pt,108.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (150.8pt,131.0pt) .. controls (169.2pt,125.7pt) and (200.6pt,116.7pt) .. (222.0pt,110.5pt) -- (229.9pt,108.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (140.2pt,131.2pt) .. controls (149.7pt,121.8pt) and (173.3pt,97.9pt) .. (189.5pt,75.1pt) .. controls (195.2pt,67.1pt) and (200.5pt,57.4pt) .. (204.4pt,50.0pt) -- (208.1pt,42.5pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (137.0pt,131.1pt) .. controls (139.6pt,121.5pt) and (145.8pt,97.7pt) .. (149.7pt,83.1pt) -- (151.8pt,75.1pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (121.1pt,135.0pt) .. controls (101.8pt,133.7pt) and (68.6pt,128.5pt) .. (50.8pt,107.9pt) .. controls (37.0pt,91.9pt) and (36.4pt,65.9pt) .. (37.6pt,50.6pt) -- (38.4pt,42.3pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (121.1pt,134.4pt) .. controls (101.3pt,132.4pt) and (66.0pt,126.3pt) .. (42.7pt,107.9pt) .. controls (16.4pt,87.0pt) and (13.9pt,74.7pt) .. (5.3pt,42.2pt) .. controls (0.0pt,22.2pt) and (22.9pt,13.1pt) .. (45.3pt,8.9pt) -- (53.5pt,7.6pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (89.6pt,98.4pt) .. controls (93.6pt,90.8pt) and (103.3pt,74.4pt) .. (116.1pt,65.7pt) .. controls (132.6pt,54.5pt) and (153.8pt,47.8pt) .. (171.8pt,43.8pt) -- (179.9pt,42.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (96.3pt,98.3pt) .. controls (106.5pt,93.3pt) and (123.6pt,85.1pt) .. (136.3pt,79.0pt) -- (143.9pt,75.4pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (84.3pt,98.3pt) .. controls (76.8pt,88.3pt) and (57.9pt,63.2pt) .. (47.0pt,48.8pt) -- (42.1pt,42.2pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (87.3pt,98.3pt) .. controls (86.8pt,84.5pt) and (85.1pt,40.1pt) .. (84.3pt,18.0pt) -- (84.0pt,9.7pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (254.0pt,98.3pt) .. controls (265.1pt,93.3pt) and (283.7pt,84.9pt) .. (297.3pt,78.8pt) -- (304.8pt,75.4pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (243.4pt,98.2pt) .. controls (241.3pt,91.2pt) and (236.7pt,76.8pt) .. (230.6pt,65.7pt) .. controls (227.3pt,59.7pt) and (222.8pt,53.5pt) .. (218.9pt,48.5pt) -- (213.7pt,42.3pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (232.0pt,98.2pt) .. controls (216.8pt,93.0pt) and (191.1pt,84.2pt) .. (173.1pt,78.1pt) -- (165.2pt,75.4pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (239.8pt,98.3pt) .. controls (230.7pt,90.5pt) and (210.0pt,74.0pt) .. (189.5pt,65.7pt) .. controls (152.6pt,50.8pt) and (107.3pt,43.9pt) .. (76.2pt,40.8pt) -- (67.7pt,40.0pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (246.2pt,98.4pt) .. controls (250.7pt,86.7pt) and (261.7pt,52.2pt) .. (245.3pt,32.8pt) .. controls (229.9pt,14.6pt) and (164.9pt,8.5pt) .. (122.3pt,6.4pt) -- (114.1pt,6.1pt);
  \draw[->, >=stealth, black!70, line width=0.29pt] (45.1pt,32.6pt) .. controls (51.7pt,27.9pt) and (62.6pt,20.3pt) .. (71.1pt,14.3pt) -- (77.9pt,9.6pt);
  \draw[->, >=stealth, dashed, gray!60, line width=0.29pt] (434.7pt,165.4pt) .. controls (434.7pt,161.0pt) and (434.7pt,154.8pt) .. (434.7pt,149.4pt) -- (434.7pt,141.0pt);
\end{tikzpicture}
Figure 8-1 Dependency graph of the LayerNorm chapter’s theorems.
Definition 102 GELU scalar function, tanh form
✓
#

The tanh approximation, jax.nn.gelu’s default and the form every Imagenette-tier run in this chapter trained: \(\mathrm{geluScalar}(x) = 0.5\, x\, \bigl(1 + \tanh (\sqrt{2/\pi }\, (x + 0.044715\, x^3))\bigr)\). The exact \(x \cdot \Phi (x)\) is Definition 105; the codegen emits either, and Theorem 111 is the VJP of both.

Definition 103 GELU scalar derivative
✓
#

Defined as Mathlib’s \(\mathrm{deriv}\, \mathrm{geluScalar}\) — so the connection to the forward is automatic, not asserted. The closed form (what the verified geluBack emitter renders) is the separate theorem geluScalarDeriv_eq, proved by assembling HasDerivAt for the inner polynomial, \(\tanh \) (via \(\tanh = \sinh /\cosh \) and the quotient rule), and the outer product.

Theorem 104 GELU Jacobian
✓

prove: \(\operatorname {pdiv}(\mathrm{gelu})\, x\, i\, j = \delta _{ij} \cdot \mathrm{geluScalar}'(x_i)\) — the diagonal activation Jacobian.

Proof ▶

Sketch: the elementwise rule (pdiv_elementwise), the template for every activation with a smooth scalar function — a scalar function applied to every coordinate has a diagonal Jacobian with the scalar derivative on the diagonal.

  1. Coordinate \(k\) of \(\mathrm{gelu}\) is \(\mathrm{geluScalar} \circ \mathrm{proj}_k\), whose Fréchet derivative at \(x\) is \(\mathrm{geluScalar}'(x_k) \cdot \mathrm{proj}_k\).
    proof: \(\mathrm{geluScalar}\) is differentiable everywhere (tanh is, via \(\tanh = \sinh /\cosh \) with \(\cosh {\gt} 0\)); HasDerivAt.comp_hasFDerivAt with the projection, a continuous linear map and its own derivative.

  2. The whole map has Fréchet derivative \(\mathrm{pi}\, \bigl(k \mapsto \mathrm{geluScalar}'(x_k) \cdot \mathrm{proj}_k\bigr)\).
    proof: hasFDerivAt_pi assembles the coordinates of 1.

  3. q.e.d.
    proof: Evaluate 2 at \((\mathbf{e}_i, j)\) (pdiv_eq_of_hasFDerivAt): \(\mathrm{proj}_j(\mathbf{e}_i) = \delta _{ij}\), giving \(\delta _{ij} \cdot \mathrm{geluScalar}'(x_i)\) (Definition 103).

Definition 105 GELU scalar function, exact form
✓

GELU as Hendrycks and Gimpel define it:

\[ \mathrm{geluErfScalar}(x) = x \cdot \Phi (x), \qquad \Phi (x) = \tfrac 12 + \int _0^x \varphi , \qquad \varphi (t) = \frac{e^{-t^2/2}}{\sqrt{2\pi }}, \]

with \(\Phi \) the standard-normal CDF and \(\varphi \) its density. This is PyTorch’s nn.GELU and jax.nn.gelu(approximate=False), the form the ConvNeXt and DeiT recipes train, and what the ImageNet pairs of this chapter and the next compute on both paths. The CDF is written as a closed form over the interval integral, so that \(\Phi ' = \varphi \) is the fundamental theorem of calculus at a continuous integrand (hasDerivAt_gaussPhi) rather than a fact about a measure.

Definition 106 GELU scalar derivative, exact form
✓

Mathlib’s \(\mathrm{deriv}\, \mathrm{geluErfScalar}\), as the tanh form’s is. The closed form is the theorem geluErfScalarDeriv_eq: \(\mathrm{geluErfScalar}'(x) = \Phi (x) + x\, \varphi (x)\), the product rule on \(x \cdot \Phi (x)\) with \(\Phi ' = \varphi \).

Theorem 107 GELU Jacobian, exact form
✓

prove: \(\operatorname {pdiv}(\mathrm{geluErf})\, x\, i\, j = \delta _{ij} \cdot \mathrm{geluErfScalar}'(x_i)\).

Proof ▶

Theorem 104’s three steps at \(\mathrm{geluErfScalar}\) in place of \(\mathrm{geluScalar}\); the differentiability witness is \(x \cdot \Phi (x)\)’s, a product of the identity and a function whose derivative the fundamental theorem of calculus supplies. q.e.d.

Definition 108 Either GELU
✓

\(\mathrm{GeluForm}\) has two values, tanh and erf; \(\mathrm{scalar}\) sends them to Definitions 102 and 105, and \(\mathrm{map}\) applies the chosen scalar componentwise, which at each value is the tanh \(\mathrm{gelu}\) and \(\mathrm{geluErf}\) by rfl. Every ConvNeXt and ViT definition and theorem from here on takes a \(\mathrm{GeluForm}\), so the renders that trained the tanh form and the ones that train the exact form are instances of one statement, and no proof outside the GELU’s own three files cases on which. The StableHLO GELU ops carry the same value, and the exact one emits through chlo.erfc (Theorem 112).

Theorem 109 LayerNorm VJP
✓
#

assume:

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

prove: \(\mathsf{HasVJP}\, (\mathrm{layerNormForward})\).

Proof ▶
  1. \(\mathrm{layerNormForward} = \mathrm{bnForward}\), definitionally: on a single feature vector, LayerNorm is the 1D normalization primitive — the BN/LN distinction (which axis, running statistics) is an engineering concern that dissolves at the per-vector math level.

  2. q.e.d.
    proof: Theorem 50 applies verbatim, with assumption 1 as its \(\varepsilon {\gt} 0\) hypothesis.

Theorem 110 Channel-LayerNorm VJP
✓

assume:

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

prove: \(\mathsf{HasVJP}\, (\texttt{chanLNTensor3}\; c\; h\; w\; \varepsilon \; \gamma \; \beta )\): LayerNorm over the channel axis at every pixel of a \((c, h, w)\) tensor, with per-channel \(\gamma , \beta \). The ConvNeXt-T chain’s twenty-two channel-LayerNorms are this witness; the head’s, after the pool, is the vector LayerNorm (Theorem 142).

Proof ▶

vjpComp over five proven pieces: reassociate, transpose, the row-wise vector LayerNorm (Theorem 109 at \(\gamma = 1, \beta = 0\), then the per-channel scale and shift), transpose back, reassociate back; see Proofs.chanLNTensor3HasVJP.

For every \(gf : \mathrm{GeluForm}\), \(\mathsf{HasVJP}\, (gf.\mathrm{map})\), with backward \(B(x, dy)_i = dy_i \cdot gf.\mathrm{scalar}'(x_i)\). At tanh the record is \(\mathrm{geluHasVJP}\) and at erf it is \(\mathrm{geluErfHasVJP}\), each by rfl; the whole-net theorems below and in Chapter 9 use it at the variable \(gf\).

Proof ▶
  1. suffices: \(B(x, dy)_i = \sum _j \operatorname {pdiv}(gf.\mathrm{map})\, x\, i\, j \cdot dy_j\).
    proof: Definition 10.

  2. q.e.d.
    proof: The Jacobian of \(gf.\mathrm{map}\) is diagonal at either form (Theorems 104 and 107, one proof over \(gf.\mathrm{scalar}\)), so the sum collapses at \(j = i\) to \(gf.\mathrm{scalar}'(x_i) \cdot dy_i\). Same template as ReLU, Swish, h-swish: diagonal Jacobian \(\Rightarrow \) one-step VJP.

Mathlib has no error function, so the book defines \(\mathrm{erf}(z) = \tfrac {2}{\sqrt\pi }\int _0^z e^{-t^2}\, dt\) and \(\mathrm{erfc} = 1 - \mathrm{erf}\). prove: with \(z = -x\sqrt{1/2}\),

\[ \Phi (x) = \tfrac 12\, \mathrm{erfc}(z), \qquad \mathrm{geluErfScalar}(x) = (0.5\, x)\, \mathrm{erfc}(z), \]
\[ \mathrm{geluErfScalar}'(x) = 0.5\, \mathrm{erfc}(z) + \tfrac {2}{\sqrt\pi }\, (0.5\, x)\, e^{-z^2}\sqrt{1/2}, \]

which are, term for term, the arithmetic of jax.nn.gelu(approximate=False) and of its jax.vjp, and the text the verified emitter prints around its one chlo.erfc. The backward bridge geluErf_back_bridge pins the emitted diagonal to \(\mathrm{geluErfHasVJP}\)’s backward by rfl.

Proof ▶
  1. \(\Phi (x) = \tfrac 12\, (1 + \mathrm{erf}(x/\sqrt2))\).
    proof: Substitute \(t = \sqrt2\, s\) in \(\int _0^x \varphi \) (intervalIntegral.integral_comp_div).

  2. \(\Phi (x) = \tfrac 12\, \mathrm{erfc}(z)\).
    proof: \(\mathrm{erf}\) is odd, its integrand being even, so \(1 + \mathrm{erf}(x/\sqrt2) = 1 - \mathrm{erf}(z) = \mathrm{erfc}(z)\). As real numbers the two spellings are equal; in floats the \(\mathrm{erfc}\) one keeps the negative tail that \(1 + \mathrm{erf}\) cancels away, which is why the emitter, like JAX, prints it.

  3. q.e.d.
    proof: The forward is 2 times \(x\); the derivative is Definition 106’s \(\Phi (x) + x\, \varphi (x)\) with \(\Phi \) by 2 and \(\varphi (x) = e^{-z^2}/\sqrt{2\pi }\) regrouped as the derivative of \(\mathrm{erfc}\) at \(z\) times \(dz/dx\) (field_simp, ring).

Four rungs of the ladder for ConvNeXt-T: the whole net has the VJP and the forward graph denotes it; the rendered backward is that VJP; the emitted step is the certified step; each node is the loss’s derivative.

assume:

  1. \(\varepsilon {\gt} 0\) at the stem’s, the four stages’, the three downsamples’ and the head’s LayerNorms, twenty-three in all [hs\(\varepsilon \), h1–h4, hd1–hd3, hh\(\varepsilon \)]

prove: the backward composed from the per-stage backwards of ConvNeXt-T — the \([3,3,9,3]\) stages at widths \(96\) to \(768\) with channel LayerNorm (convNextForwardTCh) — equals the \(\operatorname {pdiv}\)-contracted Jacobian of the forward at every input, cotangent and pixel, at either GELU form; and the typed forward graph denotes exactly that forward, with no hypothesis, and at the committed ten-class spec the function the spec denotes. Stated per example, on the drop-free f32 chain: the batched bf16 steps have no typed forward graph, and their backward is tied by the next rung.

Proof ▶

A fold of the VJP chain rule (Theorem 11) over the stem, the twelve stages and downsamples (Theorems 110 and 111 inside each block) and the head; the graph ties one rewrite per stage; see Proofs.convNextForwardTChHasVJP_correct.

assume:

  1. \(\varepsilon {\gt} 0\) at the stem’s, the four stages’, the three downsamples’ and the head’s LayerNorms, twenty-three in all [hs\(\varepsilon \), h1–h4, hd1–hd3, hh\(\varepsilon \)]

prove: the twelve-stage ConvNeXt-T input-gradient chain convnextInputGradB equals the backward of the batched forward,

\[ \sum _j \operatorname {pdiv}(\mathrm{batchMap}\, B\; \texttt{convNextForwardTCh})\, x\, i\, j \cdot dy_j , \]

for every batch, number of classes and cotangent, at either GELU form.

Proof ▶

The chain is the composed backward read stage by stage, each stage’s VJP the block’s, the downsample’s or the head’s; see Proofs.convnextInputGradB_correct. One wrinkle is ConvNeXt’s alone among the chapter nets: the \(4\times 4\) stem and the three \(2\times 2\) downsamples are even kernels, and the reversed-kernel convolution of Chapter 3 is the adjoint of a convolution shifted by one pixel there. The chain therefore takes those four kernels zero-extended to \(5\times 5\) and \(3\times 3\) (padOdd), which computes the same forward (conv2d_padOdd_eq) and is exactly the asymmetric pad the emitter writes for a strided backward.

prove: each of the 182 parameter gradients of the batched ConvNeXt-T step (\([3, 3, 9, 3]\) blocks, channel-LayerNorm at twenty-two sites, per-channel layer scale, GELU at either form) denotes the certified batch gradient at the chain cotangent of the label-smoothed cross-entropy, with the batch and the number of classes binders: convnext_adam_train_step.mlir and the convnextin_* steps emit these nodes. Stated at one replica, at either precision, with stochastic depth as an optional mask at each block’s one site, between the layer scale and the skip: at none the chain is the drop-free artifacts’, at some the drop renders’, where every node of a block reads the cotangent scaled by its example’s mask and the skip reads it raw.

Proof ▶

A fold over the 182 nodes, the chain’s cotangents the certified block, downsample and head backwards; see Proofs.CnxTieGB.cnx_net_tiedGB.

Stated at one replica, and what the data-parallel render adds. The replicas \({\gt} 1\) renders wrap each of these 182 nodes in an all-reduce mean, which denotes the mean of the replicas’ nodes (Proofs.den_allReduceMeanF_eq_dpMean). ConvNeXt has no BatchNorm, so its batch loss is a mean of per-example losses, and for such a loss the mean over replicas of the gradient is the gradient at the combined batch (Proofs.dpMeanGrad_eq_globalBatchGrad_of_perExample). That is the fact the sync-BN theorems of Chapters 5–7 prove node by node for the five BatchNorm nets, where the statistics all-reduce makes it non-trivial. Here its composition with these nodes is not stated as a theorem; the collectives are gated by the two checks of §8.5 instead.

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 115, the node at the chain cotangent from \(g\) is \(\partial L/\partial \theta \) of the batched ConvNeXt-T forward at the same mask (cnxNetB; at none, convNextForwardTCh applied per example) with that one parameter varied, for every batch and number of classes. Instantiated at the label-smoothed cross-entropy for targets summing to one.

Proof ▶

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

8.3 Example: ConvNeXt-T on Imagenette

The architecture

ConvNeXt-Tiny replaces the convolutional stages with .convNextStage blocks at four widths (\(96, 192, 384, 768\)) and depths (\(3, 3, 9, 3\)), glued by .convNextDownsample transitions (LayerNorm \(+\) \(2 \times 2\) stride-2 conv). A \(4 \times 4\) stride-4 patchify stem and a global-average-pool head bracket the stack. Figure 8-2 is the network, and its inset shows one ConvNeXt block: an inverted bottleneck with a single LayerNorm and GELU, scaled by a learnable \(\gamma \) before the residual add.

\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},
  convbn/.style = {col, draw=orange!65!black, fill=orange!12},
  blk/.style    = {col, draw=brown!70!black,  fill=brown!12},
  norm/.style   = {col, draw=teal!60!black,   fill=teal!10},
  gap/.style    = {col, draw=purple!60!black, fill=purple!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[convbn, below=0.16cm of input] (stem) {\textbf{ConvBN} patchify $3\to96$, $4\times4$, /4 \;\; $56{\times}56$};
  \node[blk, below=0.16cm of stem]   (c1)   {$3\times$ \textbf{ConvNeXt} block @\,96 \;\; $56{\times}56$};
  \node[norm, below=0.16cm of c1]    (n1)   {downsample $96\to192$ (LN $+$ $2\times2$/2) \;\; $28{\times}28$};
  \node[blk, below=0.16cm of n1]     (c2)   {$3\times$ \textbf{ConvNeXt} block @\,192 \;\; $28{\times}28$};
  \node[norm, below=0.16cm of c2]    (n2)   {downsample $192\to384$ \;\; $14{\times}14$};
  \node[blk, below=0.16cm of n2]     (c3)   {$9\times$ \textbf{ConvNeXt} block @\,384 \;\; $14{\times}14$};
  \node[norm, below=0.16cm of c3]    (n3)   {downsample $384\to768$ \;\; $7{\times}7$};
  \node[blk, below=0.16cm of n3]     (c4)   {$3\times$ \textbf{ConvNeXt} block @\,768 \;\; $7{\times}7$};
  \node[gap, below=0.16cm of c4]     (g)    {global avg pool \;\; $7\times7\times768 \to 768$};
  \node[head, below=0.16cm of g]     (d)    {\textbf{Dense} $768\to10$ \;(identity)};
  \node[logits, below=0.16cm of d]   (out)  {Logits \;\; 10 classes, softmax-CE};
  \foreach \a/\b in {input/stem,stem/c1,c1/n1,n1/c2,c2/n2,n2/c3,c3/n3,n3/c4,c4/g,g/d,d/out}
     \draw[arr](\a)--(\b);
  \foreach \n/\k in {c1/1,c2/2,c3/3,c4/4}\node[stage] at ($(\n.east)+(0.30,0)$){stage \k};
\end{tikzpicture}
\begin{tikzpicture} [
  >={Stealth[length=1.6mm]},
  every node/.style={font=\sffamily\scriptsize},
  box/.style  = {align=center, rounded corners=2pt, inner sep=2.5pt, minimum height=0.58cm, draw=brown!70!black, fill=brown!12},
  dot/.style  = {circle, draw=brown!70!black, fill=brown!16, inner sep=0pt, minimum size=0.42cm},
  term/.style = {font=\sffamily\scriptsize\itshape, inner sep=1pt},
  arr/.style  = {->, thick, gray!65, shorten >=1pt, shorten <=1pt},
  skip/.style = {->, thick, brown!70!black, shorten >=1pt},
]
  \node[term] (x){$x$};
  \node[box, right=0.4cm of x]  (dw){DW $7\!\times\!7$};
  \node[box, right=0.32cm of dw](ln){LN};
  \node[box, right=0.32cm of ln](ex){$1\!\times\!1$\\$\uparrow4\times$};
  \node[box, right=0.32cm of ex](ge){GELU};
  \node[box, right=0.32cm of ge](pr){$1\!\times\!1$\\proj};
  \node[box, right=0.32cm of pr](ls){LayerScale\\$\gamma$};
  \node[dot, right=0.4cm of ls] (sum){$+$};
  \node[term, right=0.4cm of sum](y){$y$};
  \foreach \a/\b in {x/dw,dw/ln,ln/ex,ex/ge,ge/pr,pr/ls,ls/sum,sum/y}\draw[arr](\a)--(\b);
  \draw[skip] (x.north) .. controls +(0,0.95) and +(0,0.95) .. (sum.north)
        node[midway, above, term, brown!70!black]{residual};
  \node[term, below=0.16cm of ge, gray!55!black]{one ConvNeXt block: inverted bottleneck, one norm $+$ one activation};
\end{tikzpicture}
Figure 8-2 ConvNeXt-T: a patchify stem, four stages of ConvNeXt blocks joined by downsampling layers, and a pooled head. The inset is one ConvNeXt block.

The verified trainer

The pattern is §5-2’s, minus bnChannels: 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 convnextVerified : VerifiedNetSpec where
  name     := "ConvNeXt-T"
  slug     := "convnext"
  inC      := 3
  imageH   := 224
  imageW   := 224
  data     := .imagenette
  layers   := [
    .conv 3 96 4 4, .layerNorm 96,       -- patchify 4x4/s4 + stem LN
    .convNextBlockCh 96,  .convNextBlockCh 96,  .convNextBlockCh 96,
    .layerNorm 96,  .conv 96 192 2 2,    -- downsample  96->192, 56->28
    .convNextBlockCh 192, .convNextBlockCh 192, .convNextBlockCh 192,
    .layerNorm 192, .conv 192 384 2 2,   -- downsample 192->384, 28->14
    .convNextBlockCh 384, .convNextBlockCh 384, .convNextBlockCh 384,
    .convNextBlockCh 384, .convNextBlockCh 384, .convNextBlockCh 384,
    .convNextBlockCh 384, .convNextBlockCh 384, .convNextBlockCh 384,
    .layerNorm 384, .conv 384 768 2 2,   -- downsample 384->768, 14->7
    .convNextBlockCh 768, .convNextBlockCh 768, .convNextBlockCh 768,
    .globalAvgPool, .layerNorm 768, .dense 768 10 ]   -- head: GAP -> LN -> dense
  -- Stochastic depth, read only by the `*drop` variants: one global
  -- block index over the eighteen blocks, keep_i = 1 - 0.1*i/17.
  dropSites := Array.range 18
  dropDenom := 17
  dropRate  := 0.1
-- 2. The schedule. ResNet-34's, MobileNetV2's and B0's, unchanged.
def convnextAdamConfig : VerifiedConfig where
  epochs    := 80
  batchSize := 32
-- 3. The entry point: AdamW, cosine with a 3-epoch warmup, baseLR 1e-3.
--    The EMA decay and the drop rate are env knobs.
def runConvNeXtAdam (argv : List String) : IO Unit := do
  let variant := (← IO.getEnv "LEAN_MLIR_VARIANT").getD "adam"
  let emaDecay := match (← IO.getEnv "LEAN_MLIR_EMA_DECAY_U").bind (·.toNat?) with
    | some u => u.toFloat * 1e-6
    | none   => 0.9999
  let dropNet := match (← IO.getEnv "LEAN_MLIR_DROP_RATE_U").bind (·.toNat?) with
    | some u => { convnextVerified.toNet with
                  dropKeeps := convnextVerified.dropKeepsAt (u.toFloat * 1e-6) }
    | none   => convnextVerified.toNet
  dropNet.trainAdamSched convnextAdamConfig
    (argv.head?.getD "data") 0.001 0.9 0.999 3 variant 0.0 1.0 emaDecay
def main (argv : List String) : IO Unit := runConvNeXtAdam argv

The structure follows the paper’s macro-design prescription: 4 stages with block counts \((3, 3, 9, 3)\) and channels doubling on every downsample \((96 \to 192 \to 384 \to 768)\), with the heavy 9-block stage at the \(14 \times 14\) / 384-channel resolution. The stem is a \(4 \times 4\) stride-4 conv (mirrored from ViT’s patch-embed), not the \(7 \times 7\) stride-2 stem of a ResNet, and that is one of the nine modernization items listed at the top of the chapter. The base recipe — AdamW at \(10^{-3}\), cosine with a three-epoch warmup, weight decay \(10^{-4}\), label smoothing \(0.1\) — is Chapter 5’s (§5.5.2 measures each knob), so the comparison across architectures is of the architecture.

.convNextBlockCh c is one block per line, eighteen of them. Ch names the norm: the LayerNorm inside the block reduces over the c channels at each spatial position, which is ConvNeXt’s real normalization rather than the scalar reduction the bare name suggests.

The arm, and the one tensor with no partner.

  | layerNorm d      => #[(#[d],1),(#[d],2)]           -- Ch. 8: gamma, beta
  | convNextBlockCh c =>                               -- Ch. 8
    #[(#[c,1,7,7],0),(#[c],2),(#[c],1),(#[c],2),       -- dw 7x7 + LN(gamma,beta)
      (#[4*c,c,1,1],0),(#[4*c],2),                     -- expand 1x1 (4x)
      (#[c,4*c,1,1],0),(#[c],2),                       -- project 1x1
      (#[c],3)]                                        -- layer scale, init 1e-6

Eight of those nine tensors pair off into weight-and-bias or \(\gamma \)-and-\(\beta \). The ninth, the trailing (#[c],3), has no partner: layer scale is a single learned per-channel multiplier with nothing added after it, and kind 3 is its own initialiser, the paper’s \(10^{-6}\), so a fresh block starts near the identity on its residual branch. It is the smallest parameter in the chapter and the one whose backward is a pure diagonal.

The wrong init is worth three points here. Until 2026-09-13 the verified path initialised layer scale to ones, and the same render on the same recipe landed at \(85.45\% \) against the \(82.27\% \) above, resolved at \(\pm 1.2\) per run (runs/2026-08-30-convnext-imagenette-headln/). The two are level through epoch 10 and three to four points apart from epoch 20 on (\(80.87\) against \(77.43\) at 20, \(84.18\) against \(80.03\) at 40): with \(\gamma \) at \(10^{-6}\) every residual branch’s gradient is scaled by \(\gamma \), so the branches train late, and 9,469 images over 80 epochs never make the time up. The recipe this init belongs to is 300 epochs on 1.28M images, where a late start costs nothing, and §8.5 is that regime. The book keeps the paper’s value because it is the paper’s and the JAX reference’s; the ones run is the better number on this recipe and not the paper’s initialisation.

The stem and the downsamples are spelled out, not packaged. A .convNextDownsample 96 192 is two entries here, a .layerNorm 96 and a .conv 96 192 2 2, and the patchify stem is likewise a .conv followed by a .layerNorm. The spec’s job is the parameter layout, not the block vocabulary, so it names weights rather than concepts.

182 parameter tensors, 27,827,818 scalars. convnextVerified.toSpecs is kernel-#guarded against ConvNeXtLayout.specs, an independently audited hand-list, and the ImageNet spec of §8.5 is pinned to this one at everything but its head. At 1000 classes the count is 28,589,128, which is timm’s figure for convnext_tiny exactly.

Twenty-three LayerNorms are the entire hypothesis budget. One in the stem, one per block, one per downsample, and one on the head between the pool and the classifier. Theorems 110 and 142 want \(\varepsilon {\gt} 0\) at each, and those 23 positivities are the only assumptions anywhere in the chain.

Results

§8.1 has the run: 80 epochs at about 55 seconds each, one hour and fourteen minutes, \(\mathbf{82.27\% }\) top-1 and \(\mathbf{96.51\% }\) top-5, with the loss settling on the same \({\sim }0.50\) label-smoothing floor as the three chapters before it. Table 8-1 is the four-chapter comparison — same dataset, same recipe, same eighty epochs, and every row a measurement on the verified XLA path, on one RTX 4060 Ti.

Table 8-1 The four convolutional networks on Imagenette: 80 epochs each on the verified path, on one card, columns as in Table 7-1. ResNet-34’s row is the five-seed mean of §5.5.2 (runs/2026-09-12-r34-ablation-fp32-seeds/) with one seed’s timing.

Model

Params

MLIR

Step time

Total

Top-1

Top-5

ResNet-34

21.29M

754 KB

120 ms

47 min

89.99%

98.46%

MobileNetV2

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%

  • The largest convnet in the column finishes last among them, and by a clear margin: \(82.27\% \) against \(89.99 \pm 0.32\) for ResNet-34 and \(90.04\% \) for EfficientNet-B0, from \(1.3\times \) and \(6.9\times \) their parameters. Three things contribute. ConvNeXt-T carries 27.83M parameters against 9,469 training images, which is not the data scale that capacity wants. The recipe here is the shared one, AdamW at \(10^{-3}\) with cosine and label smoothing, which is deliberately not the recipe ConvNeXt was designed around. And its layer scale starts at the paper’s \(10^{-6}\), which on this short schedule is worth three points on its own (above).

  • That is the chapter’s thesis arriving as a result rather than a claim. ConvNeXt’s paper argues the modernization lives in the training recipe, so a ConvNeXt run on somebody else’s recipe should underperform, and it does. §8.5 is the control: the same architecture, given the full DeiT pack and 300 epochs, reaches \(81.53\% \) on ImageNet-1k and beats every other network in this book. The architecture was never the variable.

  • It is the slowest per step, at 188 ms against ResNet-34’s 120 and EfficientNet-B0’s 103. The depthwise \(7 \times 7\) and the channel LayerNorms touch every spatial position twice per block, and the \(4\times \) expansion makes its \(1 \times 1\)s the largest matmuls in the column.

  • The MLIR is mid-pack at 1,019 KB, above ResNet-34’s 754 and below MobileNetV2’s 1,047 and EfficientNet-B0’s 1,316. Block count is not what separates them, since ConvNeXt’s eighteen is more than either. What separates them is what one block costs to emit: ConvNeXt spends three convolutions and a single LayerNorm per block, while an inverted residual spends three convolutions and three BatchNorms, and an MBConv adds the squeeze-excitation pair on top of that. One normalization per block instead of one per layer was item seven on the modernization list, and it is visible in the file size.

8.4 MLIR: Layer Scale

Layer scale is a per-channel learnable diagonal, \(\mathrm{layerScale}(\gamma , x) = \gamma \odot x\), and it is the simplest operator in this series. Its Jacobian is diagonal, so it is its own adjoint: layerScaleHasVJP proves \(\mathrm{back}(x, dy) = \gamma \odot dy\), the backward multiplying by the same \(\gamma \) the forward did. There is nothing to bridge past that identity, so the emitted graph is two multiplys against one tensor. Here they are in verified_mlir/convnext_adam_train_step.mlir, at the last block of the third stage (s3b2: 768 channels at \(7\times 7\), batch 32; SSA names the artifact’s, lines wrapped):

%v1445 = stablehlo.broadcast_in_dim %s3b2lg, dims = [1]
      : (tensor<768xf32>) -> tensor<32x768x7x7xf32>
%v1446 = stablehlo.multiply %v1444, %v1445 : tensor<32x768x7x7xf32>
// ...
%v1450 = stablehlo.add %v1448, %v1449 : tensor<32x768x7x7xf32>
// ...
%v1561 = stablehlo.broadcast_in_dim %s3b2lg, dims = [1]
      : (tensor<768xf32>) -> tensor<32x768x7x7xf32>
%v1562 = stablehlo.multiply %v1560, %v1561 : tensor<32x768x7x7xf32>

The forward broadcasts \(\gamma \) (%s3b2lg) along the channel axis and multiplies it into the projection’s output %v1444 (%v1446); %v1450 is the identity skip, the residual fan-in of Theorem 64. The backward broadcasts the same \(\gamma \) again and multiplies it into the block’s output cotangent %v1560 (%v1562): a diagonal map is its own transpose, so its VJP is itself, and the two multiplys are the same operation against the same tensor. cnxBlockCotInB_eq_vjp is the theorem that the block’s backward, this multiply and the GELU, LayerNorm and convolution backwards below it, denotes the certified block VJP, and Theorem 115 threads it through the eighteen blocks. One honesty note on %v1560: this is the last block, so its output cotangent comes down from the head, and the global-average-pool backward there is the one hand-written region of the file, declared in the file’s own header comment. cnxHeadDyB_eq_vjp states the value that region must compute; that its text does is trusted, as the printer’s text is. The GELU, LayerNorm, and convolution backwards around the multiply reuse their bridges. Every one holds at every input (the only hypothesis anywhere is LayerNorm’s \(\epsilon {\gt} 0\)): ConvNeXt has no operator with a corner, and neither do EfficientNet-B0 and the Vision Transformer, the two other chapter nets whose whole-network VJP needs only \(\epsilon {\gt} 0\). The full block is \(\mathrm{residual}(\mathrm{layerScale} \circ \mathrm{project} \circ \mathrm{gelu} \circ \mathrm{expand} \circ \mathrm{LN} \circ \mathrm{depthwise})\), composed by convNextBlockHasVJP.

8.5 ImageNet recipe

ConvNeXt’s architecture reached this book through the Imagenette example earlier in this chapter, and the recipe runs on both paths — the reference first, then the verified path, then the two side by side.

The reference (JAX)

The reference port wires the two ConvNeXt-specific layers (.convNextStage, .convNextDownsample) into the JAX codegen. Because that path differentiates with jax.value_and_grad, only the forward had to be written, and the backward comes for free. The spec mirrors jax/MainConvNeXtImagenet.lean, its recipe quoted as the run below trained it (the file in the tree carries the official rows the marker after Table 8-4 describes):

-- Faithful patchify stem, stage/downsample backbone, 1000 classes.
def convNeXtTinyImagenet : NetSpec where
  name   := "ConvNeXt-T (ImageNet, bf16)"
  imageH := 224
  imageW := 224
  layers := [
    .convNextStem 3 96 4,              -- patchify: 4x4 s4 conv -> channel-LN
    .convNextStage 96 3 .ln .gelu,     -- stage 1: 3 blocks @ 96  (56x56)
    .convNextDownsample 96 192,        -- 56 -> 28
    .convNextStage 192 3 .ln .gelu,    -- stage 2: 3 blocks @ 192
    .convNextDownsample 192 384,       -- 28 -> 14
    .convNextStage 384 9 .ln .gelu,    -- stage 3: 9 blocks @ 384
    .convNextDownsample 384 768,       -- 14 -> 7
    .convNextStage 768 3 .ln .gelu,    -- stage 4: 3 blocks @ 768
    .globalAvgPool,
    .layerNorm 768,                    -- head LN: the paper's GAP -> LN -> Linear
    .dense 768 1000 .identity          -- 1000-class head
  ]

-- ConvNeXt needs AdamW, not SGD; full DeiT-style recipe.
def convNeXtTinyImagenetConfig : TrainConfig where
  learningRate      := 2.5e-4       -- 4e-3 @ batch 4096, linearly scaled to 256
  batchSize         := 256
  epochs            := 80           -- validation tier of an 80->300 ladder
  optimizer         := .adam        -- AdamW (decoupled weight decay)
  weightDecay       := 0.05
  wdExcludeNormBias := true         -- timm no_weight_decay: norm g/b, bias, LayerScale
  cnxInit           := true         -- _init_weights: trunc_normal(0.02) on every conv
  cosineDecay       := true
  warmupEpochs      := 20           -- ConvNeXt paper warmup
  gradClipNorm      := 1.0          -- ours: the paper does not clip
  augment           := true
  useRandAugment    := true         -- geometric RandAugment N=2, M=9, mstd0.5, inc1
  randAugmentGeometric := true
  augBicubic        := true         -- PIL-bicubic geometry, as timm (after the run)
  randAugmentMstd   := 0.5
  randAugmentInc    := true
  useMixup          := true         -- Mixup alpha 0.8 ...
  useCutmix         := true         -- ... + CutMix alpha 1.0 (alternates per step) ...
  randomErasing     := true         -- ... + Random Erasing p 0.25 (full DeiT pack)
  randomErasingProb := 0.25
  erasingPixel      := true         -- timm mode='pixel', N(0,1) fill (after the run)
  labelSmoothing    := 0.1
  bf16              := true
  bf16Conv          := true         -- dw-7x7 + 1x1s; LN/GELU stay fp32
  f32StemHead       := true         -- classifier head in fp32 (after the run below)
  useEMA            := true         -- weight averaging, decay 0.9999
  dropPath          := 0.1          -- stochastic depth (ConvNeXt-T paper value)

-- 300-epoch "full" tier: same config, longer schedule.
def convNeXtTinyImagenetConfigFull : TrainConfig :=
  { convNeXtTinyImagenetConfig with epochs := 300 }

Unlike MobileNet and EfficientNet, ConvNeXt trains with AdamW and a small decoupled weight decay, not SGD. The modernized-convnet recipe is the architecture’s whole thesis. bf16Conv pays off especially here: the \(7\times 7\) depthwise is \(\sim 2.3\times \) faster in bfloat16 (a large kernel has enough arithmetic for the tensor cores to bite, unlike the \(3\times 3\) depthwise, which is a wash), and the inverted-bottleneck \(1\times 1\)s are matmuls that love it. The channel LayerNorm and GELU stay in fp32.

The elements of the paper recipe, wired and on:

  • the faithful patchify stem (.convNextStem: \(4\times 4\) stride-4 conv \(\to \) channel-LayerNorm, no BN/ReLU)

  • stochastic depth (drop-path 0.1) and EMA (decay 0.9999), together \({{\lt}}5\% \) per epoch

  • decoupled weight decay that excludes the norm/bias/LayerScale 1-D params (wdExcludeNormBias, timm’s no_weight_decay)

  • the full DeiT-style augmentation pack: Mixup \(\alpha 0.8\), CutMix \(\alpha 1.0\), Random Erasing \(p0.25\), and geometric RandAugment (\(N{=}2\), \(M{=}9\), mstd0.5-inc1)

The peak LR is \(2.5\mathrm{e}{-}4\) (the \(4\mathrm{e}{-}3\)@4096 official value linearly scaled to batch 256) over a 20-epoch warmup. No deviation from the paper’s architecture remains but the GELU, which every run in this chapter trained as the tanh approximation on both paths (Definition 102); the queued ImageNet pair computes the exact form (Definition 105), and where the recipe departs from the paper’s is the marker after the side-by-side table. Table 8-2 is the full 300-epoch tier, trained and evaluated on an antialiased bicubic resize and scored at the paper’s protocol, \(224\) with a \(0.875\) centre crop.

Table 8-2 ConvNeXt-T on ImageNet-1k, reference trainer (JAX): both figures the exponential moving average of the weights, scored over all \(50{,}000\) validation images.

GPU

Epochs

Per epoch

Wall-clock

Val top-1

Val top-5

4\(\times \) 3060

300

\(15.3\) min

\(76.5\) hr

\(\mathbf{81.53\% }\)

\(\mathbf{95.50\% }\)

The raw weights score \(81.51\% \) / \(95.50\% \), so weight averaging is worth \(+0.03\) points here — the cosine schedule anneals the learning rate to zero over the final epochs, and weights that have stopped moving leave the average nothing to remove.

The full 300-epoch tier (convnext-tiny-imagenet full, convNeXtTinyImagenetConfigFull) reached \(\mathbf{81.53\% }\) top-1 and \(\mathbf{95.50\% }\) top-5 on the held-out validation pass. That is the strongest result in this book’s sweep by a wide margin, ahead of ResNet-50 RSB-A3 (\(77.58\% \)), EfficientNet-B0 (\(77.17\% \)) and ResNet-34 (\(73.98\% \)), and it lands within \(\sim \)0.6% of ConvNeXt-T’s \(82.1\% \) paper headline from a from-scratch Lean\(\to \)JAX-codegen reimplementation. Two candidate explanations for that residual can be ruled out from this run: weight averaging is measured above and accounts for none of it, and the evaluation is the paper’s own, \(224\) at a \(0.875\) crop. What is left is the recipe departures above, not the architecture and not the eval protocol.

The validation curve tells the augmentation story directly: a deliberately slow start, since Mixup and CutMix blend labels, so the net under-trains for the first \(\sim \)20 epochs, compounded by the EMA shadow lagging the live weights. Then it climbs for the rest of the schedule without an overfitting turn, which is what the heavy-regularization regime predicts and what makes the long schedule worth buying.

Learning-rate robustness. The full pack was also a test of ConvNeXt’s learning-rate robustness under AdamW, the test Chapter 7 ran on RMSProp. It is robust: at the \(2.5\mathrm{e}{-}4\) peak with global-norm gradient clipping at \(1.0\), top-1 climbed across the whole 300-epoch schedule with no erosion and no divergence, the opposite of B0’s RMSProp blow-up. AdamW’s bounded, decoupled step plus the grad-clip makes the peak LR a non-event, which is what lets the heavy augmentation run to schedule instead of destabilizing it early. Figure 8-3 is the run, epoch by epoch.

\begin{tikzpicture} 
\begin{axis}[
    width=0.92\linewidth, height=6.5cm,
    xlabel={Epoch}, ylabel={Validation accuracy (\%)},
    xmin=0, xmax=305, ymin=0, ymax=100,
    xtick={0,50,100,150,200,250,300}, ytick={20,40,60,80,100},
    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=0.8pt, mark=none},
]
\addplot[blue] coordinates {
(1,0.64) (2,0.68) (3,1.30) (4,2.34) (5,3.83) (6,6.48) (7,10.54) (8,15.02) (9,19.95) (10,25.63) (11,31.31) (12,36.78) (13,41.73) (14,45.82) (15,49.22) (16,52.20) (17,55.00) (18,57.53) (19,59.66) (20,61.51) (21,63.28) (22,64.69) (23,66.12) (24,67.16) (25,68.02) (26,68.73) (27,69.46) (28,70.07) (29,70.56) (30,71.15) (31,71.58) (32,71.97) (33,72.25) (34,72.66) (35,73.09) (36,73.24) (37,73.51) (38,73.76) (39,73.98) (40,74.17) (41,74.39) (42,74.56) (43,74.74) (44,74.90) (45,75.09) (46,75.16) (47,75.35) (48,75.47) (49,75.60) (50,75.72) (51,75.84) (52,76.01) (53,76.13) (54,76.26) (55,76.30) (56,76.40) (57,76.52) (58,76.66) (59,76.70) (60,76.80) (61,76.97) (62,76.93) (63,76.98) (64,77.09) (65,77.10) (66,77.16) (67,77.18) (68,77.28) (69,77.34) (70,77.49) (71,77.55) (72,77.66) (73,77.71) (74,77.81) (75,77.78) (76,77.73) (77,77.88) (78,77.96) (79,77.86) (80,77.98) (81,77.95) (82,78.09) (83,78.11) (84,78.12) (85,78.16) (86,78.27) (87,78.40) (88,78.33) (89,78.31) (90,78.36) (91,78.43) (92,78.43) (93,78.58) (94,78.57) (95,78.62) (96,78.55) (97,78.57) (98,78.67) (99,78.74) (100,78.79) (101,78.78) (102,78.78) (103,78.82) (104,78.88) (105,78.82) (106,78.92) (107,79.00) (108,79.14) (109,79.16) (110,79.10) (111,79.14) (112,79.23) (113,79.21) (114,79.21) (115,79.18) (116,79.24) (117,79.31) (118,79.24) (119,79.19) (120,79.25) (121,79.27) (122,79.36) (123,79.34) (124,79.45) (125,79.48) (126,79.48) (127,79.44) (128,79.55) (129,79.61) (130,79.59) (131,79.61) (132,79.66) (133,79.73) (134,79.70) (135,79.70) (136,79.65) (137,79.61) (138,79.68) (139,79.66) (140,79.73) (141,79.72) (142,79.77) (143,79.80) (144,79.88) (145,79.96) (146,79.93) (147,79.90) (148,79.98) (149,80.03) (150,80.15) (151,80.18) (152,80.20) (153,80.14) (154,80.13) (155,80.10) (156,80.14) (157,80.18) (158,80.21) (159,80.25) (160,80.30) (161,80.34) (162,80.44) (163,80.42) (164,80.32) (165,80.36) (166,80.37) (167,80.51) (168,80.57) (169,80.54) (170,80.50) (171,80.59) (172,80.61) (173,80.55) (174,80.54) (175,80.57) (176,80.65) (177,80.72) (178,80.75) (179,80.74) (180,80.81) (181,80.77) (182,80.73) (183,80.72) (184,80.71) (185,80.73) (186,80.76) (187,80.82) (188,80.79) (189,80.91) (190,80.94) (191,80.86) (192,80.84) (193,80.82) (194,80.95) (195,80.89) (196,81.03) (197,80.97) (198,80.95) (199,80.99) (200,80.92) (201,80.92) (202,80.91) (203,81.07) (204,81.08) (205,81.07) (206,81.01) (207,81.01) (208,81.13) (209,81.12) (210,81.12) (211,81.20) (212,81.17) (213,81.15) (214,81.14) (215,81.16) (216,81.19) (217,81.19) (218,81.19) (219,81.15) (220,81.13) (221,81.15) (222,81.17) (223,81.28) (224,81.22) (225,81.30) (226,81.33) (227,81.33) (228,81.28) (229,81.23) (230,81.23) (231,81.24) (232,81.29) (233,81.29) (234,81.31) (235,81.28) (236,81.26) (237,81.34) (238,81.39) (239,81.43) (240,81.41) (241,81.42) (242,81.49) (243,81.45) (244,81.41) (245,81.47) (246,81.46) (247,81.54) (248,81.54) (249,81.49) (250,81.51) (251,81.48) (252,81.51) (253,81.46) (254,81.47) (255,81.44) (256,81.44) (257,81.48) (258,81.44) (259,81.45) (260,81.47) (261,81.40) (262,81.47) (263,81.45) (264,81.45) (265,81.40) (266,81.41) (267,81.40) (268,81.40) (269,81.40) (270,81.37) (271,81.39) (272,81.46) (273,81.43) (274,81.46) (275,81.50) (276,81.53) (277,81.55) (278,81.53) (279,81.53) (280,81.53) (281,81.48) (282,81.49) (283,81.52) (284,81.49) (285,81.48) (286,81.51) (287,81.48) (288,81.51) (289,81.52) (290,81.52) (291,81.55) (292,81.54) (293,81.55) (294,81.49) (295,81.49) (296,81.52) (297,81.54) (298,81.52) (299,81.54) (300,81.53)
};
\addlegendentry{top-1}
\addplot[orange] coordinates {
(1,2.47) (2,2.81) (3,4.74) (4,7.79) (5,11.38) (6,17.87) (7,25.37) (8,33.19) (9,41.29) (10,49.01) (11,56.01) (12,61.97) (13,66.93) (14,70.84) (15,73.65) (16,76.28) (17,78.78) (18,80.87) (19,82.51) (20,83.88) (21,85.17) (22,86.16) (23,86.95) (24,87.64) (25,88.31) (26,88.75) (27,89.09) (28,89.45) (29,89.83) (30,90.14) (31,90.43) (32,90.70) (33,90.95) (34,91.11) (35,91.26) (36,91.47) (37,91.63) (38,91.73) (39,91.88) (40,91.98) (41,92.15) (42,92.25) (43,92.33) (44,92.42) (45,92.50) (46,92.65) (47,92.67) (48,92.68) (49,92.72) (50,92.87) (51,92.91) (52,92.97) (53,93.03) (54,93.10) (55,93.09) (56,93.19) (57,93.26) (58,93.30) (59,93.37) (60,93.43) (61,93.41) (62,93.43) (63,93.55) (64,93.62) (65,93.61) (66,93.65) (67,93.63) (68,93.71) (69,93.77) (70,93.80) (71,93.73) (72,93.75) (73,93.81) (74,93.89) (75,93.95) (76,93.98) (77,93.99) (78,93.96) (79,93.98) (80,94.04) (81,94.05) (82,94.08) (83,94.11) (84,94.10) (85,94.10) (86,94.15) (87,94.14) (88,94.23) (89,94.26) (90,94.28) (91,94.29) (92,94.29) (93,94.35) (94,94.30) (95,94.31) (96,94.36) (97,94.36) (98,94.38) (99,94.38) (100,94.42) (101,94.44) (102,94.40) (103,94.39) (104,94.40) (105,94.43) (106,94.47) (107,94.44) (108,94.51) (109,94.44) (110,94.50) (111,94.52) (112,94.56) (113,94.60) (114,94.59) (115,94.62) (116,94.63) (117,94.65) (118,94.61) (119,94.68) (120,94.67) (121,94.65) (122,94.72) (123,94.75) (124,94.79) (125,94.78) (126,94.73) (127,94.79) (128,94.81) (129,94.86) (130,94.84) (131,94.85) (132,94.82) (133,94.87) (134,94.89) (135,94.92) (136,94.87) (137,94.90) (138,94.93) (139,94.93) (140,94.98) (141,94.98) (142,94.96) (143,94.93) (144,94.95) (145,94.91) (146,94.96) (147,94.99) (148,95.06) (149,95.10) (150,95.05) (151,95.11) (152,95.09) (153,95.00) (154,95.06) (155,95.14) (156,95.15) (157,95.12) (158,95.18) (159,95.23) (160,95.20) (161,95.19) (162,95.20) (163,95.19) (164,95.18) (165,95.23) (166,95.25) (167,95.19) (168,95.21) (169,95.24) (170,95.21) (171,95.29) (172,95.34) (173,95.35) (174,95.31) (175,95.34) (176,95.34) (177,95.30) (178,95.39) (179,95.40) (180,95.37) (181,95.41) (182,95.41) (183,95.39) (184,95.38) (185,95.43) (186,95.42) (187,95.39) (188,95.43) (189,95.42) (190,95.46) (191,95.44) (192,95.44) (193,95.47) (194,95.43) (195,95.48) (196,95.48) (197,95.47) (198,95.46) (199,95.44) (200,95.47) (201,95.50) (202,95.51) (203,95.48) (204,95.46) (205,95.46) (206,95.47) (207,95.49) (208,95.48) (209,95.47) (210,95.43) (211,95.46) (212,95.49) (213,95.49) (214,95.52) (215,95.52) (216,95.55) (217,95.53) (218,95.51) (219,95.53) (220,95.49) (221,95.47) (222,95.52) (223,95.52) (224,95.50) (225,95.53) (226,95.53) (227,95.52) (228,95.56) (229,95.56) (230,95.58) (231,95.53) (232,95.53) (233,95.52) (234,95.55) (235,95.55) (236,95.56) (237,95.55) (238,95.53) (239,95.54) (240,95.51) (241,95.52) (242,95.49) (243,95.52) (244,95.50) (245,95.54) (246,95.56) (247,95.53) (248,95.56) (249,95.52) (250,95.56) (251,95.54) (252,95.55) (253,95.53) (254,95.54) (255,95.51) (256,95.54) (257,95.53) (258,95.52) (259,95.53) (260,95.51) (261,95.51) (262,95.50) (263,95.51) (264,95.54) (265,95.54) (266,95.57) (267,95.53) (268,95.51) (269,95.51) (270,95.48) (271,95.50) (272,95.52) (273,95.50) (274,95.51) (275,95.51) (276,95.50) (277,95.49) (278,95.50) (279,95.50) (280,95.51) (281,95.49) (282,95.50) (283,95.51) (284,95.52) (285,95.49) (286,95.51) (287,95.51) (288,95.49) (289,95.49) (290,95.50) (291,95.49) (292,95.49) (293,95.48) (294,95.50) (295,95.50) (296,95.50) (297,95.49) (298,95.50) (299,95.51) (300,95.51)
};
\addlegendentry{top-5}
\end{axis}
\end{tikzpicture}
Figure 8-3 Validation accuracy per epoch of the 300-epoch ConvNeXt-T reference run: bf16, \(4\times \) 3060, every point the EMA weights scored over all \(50{,}000\) validation images.

Neither curve turns over: both rise through the schedule and flatten across the final \(\sim \)40 epochs, where the cosine has annealed the learning rate to zero.

The verified path (PJRT)

The verified trainer trains the same network through the proof-rendered StableHLO that §8.1 ran at Imagenette scale. It is apps/imagenette/MainConvNeXtImagenet.lean, built as lake build convnext-imagenet-verified:

-- Identical backbone, 1000 classes instead of 10.
def convnextImagenetVerified : VerifiedNetSpec where
  name       := "ConvNeXt-T (ImageNet-1k)"
  slug       := "convnextin"
  data       := .imagenet
  shimScript := "generated_convnext_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 64 PER DEVICE, the reference's
-- global 256 over four replicas.
def convnextImagenetConfig : VerifiedConfig where
  epochs    := 300
  batchSize := 64
  cnxInit   := true   -- ConvNeXt _init_weights: trunc_normal(0.02), as the reference

The head is the only parameter shape that moves, from \(768 \times 10\) to \(768 \times 1000\), which takes the count from 27,827,818 to 28,589,128. Three kernel #guards hold the two specs together at everything else: equal toSpecs.size, equal toSpecs.pop.pop, and a back! of (#[1000], 2).

The collectives are gated by two gates, and they prove different things. tests/TestConvNeXtDpCheck.lean pins the data-parallel forward bit-exactly against its single-device peer. That gate hands both replicas the same rows, so it is structurally blind to a shard-offset bug, and tests/TestConvNeXtShardCheck.lean closes exactly that hole by giving the replicas genuinely different data. Both gates run on this chapter’s own specs, convnext and convnextin, and together they are the empirical half of what Theorem 115 leaves unstated: that the all-reduced step is one device’s step on the global batch.

The regularizer knobs are render variants, and the run carries three of the four. Weight-decay exclusion, stochastic depth and this book’s gradient clip are spelled wx, drop and clip, and convnextin_adamdpwxclipdropbf16 carries all three at once on top of AdamW, bf16 and the four-way all-reduce. It is the artifact the run below used; Theorem 115 reaches its bf16 gradient nodes at its eighteen drop-path sites. EMA is rendered too, and convnextin_emadpwxclipdropbf16 carries all four, but it landed after this run, which trained adamdpwxclipdropbf16 and scores its raw weights, and the pairing below is made against the reference’s raw arm, which the reference section measured at \(0.03\) points under its EMA. Mixup and CutMix never enter the graph at all, because they are data-side and ride the shim. It ran as the one-shot batch-256 job:

lake run cnx-default-4gpu

Table 8-3 is the run. The chapter’s row in lake run imagenet is cnx-default-emabf16-4gpu, the official recipe, not this job; the marker after Table 8-4 says what that changes and what is owed.

Table 8-3 ConvNeXt-T on ImageNet-1k, verified path (PJRT): global batch 256 as \(4 \times 64\) replicas at the reference’s \(2.5\mathrm{e}{-4}\) and its 5,004 steps per epoch, the raw weights at epoch 300 scored over the same \(50{,}000\) validation images.

GPU

Precision

Per epoch

Epochs

Total

Val top-1

Val top-5

4\(\times \) 3060 (CUDA)

bf16

18.3 min

300

91.3 hr

\(\mathbf{81.30\% }\)

\(\mathbf{95.61\% }\)

The best epoch was 291, at \(81.39\% \).

The two paths, side by side

Table 8-4 is the two paths, row by row.

Table 8-4 ConvNeXt-T on ImageNet-1k: the reference and the verified path, row by row. Both are the paper’s 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.
 

reference (JAX)

verified path (PJRT)

lowering

JAX \(\to \) XLA

pretty(provenGraph) \(\to \) XLA

Python in the step

yes

none

ms/step

179

220

per epoch

15.3 min

18.3 min

total

76.5 hr

91.3 hr

EMA

trained, scored per epoch

none

evaluation forward

bf16

fp32

classifier head

bf16

fp32

precision

bf16 convolutions

bf16 convolutions

global batch

256 (\(4\times 64\))

256 (\(4\times 64\))

steps/epoch

5,004

5,004

peak LR

\(2.5\mathrm{e}{-4}\)

\(2.5\mathrm{e}{-4}\)

init

trunc-normal \(0.02\)

trunc-normal \(0.02\)

scored

raw weights, epoch 300

raw weights, epoch 300

val top-1

\(\mathbf{81.51\% }\)

\(\mathbf{81.30\% }\)

val top-5

\(\mathbf{95.50\% }\)

\(\mathbf{95.61\% }\)

The reference’s headline earlier is its EMA; its row here is its raw weights, because that is the arm the verified path has. [TODO: the official recipe in four places, on both paths: batch \(4096\) (accumulated \(16\times \) at \(256\), exact without BatchNorm), no gradient clip, Mixup or CutMix drawn per batch, torchvision’s crop; plus the exact erf GELU, the cosine floor at \(10^{-6}\), RandAugment’s fill at the image mean, the EMA decay constant from step \(0\), an fp32 evaluation forward, and scoring at timm’s \(288\) / \(1.0\). The pair published above predates that recipe: it is the one-shot job cnx-default-4gpu at global batch \(256\) and \(2.5\mathrm{e}{-4}\), with a global-norm clip and the tanh GELU. The chapter’s row runs the official recipe, cnx-default-emabf16-4gpu, and the pair on it is owed, both halves — cnx-default-jax-4gpu / cnx-default-emabf16-4gpu, \({\sim }76 + 109\) h.]

The two paths agree to \(0.21\) points on top-1 and \(0.11\) on top-5, inside the 95% Wilson interval (\(\pm 0.34\) at \(81\% \) on \(50{,}000\) images), at one seed each. This is the chapter where the pair can say more than that, because both checkpoints were scored on the same images in the same order, so the comparison can be paired instead of read off two intervals. Of the \(50{,}000\), both paths get \(38{,}850\) right and \(7{,}445\) wrong; \(1{,}903\) are right only on the reference and \(1{,}802\) only on the verified path. McNemar’s exact test asks whether those \(3{,}705\) discordant images split unevenly, and answers \(p = 0.10\) (scripts/demos/mcnemar.py): a net of \(101\) flips does not separate the two checkpoints. The alignment was checked rather than assumed — the two agree on \(92.6\% \) of images, where unrelated orderings would agree on \(69.7\% \). What the pair does not isolate is variance. The drop-path and Mixup masks are host-drawn on one path and jax.random’s on the other, and the seeds differ, so \(7.4\% \) of images decided differently is what two ConvNeXt-Ts differing in seed and mask source disagree on here; a two-seed pair on one path, which would bound the spread alone, has not been run. “Not separable” is the claim, not “identical”. Like ViT, ConvNeXt has no BatchNorm to normalize over a different group; unlike ViT, its verified arm ran without EMA, so this is a two-variable pair with the second variable measured at \(0.03\).

The curves are read with one caveat, and it is the EMA. The reference’s per-epoch evaluation scores its EMA shadow (decay \(0.9999\), about a two-epoch time constant) and the verified path scores its raw weights, so mid-schedule the figure compares an averaged model to a live one. The figure shows it: the verified path leads from epoch 3 to 17, by up to \(2.8\) points, while the shadow lags a fast climb; it trails by up to \(4.1\) points at epoch 33, at peak learning rate, where averaging is worth the most; and the gap closes as the cosine anneals, to a mean of \(0.28\) points over the last fifty epochs. Only the final pairing above is raw against raw. A raw reference curve cannot be rebuilt after the fact — the reference kept raw weights only for its last three epochs — which is why the reference section measured the EMA’s worth at the end, where it is \(0.03\), and not along the way, where it is several points. Table 8-5 is the selected epochs and Figure 8-4 every epoch.

Table 8-5 ConvNeXt-T validation top-1 at selected epochs: the reference’s EMA weights against the verified path’s raw weights.

epoch

5

50

100

150

200

250

300

reference (JAX, EMA)

3.83

75.72

78.79

80.15

80.92

81.51

81.53

verified path (PJRT, raw)

4.24

72.22

75.85

77.88

79.57

81.00

81.30

\(\Delta \)

\(+0.41\)

\(-3.50\)

\(-2.94\)

\(-2.27\)

\(-1.35\)

\(-0.51\)

\(-0.23\)

\begin{tikzpicture} 
\begin{axis}[
    width=0.92\linewidth, height=6.5cm,
    xlabel={Epoch}, ylabel={Validation top-1 (\%)},
    xmin=0, xmax=305, ymin=0, ymax=90,
    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=0.8pt, mark=none},
]
\addplot[blue] coordinates {
(1,0.64) (2,0.68) (3,1.30) (4,2.34) (5,3.83) (6,6.48) (7,10.54) (8,15.02) (9,19.95) (10,25.63) (11,31.31) (12,36.78) (13,41.73) (14,45.82) (15,49.22) (16,52.20) (17,55.00) (18,57.53) (19,59.66) (20,61.51) (21,63.28) (22,64.69) (23,66.12) (24,67.16) (25,68.02) (26,68.73) (27,69.46) (28,70.07) (29,70.56) (30,71.15) (31,71.58) (32,71.97) (33,72.25) (34,72.66) (35,73.09) (36,73.24) (37,73.51) (38,73.76) (39,73.98) (40,74.17) (41,74.39) (42,74.56) (43,74.74) (44,74.90) (45,75.09) (46,75.16) (47,75.35) (48,75.47) (49,75.60) (50,75.72) (51,75.84) (52,76.01) (53,76.13) (54,76.26) (55,76.30) (56,76.40) (57,76.52) (58,76.66) (59,76.70) (60,76.80) (61,76.97) (62,76.93) (63,76.98) (64,77.09) (65,77.10) (66,77.16) (67,77.18) (68,77.28) (69,77.34) (70,77.49) (71,77.55) (72,77.66) (73,77.71) (74,77.81) (75,77.78) (76,77.73) (77,77.88) (78,77.96) (79,77.86) (80,77.98) (81,77.95) (82,78.09) (83,78.11) (84,78.12) (85,78.16) (86,78.27) (87,78.40) (88,78.33) (89,78.31) (90,78.36) (91,78.43) (92,78.43) (93,78.58) (94,78.57) (95,78.62) (96,78.55) (97,78.57) (98,78.67) (99,78.74) (100,78.79) (101,78.78) (102,78.78) (103,78.82) (104,78.88) (105,78.82) (106,78.92) (107,79.00) (108,79.14) (109,79.16) (110,79.10) (111,79.14) (112,79.23) (113,79.21) (114,79.21) (115,79.18) (116,79.24) (117,79.31) (118,79.24) (119,79.19) (120,79.25) (121,79.27) (122,79.36) (123,79.34) (124,79.45) (125,79.48) (126,79.48) (127,79.44) (128,79.55) (129,79.61) (130,79.59) (131,79.61) (132,79.66) (133,79.73) (134,79.70) (135,79.70) (136,79.65) (137,79.61) (138,79.68) (139,79.66) (140,79.73) (141,79.72) (142,79.77) (143,79.80) (144,79.88) (145,79.96) (146,79.93) (147,79.90) (148,79.98) (149,80.03) (150,80.15) (151,80.18) (152,80.20) (153,80.14) (154,80.13) (155,80.10) (156,80.14) (157,80.18) (158,80.21) (159,80.25) (160,80.30) (161,80.34) (162,80.44) (163,80.42) (164,80.32) (165,80.36) (166,80.37) (167,80.51) (168,80.57) (169,80.54) (170,80.50) (171,80.59) (172,80.61) (173,80.55) (174,80.54) (175,80.57) (176,80.65) (177,80.72) (178,80.75) (179,80.74) (180,80.81) (181,80.77) (182,80.73) (183,80.72) (184,80.71) (185,80.73) (186,80.76) (187,80.82) (188,80.79) (189,80.91) (190,80.94) (191,80.86) (192,80.84) (193,80.82) (194,80.95) (195,80.89) (196,81.03) (197,80.97) (198,80.95) (199,80.99) (200,80.92) (201,80.92) (202,80.91) (203,81.07) (204,81.08) (205,81.07) (206,81.01) (207,81.01) (208,81.13) (209,81.12) (210,81.12) (211,81.20) (212,81.17) (213,81.15) (214,81.14) (215,81.16) (216,81.19) (217,81.19) (218,81.19) (219,81.15) (220,81.13) (221,81.15) (222,81.17) (223,81.28) (224,81.22) (225,81.30) (226,81.33) (227,81.33) (228,81.28) (229,81.23) (230,81.23) (231,81.24) (232,81.29) (233,81.29) (234,81.31) (235,81.28) (236,81.26) (237,81.34) (238,81.39) (239,81.43) (240,81.41) (241,81.42) (242,81.49) (243,81.45) (244,81.41) (245,81.47) (246,81.46) (247,81.54) (248,81.54) (249,81.49) (250,81.51) (251,81.48) (252,81.51) (253,81.46) (254,81.47) (255,81.44) (256,81.44) (257,81.48) (258,81.44) (259,81.45) (260,81.47) (261,81.40) (262,81.47) (263,81.45) (264,81.45) (265,81.40) (266,81.41) (267,81.40) (268,81.40) (269,81.40) (270,81.37) (271,81.39) (272,81.46) (273,81.43) (274,81.46) (275,81.50) (276,81.53) (277,81.55) (278,81.53) (279,81.53) (280,81.53) (281,81.48) (282,81.49) (283,81.52) (284,81.49) (285,81.48) (286,81.51) (287,81.48) (288,81.51) (289,81.52) (290,81.52) (291,81.55) (292,81.54) (293,81.55) (294,81.49) (295,81.49) (296,81.52) (297,81.54) (298,81.52) (299,81.54) (300,81.53)
};
\addlegendentry{reference (JAX, EMA)}
\addplot[red, dashed] coordinates {
(1,0.51) (2,0.67) (3,1.52) (4,2.51) (5,4.24) (6,7.39) (7,12.11) (8,16.82) (9,22.38) (10,28.19) (11,34.14) (12,38.89) (13,43.82) (14,47.50) (15,50.50) (16,53.39) (17,55.48) (18,57.03) (19,58.25) (20,59.77) (21,60.59) (22,61.81) (23,62.94) (24,63.62) (25,64.57) (26,65.29) (27,65.76) (28,66.89) (29,67.17) (30,67.58) (31,67.88) (32,68.65) (33,68.19) (34,68.90) (35,69.14) (36,69.41) (37,69.71) (38,70.11) (39,70.54) (40,70.70) (41,70.78) (42,70.94) (43,71.06) (44,71.41) (45,71.68) (46,71.75) (47,71.89) (48,71.85) (49,72.17) (50,72.22) (51,72.44) (52,72.56) (53,72.85) (54,72.69) (55,72.94) (56,73.18) (57,73.20) (58,73.40) (59,73.48) (60,73.51) (61,73.29) (62,73.84) (63,73.60) (64,73.98) (65,73.77) (66,73.82) (67,73.86) (68,73.96) (69,73.69) (70,74.07) (71,74.08) (72,74.05) (73,74.12) (74,74.61) (75,74.27) (76,74.41) (77,74.67) (78,74.73) (79,74.72) (80,74.61) (81,74.94) (82,74.86) (83,74.68) (84,74.78) (85,75.08) (86,74.78) (87,74.95) (88,75.31) (89,75.28) (90,75.22) (91,75.37) (92,75.35) (93,75.22) (94,75.64) (95,75.48) (96,75.70) (97,75.34) (98,75.74) (99,75.63) (100,75.85) (101,75.61) (102,75.85) (103,75.79) (104,75.90) (105,76.05) (106,76.15) (107,76.03) (108,75.93) (109,75.84) (110,76.35) (111,75.97) (112,76.11) (113,76.12) (114,76.49) (115,76.55) (116,76.61) (117,76.60) (118,76.55) (119,76.58) (120,76.57) (121,76.64) (122,76.35) (123,76.76) (124,76.71) (125,76.58) (126,76.80) (127,76.71) (128,76.98) (129,76.93) (130,76.83) (131,77.02) (132,76.81) (133,76.85) (134,77.06) (135,77.17) (136,77.09) (137,77.25) (138,77.23) (139,77.19) (140,77.38) (141,77.42) (142,77.54) (143,77.47) (144,77.38) (145,77.59) (146,77.42) (147,77.55) (148,77.41) (149,77.65) (150,77.88) (151,77.60) (152,77.89) (153,77.69) (154,77.78) (155,77.96) (156,77.87) (157,77.98) (158,78.04) (159,77.89) (160,78.10) (161,77.93) (162,78.12) (163,78.15) (164,78.61) (165,78.21) (166,78.16) (167,78.43) (168,78.28) (169,78.39) (170,78.37) (171,78.21) (172,78.55) (173,78.40) (174,78.70) (175,78.44) (176,78.89) (177,78.81) (178,78.75) (179,78.84) (180,78.91) (181,78.77) (182,79.07) (183,78.88) (184,78.90) (185,78.75) (186,79.03) (187,78.96) (188,79.06) (189,78.93) (190,79.18) (191,79.06) (192,79.20) (193,79.09) (194,79.30) (195,79.34) (196,79.36) (197,79.19) (198,79.32) (199,79.66) (200,79.57) (201,79.57) (202,79.67) (203,79.58) (204,79.53) (205,79.68) (206,79.76) (207,79.72) (208,79.82) (209,79.76) (210,79.92) (211,79.74) (212,79.73) (213,79.75) (214,79.77) (215,79.74) (216,79.90) (217,79.98) (218,80.10) (219,80.17) (220,79.97) (221,80.10) (222,80.13) (223,80.21) (224,80.19) (225,80.18) (226,80.26) (227,80.31) (228,80.27) (229,80.15) (230,80.37) (231,80.27) (232,80.32) (233,80.31) (234,80.49) (235,80.42) (236,80.54) (237,80.45) (238,80.57) (239,80.69) (240,80.53) (241,80.65) (242,80.60) (243,80.56) (244,80.66) (245,80.63) (246,80.68) (247,80.74) (248,80.84) (249,80.84) (250,81.00) (251,80.83) (252,80.89) (253,80.91) (254,80.97) (255,80.85) (256,81.00) (257,80.97) (258,80.97) (259,81.12) (260,81.03) (261,81.14) (262,81.10) (263,81.10) (264,81.09) (265,81.22) (266,81.12) (267,81.20) (268,81.23) (269,81.17) (270,81.19) (271,81.18) (272,81.16) (273,81.20) (274,81.25) (275,81.21) (276,81.27) (277,81.19) (278,81.30) (279,81.30) (280,81.29) (281,81.24) (282,81.28) (283,81.25) (284,81.35) (285,81.31) (286,81.25) (287,81.36) (288,81.37) (289,81.35) (290,81.37) (291,81.39) (292,81.35) (293,81.32) (294,81.34) (295,81.32) (296,81.30) (297,81.32) (298,81.32) (299,81.30) (300,81.30)
};
\addlegendentry{verified path (PJRT, raw)}
\end{axis}
\end{tikzpicture}
Figure 8-4 ConvNeXt-T validation top-1 per epoch on both paths: one recipe, two paths to XLA, scored over all \(50{,}000\) validation images.

The remaining \(91.3\) against \(76.5\) hours is \(1.23\times \) per step, \(220\) against \(179\) ms at the same shape on the same cards. Of the \(41\) ms, \(15\) is the feed — the fed step against the step with the shim removed, measured on this box — which is \(6.8\% \) of the step on the heaviest shim in the book (RandAugment and random erasing per image), because doubling the per-replica batch from \(32\) to \(64\) doubled the arithmetic each augmented image amortizes over. The other \(26\) is the lowering, read against §9.5’s accounting.

What the ConvNeXt recipe changes from the 2018 recipe

The ResNet-34 trainer of Chapter 5 is the 2018-era recipe: the original paper’s SGD-with-momentum plus the “bag of tricks” polish (cosine schedule, warmup, label smoothing, random-resized-crop). The config above is the 2022 ConvNeXt recipe, DeiT’s transformer-training playbook applied to a convnet. Where Chapter 5’s RSB-A3 side quest showed the 2018\(\to \)2021 jump on a fixed ResNet, this table shows the 2018\(\to \)2022 jump with the block modernized in step. Since “the recipe is the thesis” is this architecture’s whole argument, Table 8-6 is the chapter in table form. The trainAdamSched entry point is identical in both columns, and every row is a one-line change to the VerifiedNetSpec, the VerifiedConfig, or the arguments beside them.

Table 8-6 The ConvNeXt-T recipe against ResNet-34’s 2018 recipe, knob by knob.

Knob

ResNet-34 (2018)

ConvNeXt-T (2022)

What the ConvNeXt choice buys

Backbone

basic block, 21.8 M

ConvNeXt block, 28.6 M

BN+ReLU \(\to \) LN+GELU; dw-\(7\times 7\) + inverted bottleneck + LayerScale — the modernized block this chapter built

Optimizer

SGD + momentum \(0.9\)

AdamW

decoupled decay + per-parameter step scale; LR-robust where B0’s RMSProp proved fragile

Peak LR

\(0.1\)

\(2.5\mathrm{e}{-4}\)

linearly scaled from the official \(4\mathrm{e}{-3}\)@4096 — AdamW’s scale, not comparable to SGD’s \(0.1\)

Warmup

5 epochs

20 epochs

heavy augmentation + adaptive optimizer want a long, gentle ramp

Epochs

90

300

heavy regularization needs a long schedule to pay, and converts it straight into accuracy rather than into overfitting

Loss

softmax cross-entropy

CE over soft targets

unlike A3’s BCE switch, ConvNeXt keeps CE — the softening comes from the Mixup/CutMix labels

Weight decay

\(10^{-4}\), weights only

\(0.05\), skip 1-D params

\(500\times \) stronger, never on norm/bias/LayerScale (timm’s no_weight_decay)

Augmentation

RRC + hflip

full DeiT pack

RandAugment + Mixup + CutMix + Random Erasing — where the modernization actually lives

Stochastic depth

none

drop-path \(0.1\)

randomly drops whole residual branches per step — the depth regularizer

EMA

none

decay \(0.9999\)

evaluates a smoothed shadow of the weights; with drop-path, together \({{\lt}}5\% \) per epoch

Grad clip

none

global-norm \(1.0\)

this book’s addition, not the paper’s: cheap insurance that made the peak LR a non-event

What carries over unchanged: cosine LR decay, the warmup-then-anneal structure, random-resized-crop + horizontal flip as the base augmentation, label smoothing \(0.1\), batch 256 (the paper’s own batch is \(4096\), and the peak LR is scaled for it), train and eval both at \(224\) (no FixRes), and bf16 convolution under an fp32 classifier head.

The augmentation row is the full DeiT pack: Mixup (Zhang et al. 2017, arXiv:1710.09412), CutMix (Yun et al. 2019, arXiv:1905.04899), Random Erasing (Zhong et al. 2017, arXiv:1708.04896), and RandAugment (Cubuk et al. 2019, arXiv:1909.13719). All four are implemented as data-only kernels that mutate the input tensor before the graph sees it, tier 1 in our codegen scope, no MLIR plumbing required. Mixup and CutMix route through a soft-label train-step variant since they produce fractional labels. Random Erasing and RandAugment leave labels alone.

The through-line is the same “regularize harder, train bigger” as RSB-A3, arrived at from the transformer side: keep cross-entropy and soften the targets instead of switching losses, AdamW instead of LAMB (the batch stays moderate), stochastic depth and EMA from the ViT toolkit, and a \(3\times \) longer schedule to let the regularization pay.

8.6 Side quest: scaling to ConvNeXt-S and B

The axis. Scaling ConvNeXt-T is two moves. S deepens stage 3 from 9 blocks to 27 and touches nothing else; B does that and widens every stage, \(96/192/384/768 \to 128/256/512/1024\). On the JAX side both are parameter edits. On the verified side they are not the same job at all. Table 8-7 is the three sizes.

Table 8-7 ConvNeXt-T, S and B: shape, parameter count, and graph time in fp32 and bf16.

model

shape

params

graph, one card bs\(32\): fp32 \(\to \) bf16

ConvNeXt-T

\(9\) blocks, \(96\) wide

\(28.6\) M

\(150.6 \to 68.5\) ms  (\(2.20\times \))

ConvNeXt-S

\(27\) blocks, \(96\) wide

\(50{,}223{,}688\)

\(255.2 \to 105.3\) ms  (\(\mathbf{2.42\times }\))

ConvNeXt-B

\(27\) blocks, \(128\) wide

\(88{,}591{,}464\)

\(378.6 \to 158.0\) ms  (\(2.40\times \))

Every number in this section was measured on 4\(\times \) RTX 4060 Ti (16 GB, CUDA 12.9). Counts are #guarded at \(50{,}223{,}688\) and \(88{,}591{,}464\), \(344\) tensors each, the head LayerNorm’s \(\gamma \) and \(\beta \) included.

A full schedule. Table 8-8 is four cards, \(300\) epochs at the jobs’ own shape — \(64\) per replica, sixteen micro-batches accumulated to the official \(4096\), \(312\) updates an epoch — with each path’s own trainer timed on real ImageNet (the 2026-10-12 smokes).

Table 8-8 Measured step time and projected 300-epoch training time for ConvNeXt-S and B on four cards, on each path; totals include the per-epoch evaluation.
 

reference (JAX)

verified path (PJRT)

4 cards, bf16

ms/micro-batch

300 epochs

ms/micro-batch

300 epochs

ConvNeXt-S

\(247\)

\(106\) h  (\(4.4\) d)

\(323\)

\(144\) h  (\(6.0\) d)

ConvNeXt-B

\(352\)

\(151\) h  (\(6.3\) d)

\(491\)

\(219\) h  (\(9.1\) d)

The verified steps are fed, \(3\)–\(7\) ms of them waiting on the shim. All four are compute-bound.

What is queued. Both pairs exist. The verified jobs are cnx{s,b}-default-emabf16-4gpu, on the renders convnext{s,b}in_emaaccdp16xwxdroperfbf16, and their references are cnx{s,b}-default-jax-4gpu. In: ConvNeXt-T’s recipe as its marker has it — the official batch \(4096\) as \(16\) accumulated micro-batches of \(64\) per replica, no clip — with drop-path at the paper’s \(0.4\) for S and \(0.5\) for B, EMA scored, cnxInit on both paths, and _fwd_s288 forwards for timm’s test protocol. The accumulation is the recipe’s, not memory’s: at \(64\) per replica the accumulating renders peak at \(7.21\) GiB for S and \(10.42\) for B.

[TODO: as T’s; neither has run —
cnxs-default-jax-4gpu / cnxs-default-emabf16-4gpu,
cnxb-default-jax-4gpu / cnxb-default-emabf16-4gpu.]

8.7 Recap

GELU is a smooth scalar function, in its tanh approximation and in the exact \(x \cdot \Phi (x)\) alike, so its Jacobian is a diagonal like ReLU’s with a derivative that exists everywhere; LayerNorm is BatchNorm along the channel axis, and its backward is the same emitted graph; layer scale is a per-channel multiply and the patchify stem is a strided convolution. Nothing here needed new analysis, which is the point: the full ConvNeXt-T, eighteen blocks, is proved from the kit as it stood, and the step is tied (Theorem 115). What the chapter adds is the recipe as the thesis, the 2018 to 2022 jump laid out knob by knob, with the architecture modernized in step, and a reference run that scores its EMA weights against a verified path that scores its raw ones, which is why the two are compared only where the schedule has annealed.

One thing is left in Part 1 that the kit has not replaced: the convolution itself. The next chapter removes it. Attention is matrix algebra, softmax over the rows of a product, and proving it needs the matrix-level VJP records the first chapter set aside, a Jacobian that is a rank-one correction to a diagonal, and a composition rule pushed higher than anywhere else in the book.