SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor

Verschil tussen prefill en decode bij LLM-latentie

Begrijp waarom prefill compute-bound is voor de time to first token en decode memory-bound voor tokens per seconde. Meet deze fasen apart om uw LLM-prestaties te optimaliseren.

Prefill versus decode, in één alinea

Het onderscheid tussen prefill en decode verklaart de meeste vragen over latentie bij het zelf hosten van een LLM (large language model). Bij prefill wordt de volledige prompt in één keer gelezen, waarbij de snelheid wordt beperkt door de rekenkracht. Bij decode wordt het antwoord token voor token gegenereerd, waarbij de snelheid wordt beperkt door de geheugenbandbreedte. De 'time to first token' is een prefill-waarde, terwijl het aantal tokens per seconde een decode-waarde is.

Beide fasen draaien op dezelfde GPU (graphics processing unit), met dezelfde gewichten en binnen hetzelfde proces, waardoor het logisch is om ze als één werklast te beschouwen. Ze gedragen zich echter als twee verschillende programma's die één apparaat delen. Door ze van elkaar te scheiden, wordt een lange lijst met verwarrende resultaten plotseling inzichtelijk.

Waarom is prefill compute-bound?

Tijdens de prefill-fase wordt de volledige prompt in één keer door elke laag verwerkt. Een prompt van 2.000 tokens levert elke matrixvermenigvuldiging 2.000 rijen aan werk op, waardoor de GPU veel rekenwerk verricht voor elke byte aan gewichten die wordt geladen. Deze verhouding, rekenkracht per verplaatste byte, wordt rekenintensiteit genoemd; prefill heeft een hoge rekenintensiteit. Het apparaat werkt dicht bij zijn rekenlimiet en de geheugenbus heeft capaciteit over.

Prefill levert twee resultaten op: de KV cache (de key- en value-tensoren) voor elk prompt-token en het eerste output-token. Er bereikt niets de gebruiker totdat deze fase is voltooid; daarom zijn de prefill-tijd en de time to first token (TTFT) nagenoeg gelijk aan elkaar.

De kosten van prefill nemen toe met de lengte van de prompt. Het lineaire deel bestaat uit het matrixwerk per laag. Het kwadratische deel is de attention-fase, waarbij elk token aandacht besteedt aan elk voorgaand token; dit wordt relevant bij een lange context. Het verdubbelen van de prompt zorgt er dus voor dat de TTFT ten minste verdubbelt.

U kunt dit binnen een minuut waarnemen. Verstuur een prompt van 200 tokens naar uw server en daarna een prompt van 2.000 tokens, waarbij u telkens om hetzelfde aantal output-tokens vraagt. De TTFT stijgt aanzienlijk. De streamsnelheid na het eerste token verandert nauwelijks.

Waarom is decode memory bandwidth bound?

Decode produceert één token per stap. Om dat ene token te produceren, moet de GPU elk gewicht in het model uit het geheugen lezen, elk gewicht gebruiken voor een paar operaties en het vervolgens weer verwerpen. De rekenintensiteit is bijna 1, waardoor de rekenkernen het grootste deel van de tijd wachten.

Decode is traag omdat voor elk token het volledige model uit het geheugen moet worden gelezen; de geheugenbus bepaalt dus het tempo en de rekenkernen zijn inactief.

Dit betekent dat de bovengrens voor de snelheid van een enkele decode-stream een berekening is die u op papier kunt uitvoeren. Deel de geheugenbandbreedte door het aantal bytes dat de gewichten in beslag nemen.

ChartPublished memory bandwidth and the decode ceiling it implies for a 16 GB model
The data behind this chart
[
  {
    "device": "CPU, dual channel DDR5-5600",
    "mem_bandwidth_gb_s": 90,
    "decode_ceiling_tok_s": 6
  },
  {
    "device": "NVIDIA A10G",
    "mem_bandwidth_gb_s": 600,
    "decode_ceiling_tok_s": 38
  },
  {
    "device": "NVIDIA L40S",
    "mem_bandwidth_gb_s": 864,
    "decode_ceiling_tok_s": 54
  },
  {
    "device": "NVIDIA RTX 4090",
    "mem_bandwidth_gb_s": 1008,
    "decode_ceiling_tok_s": 63
  },
  {
    "device": "NVIDIA A100 80GB SXM",
    "mem_bandwidth_gb_s": 2039,
    "decode_ceiling_tok_s": 127
  },
  {
    "device": "NVIDIA H100 SXM",
    "mem_bandwidth_gb_s": 3350,
    "decode_ceiling_tok_s": 209
  }
]

De kolom voor bandbreedte bevat de gepubliceerde specificatie van elke fabrikant. De kolom voor de bovengrens is dat cijfer gedeeld door 16 GB, de grootte van een model met 8 miljard parameters opgeslagen met 16-bit precisie. Het is een theoretische berekening, geen benchmarkresultaat. Uw gemeten snelheid zal hieronder uitvallen. Het is nuttig om te weten hoeveel lager deze ligt, omdat dit aangeeft of u uw serving-stack moet optimaliseren of dat u uw hardware moet aanpassen.

Lees de 6 rijen in volgorde en het patroon wordt duidelijk. Een CPU op dual-channel DDR5 verplaatst ongeveer 90 GB/s, wat de decode voor dat model beperkt tot ongeveer 6 tokens per seconde. Een L40S komt uit in de buurt van 54. Een H100 SXM, met een gepubliceerde bandbreedte van 3350 GB/s, zit in de buurt van 209.

Dit is ook de reden waarom kwantisatie de krachtigste methode is om de decode-snelheid te verhogen. Sla hetzelfde model op met 8 bits in plaats van 16 en u halveert het aantal bytes dat per token wordt gelezen, waardoor de bovengrens ruwweg verdubbelt. U heeft geen extra rekenkracht toegevoegd. U heeft simpelweg minder geheugen verplaatst.

Hoe meet ik elke fase op mijn eigen server?

Ollama retourneert de verdeling in de response body. Vraag om een non-streaming completion en lees de tellers uit.

curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Explain memory bandwidth in two sentences.",
  "stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'

Gebruik een model-tag die u daadwerkelijk heeft binnengehaald; ollama list toont u welke dit zijn. prompt_eval_count en prompt_eval_duration zijn prefill: het aantal prompt-tokens en de tijd die daaraan is besteed. eval_count en eval_duration zijn decode. De tijdsduur is in nanoseconden, dus de decode-snelheid is eval_count / eval_duration * 1e9 en de prefill-snelheid is prompt_eval_count / prompt_eval_duration * 1e9. Verwacht dat de prefill-snelheid bij hetzelfde verzoek aanzienlijk hoger uitvalt dan de decode-snelheid. Dat verschil is wat de rest van deze tekst verklaart.

Voor een OpenAI-compatibele server zoals vLLM kan curl de tijd tot de eerste byte voor u meten.

curl -N -s -o /dev/null \
  -w 'pretransfer %{time_pretransfer}s  first_byte %{time_starttransfer}s\n' \
  http://localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'

time_starttransfer is het moment waarop de eerste body-byte arriveerde, dus met "stream": true is dit TTFT inclusief de verbindingstijd. Trek time_pretransfer af om de opstartkosten te verwijderen. Voer het commando twee keer uit en behoud het tweede resultaat, omdat de eerste aanroep een cold model load kan bevatten.

vLLM publiceert de verdeling ook als Prometheus-metrics op /metrics. Voer curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' uit en u krijgt de histogrammen vllm:time_to_first_token_seconds en vllm:inter_token_latency_seconds. Voeg vllm:num_requests_running en vllm:num_requests_waiting toe voor de wachtrijlengte, en vllm:kv_cache_usage_perc voor de cache-druk. Die vijf namen vormen het volledige dashboard.

Onder belasting stuurt vllm bench serve --model <name> --num-prompts 200 --request-rate 4 de draaiende server aan en rapporteert de tijd tot het eerste token en de latentie per output-token met percentielen; dit is de enige manier om te zien hoe de twee fasen met elkaar concurreren. Voordat u iets optimaliseert, stelt u een zuivere baseline vast: de methode in tokens per seconde meten op een lokale LLM levert u een resultaat op dat een herstart overleeft.

Waarom zorgt een lange systeemprompt voor vertraging bij het eerste token, maar niet bij de streaming-snelheid?

Omdat de systeemprompt uitsluitend bestaat uit prefill-werk. Deze wordt eenmalig verwerkt, in dezelfde doorloop als de rest van de prompt, voordat het eerste token verschijnt. Na die doorloop bestaat de prompt enkel nog als KV-cache-items, en bij het decoderen worden deze samen met de rest gelezen. Een systeemprompt van 3.000 tokens verhoogt dus bij elk verzoek de TTFT, terwijl het aantal tokens per seconde vrijwel gelijk blijft.

Vrijwel, maar niet exact. Die extra KV-items worden bij elke decodeerstap opnieuw gelezen, waardoor een zeer lange prompt het decoderen enigszins vertraagt. De volgende sectie behandelt dit.

De oplossing is om het opnieuw berekenen van hetzelfde voorvoegsel te stoppen. Een server met prefix caching behoudt de KV-cache van een gedeeld voorvoegsel en hergebruikt deze, waardoor het tweede verzoek met dezelfde systeemprompt dat deel van de prefill volledig overslaat. vLLM noemt dit automatische prefix caching; controleer vllm serve --help op uw versie, aangezien de standaardinstelling in verschillende releases is gewijzigd. Die KV-cache in de GPU is iets anders dan de prompt-cache waarvoor een API-provider u factureert, en het verschil tussen een KV-cache en een prompt-cache is het lezen waard voordat u een van beide optimaliseert.

Waarom vertraagt het decoderen naarmate de context voller raakt?

Er zijn twee redenen, beide gerelateerd aan de KV cache.

De eerste is bandbreedte. Bij elke decodeerstap leest de attention-laag de keys en values van elk voorgaand token. De gewichten vormen een vaste kostprijs per token. De KV cache is een groeiende factor. U kunt de omvang berekenen op basis van de config.json van het model: bytes per token is gelijk aan 2 vermenigvuldigd met num_hidden_layers, met num_key_value_heads, met de head dimension (hidden_size gedeeld door num_attention_heads), vermenigvuldigd met de bytes per element. De leidende 2 staat voor één key en één value.

Voor een gangbare opzet met 8 miljard parameters, 32 lagen, 8 key- en value-heads onder GQA (grouped query attention), een head dimension van 128 en 16-bit precisie, is dat 2 x 32 x 8 x 128 x 2 = 131.072 bytes, oftewel ongeveer 128 KiB per token. Een conversatie van 8.000 tokens bevat daarom ongeveer 1 GB aan KV cache, per verzoek.

De tweede reden is capaciteit. Die 1 GB is geheugen dat niet beschikbaar is voor gewichten of de context van een andere gebruiker. De server bepaalt de omvang van de KV-pool eenmalig bij het opstarten, in vLLM via --gpu-memory-utilization, en wanneer de pool vol is, wachten nieuwe verzoeken. Een stijgende vllm:num_requests_waiting terwijl vllm:kv_cache_usage_perc dicht bij 1 blijft, is het exacte kenmerk van die status. Sommige stacks onderbreken een lopend verzoek en herberekenen de cache later in plaats van het in de wachtrij te plaatsen; de gebruiker ervaart dit als een hapering midden in een stream.

Een lange context kost u dubbel: meer prefill-werk aan het begin en meer geheugenleestijd per token voor de rest van het antwoord.

Waarom verbetert batching de doorvoer, maar verslechtert het de tail latency?

Omdat decoderen bandbreedtegebonden is, zijn extra verzoeken aan de rekenkant vrijwel gratis. Eén keer lezen van de gewichten kan voor elke sequentie in de batch een token genereren, waardoor de totale doorvoer bijna lineair stijgt met de batchgrootte totdat de KV-pool uitgeput raakt of de batch groot genoeg wordt om weer rekenkrachtgebonden te worden. Continuous batching herbouwt de batch bij elke stap; een voltooid verzoek vertrekt en een verzoek uit de wachtrij sluit aan zonder te wachten op de rest.

De rekening wordt gepresenteerd in de percentielen. Het volgende token van elke gebruiker wacht nu op het traagste onderdeel van een gedeelde stap, waardoor de p50 (de mediaan) acceptabel blijft, terwijl de p99 (het traagste 1 op de 100 verzoeken) oploopt. De p99 is wat gebruikers opmerken, omdat dit de pauze midden in een zin is.

Prefill maakt dit effect sterker. Een grote prompt die halverwege een stream binnenkomt, houdt het apparaat één lange stap bezet, waardoor iedereen die op dat moment streamt een vertraging ervaart. Chunked prefill heft dit grotendeels op door een lange prompt in stukken te knippen en elk stuk te mengen in de decode-batches. Sinds augustus 2026 doet de vLLM V1 engine dit standaard en wordt de balans beheerd via --max-num-batched-tokens. De vLLM-documentatie over tuning verwoordt de afweging helder: kleinere waarden, rond 2048, zorgen voor een betere inter-token latency (ITL) omdat minder prefills de decodes onderbreken, en grotere waarden zorgen voor een betere TTFT omdat er meer prefill-tokens in één batch passen. Die ene vlag is de afweging tussen prefill en decode, blootgesteld als een getal dat u kunt aanpassen. Waar de p99 niet langer acceptabel is, is een vraag van capaciteit, en hoeveel gelijktijdige gebruikers één zelfgehost LLM kan bedienen werkt dit uit met dezelfde statistieken.

Waarom verandert een krachtigere GPU soms niets?

Omdat krachtiger meestal meer rekenkracht betekent, terwijl decoderen geen rekenkracht vereist.

Vergelijk twee rijen uit de bovenstaande tabel. De A100 80GB heeft een gepubliceerde bandbreedte van 2039 GB/s tegenover de L40S met 864 GB/s, en het decodeerplafond volgt dit exact: 127 tokens per seconde versus 54. De RTX 4090 is volgens de meeste maatstaven een zeer snelle kaart en de 1008 GB/s zorgt voor een plafond van 63. Wat er ook verschilt tussen twee kaarten, single-stream decoderen volgt de bandbreedtelijn op de specificatiekaart.

Er zijn dus twee manieren om decoderen te versnellen: minder bytes per token lezen (kwantiseer de gewichten of draai een kleiner model), of meer bandbreedte aanschaffen. Prefill is het tegenovergestelde geval. Dit vereist rekenkracht, dus een snellere kaart verkort daadwerkelijk de TTFT bij lange prompts. Als de klacht is dat het eerste token vier seconden duurt, kan betere hardware dit verhelpen. Als de klacht is dat de tekst langzaam wordt getypt, zal dit waarschijnlijk niet helpen.

Is het verstandig om prefill en decode op aparte workers uit te voeren?

De grote serving-stacks doen precies dit; deze techniek wordt prefill and decode disaggregation genoemd. Eén pool met workers voert alleen prefill uit, een tweede pool voert alleen decode uit, en de KV cache die door de eerste is opgebouwd, wordt via een snelle interconnect naar de tweede overgedragen. Dit werkt omdat de fasen verschillende hardware en verschillende scheduling-behoeften hebben. Prefill vereist rekenkracht en grote batches tokens. Decode vereist bandbreedte en veel gelijktijdige sequenties. Door ze te splitsen kan elke pool afzonderlijk schalen en wordt voorkomen dat één enorme prompt alle actieve streams blokkeert.

Op een enkele VPS (virtual private server) met één GPU is dit bijna nooit de moeite waard. U zou één apparaat tegen zichzelf laten strijden en een pointer veranderen in een netwerkoverdracht van gigabytes aan cache. De techniek loont pas zodra u over voldoende accelerators beschikt om volledige machines aan elke fase toe te wijzen, en over voldoende constant verkeer om beide pools bezet te houden. Daaronder biedt chunked prefill grotendeels dezelfde isolatie met slechts één flag.

Wat u moet wijzigen bij ongunstige waarden

Wanneer TTFT te hoog is:

  • Kort de prompt in. Prefill-kosten tellen mee voor prompt-tokens en de system prompt wordt bij elk verzoek in rekening gebracht.
  • Schakel prefix caching in zodat een herhaalde prefix slechts één keer wordt berekend in plaats van telkens opnieuw.
  • Verhoog --max-num-batched-tokens zodat er meer prefill-werk per stap wordt uitgevoerd.
  • Controleer de wachtrij voordat u het model de schuld geeft. Een vllm:num_requests_waiting boven nul betekent dat het verzoek nog niet was gestart; dit duidt op een capaciteitsprobleem.

Wanneer het aantal tokens per seconde te laag is:

  • Kwantiseer de gewichten. Minder bytes per gewicht betekent minder bytes gelezen per token.
  • Vergelijk de gepubliceerde geheugenbandbreedte van uw kaart met de bovenstaande tabel en kijk hoe dicht u bij het maximum zit.
  • Verlaag --max-num-batched-tokens zodat prefills het decoderen minder vaak onderbreken.
  • Controleer de contextlengte. Een gesprek dat is gegroeid tot duizenden tokens leest bij elke stap een veel grotere KV-cache.

De runtime is hier ook van belang, omdat Ollama en vLLM prefill en decode anders inplannen, en een instelling die bij de een helpt, kan bij de ander geen effect hebben. Meet eerst, in beide fasen, en wijzig daarna slechts één ding.

FAQ

Waarom duurt het eerste token seconden, maar streamen de rest snel?

Het wachten is de prefill-fase, en het streamen is de decode-fase. Prefill verwerkt de volledige prompt in één rekenintensieve doorgang voordat er output bestaat; de kosten hiervan nemen dus toe met de lengte van de prompt. Decode genereert vervolgens één token per stap met een snelheid die wordt bepaald door de geheugenbandbreedte, wat vrijwel onafhankelijk is van de lengte van de prompt. Een lange systeemprompt bij elk verzoek is de gebruikelijke oorzaak. Prefix caching verwijdert het herhaalde deel van die kosten.

Vertraagt een langere prompt het aantal tokens per seconde?

Een klein beetje, en om een andere reden dan TTFT. Elke decode-stap leest de keys en values van alle voorgaande tokens, dus een grotere KV cache betekent meer bytes lezen per token. Voor een standaard layout met 8 miljard parameters is de cache ongeveer 128 KiB per token, dus een context van 8,000 tokens is ruwweg 1 GB die bij elke stap wordt geraadpleegd. Het grootste effect van een lange prompt is nog steeds op TTFT, niet op de streamsnelheid.

Welke GPU-specificatie voorspelt de decode-snelheid?

Geheugenbandbreedte. Deel de gepubliceerde bandbreedte door de grootte van de gewichten in het geheugen en u heeft het rekenkundige plafond voor één stream. Een kaart met meer rekenkracht maar dezelfde bandbreedte zal niet sneller streamen. Dat is ook de reden waarom kwantisatie naar 8 bits de decode-snelheid ruwweg verdubbelt: het halveert het aantal gelezen bytes per token zonder de rekenkracht te beïnvloeden.

Waarom stijgt de doorvoer als ik gebruikers toevoeg, maar voelt elke gebruiker het trager?

Eén keer lezen van de gewichten bedient een token voor elke sequentie in de batch, dus het totaal aantal tokens per seconde stijgt met de batchgrootte. Elk individueel token wacht nu op een gedeelde stap, waardoor de latentie per gebruiker tegelijkertijd toeneemt. Monitor de p99 inter-token latentie, niet het totale doorvoernummer, en controleer vllm:num_requests_waiting om te zien of verzoeken in de wachtrij staan in plaats van worden uitgevoerd.