What is KV-cache and how does prefix caching speed up LLM inference?
865 views
67 of 106
1
The problem
Question presented to candidate:
"When a large language model generates text, people say it uses a KV-cache. What exactly is being cached, why is it allowed, and what does it cost? And what is prefix caching on top of that: how does it make a request faster, and when does it not help?"
What a strong answer should cover:
Attention computes a query, key and value per token; the KV-cache stores the keys and values of all earlier tokens so each new token computes only its own, turning generation from re-processing the whole sequence every step into one new position per step
Why it is exact: in a causal (decoder-only) model a token never attends to later tokens, so earlier keys and values never change as text is appended
The cost: GPU memory of 2 x layers x KV heads x head dim x bytes per token (128 KiB for Llama 3 8B, 320 KiB for Llama 3 70B in 16-bit), and how grouped-query attention shrinks it
Prefix caching: keep KV blocks after a request ends, identify them by a hash of all tokens up to and including the block, and reuse them when a new prompt starts with the same tokens; this skips prefill and cuts time to first token
Limits: only an exact prefix match counts (one changed token early invalidates everything after it), it does not speed up decode, and cached blocks compete for memory under LRU eviction
Code / implementation expected: Optional โ the examples run a toy two-layer transformer with and without a KV-cache (same tokens, 18 versus 78 K/V computations) and a block-hash prefix cache with LRU eviction across seven requests.
kv-cache
llm
optimization
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Engineers preparing for interviews who know roughly what an LLM is but have never looked inside one. Attention, keys, values and prefill are explained from zero.
Difficulty: Hard
Verified with: Node 22 and Python 3.12 for the transformer example; Node 22, Python 3.12, Go 1.24 and Java 21 for the prefix cache (identical output in each). Model shapes and engine facts (vLLM, SGLang) are cited inline, checked in October 2026.
How to read this doc: Plain language first, then ๐ Interview term: callouts with the words an interviewer expects. Every number comes from the two example programs, quoted in full in the Verified section. How a whole GPU fleet is built around this (queues, batching, routing) is on the AI inference infrastructure page; what API providers c
The solution is hidden so you can try first
18 min read ยท 12 sections
4
Read the code
KV-cache from first principles: a toy causal transformer generating the same tokens with and without a cache
// Block-hash prefix cache (the idea behind automatic prefix caching), LRU eviction.// Block size 4 tokens for readability; capacity 8 blocks. Words stand for token ids.constBLOCK = 4, CAP = 8;
functionfnv1a(s) {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
return h.toString(16).padStart(8, "0");
}
const cache = newSet(); // block hashes whose K/V tensors are residentlet order = []; // LRU order: front = evict firstlet totalTokens = 0, totalComputed = 0;
functionblockHashes(tokens) {
const out = []; let parent = "root";
for (let i = 0; i + BLOCK <= tokens.length; i += BLOCK) {
parent = fnv1a(parent + "|" + tokens.slice(i, i + BLOCK).join(" "));
out.push(parent); // a block hash covers ALL tokens before it, not just its own 4
}
return out;
}
functionserve(name, text) {
const tokens = text.split(" ");
const hashes = blockHashes(tokens);
let hit = 0;
while (hit < hashes.length && cache.has(hashes[hit])) hit++; // stop at the first missconst evicted = [];
for (let b = hit; b < hashes.length; b++) {
if (cache.has(hashes[b])) continue;
while (cache.size >= CAP) {
const victim = order.find((h) => !hashes.includes(h));
order = order.filter((h) => h !== victim); cache.delete(victim); evicted.push(victim.slice(0, 4));
}
cache.add(hashes[b]);
}
// touch tail first so the first block ends up most recent: parents outlive childrenfor (let b = hashes.length - 1; b >= 0; b--) { order = order.filter((h) => h !== hashes[b]); order.push(hashes[b]); }
const computed = tokens.length - hit * BLOCK;
totalTokens += tokens.length; totalComputed += computed;
console.log(`${name}: ${tokens.length} tokens, ${hashes.length} full blocks, hit ${hit} blocks (${hit * BLOCK} tokens), prefill ${computed}` +
(evicted.length ? `, evicted ${evicted.join(",")}` : "") + `, cached ${cache.size}/${CAP}`);
return hashes;
}
constSYS = "you are the support bot for acme corp . be brief .";
console.log(`system prompt = ${SYS.split(" ").length} tokens = 3 blocks of ${BLOCK}`);
const h1 = serve("r1 first user ", SYS + " how do i reset my password ?");
console.log(`r1 block hashes ${h1.map((h) => h.slice(0, 4)).join(" -> ")}`);
serve("r2 other user ", SYS + " where is my order ?");
serve("r3 r1 retried ", SYS + " how do i reset my password ?");
serve("r4 one word off", SYS.replace("support", "sales") + " where is my order ?");
serve("r5 r1 turn two ", SYS + " how do i reset my password ? click forgot password on the login page . thanks , and my email ?");
serve("r6 new topic ", "translate to french : the cat sleeps on the warm mat today");
serve("r7 back to sys ", SYS + " can i change my plan ?");
console.log(`total: ${totalTokens} prompt tokens, ${totalComputed} computed, ${totalTokens - totalComputed} served from cache (${(100 * (totalTokens - totalComputed) / totalTokens).toFixed(1)}%)`);
5
Discussion
Other approaches, follow-ups and how it went in real interviews.