Scaled Dot-Product Attention with a Causal Mask
Using only Python lists, implement `attention(Q, K, V, causal=False)` where `Q` is `Tq x d`, `K` is `Tk x d` and `V` is `Tk x dv`. Compute `softmax(Q K^T / sqrt(d)) V` row by row. With `causal=True`, query i (aligned to the **end** of the key sequence, so `Tq <= Tk` works for incremental decoding) may only attend to keys at positions `<= i + (Tk - Tq)`.
The 1/sqrt(d) scale keeps the dot products' variance near 1 as the head dimension grows; without it softmax saturates to one-hot and gradients vanish. The causal mask is applied as -inf **before** softmax, never as a zero after it, or the remaining weights do not sum to 1. Aligning queries to the end of the keys is the detail that lets the same function serve both full prefill (Tq = Tk) and one-token decode against a KV cache (Tq = 1).
Try it first.
tests.py · 6 passing
close = lambda A, B: all(abs(x - y) < 1e-12 for ra, rb in zip(A, B) for x, y in zip(ra, rb))
Q = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
K = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
V = [[1.0, 0.0], [0.0, 1.0], [5.0, 5.0]]
out = attention(Q, K, V, causal=True)
assert close([out[0]], [V[0]])
s = 1 / math.sqrt(2)
w = [math.exp(0.0), math.exp(s)]
assert close([out[1]], [[w[0] / sum(w), w[1] / sum(w)]])
full = attention(Q, K, V)
assert close([full[2]], [out[2]])
assert close(attention([[3.0, 4.0]], [[1.0, 1.0]] * 3, V), [[2.0, 2.0]])
assert close(attention([Q[2]], K, V, causal=True), [out[2]])
try:
attention(Q, K[:2], V[:2], causal=True)
assert False
except ValueError:
passMemory, not FLOPs, is the bottleneck at long context: the naive version materialises a Tq x Tk score matrix. FlashAttention tiles Q and K, uses the online softmax (running max and sum) and never writes the full matrix to HBM. Also be ready to explain sliding-window and ALiBi or RoPE as changes to the score matrix, not to this skeleton.