One attention head in plain NumPy
This is the attention formula from 8a. Self-attention, line for line:
import numpy as np
def softmax(x, axis=-1):
x = x - x.max(axis=axis, keepdims=True) # numerical stability
e = np.exp(x)
return e / e.sum(axis=axis, keepdims=True)
def causal_self_attention(X, W_Q, W_K, W_V):
# X: [n, d_model] W_*: [d_model, d_head]
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
d_k = Q.shape[-1]
scores = (Q @ K.T) / np.sqrt(d_k) # [n, n]
mask = np.triu(np.ones_like(scores), k=1) # 1s above the diagonal = future
scores = np.where(mask == 1, -1e9, scores) # block looking ahead
weights = softmax(scores, axis=-1) # each row sums to 1
return weights @ V # [n, d_head]
# toy run
n, d_model, d_head = 5, 16, 8
rng = np.random.default_rng(0)
X = rng.normal(size=(n, d_model))
out = causal_self_attention(
X,
rng.normal(size=(d_model, d_head)),
rng.normal(size=(d_model, d_head)),
rng.normal(size=(d_model, d_head)),
)
print(out.shape) # (5, 8)The training loop shape in PyTorch
import torch
import torch.nn.functional as F
model = MyTinyGPT(...) # embeddings -> blocks -> unembedding
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
for step, (x, y) in enumerate(loader): # y is x shifted left by one token
logits = model(x) # [batch, seq, vocab]
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
y.view(-1),
)
opt.zero_grad()
loss.backward() # backprop through everything
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()Generation loop
@torch.no_grad()
def generate(model, tokens, max_new=100, temperature=0.8):
for _ in range(max_new):
logits = model(tokens)[:, -1, :] # last position only
probs = F.softmax(logits / temperature, dim=-1)
next_tok = torch.multinomial(probs, num_samples=1)
tokens = torch.cat([tokens, next_tok], dim=1)
return tokensThat’s genuinely the skeleton. Production systems add KV caching, batching, better sampling, and a mountain of engineering, but the core idea fits on a page.
← 14. Limitations and failure modes · Contents · 16. Where to learn more →