Prove cached == recompute (one layer)
# grade this stage (from your course directory)
npx theinference test --stage prove-one-layer --watchLast step you returned one position. Now you prove the whole layer. This is the "the optimization changes nothing" guarantee in miniature: walk a length-n sequence one token at a time from an empty cache, and at every position the cached attention output must equal what a full recompute produces there.
If this holds at all n positions for one layer, it holds for twelve, and it holds across a whole generation — which is exactly the headline you'll cash out in later chapters. The point of the cache is that it's an identity, not an approximation.
What to build
You already have layerAttnIncremental from the previous step — it walks a fresh cache across the sequence and returns every position's attention-sublayer output. This step just exposes all of those rows instead of the last one, and the grader checks every single position against recompute.
No new model code is required: the adapter embeds the ids and runs layerAttnIncremental over layer 0. If the previous step passed, the work here is wiring plus reading the warning below for what failure modes look like.
Implement runLayerEquiv
Wire runLayerEquiv(seqIds): embed the ids, run layerAttnIncremental over layer 0 with a single fresh cache, and return all per-position attention-sublayer outputs — an n × 768 array, one row per token. Each row is the incremental cached decode output for that position; the grader compares the whole stack to the recompute oracle.
import { embed } from "./src/gpt2.ts";
import { layerAttnIncremental, loadWeights } from "./src/kvcache.ts";
export async function runLayerEquiv(seqIds: number[]): Promise<number[][]> {
const w = await loadWeights(WEIGHTS);
// one fresh cache, decoded incrementally across all n positions
return layerAttnIncremental(embed(seqIds, w), w, 0); // [n][768]
}
The pattern of which positions diverge tells you exactly what's wrong:
- Only the first position matches, later ones drift more and more — your position counter isn't advancing. You're attending over a stale or short cache, so every step after the first sees the wrong history. (The classic: looping
joveroldLeninstead ofoldLen+1.) - Every position is off by a constant — your
wpeindexing base is wrong (off-by-one or reset to 0), so every token is embedded at the wrong absolute position. - First position fine, second onward wrong but stable — you re-applied a causal mask and are masking out legitimate past positions. Use the shape of the divergence to localize before you change anything.
Every position's row is compared to the recompute oracle (attnSublayer at layer 0) with allclose, atol 1e-4, rtol 1e-3. One divergent position fails the whole step — there's nowhere for a position bug to hide. The tolerance accounts only for float reassociation between the cached and recompute summation orders; anything larger is a genuine error.