Batch Normalization VJP #
This is the first layer where the casual "stare and differentiate" approach
breaks down. In dense and conv layers, every output cell yⱼ depends on
its input independently of the other inputs. In batch norm, every output
depends on every input through the mean and variance reductions, so
the Jacobian is dense and the chain rule has to do real work.
The famous result we'll derive: the input gradient collapses to a single
three-term closed form that doesn't expose the individual contributions
from mean and variance. This is the "consolidated" BN backward formula
that every ML framework hard-codes (because deriving it on the fly is a
pain). It's what MlirCodegen.emitConvBnBackward emits:
%cbg_t5 = istd * (N * d_xhat - sum(d_xhat) - xhat * sum(d_xhat * xhat))
%cbg_dconv = (1/N) * %cbg_t5
This file:
- Defines BN forward step by step (mean → var → istd → xhat → affine).
- States the easy parameter gradients (γ, β).
- Walks through the derivation of the hard input gradient and states the consolidated formula.
A note on shapes #
The actual implementation reduces over [batch, h, w] per channel. For
clarity, this file works on a single 1D Vec n (think of n as
B · H · W flattened, for one channel). The math is identical; only the
indexing changes when you go to 4D.
References #
- Ioffe & Szegedy 2015, Batch Normalization. https://arxiv.org/abs/1502.03167
- Chan, Golub, LeVeque 1982, Updating Formulae and a Pairwise Algorithm for Computing Sample Variances (COMPSTAT; the pairwise combine rule). https://doi.org/10.1007/978-3-642-51461-6_3
bnMean is Mathlib's finite average 𝔼 i, x i (Finset.expect), so its reindexing and
product lemmas apply.
Population variance: σ² = (1/N) Σᵢ (xᵢ − μ)²
Equations
- Proofs.bnVar n x = (∑ i : Fin n, (x i - Proofs.bnMean n x) * (x i - Proofs.bnMean n x)) / ↑n
Instances For
Inverse standard deviation: istd = 1 / √(σ² + ε)
Equations
- Proofs.bnIstd n x ε = 1 / √(Proofs.bnVar n x + ε)
Instances For
Second moment: E[x²] = (1/N) Σᵢ xᵢ².
At equal shard sizes the second moment of the union is the mean of the shards' second
moments (bnMeanSq_shard). bnSyncTensor4 and the sync backward are stated at μ and a
second moment m2; the emitted sync-BN forward exchanges μ and σ² (two rounds, Chan's
parallel variance, bnVar_shard_chan), and its den supplies m2 := σ² + μ².
Equations
- Proofs.bnMeanSq n x = (∑ i : Fin n, x i * x i) / ↑n
Instances For
The mean of a sharded vector is the mean of the shards' means — at EQUAL shard
sizes, which is what makes allReduceMeanF (a plain mean over replicas) the right
collective for BatchNorm statistics.
Stated at an arbitrary shard e, in the style DataParallel.meanLoss_shard sets: WHICH
cells land on which replica never enters, only that together they are the whole. The
contiguous cut the DP shim makes is the finProdFinEquiv instance.
…and so is the second moment. Free from bnMean_shard: bnMeanSq n x is
definitionally bnMean n (x·x), and squaring commutes with the shard.
The variance does not shard this way: bnVar of the union is not the mean of the shards'
bnVar unless every shard mean coincides, and that spread is what a per-replica BatchNorm
drops. The variance's shard formula is bnVar_shard_chan, which adds each shard mean's
squared offset from the global mean.
Chan's parallel variance: the variance of the whole is the mean over shards of each shard's OWN variance plus its mean's squared offset from the global mean.
What a synchronised BatchNorm exchanges instead of E[x²]: every term is a two-pass
quantity on its shard, so there is no E[x²] − μ² cancellation anywhere in f32. Proved by
expanding every variance as E[x²] − μ² (bnVar_eq_bnMeanSq_sub_sq) and collecting with
bnMean_shard / bnMeanSq_shard; stated at an arbitrary shard e, like them.
Normalized output: x̂ᵢ = (xᵢ − μ) · istd
x̂ has mean 0 and variance 1 (up to ε-correction). It's the
"centered, unit-scaled" version of x.
Equations
- Proofs.bnXhat n ε x i = (x i - Proofs.bnMean n x) * Proofs.bnIstd n x ε
Instances For
The full BN forward: yᵢ = γ · x̂ᵢ + β
γ and β are learnable per-channel parameters that restore the
network's representational freedom that normalization took away.
Without them, BN would force every layer's output to have mean 0,
variance 1 — too constraining.
MLIR (MlirCodegen.emitConvBnTrain):
%cbn_g_bc = broadcast %g
%cbn_gn = multiply %cbn_norm, %cbn_g_bc
%cbn_bt_bc = broadcast %bt
%cbn_pre = add %cbn_gn, %cbn_bt_bc
Equations
- Proofs.bnForward n ε γ β x i = γ * Proofs.bnXhat n ε x i + β
Instances For
BN of a constant vector is the (constant) shift β — centering zeroes
the normalized term, killing the √. The zero-weight blocks of the concrete
ResNet-34 and MobileNetV2 witnesses collapse through it.
γ gradient: dγ = Σᵢ dyᵢ · x̂ᵢ
γ is a scalar that multiplies each x̂ᵢ. By the product rule:
∂yᵢ/∂γ = x̂ᵢ. Summing over the output cotangent dy:
dγ = Σᵢ dyᵢ · x̂ᵢ.
This is just an inner product of dy with x̂ — no mean/variance
chain-rule trickery, because γ doesn't enter the reduction.
MLIR (MlirCodegen.emitConvBnBackward):
%cbg_gn = multiply %effGrad, %cbn_norm
%d_g = reduce add %cbg_gn across dimensions = [0, 2, 3]
Equations
- Proofs.bnGradGamma n ε x dy = ∑ i : Fin n, dy i * Proofs.bnXhat n ε x i
Instances For
β gradient: dβ = Σᵢ dyᵢ
β is added to every output, so ∂yᵢ/∂β = 1 and the gradient is
just the sum of the output cotangents. Even simpler than dγ.
MLIR (same function): %d_bt = reduce add %effGrad across dimensions = [0, 2, 3]
Equations
- Proofs.bnGradBeta n dy = ∑ i : Fin n, dy i
Instances For
Why the input gradient is hard #
The output yⱼ depends on xᵢ through three paths:
(a) Directly: xⱼ appears in (xⱼ − μ) (only when i = j).
(b) Via μ: μ is (1/N) Σₖ xₖ, so changing xᵢ changes μ
by 1/N, which shifts every (xⱼ − μ).
(c) Via σ²: σ² is (1/N) Σₖ (xₖ − μ)², so changing xᵢ
changes σ², which changes istd, which scales
every x̂ⱼ.
So ∂yⱼ/∂xᵢ ≠ 0 for every (i, j) pair — the Jacobian is dense.
Naively, the VJP costs O(N²); the consolidated form turns it into O(N)
by collapsing the cancellations algebraically.
The derivation #
Strip off the affine layer first: let dx̂ᵢ := γ · dyᵢ. Then we need
the VJP of bnXhat (the normalize step) at the cotangent dx̂.
For x̂ⱼ = (xⱼ − μ) · istd, the chain rule gives:
∂x̂ⱼ/∂xᵢ = (∂xⱼ/∂xᵢ − ∂μ/∂xᵢ) · istd + (xⱼ − μ) · ∂istd/∂xᵢ
We need three sub-derivatives:
∂xⱼ/∂xᵢ = δᵢⱼ (identity)
∂μ/∂xᵢ = 1/N (mean is linear in x)
∂σ²/∂xᵢ = (2/N) · (xᵢ − μ) (exactly: the −1/N term
multiplies Σₖ (xₖ − μ) = 0;
`bnVarDeriv_basisVec`)
∂istd/∂xᵢ = (−1/2) · istd³ · ∂σ²/∂xᵢ
= −istd³ · (xᵢ − μ) / N
= −istd · x̂ᵢ / N (since x̂ᵢ = (xᵢ−μ)·istd)
Substituting:
∂x̂ⱼ/∂xᵢ = (δᵢⱼ − 1/N) · istd − (xⱼ − μ) · istd · x̂ᵢ / N
= istd · (δᵢⱼ − 1/N − x̂ⱼ · x̂ᵢ / N)
= (istd / N) · (N · δᵢⱼ − 1 − x̂ᵢ · x̂ⱼ)
Now contract with dx̂ to get the input cotangent of the normalize step:
dxᵢ = Σⱼ (∂x̂ⱼ/∂xᵢ) · dx̂ⱼ
= (istd / N) · Σⱼ (N · δᵢⱼ − 1 − x̂ᵢ · x̂ⱼ) · dx̂ⱼ
= (istd / N) · (N · dx̂ᵢ − Σⱼ dx̂ⱼ − x̂ᵢ · Σⱼ x̂ⱼ · dx̂ⱼ)
This is the consolidated formula — three terms, two scalar reductions
(Σⱼ dx̂ⱼ and Σⱼ x̂ⱼ · dx̂ⱼ), one elementwise broadcast. O(N) work
instead of O(N²). And it's exactly what the MLIR emits.
The consolidated BN input gradient.
dxᵢ = (1/N) · istd · (N · dx̂ᵢ − Σⱼ dx̂ⱼ − x̂ᵢ · Σⱼ x̂ⱼ · dx̂ⱼ)
where dx̂ᵢ = γ · dyᵢ (gradient pulled back through the affine
layer first).
This matches MlirCodegen.emitConvBnBackward's %cbg_t* chain:
%cbg_t1 = N * d_xhat
%cbg_t2 = %cbg_t1 - sum(d_xhat) -- subtract mean
%cbg_t3 = xhat * sum(xhat * d_xhat)
%cbg_t4 = %cbg_t2 - %cbg_t3 -- the three-term combo
%cbg_t5 = istd * %cbg_t4
%cbg_dconv = (1/N) * %cbg_t5
Equations
- One or more equations did not get rendered due to their size.
Instances For
The normalised activation under HANDED-IN statistics:
x̂ᵢ = (xᵢ − μ)·(m2 − μ² + ε)^(−1/2). bnXhat's peer — same function, but reading its
statistics rather than reducing x for them, so under DP they can be the all-reduced
global ones. Shared by the sync backward and by the dy-statistics it consumes, so the two
cannot drift apart.
Instances For
bnSyncXhat is pointwise, and its size index never enters the value — which is what lets a
global row's x̂ restrict to a shard's x̂ at the SAME handed-in statistics.
bnSyncXhat at its own statistics is bnXhat. The normalised-activation peer of
bnEvalForward_at_own_stats; needed wherever a sync node's x̂ has to be recognised as the
training x̂ — in particular inside the dy-reductions the sync backward consumes.
The synchronised batch-norm input-VJP — every reduction handed in.
bnGradInput computes all four of its scalars from x and dy: the mean, the inverse
standard deviation, and the two sums. This one takes μ, the second moment m2 and the
two reduction means mdy, mdyx as arguments, so under data parallelism they can be the
all-reduced global ones and a replica can produce the shard-r block of the global-batch
gradient.
It is expressible this way because bnGradInput can be written as
istd · (dx̂ᵢ − mean(dx̂) − x̂ᵢ · mean(x̂·dx̂)). Both reductions are means, and a mean over
equal shards is the mean of the shards' means (bnMean_shard), so both survive an
allReduceMeanF, as μ does in the forward. Nothing here needs a sum.
Equations
Instances For
R = 1: the sync backward at its own statistics is bnGradInput.
The backward peer of bnEvalForward_at_own_stats: handed the statistics and reductions
the batch would itself have computed, the sync backward denotes the existing three-term
formula. So a single-device sync render computes the same function as the per-batch
bnGradInput.
Cleaner view: BN as a composition #
The BN forward is really two steps glued together:
- Normalize (
bnXhat): the hard part with mean/var/istd reductions.Vec n → Vec n, no parameters. - Affine (
fun v i => γ · vᵢ + β): elementwise scale-and-shift. The parameters γ, β live here.
If we had a HasVJP instance for each, we could compose them with
vjpComp from Tensor.lean and get the full BN VJP "for free."
The affine VJP is trivial: ∂(γ · vᵢ + β)/∂vⱼ = γ · δᵢⱼ → back(v, dy)ᵢ = γ · dyᵢ
The normalize VJP is the consolidated three-term formula above (with
γ = 1, since the affine has been factored out).
We state both as HasVJP instances. Their composition (via vjpComp)
gives the full BN input gradient — and the parameter gradients are
collected at the affine layer alongside.
The normalize step as a function Vec n → Vec n (no params except ε).
Equations
- Proofs.bnNormalize n ε = Proofs.bnXhat n ε
Instances For
The affine step as a function Vec n → Vec n (γ, β as constants).
Equations
- Proofs.bnAffine n γ β v i = γ * v i + β
Instances For
BN as the composition of normalize and affine.
The consolidated three-term formula is a theorem: we factor bnXhat as the elementwise product of
the centered input and the broadcast istd, apply pdiv_mul, and
collapse via ring using the x̂ᵢ = (xᵢ - μ) · istd identity.
Both elementary calculus facts are proved from the foundation:
pdiv_bnCentered— ∂(xⱼ - μ(x))/∂xᵢ = δᵢⱼ - 1/n. Proved viapdiv_of_linear: centering is a linear map.pdiv_bnIstdBroadcast— ∂istd(x,ε)/∂xᵢ = -istd³ · (xᵢ - μ) / n. Proved via the centering CLM +HasFDerivAt.sqrt(underbnVar + ε > 0)(hasDerivAt_inv).comp_hasFDerivAt. The centered sum collapses byΣ_k (x_k − μ) = 0. Carries(hε : 0 < ε)hypothesis throughout.
The three-term formula falls out by ring manipulation alone.
Centered input: (x - μ(x)) as a Vec n → Vec n function.
Equations
- Proofs.bnCentered n x j = x j - Proofs.bnMean n x
Instances For
Broadcast inverse-stddev: istd(x,ε) as a Vec n → Vec n function
(constant in the output index, just lifted for pdiv_mul).
Equations
- Proofs.bnIstdBroadcast n ε x x✝ = Proofs.bnIstd n x ε
Instances For
bnXhat factors as bnCentered · bnIstdBroadcast (elementwise product).
Centered-input Jacobian — proved from foundation rules.
∂(xⱼ - μ(x))/∂xᵢ = δᵢⱼ - 1/n
bnCentered is linear, so pdiv_of_linear reads the Jacobian off the basis vector:
eᵢ has entry δᵢⱼ and mean 1/n.
Smoothness of bnIstdBroadcast — proved from Mathlib calculus.
bnIstdBroadcast n ε x = 1/√(σ²(x) + ε). Since σ²(x) ≥ 0 (sum
of squares ÷ n ≥ 0) and ε > 0, the argument bnVar + ε is
everywhere positive, so Real.sqrt is differentiable
(Differentiable.sqrt with non-zero hypothesis), and its
reciprocal is differentiable too.
The centering map y ↦ y k − μ(y) as a continuous linear functional: coordinate k minus
the mean. The variance is the mean of its squares, so this is what its derivative is built
from.
Equations
- Proofs.bnCenterCLM n k = ContinuousLinearMap.proj k - (↑n)⁻¹ • ∑ i : Fin n, ContinuousLinearMap.proj i
Instances For
The derivative of the variance at x: (1/n) Σ_k 2 (x_k − μ) · center_k.
Equations
- Proofs.bnVarDeriv n x = (↑n)⁻¹ • ∑ k : Fin n, (2 * (x k - Proofs.bnMean n x)) • Proofs.bnCenterCLM n k
Instances For
The variance's derivative. σ² = (1/n) Σ_k (center_k)², and each center_k is linear,
so the product rule gives 2 center_k(x) · center_k per term.
Broadcast inverse-stddev Jacobian.
∂istd(x,ε)/∂xᵢ = -istd³(x,ε) · (xᵢ - μ(x)) / n
istd = (√(σ²+ε))⁻¹: the chain rule through Real.sqrt and x ↦ x⁻¹ on top of
bnVar_hasFDerivAt, evaluated at basisVec i by bnVarDeriv_basisVec.
The BN normalize Jacobian — derived, not axiomatized.
pdiv (bnNormalize n ε) x i j = (istd / n) · (n · δᵢⱼ − 1 − x̂ᵢ · x̂ⱼ)
Proof: factor bnXhat = bnCentered · bnIstdBroadcast, apply
pdiv_mul, substitute the two elementary Jacobians, then expand
x̂ₖ = (xₖ - μ) · istd and collapse with ring.
Affine VJP (the easy half): back(v, dy)ᵢ = γ · dyᵢ.
Each input enters one output multiplied by γ; the gradient comes
back scaled by γ.
Equations
- Proofs.bnAffineHasVJP n γ β = { backward := fun (_v dy : Proofs.Vec n) (i : Fin n) => γ * dy i, correct := ⋯ }
Instances For
Normalize VJP (the hard half): the consolidated formula with γ = 1.
back(x, dx̂)ᵢ = (1/N) · istd · (N · dx̂ᵢ − Σⱼ dx̂ⱼ − x̂ᵢ · Σⱼ x̂ⱼ · dx̂ⱼ)
Equations
- One or more equations did not get rendered due to their size.
Instances For
The BN VJP from the composition — chain rule glues affine ∘ normalize.
This is the structural payoff: once bnNormalizeHasVJP and
bnAffineHasVJP are in hand, the full BN input gradient comes
from one application of vjpComp. The chain rule mechanically
threads dy → dx̂ → dx:
dx̂ᵢ = γ · dyᵢ (from bnAffineHasVJP)
dxᵢ = (1/N · istd) · (N · dx̂ᵢ − …) (from bnNormalizeHasVJP)
The composition is exactly the two-step backward pass that the
MLIR emits (MlirCodegen.emitConvBnBackward): d_norm = grad * gamma_bc followed by
the consolidated three-term formula.
Equations
- One or more equations did not get rendered due to their size.
Instances For
bnForward is differentiable everywhere (for ε > 0).
Reuses the exact differentiability argument inside bnHasVJP:
bnForward = bnAffine ∘ bnNormalize, where bnNormalize is the
product of bnCentered (affine, hence smooth) and bnIstdBroadcast
(smooth because bnVar + ε > 0 keeps the Real.sqrt away from its
kink — see bnIstdBroadcast_differentiable), and bnAffine is affine. The
ε > 0 hypothesis is what licenses the inverse-sqrt smoothness. This
is the differentiability witness vjpCompAt needs to chain bn
into the conv→bn→relu block.
The standalone end-to-end theorem: bnGradInput is the correct VJP
of bnForward. Follows from bnHasVJP by definitional unfolding.
BN acts on coordinate differences by γ·istd — the exact identity that propagates the
carrier undamped through every BN, and the reason no BN-variance derivative is ever taken:
the difference is γ · (difference) · istd with istd evaluated at the same activation.
The two-sided BN margin |bn − β| ≤ |γ|·√n, with no mean/variance computation
(bnXhat_sq_le). This is what makes a large β keep a relu off its
kink at every input, so the structural net needs no eventually-argument for its relus.
bnIstd is continuous in the activation (ε > 0); a fun_prop atom.