SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

Prefill vs decode: perché il primo token è lento

Il prefill limita il time to first token perché è compute bound; il decode limita i token al secondo perché è memory bandwidth bound. Misurali separatamente.

Prefill e decode, in un paragrafo

La distinzione tra prefill e decode spiega la maggior parte delle questioni relative alla latenza di un LLM (large language model) self-hosted. Il prefill legge l’intero prompt in un’unica passata ed è limitato dalla capacità di calcolo. Il decode genera la risposta un token alla volta ed è limitato dalla larghezza di banda della memoria. Il time to first token è un valore del prefill. I token al secondo sono un valore del decode.

Entrambe le fasi vengono eseguite sulla stessa GPU (graphics processing unit), con gli stessi pesi e all’interno dello stesso processo, quindi è naturale considerarle un unico carico di lavoro. Si comportano come due programmi diversi che condividono lo stesso dispositivo. Separando le due fasi, una lunga serie di risultati apparentemente confusi diventa comprensibile.

Perché il prefill è vincolato dal calcolo?

Il prefill attraversa l’intero prompt in ogni layer una sola volta. Un prompt di 2,000 token fornisce a ogni moltiplicazione matriciale 2,000 righe di lavoro. La GPU esegue quindi molte operazioni aritmetiche per ogni byte di pesi caricato. Questo rapporto, cioè le operazioni aritmetiche per byte trasferito, si chiama intensità aritmetica. Il prefill ha un’intensità aritmetica elevata. Il dispositivo opera vicino al proprio limite di calcolo, mentre il bus di memoria ha capacità inutilizzata.

Il prefill produce due elementi: la KV cache, cioè i tensori key e value, per ogni token del prompt, e il primo token di output. Nessun contenuto viene inviato al lettore finché questa fase non termina. Per questo il tempo del prefill e il time to first token (TTFT) sono quasi la stessa misura.

Il costo del prefill cresce con la lunghezza del prompt. La componente lineare è il lavoro delle moltiplicazioni matriciali eseguite da ogni layer. La componente quadratica riguarda l’attention, nella quale ogni token presta attenzione a tutti i token precedenti, e diventa rilevante con contesti lunghi. Pertanto, raddoppiare il prompt almeno raddoppia il TTFT.

Puoi osservarlo in un minuto. Invia al tuo server prima un prompt di 200 token e poi uno di 2,000 token, chiedendo ogni volta lo stesso numero di token di output. Il TTFT aumenta rapidamente. La velocità di streaming dopo il primo token cambia appena.

Perché il decode è limitato dalla larghezza di banda della memoria?

Il decode produce un token per ogni passaggio. Per produrre quel singolo token, la GPU deve leggere dalla memoria tutti i pesi del modello, usare ogni peso per un paio di operazioni e poi scartarlo. L'intensità aritmetica è prossima a 1, quindi le unità di calcolo trascorrono la maggior parte del tempo in attesa.

Il decode è lento perché ogni token richiede la lettura dell'intero modello dalla memoria. È quindi il bus di memoria a determinare il ritmo, mentre le unità di calcolo restano inattive.

Per questo il limite superiore della velocità di decode su un singolo stream si può calcolare. Dividi la larghezza di banda della memoria per lo spazio occupato dai pesi.

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
  }
]

La colonna della larghezza di banda riporta il valore pubblicato nelle specifiche di ciascun produttore. La colonna del limite superiore contiene quel valore diviso per 16 GB, cioè la dimensione di un modello da 8 miliardi di parametri memorizzato con precisione a 16 bit. È un calcolo aritmetico, non il risultato di un benchmark. La velocità misurata sarà inferiore. Sapere di quanto è utile, perché indica se devi intervenire sullo stack di serving o sull'hardware.

Leggi le 6 righe in ordine e il quadro è chiaro. Una CPU con DDR5 a doppio canale trasferisce circa 90 GB/s, limitando il decode a circa 6 token al secondo per quel modello. Un L40S raggiunge circa 54. Un H100 SXM, con una larghezza di banda dichiarata di 3350 GB/s, si colloca vicino a 209.

Per lo stesso motivo, la quantizzazione è la leva singola più efficace per aumentare la velocità di decode. Se memorizzi lo stesso modello a 8 bit invece che a 16 bit, dimezzi i byte letti per token e il limite superiore raddoppia circa. Non hai aggiunto capacità di calcolo. Hai trasferito meno dati in memoria.

Come misuro ogni fase sul mio server?

Ollama restituisce la suddivisione nel corpo della risposta. Richiedi un completamento non in streaming e leggi i contatori.

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}'

Usa un tag di modello che hai effettivamente scaricato, come mostrato da ollama list. prompt_eval_count e prompt_eval_duration riguardano il prefill: il numero di token del prompt e il tempo impiegato. eval_count e eval_duration riguardano il decode. Le durate sono espresse in nanosecondi, quindi la velocità di decode è eval_count / eval_duration * 1e9 e la velocità di prefill è prompt_eval_count / prompt_eval_duration * 1e9. Nella stessa richiesta, la velocità di prefill dovrebbe risultare molto più alta di quella di decode. Il resto di questa guida spiega il motivo di questa differenza.

Per un server compatibile con OpenAI, come vLLM, curl può misurare per te il tempo fino al primo byte.

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 indica il momento in cui è arrivato il primo byte del corpo della risposta, quindi con "stream": true include il TTFT e la configurazione della connessione. Sottrai time_pretransfer per rimuovere il costo della configurazione. Esegui il test due volte e conserva il secondo risultato, perché la prima chiamata può includere il caricamento a freddo del modello.

vLLM pubblica inoltre la suddivisione come metriche Prometheus su /metrics. Esegui curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' per ottenere gli istogrammi vllm:time_to_first_token_seconds e vllm:inter_token_latency_seconds. Aggiungi vllm:num_requests_running e vllm:num_requests_waiting per la profondità della coda e vllm:kv_cache_usage_perc per la pressione sulla cache. Questi cinque nomi costituiscono l'intero dashboard.

Sotto carico, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 misura il server in esecuzione e restituisce il tempo fino al primo token e la latenza per token di output con i percentili. È l'unico modo per osservare come le due fasi entrano in competizione. Prima di ottimizzare qualsiasi parametro, raccogli una baseline pulita: il metodo descritto in misurare i token al secondo su un LLM locale produce un risultato che resta valido anche dopo un riavvio.

Perché un prompt di sistema lungo ritarda il primo token, ma non la velocità dello streaming?

Perché il prompt di sistema richiede solo lavoro di prefill. Viene elaborato una volta, nello stesso passaggio del resto del prompt, prima che venga prodotto il primo token. Dopo questo passaggio esiste soltanto come voce nella KV cache, che il processo di decode legge insieme a tutto il resto. Di conseguenza, un prompt di sistema di 3,000 token aumenta il TTFT a ogni richiesta, lasciando quasi invariato il numero di token al secondo.

Quasi, non esattamente. Le voci aggiuntive della KV cache vengono lette nuovamente a ogni passaggio di decode, quindi un prompt molto lungo rallenta leggermente anche il decode. La sezione successiva tratta questo aspetto.

La soluzione consiste nell'evitare di ricalcolare lo stesso prefisso. Un server con il prefix caching conserva nella KV cache il prefisso condiviso e lo riutilizza. La seconda richiesta con lo stesso prompt di sistema salta quindi interamente quella parte del prefill. In vLLM questa funzione si chiama automatic prefix caching; verifica vllm serve --help nella tua versione, perché l'impostazione predefinita è cambiata tra le release. La KV cache nella GPU è diversa dalla prompt cache per cui un provider API può addebitare un costo. Prima di ottimizzare una delle due, conviene leggere la differenza tra KV cache e prompt cache.

Perché il decode rallenta con l'aumentare del contesto?

Le ragioni sono due ed entrambe riguardano la KV cache.

La prima è la larghezza di banda. A ogni fase di decode, il meccanismo di attenzione legge le key e i value di tutti i token precedenti. I pesi hanno un costo fisso per token. La KV cache invece cresce nel tempo. Le sue dimensioni si possono calcolare dal config.json del modello: i byte per token equivalgono a 2 moltiplicato per num_hidden_layers, per num_key_value_heads, per la dimensione della head (hidden_size diviso num_attention_heads), per i byte per elemento. Il primo 2 conta una key e un value.

In una configurazione comune da 8 miliardi di parametri, con 32 layer, 8 head key e value sotto GQA (grouped query attention), dimensione della head pari a 128 e precisione a 16 bit, il calcolo è 2 x 32 x 8 x 128 x 2 = 131,072 byte, circa 128 KiB per token. Una conversazione di 8,000 token contiene quindi circa 1 GB di KV cache per richiesta.

La seconda è la capacità. Quell'1 GB è memoria che non può contenere i pesi o il contesto di un altro utente. Il server dimensiona il proprio pool KV una sola volta all'avvio; in vLLM lo fa tramite --gpu-memory-utilization. Quando il pool è pieno, le nuove richieste restano in attesa. L'aumento di vllm:num_requests_waiting mentre vllm:kv_cache_usage_perc rimane vicino a 1 è il segnale esatto di questa condizione. Alcuni stack sospendono una richiesta in esecuzione e ricalcolano in seguito la relativa cache invece di accodarla. Per l'utente questo si manifesta come un'interruzione nel mezzo dello stream.

Un contesto lungo ha quindi due costi: più lavoro di prefill all'inizio e più memoria letta a ogni token per il resto della risposta.

Perché il batching aumenta il throughput ma peggiora la tail latency?

Poiché il decode è vincolato alla larghezza di banda, le richieste aggiuntive hanno un costo ridotto sul piano computazionale. Una singola lettura dei pesi può produrre un token per ogni sequenza nel batch. Il throughput totale aumenta quindi quasi linearmente con la dimensione del batch, finché si esaurisce il pool KV oppure il batch diventa abbastanza grande da essere nuovamente vincolato al calcolo. Il continuous batching ricostruisce il batch a ogni step. Una richiesta completata può quindi uscire e una richiesta in coda può entrare senza attendere le richieste vicine.

Il costo emerge nei percentili. Il token successivo di ogni utente deve ora attendere la parte più lenta di uno step condiviso. Il p50, cioè la mediana, resta accettabile, mentre il p99, cioè la richiesta più lenta su 100, aumenta. Il p99 è il valore che gli utenti percepiscono, perché corrisponde alla pausa nel mezzo di una frase.

Il prefill rende il problema più evidente. Quando arriva un prompt di grandi dimensioni durante lo streaming, il dispositivo viene occupato per un unico step lungo e tutti gli stream attivi subiscono un'interruzione. Il chunked prefill riduce gran parte di questo effetto suddividendo un prompt lungo in parti e inserendo ogni parte nei batch di decode. Ad agosto 2026 il motore vLLM V1 lo esegue per impostazione predefinita ed espone questo bilanciamento tramite --max-num-batched-tokens. La documentazione di tuning di vLLM descrive chiaramente il compromesso: valori più bassi, intorno a 2048, migliorano la latenza tra token (ITL), perché meno operazioni di prefill interrompono il decode; valori più alti migliorano il TTFT, perché più token di prefill entrano in un singolo batch. Quel flag esprime il rapporto tra prefill e decode come un numero che si può modificare. Il punto in cui il p99 non è più accettabile dipende dalla capacità. quanti utenti simultanei può servire un LLM self-hosted lo determina usando le stesse metriche.

Perché una GPU più grande a volte non cambia nulla?

Perché, di solito, una GPU più grande offre più capacità di calcolo, mentre il decode non richiede capacità di calcolo.

Confrontate due righe della tabella precedente. L'A100 80GB offre una larghezza di banda dichiarata di 2039 GB/s, contro i 864 GB/s della L40S, e il limite del decode segue esattamente questo rapporto: 127 token al secondo contro 54. La RTX 4090 è una scheda molto veloce sotto la maggior parte degli aspetti e i suoi 1008 GB/s portano il limite a 63. Indipendentemente dalle altre differenze tra due schede, il decode su un singolo stream segue il valore della larghezza di banda riportato nelle specifiche.

Esistono quindi due modi per accelerare il decode: leggere meno byte per token, quantizzando i pesi oppure eseguendo un modello più piccolo, o acquistare una scheda con maggiore larghezza di banda. Il prefill è il caso opposto. Richiede capacità di calcolo, quindi una scheda più veloce riduce effettivamente il TTFT sui prompt lunghi. Se il problema è che il primo token arriva dopo quattro secondi, un hardware migliore può risolverlo. Se il problema è che il testo viene generato lentamente, probabilmente non sarà così.

Eseguire prefill e decode su worker separati?

I grandi sistemi di serving fanno esattamente questo. La tecnica si chiama disaggregazione di prefill e decode. Un pool di worker esegue soltanto il prefill, mentre un secondo pool esegue soltanto il decode. La cache KV costruita dal primo pool viene trasferita al secondo tramite un'interconnessione veloce. Funziona perché le due fasi richiedono hardware e criteri di scheduling diversi. Il prefill richiede capacità di calcolo e batch di token grandi. Il decode richiede larghezza di banda e molte sequenze simultanee. Separare le due fasi consente di scalare ogni pool in modo indipendente e impedisce che un prompt molto grande blocchi tutti gli stream attivi.

Su un singolo VPS (virtual private server) con una sola GPU, quasi mai conviene. Divideresti un unico dispositivo rispetto a se stesso e trasformeresti un puntatore in un trasferimento di gigabyte di cache attraverso la rete. La tecnica diventa vantaggiosa quando hai abbastanza acceleratori per dedicare intere macchine a ciascuna fase e abbastanza traffico costante per mantenere attivi entrambi i pool. Al di sotto di questa soglia, il prefill segmentato offre quasi lo stesso isolamento usando un solo flag.

Cosa modificare quando i numeri non sono corretti

Quando il TTFT è troppo alto:

  • Riduci il prompt. Il costo del prefill dipende dal numero di token del prompt e il prompt di sistema viene conteggiato a ogni richiesta.
  • Attiva il caching dei prefissi, in modo che un prefisso ripetuto venga elaborato una sola volta anziché a ogni richiesta.
  • Aumenta --max-num-batched-tokens, così ogni step esegue una quantità maggiore di lavoro di prefill.
  • Controlla la coda prima di attribuire il problema al modello. Un valore di vllm:num_requests_waiting superiore a zero indica che la richiesta non era ancora iniziata: si tratta di un problema di capacità.

Quando i token al secondo sono troppo pochi:

  • Quantizza i pesi. Meno byte per peso significa leggere meno byte per token.
  • Confronta la larghezza di banda della memoria pubblicata per la tua scheda con il grafico precedente e verifica quanto sei vicino al limite massimo.
  • Riduci --max-num-batched-tokens, così il prefill interrompe il decoding meno spesso.
  • Controlla la lunghezza del contesto. Una conversazione che arriva a migliaia di token legge una cache KV molto più grande a ogni step.

Anche il runtime è importante, perché Ollama e vLLM pianificano il prefill e il decoding in modo diverso e un'impostazione utile per uno può non produrre alcun effetto sull'altro. Misura prima entrambe le fasi, quindi modifica un solo parametro alla volta.

FAQ

Perché il primo token richiede alcuni secondi, mentre i successivi vengono trasmessi rapidamente?

L'attesa corrisponde al prefill, mentre la trasmissione dei token corrisponde al decode. Il prefill elabora l'intero prompt in un unico passaggio limitato dalla capacità di calcolo, prima che sia disponibile qualsiasi output. Il relativo costo aumenta quindi con la lunghezza del prompt. Il decode emette un token per ogni passaggio, a una velocità determinata dalla larghezza di banda della memoria, che è quasi indipendente dalla lunghezza del prompt. La causa più comune è un system prompt lungo, inviato a ogni richiesta. Il prefix caching elimina il costo della parte ripetuta.

Un prompt più lungo riduce il numero di token al secondo?

Sì, ma in misura limitata e per un motivo diverso dal TTFT. A ogni passaggio di decode vengono lette le key e le value di tutti i token precedenti, quindi una KV cache più grande richiede la lettura di più byte per token. In una configurazione comune con 8 miliardi di parametri, la cache occupa circa 128 KiB per token. Un contesto di 8,000 token corrisponde quindi a circa 1 GB, che viene letto a ogni passaggio. L'effetto più rilevante di un prompt lungo riguarda comunque il TTFT, non la velocità di trasmissione.

Quale specifica della GPU permette di prevedere la velocità del decode?

La larghezza di banda della memoria. Dividendo la larghezza di banda dichiarata per la dimensione dei pesi in memoria si ottiene il limite teorico per un singolo flusso. Una scheda con maggiore capacità di calcolo, ma con la stessa larghezza di banda, non trasmetterà i token più rapidamente. Per questo la quantizzazione a 8 bit raddoppia approssimativamente la velocità del decode: dimezza i byte letti per token senza modificare il calcolo.

Perché il throughput aumenta quando aggiungo utenti, ma ogni utente percepisce prestazioni peggiori?

Una singola lettura dei pesi elabora un token per ogni sequenza nel batch, quindi il numero totale di token al secondo aumenta con la dimensione del batch. Ogni singolo token deve però attendere un passaggio condiviso, quindi la latenza per utente aumenta nello stesso momento. Monitorare la latenza inter-token p99, non il valore di throughput aggregato, e controllare vllm:num_requests_waiting per verificare se le richieste sono in coda invece di essere in esecuzione.