Skip to content

Bridges: DSA to LLMs

Memoization and queues to the KV cache

Caching past work to speed generation

The one optimization that lets a chatbot answer in real time is nothing new - it is memoization plus an append-only queue, wearing a transformer's clothes.

You have already built both halves of the KV cache. In Recursion and the call stack you watched memoization turn an exponential Fibonacci into a linear one by storing each result the first time it was computed. In Stacks, queues, and deques you met the queue - an append-only buffer you only ever add to the end of. Bolt them together inside a transformer and you get the KV cache: each past token's key and value vectors are computed once, stored, and reused, so producing the next token stays work instead of the you would pay by recomputing the whole attention triangle from scratch.

Memoization, one level up

Recall self-attention: to attend, each token is projected into a query, a key, and a value vector. A new token's query is compared (dot product) against the key of every token at or before it - the causal mask means you never look ahead - and those scores mix the values into the output. The keys and values of the tokens already in the context do not change when a new token arrives. So recomputing them every step is exactly the wasted work naive Fibonacci did: recalculating over and over inside .

Memoize, then append

Memoization is a deal: spend memory to stop repeating work. The KV cache takes that deal. The first time the model sees a token it computes that token's key and value, then pushes them onto the back of a buffer - a queue that only ever grows. Every later step reads the whole buffer for free and appends exactly one new row. No lookups fail, nothing is ever recomputed. The cache is the growing sequence's memoized state, carried from step to step just like the accumulator in a recurrence.

Per-token cost, and the size of the cache

At the step where the context holds tokens, the causal attention pattern is a lower triangle. Recomputing all of it costs

With the cache you compute only the new query's row against the stored keys - dot products. Summed over generating tokens, that is the difference between a cubic and a quadratic total: versus . The price you pay is memory that grows linearly with context: the cache stores scalars (keys and values, every layer).

Watch the recompute vanish

Two runs generate the same sentence one token at a time. Press Generate next token. On the left, with no cache, the model rebuilds the entire triangular attention pattern every step - the already-seen rows flash red because that work was done before and thrown away. On the right, the KV cache greys out every stored key/value and computes only the single new bottom row. Watch the two cell counters diverge: identical at token 1, then quadratic versus linear.

Generating with vs. without a KV cache. Step to add a token.

rows = query per token () · columns = keys () · lower triangle = the causal mask

Thecatsatonthematandpurred
No cache - recompute the whole triangle
this step 1 cells · 0 redundant · total 1
KV cache - reuse stored keys/values
this step 1 cells · cache holds 1 k/v · total 1
computed this step (new row)recomputed - wasted workreused from cache (stored)
Total dot products computed so far
No cache
1
KV cache
1
step 1/8 · context 1 tok · KV cache = 1× less work this step

The aha to catch: at token 1 both runs do exactly one dot product - the cache buys you nothing yet. But the red triangle's area grows like while the cached run only ever adds a single row of length . By the eighth token the no-cache run is doing 36 dot products to the cache's 8, and almost all of that 36 is re-deriving keys and values it already computed and discarded. The cache never recomputes - it only remembers and appends.

# No cache: re-feed the ENTIRE sequence, recomputing K,V for all of it -> O(n^2)/token
seq = [BOS]
for _ in range(max_new):
    logits = model(seq)                 # attends over ALL of seq from scratch
    seq.append(argmax(logits))

# KV cache: compute each token's K,V once, keep appending to the buffer -> O(n)/token
seq, cache = [BOS], None
for _ in range(max_new):
    logits, cache = model(seq[-1:], past=cache)  # feed ONLY the new token
    seq.append(argmax(logits))         # cache grows by one row (append-only queue)
The whole trick, in the generation loop

How big does the cache get?

Because the cache is a pure function of the tokens seen so far, it is also shareable. Two requests that begin with the same long system prompt can reuse its keys and values instead of each recomputing them - this is prompt (prefix) caching. The provider stores the prompt's KV once and every later turn starts from it, so you are answered faster and billed less for the shared prefix. The flip side is the linear memory cost from the math above: with long contexts the KV cache, not the model weights, becomes the thing that fills the GPU - which is why techniques like paged and grouped-query caches exist.

Check yourself

With a KV cache, generating each new token costs…

What does the KV cache actually store between generation steps?

Why does a KV cache turn per-token generation from O(n^2) into O(n)?

Try to state it, then check.

Lock it in

  • The KV cache is memoization plus an append-only queue: each past token's key and value are computed once, stored, and reused.
  • Past keys and values never change under a causal mask, so recomputing them every step is the same wasted work naive Fibonacci did.
  • With the cache, each new token scores its query against the stored keys - - turning an generation into overall.
  • The price is memory linear in context ( scalars); being a pure function of the prefix, the cache is also shareable - that is prompt caching.

Primary source

Jay Alammar's The Illustrated GPT-2 draws the KV cache directly - showing how each step reuses the keys and values of prior tokens rather than recomputing them. The Hugging Face LLM Course introduces the transformer and its autoregressive decoding, the setting in which the cache (past_key_values / use_cache) pays off.

Ask your teacher

This is the same memoization that turned exponential Fibonacci into linear back in the recursion lesson: store each key/value once and the autoregressive loop drops from to per token - memoization meeting the append-only queue. Want me to trace exactly what one layer's cache holds after each step, or show why grouped-query attention shrinks it? Ask and I will add the follow-up.