De opbouw van KV-caching in transformer-architecturen
Inleiding
Binnen moderne taalmodellen is efficiëntie een van de belangrijkste pijlers voor praktische toepasbaarheid. Wanneer een model tekst genereert, gebeurt dit token voor token. Dit autoregressieve karakter van taalmodellen betekent dat elk nieuw gegenereerd woord (of woorddeel) afhankelijk is van alle voorgaande tokens. In een naïeve implementatie van een transformer zou het model bij elke stap de volledige geschiedenis opnieuw moeten berekenen. Dit leidt tot een enorme hoeveelheid overbodige berekeningen.
Om deze computationele redundantie tegen te gaan, maken systemen gebruik van Key-Value (KV) caching. Deze techniek slaat de tussentijdse berekeningen van eerdere tokens op in het geheugen, zodat het model bij de generatie van een nieuw token alleen de berekeningen voor dat specifieke token hoeft uit te voeren. Hoewel KV-caching de benodigde rekenkracht drastisch vermindert, introduceert het een nieuwe uitdaging: een aanzienlijke claim op het videogeheugen (VRAM) van de grafische processor (GPU).
Waarom KV-caching bestaat
Om de noodzaak van KV-caching te begrijpen, moeten we terug naar de basis van hoe een transformer-model informatie verwerkt. In het artikel over wat is een transformer wordt beschreven hoe lagen van self-attention worden gebruikt om verbanden te leggen tussen tokens. Binnen deze self-attention-lagen worden voor elk token drie vectoren gegenereerd: de Query (Q), de Key (K) en de Value (V).
Bij het berekenen van attention vergelijkt de Query-vector van het huidige token zich met de Key-vectoren van alle voorgaande tokens (inclusief zichzelf). Dit resulteert in attention-scores die bepalen hoeveel gewicht er aan de bijbehorende Value-vectoren moet worden toegekend. De resulterende output is een gewogen som van deze Value-vectoren.
Tijdens het autoregressieve generatieproces (het opeenvolgend voorspellen van tokens) verandert de context uit het verleden niet. De representaties van de tokens die al zijn geschreven of ingelezen, blijven constant. Zonder caching zou de transformer bij token nummer 100 de Key- en Value-vectoren van tokens 1 tot en met 99 volledig opnieuw moeten berekenen. Door de reeds berekende K- en V-vectoren op te slaan in een cache (de KV-cache), hoeft het systeem bij elke stap alleen de Q-, K- en V-vectoren voor het allernieuwste token te berekenen. De nieuwe Q-vector voert vervolgens een dot-product uit met alle opgeslagen K-vectoren uit de cache, waarna de gewogen som met de opgeslagen V-vectoren wordt bepaald.
De opbouw en structuur van de cache
De KV-cache is geen platte lijst van vectoren; het is een gestructureerde multidimensionale tensor. De grootte en vorm van deze tensor hangen direct samen met de specifieke architectuur van het model. De cache wordt opgebouwd op basis van de volgende dimensies:
- Lagen (Layers): Elk transformer-model bestaat uit meerdere op elkaar gestapelde attention-lagen. Voor elke individuele laag moet een aparte KV-cache worden bijgehouden.
- Attention-heads: Binnen elke laag is de attention-berekening opgesplitst in meerdere parallelle koppen (heads). De cache slaat de K- en V-vectoren per head op.
- Tokens (Sequence Length): De cache groeit naarmate de sequence (de context en de gegenereerde tokens samen) langer wordt. Dit is de dynamische dimensie van de cache.
- Head Dimensie (Head Dim): Dit is de vectorgrootte per attention-head (meestal de verborgen dimensie gedeeld door het aantal heads).
De vorm (shape) van de KV-cache tensor kan ruwweg worden weergegeven als:
[lagen, 2 (Key en Value), batch_grootte, heads, tokens, head_dim]
Deze cache leeft direct in het snelste geheugen van de accelerator (meestal VRAM op een GPU of HBM op een TPU). Omdat de cache voor elk nieuw token moet worden uitgebreid, groeit het geheugenbeslag lineair met de lengte van de context. Bij lange conversaties of het verwerken van grote documenten kan deze lineaire groei ertoe leiden dat het geheugen snel verzadigd raakt, zelfs als de modelparameters zelf gemakkelijk in het geheugen passen.
Prefill versus Decode: Twee verschillende fasen
Tijdens het uitvoeren van inference doorloopt het model twee fundamenteel verschillende fasen: de prefill-fase en de decode-fase. Deze tweedeling heeft grote invloed op hoe de KV-cache wordt gevuld en gebruikt. Zie voor een breder perspectief op deze stappen het artikel over inference-uitgelegd.
De Prefill-fase
In de prefill-fase verwerkt het model de initiële invoer (de prompt). Omdat alle tokens in de prompt direct bekend zijn, kan deze verwerking parallel plaatsvinden. De GPU kan alle matrixvermenigvuldigingen voor de prompt-tokens tegelijkertijd uitvoeren. Tijdens deze fase wordt de KV-cache voor het eerst opgebouwd: de K- en V-vectoren voor de gehele prompt worden berekend en in het geheugen opgeslagen. Deze fase is computationeel zwaar (compute-bound) en maakt optimaal gebruik van de parallelle rekenkernen van de hardware.
De Decode-fase
Zodra de prefill-fase is voltooid en het eerste nieuwe token is gegenereerd, start de decode-fase. Hier genereert het model opeenvolgend token na token. Omdat elk token pas berekend kan worden nadat het voorgaande token bekend is, is deze fase inherent sequentieel. De GPU kan hier niet langer parallel over de tijdsdimensie rekenen.
Tijdens elke decode-stap haalt het model de volledige KV-cache op uit het geheugen, voegt de nieuw berekende K- en V-vectoren van de huidige stap toe aan de cache, en voert de attention-berekening uit. Omdat er relatief weinig berekeningen worden uitgevoerd in vergelijking met de hoeveelheid data die uit het geheugen moet worden geladen, is de decode-fase bandwidth-bound (geheugenbandbreedte-beperkt). Dit verklaart waarom de generatiesnelheid vaak stagneert bij grotere contexten: het verplaatsen van de almaar groeiende KV-cache van het VRAM naar de rekenkernen vormt de voornaamste flessenhals.
Geheugenafwegingen en VRAM-verbruik
Het VRAM-verbruik van een transformer tijdens inference bestaat grofweg uit twee delen: de statische modelgewichten en de dynamische KV-cache. Terwijl de modelgewichten constant blijven, groeit de KV-cache met elke stap. Dit betekent dat het maximale geheugenverbruik direct wordt bepaald door de batchgrootte en de contextlengte.
Om een beeld te geven van de verhoudingen kunnen we kijken naar de geheugenvoetafdruk. De hoeveelheid geheugen (in bytes) die nodig is voor de KV-cache kan wiskundig worden uitgedrukt als:
Geheugen (bytes) = 2 × lagen × heads × head_dim × tokens × batch_grootte × bytes_per_element
Hierin staat de factor 2 voor de twee componenten: de Key- en de Value-vector. De bytes_per_element hangt af van de precisie waarin het model draait (bijvoorbeeld 2 bytes voor 16-bits floating-point FP16/BF16, of 1 byte voor FP8/INT8-kwantisatie).
Uit deze formule blijkt dat de relatie met de contextlengte en de batchgrootte strikt lineair is. Als de contextlengte verdubbelt, verdubbelt ook de benodigde KV-cache. Als de batchgrootte (het aantal gebruikers dat tegelijkertijd wordt bediend) verdubbelt, verdubbelt de cache eveneens. Dit zorgt ervoor dat bij grootschalige systemen de KV-cache al snel groter kan worden dan de modelparameters zelf. Dit dwingt ontwikkelaars tot het maken van keuzes tussen de maximale contextlengte en de doorvoer (throughput) van het systeem.
Technieken om de KV-cache te verkleinen
Vanwege de enorme druk die de KV-cache legt op het videogeheugen, zijn er verschillende architecturale aanpassingen ontwikkeld om de cachegrootte te reduceren zonder dat dit ten koste gaat van de modelprestaties. In de basis van het attention-mechanisme, beschreven in het artikel over attention-uitgelegd, heeft elke Query-head een eigen Key- en Value-head. Moderne varianten wijken hier echter van af.
| Techniek | Verhouding Query vs KV Heads | Impact op KV-cache |
|---|---|---|
| Multi-Head Attention (MHA) | 1:1 (Elke Q-head heeft eigen K- en V-head) | Geen reductie (maximale cachegrootte) |
| Multi-Query Attention (MQA) | Veel-op-1 (Alle Q-heads delen 1 K- en V-head) | Drastische reductie (tot wel 90%+ kleiner) |
| Grouped-Query Attention (GQA) | Veel-op-enkele (Q-heads zijn gegroepeerd per K/V-head) | Gebalanceerde reductie (meest gebruikt in moderne LLM's) |
Multi-Query en Grouped-Query Attention
Bij Multi-Query Attention (MQA) delen alle attention-heads in een laag één enkele Key- en Value-head. Dit verkleint de cache met een factor die gelijk is aan het aantal heads (vaak 32 of meer). Hoewel dit de geheugenbandbreedte enorm ontlast, kan het leiden tot een lichte daling in de nauwkeurigheid van het model, omdat het model minder capaciteit heeft om complexe patronen te onderscheiden.
Als gulden middenweg is Grouped-Query Attention (GQA) geïntroduceerd. Hierbij worden de Query-heads verdeeld in groepen (bijvoorbeeld 8 groepen), waarbij elke groep één Key- en Value-head deelt. Dit biedt een uitstekend compromis: het behoudt nagenoeg de volledige kwaliteit van MHA, terwijl het de KV-cache aanzienlijk verkleint.
Sliding-Window Attention en PagedAttention
Naast aanpassingen in de modelarchitectuur zijn er ook dynamische technieken op softwareniveau. Sliding-window attention begrenst de maximale grootte van de cache door alleen de K- en V-vectoren van de meest recente tokens (bijvoorbeeld de laatste 4096 tokens) te bewaren. Oudere tokens worden uit de cache verwijderd, waardoor de cache niet meer oneindig lineair doorgroeit.
Een andere belangrijke innovatie is PagedAttention (geïntroduceerd door vLLM). In traditionele systemen moet de KV-cache in een aaneengesloten blok in het VRAM worden opgeslagen. Dit leidt tot fragmentatie en verspilling, omdat systemen vooraf geheugen moeten reserveren voor de maximaal mogelijke sequence-lengte. PagedAttention verdeelt de KV-cache in kleine, niet-aaneengesloten pagina's (vergelijkbaar met virtueel geheugenbeheer in besturingssystemen), waardoor fragmentatie nagenoeg wordt geëlimineerd en er aanzienlijk meer batches gelijktijdig kunnen worden gedraaid.
Cache-gerelateerde artefacten en grenzen
Het is belangrijk om de KV-cache op modelniveau niet te verwarren met response-caching op API-niveau. In het artikel over caching van LLM-antwoorden wordt uitgelegd hoe volledige antwoorden van het model worden opgeslagen om herhaalde vragen direct te beantwoorden. De KV-cache daarentegen werkt onder de motorkap van het model tijdens het rekenproces en bevat ruwe activatievectoren, geen tekst.
Een belangrijk kenmerk van de KV-cache is dat deze strikt contextafhankelijk en sequentieel is. Omdat de positionele encodering van tokens invloed heeft op hoe de Key- en Value-vectoren worden opgebouwd, kun je een KV-cache niet zomaar splitsen of hergebruiken voor een compleet andere conversatie. Elke wijziging in de prompt (zelfs het aanpassen van een enkele letter aan het begin) maakt de volledige daaropvolgende KV-cache ongeldig. De cache moet in zo'n geval vanaf het punt van wijziging opnieuw worden opgebouwd via een prefill-stap.
Bij systemen die lange, statische documenten als basis gebruiken (zoals een handleiding waarover meerdere vragen worden gesteld), kan gebruik worden gemaakt van geavanceerde technieken. Zie voor meer informatie over het hergebruiken van prompts het artikel over context-caching uitgelegd. Hierbij wordt de KV-cache van het statische gedeelte (het document) opgeslagen op schijf of in een minder snel geheugensegment, om het bij een nieuwe vraag snel in te laden en zo de prefill-tijd te verkorten.
Praktische observatie en monitoring
In de praktijk is het gedrag van de KV-cache direct zichtbaar wanneer je lokale inference-servers (zoals vLLM, llama.cpp of Ollama) draait. Bij het opstarten van een model zie je vaak dat een groot deel van het VRAM direct wordt geclaimd voor de modelparameters. Zodra er verzoeken binnenkomen, stijgt het VRAM-verbruik verder. Dit is de dynamische toewijzing van de KV-cache.
Veel inference-engines geven in hun logs statistieken weer zoals kv_cache_usage of tonen het percentage van de beschikbare cache-blocks dat bezet is. Als dit percentage de 100% nadert, moet de server aanvragen in de wachtrij plaatsen (queueing) of actieve sessies afbreken omdat er geen ruimte meer is om nieuwe tokens te genereren. Bij clouddiensten zie je in de metadata van API-responsen soms termen als "cached tokens". Dit duidt op prompt-caching aan de serverzijde, waarbij de provider de KV-cache van veelvoorkomende systeemprompts herbruikt om kosten en latentie te drukken.


