Transformer Bug Hunt: Fix a Broken Mini-GPT - End-to-End Implementations
Implement Transformer Bug Hunt: Fix a Broken Mini-GPT in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
Transformer Bug Hunt: Fix a Broken Mini-GPT
What you'll build
A working gpt_forward by debugging a broken one. Reading and fixing someone else's model code is a common interview format, and these four bugs are the classic silent ones: the code runs and the shapes are right, but the numbers are wrong.
The setup
The starter code is a tiny GPT-style language model: token and position embeddings, one causal self-attention block, one MLP block, a final LayerNorm and an LM head. It runs without errors on short inputs, but its outputs are wrong.
There are exactly 4 bugs, all inside gpt_forward. The helpers init_params, layer_norm, softmax and gelu are correct. Find and fix the bugs; you shouldn't need to rewrite the model.
Reference architecture (what the code should compute)
For token ids tokens of length T (positions t = 0 .. T-1), with d = d_model:
x = wte[tokens] + wpe[t] # learned positions, t starts at 0
h = LN1(x)
q, k, v = h @ W_q, h @ W_k, h @ W_v
S = q @ k.T / sqrt(d)
S[i, j] = -inf for j > i # causal mask, applied BEFORE the softmax
att = softmax(S) # row-wise, each row sums to 1
x = x + (att @ v) @ W_o # residual 1
x = x + GELU(LN2(x) @ W_1 + b_1) @ W_2 + b_2 # residual 2 (pre-LN)
logits = LN_f(x) @ wte.T # tied LM head
LN(x) = g * (x - mean) / sqrt(var + 1e-5) + b over the last axis (population variance), and GELU uses the tanh approximation.
Expected Return Format
gpt_forward(tokens, params) returns a tuple (logits, att):
logits:(T, vocab_size)arrayatt:(T, T)attention probabilities, rowiholds queryi's weights
How it is tested
The tests check properties every correct decoder must have (causality, attention rows summing to 1), compare against an independent reference computation with the attention and MLP outputs switched off, and check exact logits and attention values for fixed init_params weights.
Debugging tips
Write small experiments in the editor: inspect att, try a single token, zero out W_o or W_2, compare positions.
Constraints
Edit only gpt_forward; keep the signature and the (logits, att) return.
Follow-up
- Which of these bugs would still let training loss go down? (Most of them: that is why they are dangerous.)
- What unit tests would have caught each bug before training?
Examples
Input: logits, att = gpt_forward([3, 1, 4, 1, 5], init_params()) Output: logits.shape == (5, 11), att.shape == (5, 5)
In the fixed model every row of att sums to 1 and att is lower-triangular.
Input: gpt_forward([6], init_params(seed=3))[1] Output: array([[1.]])
A single token can only attend to itself.
Practice in Python
import numpy as np
def init_params(vocab_size=11, d_model=8, max_len=8, seed=0):
"""Deterministic random weights for a 1-layer, single-head model. (Correct.)"""
rng = np.random.default_rng(seed)
def w(*shape, scale=0.5):
return rng.standard_normal(shape) * scale
d = d_model
return {
'wte': w(vocab_size, d), 'wpe': w(max_len, d),
'ln1_g': 1 + w(d, scale=0.1), 'ln1_b': w(d, scale=0.1),
'W_q': w(d, d), 'W_k': w(d, d), 'W_v': w(d, d), 'W_o': w(d, d),
'ln2_g': 1 + w(d, scale=0.1), 'ln2_b': w(d, scale=0.1),
'W_1': w(d, 4 * d), 'b_1': w(4 * d, scale=0.1),
'W_2': w(4 * d, d), 'b_2': w(d, scale=0.1),
'lnf_g': 1 + w(d, scale=0.1), 'lnf_b': w(d, scale=0.1),
}
def layer_norm(x, g, b, eps=1e-5):
"""(Correct.)"""
mu = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
return g * (x - mu) / np.sqrt(var + eps) + b
def softmax(x):
"""Row-wise softmax over the last axis. (Correct.)"""
e = np.exp(x - x.max(axis=-1, keepdims=True))
return e / e.sum(axis=-1, keepdims=True)
def gelu(x):
"""GELU, tanh approximation. (Correct.)"""
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x ** 3)))
def gpt_forward(tokens, params):
"""
Forward pass of a 1-layer, single-head, pre-LayerNorm GPT.
Args:
tokens: List of T token ids (T <= max_len)
params: Dict from init_params
Returns:
logits: (T, vocab_size)
att: (T, T) attention probabilities (row i = how query i attends)
"""
p = params
tokens = np.asarray(tokens)
T = len(tokens)
d = p['wte'].shape[1]
# Token + position embeddings
x = p['wte'][tokens] + p['wpe'][1:T + 1]
# Causal self-attention block
h = layer_norm(x, p['ln1_g'], p['ln1_b'])
q, k, v = h @ p['W_q'], h @ p['W_k'], h @ p['W_v']
scores = q @ k.T
att = softmax(scores)
mask = np.tril(np.ones((T, T)))
att = att * mask
x = x + (att @ v) @ p['W_o']
# MLP block
h = layer_norm(x, p['ln2_g'], p['ln2_b'])
x = h + gelu(h @ p['W_1'] + p['b_1']) @ p['W_2'] + p['b_2']
# Final LayerNorm and tied LM head
x = layer_norm(x, p['lnf_g'], p['lnf_b'])
logits = x @ p['wte'].T
return logits, att
The interactive editor loads with Python and NumPy. Run your implementation against 8 tests, or use the progressive hints.