MobileNetV4 — the batched UIB backward, and the four families as one chain #
The block- and stage-level backward of MobileNetV4-Conv-M at the batched index: the depthwise
stages, the UIB body, the stride-2 body, the fused stage and the head, each a CertLayer whose
backward graph is faithful to its certified VJP. The identity skip is CertLayer.residual, applied
by the caller.
The four families are one chain #
The UIB body is preDW? → expand → postDW? → project, and k = 0 omits a depthwise. Both
depthwise positions are channel- and shape-preserving — preDW : ic → ic, postDW : mid → mid —
so an absent one is the identity layer (CertLayer.id') in the same slot:
| family | pre | post | as a chain |
|---|---|---|---|
| ExtraDW | ✓ | ✓ | chain [preDW, expand, postDW, project] |
| ConvNeXt-like | ✓ | ✗ | chain [preDW, expand, id', project] |
| IB / MBConv | ✗ | ✓ | chain [id', expand, postDW, project] |
| FFN | ✗ | ✗ | chain [id', expand, id', project] |
mnv4UibBody takes the two depthwise slots as CertLayer arguments and the caller passes id'
where the table says k = 0 — one body, no case split, and no dispatch that could disagree with
the forward's (mnv4BodyOfRow reads the slots off the UibSpec row).
The two depthwise stages, and where the activation is #
timm's mobilenetv4_conv_medium (the pinned spec, checked by scripts/parity/mnv4_timm_parity.py): the pre-DW
(dw_start) is depthwise → BN with no activation (dwbB, globally certified); the post-DW
(dw_mid) is depthwise → BN → relu (dwbReluB, and its strided peer dwbReluBstrided).
Relu, not relu6 — MobileNetV2 uses relu6 one file over. The relu stage is
bnReluStageHasVJPAt (Foundation/Batched/StageLayers, generic in the op) at depthwiseFlat;
its backward graph masks with .selectPos, relu's one-sided mask.
Contents #
- the depthwise stages — BN-only (
mnv4DWBnLayer, the pre-DW) and BN-relu, stride-1 and strided (mnv4DWReluLayer,mnv4DWReluStridedLayer, the post-DW) — with backward graphs; - the family-collapsing body
mnv4UibBodyand the stride-2 formmnv4UibStridedBody(the post-DW carries the stride, timm'sdw_midrule); - the fused stage (
mnv4FusedStage, stage 0):cbReluStridedLayerthenprojLayer; - the head
mnv4Head: conv-bn-relu, GAP,conv_head-bn-relu on the pooled features, dense; UibParams, the row-typed weight record, and the table-drivenk = 0dispatch.
The net level consumes these block by block: the whole-net forward, graph and VJP are
MobileNetV4FullB / MobileNetV4FullBVJP, the train-step tie is MobileNetV4StepTieB, and the
input-gradient tie is MobileNetV4WholeBackCertifiedTieB.
The stem is not here and is not a CertLayer: no render emits a gradient into %x, so there is
no backward graph for it, and a net-level forward composes the stem's VJP by vjpCompAt rather
than CertLayer.comp.
Batched depthwise → bn stage, no activation — MobileNetV4's pre-DW (dw_start in timm's
UniversalInvertedResidual, whose BatchNormAct2d carries Identity).
Equations
- Proofs.StableHLO.dwbB N W b ε γ β = Proofs.StableHLO.bnBatchLA N c h w ε γ β ∘ Proofs.StableHLO.batchMap N (Proofs.depthwiseFlat W b)
Instances For
The pre-DW stage as a CertLayer. Globally certified (ok = True): with no activation there
is no kink — the projLayer situation, at a depthwise.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Batched depthwise → bn → relu stage. Plain relu, not relu6: MNv4's UIB blocks use
relu where MobileNetV2's use relu6, and dwbrB (one file over) is the relu6 one.
Equations
- Proofs.StableHLO.dwbReluB N W b ε γ β = Proofs.relu (N * (c * h * w)) ∘ Proofs.StableHLO.bnBatchLA N c h w ε γ β ∘ Proofs.StableHLO.batchMap N (Proofs.depthwiseFlat W b)
Instances For
Batched STRIDE-2 depthwise → bn → relu stage — the post-DW of a downsampling UIB block,
which carries its stride (timm's dw_mid).
Equations
- Proofs.StableHLO.dwbReluBstrided N W b ε γ β = Proofs.relu (N * (c * h * w)) ∘ Proofs.StableHLO.bnBatchLA N c h w ε γ β ∘ Proofs.StableHLO.batchMap N (Proofs.depthwiseStride2Flat W b)
Instances For
A depthwise-bn-relu stage as a CertLayer — the stride-1 post-DW (c := mid). The pre-DW is
mnv4DWBnLayer above: same depthwise, no activation.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The UIB body, for all four families at once.
preDW and postDW are CertLayer arguments, so the caller passes mnv4DWReluLayer where
the block table has k > 0 and CertLayer.id' where it has k = 0. ExtraDW, IB, ConvNeXt-like
and FFN are then four applications of this one definition, not four proofs.
This is only possible because both depthwise positions are shape-preserving: an absent
depthwise leaves the chain's types unchanged, so id' slots in without a case split.
Equations
- Proofs.StableHLO.mnv4UibBody N preDW expand postDW project = preDW.comp (expand.comp (postDW.comp project))
Instances For
The stride-1 collapse does not extend here, and the reason is the type.
At stride 1 an absent depthwise is id' because the slot is shape-preserving. At stride 2 the
depthwise that carries the stride maps (2h, 2w) ↦ (h, w) — a different type — so it cannot be
replaced by an identity. timm puts the stride on dw_mid whenever a block has one, so in all three
Conv-M downsamples (rows 1, 3, 11, all ExtraDW) the optional pre-DW and the expand run at the INPUT
resolution 2h and the strided post-DW takes it to h. The pre-DW is still a slot (at 2h).
Putting the stride on the pre-DW instead (expand at h) is a different function with the same
parameter shapes.
All three stride-2 blocks change channels (ic ≠ oc), so none has a skip: the block IS the
body, with no CertLayer.residual wrapper. Adding one would not typecheck, which is the good case.
The STRIDE-2 depthwise-bn-relu stage as a CertLayer — the depthwise that carries a UIB
block's stride. Not an endomorphism (that is the whole point), so it composes via comp.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Stride-2 UIB body: the optional pre-DW and the expand at the input resolution 2h, the
post-DW carrying the stride to h, the project at h.
Equations
- Proofs.StableHLO.mnv4UibStridedBody N preDW expand postDW project = preDW.comp (expand.comp (postDW.comp project))
Instances For
MNv4's stage 0 is .fusedMbConv 32 48 4 3 2 1 false .relu (timm's EdgeResidual): a regular
k×k conv (not a depthwise) doing expansion and downsampling at once, then a 1×1 project.
32 → mid = 32·4 = 128 → 48, stride 2, symmetric padding, relu — so the strided conv-bn-relu
stage is cbReluStridedLayer and the project is projLayer (both Batched.StageLayers).
MNv4's fused stage (stage 0) — the strided k×k conv-bn-relu, then the 1×1 project.
No skip: ic = 32 ≠ 48 = oc and stride 2, so the stage IS the body.
Equations
- Proofs.StableHLO.mnv4FusedStage N fusedConv project = fusedConv.comp project
Instances For
timm's head: Conv-M's cn_r1_k1_s1_c960 (1×1 256 → 960, BN, relu) at 7×7, then global
pool, then conv_head (1×1 960 → 1280) → norm_head (BN) → relu on the POOLED [N, 960, 1, 1],
then the classifier. The second conv-bn-relu is cbReluLayer at h = w = 1, so its BatchNorm
normalises over the batch alone. Running conv_head at 7×7 before the pool would be a different
function: batch BN and relu do not commute with the pool.
An index relabelling as a CertLayer: the same vector read at m instead of n along a
proved n = m. The forward is the Fin.cast gather, the VJP reindexHasVJP, and the graph
castIdx, which emits no text (skel erases indices). The head needs two: the pooled [N, c]
is conv_head's [N, c, 1, 1], and its [N, oc, 1, 1] output is the classifier's [N, oc];
c * 1 * 1 and c are equal but not definitionally so at a variable c.
Equations
- One or more equations did not get rendered due to their size.
Instances For
MNv4's head: conv-bn-relu, GAP, conv_head-bn-relu on the pooled features, classifier,
with the two 1×1 relabellings between. Only the two conv stages carry a smoothness condition
(their relus); GAP, the casts and dense are global.
Equations
- Proofs.StableHLO.mnv4Head N headConv gap convHead cls = headConv.comp (gap.comp ((Proofs.StableHLO.castLayer ⋯).comp (convHead.comp ((Proofs.StableHLO.castLayer ⋯).comp cls))))
Instances For
What this section fixes. Above, mnv4UibBody takes its depthwise slots as arguments,
so passing id' where block 4's real pre-DW belongs is well-typed and still certified — graph
and VJP both move with the caller's arguments, so the theorem stays true about the wrong net.
Types catch the stride split (resolution is in the type); they catch nothing about k = 0 vs
k > 0, because that slot is shape-preserving — the very property that made the collapse possible.
The fix is to make the slot a function of the block table's k, so the proof side runs the
same k = 0 dispatch the render does, off the same mnv4Blocks list — one table, not two
readings. mnv4-fwd-smoke already pins the render against that table; these #guards pin the
table's own shape, so a bad edit fails at lake env lean rather than becoming a silent net.
The pre-depthwise slot, dispatched on the table's preDWk. k = 0 ⇒ id' — the same
rule uibFwdSkipB emits, computed rather than chosen. k > 0 ⇒ the BN-only mnv4DWBnLayer.
Equations
- Proofs.StableHLO.mnv4PreDWSlot N preDWk W b ε hε γ β = if preDWk = 0 then Proofs.StableHLO.CertLayer.id' (N * (c * h * w)) else Proofs.StableHLO.mnv4DWBnLayer N W b ε hε γ β
Instances For
The post-depthwise slot, same dispatch on postDWk.
Equations
- Proofs.StableHLO.mnv4PostDWSlot N postDWk W b ε hε γ β = if postDWk = 0 then Proofs.StableHLO.CertLayer.id' (N * (c * h * w)) else Proofs.StableHLO.mnv4DWReluLayer N W b ε hε γ β
Instances For
Equations
Equations
- One or more equations did not get rendered due to their size.
Instances For
Equations
- Proofs.StableHLO.instBEqUibFamily.beq x✝ y✝ = (x✝.ctorIdx == y✝.ctorIdx)
Instances For
Equations
Which family a table row denotes.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The gap this closes. A block builder that reads the dispatch from the table but takes
its weights as separate arguments lets a caller pair row 4's ks with row 7's widths. The dispatch
was table-driven; the wiring was not.
Fix: index the parameter record by the row. Every width in UibParams s is a projection of
s — s.ic, s.oc, s.ic * s.expand, s.preDWk, s.postDWk. A record with widths that
disagree with its row cannot be constructed, so the block builder needs no side conditions and
no #guard: it is impossible by typing rather than checked after the fact.
What this still does not pin. Rows with the same (ic, oc, expand, preDWk, postDWk, h) —
{4, 5, 7}, {8, 10}, {12, 18}, {13, 14} and {15, 19, 20} — have records of the same type, so
swapping two such blocks' weights typechecks. Typing pins shape
(channels, expand ratio, kernel extents, resolution); it cannot pin identity between rows that
are shape-identical. Closing that needs the weights to come from one indexed array — a renderer
concern, since the render already folds mnv4Blocks in order.
A depthwise slot's parameters, typed by its kernel extent. None at k = 0, where the slot
is id' and nothing reads a weight; a DWBnParams at k > 0. Two records that agree on
every weight the net reads are therefore equal.
Equations
Instances For
The slot's parameters as a DWBnParams at any k: the stored ones at k > 0; at k = 0, a
fixed placeholder (zero kernel, ε = 1) that the id' slot never reads.
Instances For
One UIB block's parameters, typed by its table row. Every width is a projection of s, so
a record whose widths disagree with the row is not constructible, and the two depthwise slots
carry parameters only when the row's kernel is nonzero (DWSlot). Bias-free convs still carry
a b because the stage vocabulary takes one; the render binds it to %zb{c}.
pre-depthwise, at
s.icchannels ands.preDWkextent.expand
1x1,s.ic -> s.ic * s.expand.- ee : ℝ
post-depthwise, at the EXPANDED width and
s.postDWkextent.project
1x1,s.ic * s.expand -> s.oc.- ez : ℝ
Instances For
The slot parameters by name, read through DWSlot.params (placeholders at k = 0).
A UIB body built entirely from its table row. Dispatch from s.preDWk/s.postDWk,
widths and resolution from s, weights from a record that cannot disagree with s. Nothing
here is a free argument: given s, the only freedom left is the numeric values.
This is the body (ic -> oc). The identity skip is CertLayer.residual on top and needs
oc = ic, which holds for exactly the eighteen non-stride2 rows (guarded below) — applied by
the caller at a concrete row, where it is rfl and needs no transport.
Equations
- One or more equations did not get rendered due to their size.
Instances For
A strided body built entirely from its table row — mnv4BodyOfRow's sibling for the
three stride-2 rows (1, 3, 11). The pre-DW is a slot at the input resolution 2h, dispatched
on s.preDWk; the post-DW carries the stride and so is NOT a slot (it cannot be id', which is
why every strided row must have one — guarded above).
Equations
- One or more equations did not get rendered due to their size.