Skip to content
mlmentorship

Implement a pre-norm Transformer decoder block

The code tests tensor contracts, causal masking, stable attention, residual structure, and whether you can connect a toy block to production kernels.

Published · 6 min read ·Core ·Mixed

ML implementation · active recall

Practice before you read

40 minutes. Clarify the contract, implement a correct baseline, test edge cases, then optimize.

ML implementation · closed-book attempt

Implement a pre-norm Transformer decoder block

Clarify the contract, implement a correct baseline, test edge cases, then optimize.

40:00recommended time

Closing or reloading clears the scratchpad. Only score, weak rubric dimensions, attempt count, and retry date can be stored locally.

30-second answer map

Visual first · depth when needed

Trace the unchanged residual stream through a pre-norm decoder block while normalized branches compute and add attention and MLP updates.

Preparing the visual…

Implement a pre-norm Transformer decoder block from projections and tensor operations. Do not call a built-in attention module.

Get the contract right before writing code:

  • input and output: [batch, time, d_model];
  • d_model divisible by num_heads;
  • each position may attend only to itself and earlier positions;
  • softmax runs stably;
  • residual paths preserve shape;
  • gradients reach input and parameters.

The minimal structure

A pre-norm block is:

Learning objective: trace the unchanged residual stream through a pre-norm decoder block while normalized branches compute and add attention and MLP updates.

Pre-norm block topology

Normalize the update branches, not the identity stream.

Residual stream through a pre-norm Transformer decoder block A vertical solid identity rail carries input h with shape batch by time by model dimension through two additions. At stage one, a dashed branch copies h through LayerNorm and causal self-attention, then rejoins the rail at a plus node to produce h prime. At stage two, another dashed branch copies h prime through LayerNorm and the position-wise MLP, then rejoins at a second plus node to produce output y. LayerNorm appears only on the branches, so the raw residual states bypass both normalization and sublayers. RESIDUAL STREAM · [B, T, C] THROUGHOUT h [B,T,C] + + y [B,T,C] LayerNorm 1 LN(h) Causal self-attn normalized Q, K, V future keys blocked UPDATE 1 h′ = h + Attention(LN(h)) h′ [B,T,C] LayerNorm 2 LN(h′) MLP feature update UPDATE 2 y = h′ + MLP(LN(h′)) Solid rail: unchanged identity path Dashed: normalized learned update
Read it this way: follow the solid rail from h to y: neither LayerNorm sits on that identity path. At each numbered update, copy the current residual state into the dashed branch, normalize the copy, run one sublayer, and add the result back. The second branch must start from h′, not the original h. Structure checked against Attention Is All You Need, Xiong et al.'s Pre-LN analysis, and the PyTorch decoder-layer contract; the graphic is original.

Inside attention:

  1. project to , , and ;
  2. reshape [B, T, 3C] into heads;
  3. compute ;
  4. mask positions where key index is greater than query index;
  5. apply softmax along the key dimension, preferably in FP32;
  6. multiply by ;
  7. reassemble heads and apply the output projection.

The causal-mask test is stronger than checking a triangular tensor. Change future input tokens and verify earlier outputs remain unchanged.

Reference sketch

qkv = self.qkv(hidden)
q, k, v = qkv.chunk(3, dim=-1)
q = q.view(B, T, H, D).transpose(1, 2)
k = k.view(B, T, H, D).transpose(1, 2)
v = v.view(B, T, H, D).transpose(1, 2)

scores = q @ k.transpose(-1, -2) / math.sqrt(D)
# True means block this query-key pair before softmax.
blocked = torch.triu(torch.ones(T, T, dtype=torch.bool, device=hidden.device), diagonal=1)
scores = scores.masked_fill(blocked, float("-inf"))
weights = torch.softmax(scores.float(), dim=-1).to(v.dtype)
context = weights @ v
context = context.transpose(1, 2).contiguous().view(B, T, C)
return self.output(context)

The sketch is not the entire interview. Tests and explanation determine level.

What an L4 answer sounds like

The candidate produces the correct formula but loses track of shapes, applies softmax over the query axis, or uses a mask whose boolean convention is inverted. They validate only output shape.

What an L5 answer adds

An L5 candidate writes shape comments, uses a causal-invariance test, checks gradients, and explains scaling. They know why contiguous() may be needed after transpose and why raw view() on a non-contiguous tensor can fail or misrepresent layout.

They test:

  • one token;
  • multiple heads;
  • future-token invariance;
  • finite output under large logits;
  • backward propagation;
  • invalid head dimensions.

What an L6 answer adds

An L6 candidate connects the block to the real stack without derailing implementation. They explain:

  • fused QKV projection;
  • FlashAttention avoiding materialized scores;
  • rotary position encoding entering and ;
  • GQA or MQA reducing KV-cache size;
  • KV caching changing inference from full self-attention to one-query incremental attention;
  • tensor and sequence parallelism changing projection and activation ownership;
  • dropout and deterministic behavior in training versus evaluation.

They distinguish algorithmic equivalence from kernel behavior. A mathematically correct implementation can still be unusable because it materializes attention or launches many tiny kernels.

Tells that get you a strong-hire vote

  • Shapes are explicit at every reshape and transpose.
  • The mask convention is proved with future-token invariance.
  • Scaling and softmax axis are correct.
  • Softmax stability and low precision are discussed.
  • Residual and normalization order matches the requested block.
  • Tests include gradients and causality, not only shape.
  • Production differences are concise and technically correct.

Tells that get you down-leveled

  • Copying a remembered snippet without shape reasoning.
  • Building a lower-triangular mask but not knowing whether True means keep or block.
  • Softmax over the wrong dimension.
  • Ignoring non-contiguous layout after transpose.
  • Claiming the toy implementation is FlashAttention-ready.
  • Explaining every Transformer variant before producing working code.

Common follow-up

“Why pre-norm instead of post-norm?”

Pre-norm gives the residual stream a cleaner identity path, which generally improves gradient flow and stability in deep Transformers. Post-norm can work and was used in the original Transformer, but deep modern stacks usually need more care with initialization and schedule. The choice changes block order, not the attention mechanism itself.

Use the implementation starter before copying the reference sketch.

Related: implement attention from scratch, Transformer architecture, and FlashAttention.