blog/kv-cache-de-motor-achter-snelle-llm-inference.mdx
Alle notities

11 maart 2026

De KV-cache: waarom dezelfde generatie 142 seconden of 11 seconden duurt

Ik heb de GPT-inferentieloop met de hand herbouwd bovenop Andrej Karpathy's microgpt om precies te zien waar de kosten zitten. Zonder de KV-cache duurde het genereren van 256 tokens vanaf een prompt van 512 tokens 142 seconden. Met cache: 11 seconden. Hier lees je wat er werkelijk gebeurt, wat er op schaal misgaat, en de vuistregels die ik eruit heb gedestilleerd voor het dimensioneren van productie-inferentie.

Jermaya Leijen

Jermaya Leijen

Google Ads-specialist & AI-engineer

Ik heb inmiddels aardig wat LLM-features in productie gebracht. Ik wist dat tokens er één voor één uitkomen en dat lange context duur is. Toch bleven latency en GPU-kosten me verrassen: een feature die in de demo vlot voelde, werd traag zodra de context groeide. Wat ik miste, was een mentaal model van waar die kosten precies zitten, en zonder dat model raad je maar wat als je gaat optimaliseren.

Elke week duik ik diep in een AI-onderwerp en test ik het in de praktijk. Deze week dook ik een laag onder de API en herbouwde ik de inferentieloop met de hand: het stuk code dat een getraind model token voor token tekst laat genereren. Ik deed dat bovenop Andrej Karpathy’s microgpt, de ~200 regels pure Python die hij in februari 2026 publiceerde en die de volledige algoritmische essentie van een GPT bevatten. Geen PyTorch, geen numpy, geen Triton, geen flash-attention, geen kernel-autotuning. Losse matrixvermenigvuldigingen met een eigen autograd-engine erin, en cache = {} letterlijk uitgeschreven als een Python dict. Je kunt op elke stap een print() zetten.

Het concept dat me het meeste inzicht gaf: de KV-cache.

Wat de KV-cache eigenlijk is

Een transformer die tekst genereert doet bij elke stap hetzelfde: neem de hele prompt tot nu toe, projecteer elk token naar een Query-, een Key- en een Value-vector op elke laag, run attention, voorspel het volgende token. Lees die drie vectoren als: wat zoek ik (Query), wat bied ik aan (Key), wat draag ik bij (Value). De K en de V geven de KV-cache zijn naam.

Wat in studieboeken niet goed uit de verf komt: de K- en V-vectoren van een token veranderen niet meer zodra dat token in de sequentie staat. K en V van token #1 zijn op stap 5 hetzelfde als op stap 2. Alleen de Q voor de huidige positie is nieuw.

Dus heb je twee keuzes:

  • Herbereken K en V voor elk token bij elke stap. Simpel en correct, maar verspillend.
  • Bereken K en V één keer, sla ze op, voeg bij elke stap de K en V van het nieuwe token toe. Dit is de KV-cache.

Ik heb beide versies herbouwd bovenop microgpt. Zelfde modelgewichten, zelfde tokenizer, zelfde prompts. Daarna genereerde ik 256 tokens vanaf een prompt van 512 tokens en keek waar de tijd naartoe ging.

KV cache architecture: with cache vs without cache

Wat de cijfers zeiden

Zonder cache duurde het genereren van 256 tokens vanaf een prompt van 512 tokens op mijn CPU-run 142 seconden. De tijd per stap groeide netjes lineair mee met de sequentielengte, want elke stap herberekende alle eerdere K,V-projecties opnieuw. Op stap 1 ging dat over 512 tokens, op stap 256 over 767: ongeveer anderhalf keer zoveel K,V-werk in die laatste stap.

Met cache duurde dezelfde generatie 11 seconden, bijna 13 keer zo snel. Elke stap deed nu dezelfde vaste hoeveelheid K,V-projectiewerk voor precies één nieuw token, ongeacht positie. De staart van de curve was compleet vlak. Dat de winst over de hele run zoveel groter is dan die factor anderhalf per stap, komt doordat het herberekende werk zich bij elke stap opnieuw opstapelt.

Dat is geen kleine optimalisatie. Het bepaalt of een feature in productie blijft of stilletjes wordt teruggedraaid omdat hij te traag is. Vrijwel elk LLM dat je in productie tegenkomt, draait met de cache actief. Zonder de cache zouden GPT-achtige modellen in de praktijk onbruikbaar traag zijn.

Waar het geheugen naartoe gaat

De cache is snel omdat hij vooraf berekend is. Hij is duur omdat hij ergens moet zitten: in het VRAM, het werkgeheugen van de GPU.

Voor elk token in je context, op elke laag van het model, sla je één K-vector en één V-vector op. De totale cachegrootte is ongeveer:

2 × n_layers × n_heads × head_dim × seq_len × bytes_per_param

Bij modellen met gedeelde K en V (zie GQA verderop) vul je het aantal KV-koppen in, niet het aantal query-koppen. Ik heb de som ingevuld voor een paar echte modellen. Voor Llama-3-8B met 8K context in fp16 komt er precies 1 GB GPU-geheugen uit per gelijktijdig verzoek. Voor Llama-3-70B met 32K context is dat ruim 10 GB alleen al voor de cache. Dat het niet meer is, komt doordat dat model K en V over acht koppen deelt in plaats van over vierenzestig; zonder die ingreep had er acht keer zoveel gestaan. De modelgewichten zelf veranderen niet met de contextlengte. De cache wel.

Daarom lopen GPU's die nominaal "genoeg" VRAM hebben vast bij tientallen gelijktijdige gebruikers, en daarom zakt de batchgrootte zodra prompts langer worden. Bij lange context is de cache de eerste plek waar het knelt.

Waar het misging

Om de kosten concreet te maken heb ik vier scenario's gedraaid op de gecachte versie van microgpt. De tijden komen uit die run; de geheugengetallen zijn de formule hierboven ingevuld voor een productiemodel op Llama-3-8B-schaal, want microgpt zelf is daar veel te klein voor.

Korte prompts, korte generatie. 256 tokens in, 256 uit. De cache blijft klein. Latency wordt gedomineerd door de feed-forward-lagen van het model (FFN). De cache is in wezen gratis. Dit zijn de meeste chatworkloads.

Lange prompts, korte generatie. 8000 tokens in, 100 uit. De cache raakt vol voordat het eerste token van de generatie er is: er is een eenmalige kost voor het verwerken van de volledige prompt, in het vak heet dat prefill (~3,4 s in mijn run), waarna de generatie snel gaat. Dit is de RAG-achtige stijl van "stop opgehaalde chunks in de context". De prefill is wat gebruikers als latency ervaren.

Korte prompts, zeer lange generatie. 100 tokens in, 8000 uit. De cache groeit token voor token. De kost per stap is constant qua berekening, maar het geheugen loopt lineair op. Bij token 8000 zit je op ~1 GB cache voor één enkel verzoek.

Gelijktijdige verzoeken. Ik heb 32 streams van het vorige scenario gebatcht. Cachegeheugen is per stream, want streams delen hun cache niet. Het totale beslag ging over de 32 GB heen nog voordat het model zelf geladen was. Hier beginnen productiesystemen hun eigen GPU-budget op te eten.

Elk scenario ontkrachtte een andere intuïtie die ik had. "Lange context is duur" blijkt twee compleet verschillende dingen te betekenen: een lange prompt kost je prefill, een lange generatie kost je geheugengroei. Veel engineers gooien die twee op één hoop; ik deed het zelf ook. Naast elkaar gezet:

Lange promptLange generatie
Waar de kost zitEenmalige prefill voordat het eerste token verschijntCachegeheugen dat token voor token groeit
Wat de gebruiker merktLange wachttijd tot het eerste tokenWeinig, tot de GPU bij drukte volloopt
Typische workloadRAG: opgehaalde chunks in de contextLange antwoorden, agents, codegeneratie
Waar je aan draaitKortere prompts, hergebruik van een vaste prefixGQA, sliding window, cache-quantisatie

Wat productiesystemen hieraan doen

Zodra je de cache kunt doorzien, wordt veel recente inferentie-infrastructuur ineens een stuk minder mysterieus:

  • Multi-Query Attention (MQA) en Grouped-Query Attention (GQA): deel K en V over meerdere Q-heads. Bij de gangbare verhoudingen krimpt de cache 4–8×. Llama 3, Mistral en Gemma gebruiken dit allemaal.
  • Sliding window attention: houd alleen de meest recente N tokens in de cache. De cache krijgt een vaste grootte. Je verliest wat langeafstands-recall, wat modellen compenseren door een deel van de lagen wél de volledige context te laten zien. Mistral en Gemma 2 gebruiken dit.
  • PagedAttention (vLLM): wijs de cache niet toe als één groot aaneengesloten blok per verzoek. Behandel het als virtueel geheugen: verdeel het in pagina's en deel blokken tussen verzoeken met dezelfde prefix. De vLLM-makers meldden daarmee 2 tot 4× meer doorvoer bij gelijke latency dan de toen gangbare serving-systemen, en onder zware belasting een veelvoud daarvan.
  • Cache-quantisatie: sla K en V op als int8 of int4. Vanaf fp16 scheelt dat de helft respectievelijk driekwart van het geheugengebruik, bij beperkt nauwkeurigheidsverlies.

Dit zijn geen noodgrepen. Het zijn bewuste keuzes, gemaakt met exacte kennis van de kosten, en ze worden pas logisch als je het geheugenbeslag van de cache als vertrekpunt neemt.

En als je zelf geen GPU's beheert?

De meeste Nederlandse teams draaien geen eigen inferentie; ze bouwen op de API's van OpenAI, Anthropic of Google. Ook dan betaal je hier elke maand voor. De prijsstelling van die API's volgt de mechanica hierboven vrij direct. Input-tokens zijn goedkoper dan output-tokens, omdat de prompt in één keer parallel door het model gaat terwijl elk uitvoertoken een eigen ronde vraagt. Eerder verwerkte input-tokens zijn nog eens veel goedkoper, en dat is letterlijk de KV-cache: de provider bewaart de K- en V-vectoren van dat promptdeel en slaat de prefill over. En boven een bepaalde contextdrempel rekenen aanbieders een hoger tarief per token, omdat het geheugenbeslag per verzoek daar hard oploopt.

Concreet: wie zijn prompts zo opbouwt dat het stabiele deel vooraan staat (systeemprompt, instructies, vaste voorbeelden) en het wisselende deel achteraan, laat de provider de cache van dat vaste stuk hergebruiken en betaalt daar fors minder voor. Dat is een aanpassing van een middag, geen infrastructuurproject. Zelf hosten wordt naar mijn inschatting pas interessant bij serieus volume of strenge eisen aan waar je data draait; en dan heb je de rekensom uit dit artikel nodig om je GPU's te dimensioneren.

Wat ik nu tegen engineers zeg

Drie vuistregels die ik na dit experiment overhield:

  1. Als je inferentie traag is en je weet niet waarom, profileer prefill apart van generatie. Systemen met lange prompts zijn prefill-bound. Systemen met lange generatie zijn geheugen-bound. De oplossingen zien er compleet anders uit.
  2. VRAM-rekensom is weights + cache × concurrency, niet alleen weights. De meeste teams die ik zie, dimensioneren GPU's op basis van het model en raken dan in paniek wanneer gelijktijdig verkeer alles laat crashen.
  3. Als je modellen kiest voor productie, kijk of ze GQA of MQA gebruiken. Twee modellen met identieke benchmarkscores kunnen qua servingkosten 4–8× uiteenlopen.

Waarom microgpt nog steeds de moeite waard is

De reden dat ik steeds teruggrijp op microgpt is dat je dit experiment er zelf echt mee kunt doen. Je kunt de cache eruit commentariëren, kijken hoe de latency explodeert, hem weer aanzetten en kijken hoe de latency weer instort. Je hoeft geen blogpost of paper te vertrouwen. Je kunt je eigen print-statements tussen de regels zetten en precies zien wat er wordt herberekend.

De volledige algoritmische essentie van een GPT past echt in 200 regels. Het meeste wat productie-inferentie snel maakt, zoals paging, quantisatie en kernel fusion, is engineering bovenop die 200 regels. Het inzicht dat het zwaarst weegt in de vraag waarom moderne LLM's economisch überhaupt levensvatbaar zijn, is die handvol regels die herberekening verandert in een dictionary lookup.

Wil je weten wat er echt gebeurt binnen de modellen die jij in productie brengt, dan zou ik hier beginnen.

Jermaya Leijen

Over de auteur

Jermaya Leijen

Hoi, ik ben Jermaya. Sinds 2013 zit ik in Google Ads en de laatste jaren bouw ik AI-agents die het repeterende werk overnemen. Hier schrijf ik op wat ik in de praktijk tegenkom: wat werkt, wat niet, en hoe ik het zelf zou aanpakken. Een vraag of gewoon even sparren? Ik lees alles. Bekijk mijn werk of stuur me een bericht.