Take the row [1, 3, 5, 7] and run it through all three. The numbers are small enough to check by hand; they are also exactly what the lab above displays.
mean μ = (1+3+5+7)/4 = 4
variance σ² = (9+1+1+9)/4 = 5 σ = √5 = 2.2361
BatchNorm column stats (same row, as one column of a batch):
x̂ = [(1−4), (3−4), (5−4), (7−4)] / 2.2361
= [−1.3416, −0.4472, +0.4472, +1.3416]
with γ = 2, β = 0.5:
y = [−2.1833, −0.3944, +1.3944, +3.1833]
LayerNorm on the same row: identical arithmetic, applied per
sample: x̂ has mean 0 and rms 1 before γ and β.
RMSNorm (γ = 1):
rms = √((1+9+25+49)/4) = √21 = 4.5826
y = x / 4.5826 = [0.2182, 0.6547, 1.0911, 1.5275]
mean(y) = 0.8729 ← not centered
rms(y) = 1.0000 ← scale normalized
batch size 1, BatchNorm: x̂ = (x − x)/√(0 + ε) = 0
output = γ·0 + β = 0 — the signal is erased, only β survives.
running mean with momentum 0.1 (starting from 0), batch means 4.0, 4.5, 5.0:
after batch 1 0.9×0 + 0.1×4.0 = 0.400
after batch 2 0.9×0.4 + 0.1×4.5 = 0.810
after batch 3 0.9×0.81 + 0.1×5.0 = 1.229
after k batches of a constant mean μ: 1 − 0.9ᵏ → 65% at k = 10.
the running average lags on purpose; that lag is the stability.
Two details worth noticing. LayerNorm output really is zero-mean unit-variance before γ and β — the lab’s readout shows mean 0.0000 and rms 1.0000 for it. RMSNorm is not zero-mean: on this row the output mean is 0.8729. What it keeps is the scale guarantee, and the source’s measurements found that is most of the benefit. The running-statistics trace is the other half of the design: inference does not use today’s batch at all, which is why forgetting model.eval() makes a BatchNorm model misbehave.