Exercise np-06 — RoPE and an incremental KV-cache decode, in NumPy

A Java engineer reading model.generate(prompt) sees one call return a string and trusts that whatever happens token-by-token underneath is just "the same forward pass, run 100 times". It is not: a correct decode loop never recomputes an old token's Key or Value, and the position it feeds into position-aware attention has to stay absolute even though each step only ever sees one new token. Inside an LLM showed the KV cache saving work; this exercise builds the two pieces that make that savings possible — rotary position embeddings (RoPE) and the cache itself — by hand, on top of the attention you already wrote.

~90 minruns in the browser 7 checksnp-06

What you're building

Two things worth reading before you start. rope_freqs(8, 10000) comes out to a clean [1.0, 0.1, 0.01, 0.001] — each pair turns exactly 10× slower than the last, because the exponent steps in units of 2/8. And the whole point of the cache: check 7 inspects the actual shape decoder.project_k was called with at every step. A decode_incremental that is numerically correct but re-projects the growing prefix from scratch each time (recompute-all.py) still passes the "matches forward_full" check — it is only wasteful, not wrong — so a separate check exists purely to catch the person who "fixed" the slow version by making it fast without checking it still returns the right numbers (cache-off-by-one.py is that failure, the other direction).

If you get stuck