Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Layer Normalization
The stabilizer wrapped around every Transformer sub-layer: re-center and re-scale a vector to mean 0 and variance 1, then let two learned parameters stretch and shift it back. Keeps activations well-behaved so deep nets actually train. No numpy — just the math.
The problem
Implement layer normalization over a single vector x. Compute the mean and (population) variance of x, normalize each element to zero mean and unit variance, then apply a per-element scale gamma and shift beta: y[i] = gamma[i] · (x[i] − mean) / √(var + eps) + beta[i]. Use eps = 1e-5 for numerical stability (so a constant input does not divide by zero). Return the list y.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement layer_norm(x, gamma, beta, eps=1e-5). Center and scale x to mean 0 / variance 1, then apply the per-element scale gamma and shift beta.
Examples and constraints 5 examples
x=[1,2,3,4], gamma=[1,1,1,1], beta=[0,0,0,0][-1.3416354199689269, -0.447211806656309, 0.447211806656309, 1.3416354199689269]x=[0,10], gamma=[1,1], beta=[0,0][-0.9999998000000601, 0.9999998000000601]x=[7,7,7], gamma=[1,1,1], beta=[0,0,0][0.0, 0.0, 0.0]x=[1,2,3], gamma=[2,2,2], beta=[5,5,5][2.5505286281832196, 5.0, 7.449471371816781]x=[0,0.002], gamma=[1,1], beta=[0,0][-0.3015113445777636, 0.3015113445777636]- Use the population variance:
mean((x − mean)²), dividing byn(notn−1). - Add
eps = 1e-5inside the square root before dividing. - gamma and beta are per-element (same length as
x) — scale then shift each normalized value. - Pyodide has no numpy — use the
mathstdlib and plain lists only.
Hints 4 ideas
- mean = sum(x) / n; var = sum((xi - mean)**2 for xi in x) / n (divide by n).
- The denominator is sqrt(var + eps) — the eps is what saves you when var is 0.
- Normalize each element: (x[i] - mean) / denom.
- Then scale and shift: gamma[i] * normalized + beta[i].
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious shortcut gets both statistics in one pass from running sums, using the textbook identity var = Σx² / n − mean². It is algebraically correct and costs O(n) time and O(1) extra space, the same as the right answer, so speed is not the problem: in floating point it cancels catastrophically when the values are large compared with their spread. For [1e8+1, 1e8+2, 1e8+3, 1e8+4] it computes a variance of 2.0 instead of 1.25, and with an offset of 1e9 it computes 0.0, so the outer outputs come out near ±474 instead of ±1.34. The other naive version, recomputing the mean and variance inside the per-element loop, is correct but O(n²).
The approach
Compute the statistics in two passes: the mean first, then the variance as the average squared deviation from that mean. Subtracting the mean before squaring keeps the numbers small, so nothing is lost to cancellation, and the result ignores any constant added to every input: [1e8+1, …, 1e8+4] normalizes exactly like [1, 2, 3, 4]. After normalizing, the vector has mean 0 and variance var / (var + eps), which is 1 up to the epsilon; gamma and beta then let the model choose a different scale and center for each feature.
The reference solution does exactly that with plain lists: n = len(x), mean = sum(x) / n, var = sum((xi - mean) ** 2 for xi in x) / n (the population variance, dividing by n), and denom = math.sqrt(var + eps), computed once rather than once per element. The return line builds gamma[i] * (x[i] - mean) / denom + beta[i] for every index i.
Walk through an example
Trace the first example, layer_norm([1, 2, 3, 4], [1, 1, 1, 1], [0, 0, 0, 0]), where gamma is all ones and beta all zeros, so the output is the raw normalized vector.
| step | computation | value |
|---|---|---|
| 1 | n = len(x) | 4 |
| 2 | mean = sum(x) / n | 10 / 4 = 2.5 |
| 3 | var: deviations −1.5, −0.5, 0.5, 1.5, squared and averaged | (2.25 + 0.25 + 0.25 + 2.25) / 4 = 1.25 |
| 4 | denom = math.sqrt(var + eps) | √1.25001 ≈ 1.1180385 |
| 5 | y[0] = 1 * (1 - 2.5) / denom + 0 | ≈ −1.3416354 |
| 6 | y[1] = 1 * (2 - 2.5) / denom + 0 | ≈ −0.4472118 |
| 7 | y[2] and y[3], mirror images | ≈ 0.4472118 and 1.3416354 |
| 8 | check the output | mean 0.0; variance 0.999992 = 1.25 / 1.25001 |
Complexity
Optimal (two-pass): O(n) time, for one pass to sum, one to accumulate squared deviations and one to build the output, and O(n) space for the returned list (O(1) extra). Brute force: the one-pass sum-of-squares version is also O(n) time and O(1) extra space but numerically unstable; recomputing the statistics for every element is O(n²).
Edge cases to test
- Constant input:
[7, 7, 7]→[0.0, 0.0, 0.0]. The variance is 0 and every deviation is exactly 0, so the epsilon only has to keep the denominator nonzero; without it, Python raisesZeroDivisionErrorand JavaScript producesNaNfrom 0 / 0. - Variance below epsilon:
[0, 0.002]has variance1e-6, soeps = 1e-5dominates and the output is about±0.3015, not ±1. Epsilon is not negligible for features with a tiny spread. - The output variance is
var / (var + eps), a little under 1:0.999992for[1, 2, 3, 4]. A test that compares with exactly 1 needs a tolerance. - A single element:
layer_norm([5], [3], [0.5])→[0.5]. The deviation is 0, so onlybetasurvives. - A large common offset:
[1e8+1, 1e8+2, 1e8+3, 1e8+4]gives the same output as[1, 2, 3, 4]with the two-pass formula; the one-passΣx² / n − mean²version does not. - An empty vector raises
ZeroDivisionErroratsum(x) / n. Normalizing zero features is undefined, so reject it explicitly.
Common mistakes
- Normalizing over the wrong axis — layer norm is per-example over features, unlike batch norm which normalizes over the batch.
- Forgetting the epsilon, which divides by zero on a constant (zero-variance) vector.
- Dropping the learned
γ/β, so the layer cannot rescale or re-center after normalizing. - Using the sample variance, dividing by
n − 1: it does not match the statement or PyTorch’snn.LayerNorm, and it divides by zero for a single element.
Follow-ups an interviewer asks next
- How does this extend to a batch of token vectors?
- For activations shaped
(batch, sequence, d), normalize each length-dvector on its own: mean and variance over the last axis only.gammaandbetahave lengthdand are shared by every token. Because no statistic crosses tokens or examples, one token’s output never depends on what else is in the batch, so training and inference behave identically. - What is RMSNorm, and why do many LLMs use it?
- RMSNorm drops the mean:
y = gamma · x / sqrt(mean(x²) + eps), with nobeta. That removes one reduction per vector and one parameter vector, the RMSNorm paper reports comparable results with less computation, and LLaMA uses it. The trade-off is that it rescales without re-centering, so a vector with a large constant offset keeps that offset. - How would you compute it in a single pass, for example over a stream?
- Use Welford’s algorithm. For the
k-th value, setdelta = x - mean, thenmean += delta / kandm2 += delta * (x - mean); at the end,var = m2 / n. It reads each value once, like the sum-of-squares shortcut, but it accumulates deviations from the running mean, so it avoids the catastrophic cancellation. - Where does layer norm go in a Transformer block?
- The original Transformer applied it after each residual addition (post-norm). GPT-2 moved it to the input of each sub-block (pre-norm), leaving the residual path as an unnormalized sum, and added one more layer norm after the final block. Pre-norm is generally easier to train in deep stacks, while post-norm typically needs a learning-rate warmup to stay stable.
Where this shows up
Layer norm (and its cheaper cousin RMSNorm) stabilizes training in every Transformer, wrapped around the attention and feed-forward blocks. Because it is computed per example and not across the batch, it is independent of batch size — which is exactly why it, not batch norm, suits variable-length sequence models.
Reference solution
Reference solution — Python
import math
def layer_norm(x, gamma, beta, eps=1e-5):
n = len(x)
mean = sum(x) / n
var = sum((xi - mean) ** 2 for xi in x) / n
denom = math.sqrt(var + eps)
return [gamma[i] * (x[i] - mean) / denom + beta[i] for i in range(n)]
print(layer_norm([1, 2, 3, 4], [1, 1, 1, 1], [0, 0, 0, 0])) # ~[-1.34, -0.45, 0.45, 1.34]
print(layer_norm([7, 7, 7], [1, 1, 1], [0, 0, 0])) # -> [0.0, 0.0, 0.0]Reference solution — TypeScript
function layerNorm(x: number[], gamma: number[], beta: number[], eps = 1e-5): number[] {
const n = x.length;
const mean = x.reduce((a, b) => a + b, 0) / n;
const varr = x.reduce((a, b) => a + (b - mean) ** 2, 0) / n;
const denom = Math.sqrt(varr + eps);
return x.map((xi, i) => (gamma[i] * (xi - mean)) / denom + beta[i]);
}
console.log(layerNorm([1, 2, 3, 4], [1, 1, 1, 1], [0, 0, 0, 0])); // ~[-1.34, -0.45, 0.45, 1.34]
console.log(layerNorm([7, 7, 7], [1, 1, 1], [0, 0, 0])); // -> [0, 0, 0]Questions about Layer Normalization
What is the difference between layer normalization and batch normalization?
Batch normalization computes the mean and variance of each feature across the examples in a batch, so one example’s output depends on which other examples share its batch, and inference has to use running averages collected during training. Layer normalization computes the mean and variance across the features of a single example, so every example, or every token in a Transformer, is normalized on its own. That makes layer norm independent of batch size and identical at training and inference time, which suits variable-length sequences and the small batches common when serving language models.
Why is epsilon added inside the square root in layer norm?
Because the variance can be zero or very close to it. A constant input vector has variance exactly zero, and without epsilon the normalization divides zero by zero, which raises an error in Python and produces NaN in JavaScript. Adding a small epsilon, 1e-5 here and in PyTorch’s LayerNorm by default, keeps the denominator at least the square root of epsilon. The side effect is that the output variance becomes var divided by var plus epsilon, slightly below one, and well below one when the input variance is itself comparable to epsilon.
Does layer normalization use population or sample variance?
Population variance: the sum of squared deviations divided by n, not by n minus 1. The goal is to rescale this particular vector, not to estimate the variance of a larger population it was drawn from, so the bias correction that dividing by n minus 1 provides has no purpose here. PyTorch documents that its LayerNorm uses this biased estimator, the same as torch.var with unbiased set to False. Dividing by n minus 1 would also break for a vector with a single element, where it divides by zero.
What do gamma and beta do in layer normalization?
They are a learned scale and shift for each feature, applied after normalizing. Normalization forces every vector to mean zero and variance one, which may not be what the next layer needs, so gamma multiplies each feature and beta adds an offset. Together they let the network learn any scale and center per feature, including undoing the normalization entirely if that helps. They usually start at one and zero, so a freshly initialized layer is a plain normalization. In this challenge they are passed in as lists the same length as x.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Single-Head AttentionThe operation at the heart of every Transformer: scaled dot-product attention. Given queries, keys, and values, let each query pull a weighted blend of the values — softmax(QKᵀ/√d)·V — with a numerically stable softmax and no numpy, just the math. Solve it in Python or TypeScript.Read →
- Cross-Entropy LossThe loss that trains almost every classifier and language model. It measures how surprised the model was by the right answer — high probability on the true class → near 0, confidently wrong → explodes. Return -log(p[target]), eps-guarded. Solve it in Python or TypeScript.Read →
- TF-IDFThe scoring that ran search for decades — and still seeds hybrid retrieval today. Reward a word frequent in one document but rare across the corpus, shrug off words that appear everywhere: tf·log(N/df). Solve it in Python or TypeScript.Read →
- BPE Merge StepOne step of how every tokenizer vocabulary is built: count adjacent symbol pairs across the corpus, pick the most frequent (ties break lexicographically), and merge it everywhere, left to right, without overlaps. Run it a few thousand times and you have byte-pair encoding. Solve it in Python or TypeScript.Read →
- Beam Search DecoderGreedy decoding takes the best next token and never looks back — straight into garden paths. Keep the k best partial sequences alive at every step, with a product score and a clean tie-break, and watch k=2 escape a trap k=1 falls into. Solve it in Python or TypeScript.Read →