Guide sull’IA · 11 min di lettura

Che cos'è una KV cache? Come gli LLM riutilizzano gli stati di attenzione

Scopri cos'è una KV cache, come accelera la decodifica degli LLM, cresce con il contesto e influisce sui costi delle API e sul prompt caching.

Illustrazione di una sequenza di tessere colorate di token che alimentano blocchi di cache impilati, da cui esce un flusso luminoso più veloce

Una KV cache (cache Key-Value) è un’ottimizzazione dell’inferenza in cui un modello linguistico di grandi dimensioni memorizza, per ogni livello, i tensori di attenzione Key e Value calcolati per i token già elaborati. Durante la decodifica autoregressiva, il modello riutilizza questi stati anziché ricalcolare tutti i token precedenti a ogni passaggio, riducendo i calcoli ridondanti man mano che le sequenze crescono.

Sia che si chiamino API ospitate sia che si eseguano modelli a pesi aperti sulle proprie GPU, la KV cache è centrale per latenza, pressione sulla memoria e throughput. Capirne il funzionamento spiega perché il tempo al primo token viene percepito diversamente dalla velocità di streaming, perché i contesti lunghi consumano tanta VRAM e perché il prompt caching tra richieste API richiede prefissi esattamente identici.

Come funziona l’attenzione senza una KV cache?

Senza una KV cache, un Transformer autoregressivo deve ricalcolare da zero le proiezioni di attenzione dell’intera sequenza ogni volta che genera un nuovo token in un modello di IA.

In ogni livello del decoder Transformer, la self-attention proietta la rappresentazione di ogni token di input in tre vettori:

  • Query ($Q$): rappresenta la posizione corrente che cerca il contesto pertinente.
  • Key ($K$): rappresenta ciò che ogni posizione offre per il confronto con le Query.
  • Value ($V$): contiene le informazioni aggregate dopo il calcolo dei pesi di attenzione dai prodotti scalari Query-Key.

Poiché i modelli linguistici con solo decoder generano il testo un token alla volta, per generare il token $t+1$ basta che il vettore Query del token più recente presti attenzione alle Key e ai Value dei token da $1$ a $t$. Se il runtime elimina quelle Key e quei Value dopo aver emesso il token $t$, il passaggio $t+1$ deve far ripassare ogni token del prefisso attraverso tutti i livelli solo per ricreare esattamente gli stessi tensori $K$ e $V$. Con l’allungarsi del prompt e dell’output, questo lavoro ripetuto cresce quadraticamente nell’intero processo di generazione.

Perché l’attenzione causale rende possibile il caching KV?

La mascheratura causale garantisce che i tensori Key e Value di un token dipendano solo dai token precedenti, mai da quelli generati in seguito.

In un decoder causale, da sinistra a destra, al token $i$ è impedito prestare attenzione a qualsiasi posizione $j > i$. Poiché l’informazione scorre solo in avanti nella sequenza:

  1. La rappresentazione del token $i$ al livello $l$ è completamente fissata una volta elaborati i token $1 \dots i$.
  2. Aggiungere il token $i+1$ alla fine della sequenza non modifica gli stati nascosti, le Key o i Value delle posizioni $1 \dots i$ in nessun livello.
  3. Il runtime può aggiungere i vettori appena calcolati $K_{i+1}$ e $V_{i+1}$ a un buffer per livello, la KV cache, e passare solo il token più recente al successivo passaggio di decodifica.

Come documentato nella guida alla KV cache di Hugging Face Transformers e nella panoramica dell’architettura della cache di Transformers, il caching trasforma ogni passaggio di decodifica da una scansione dell’intera sequenza a un aggiornamento incrementale di un singolo token, che legge dalla memoria le Key e i Value precedenti.

In che modo Prefill e Decode usano diversamente la KV cache?

L’inferenza di un LLM si divide in due fasi distinte, Prefill e Decode, che interagiscono con la KV cache in modi sostanzialmente diversi e incontrano differenti colli di bottiglia hardware.

Fase di inferenza Cosa accade alla KV cache Principale collo di bottiglia hardware Metrica visibile all’utente
Prefill (elaborazione del prompt) Calcola $K$ e $V$ in parallelo per tutti i token del prompt e riempie la cache iniziale in ogni livello. Limitato dal calcolo (grandi moltiplicazioni tra matrici sull’intero prompt). Tempo al primo token (TTFT).
Decode (generazione dei token) Calcola $Q$, $K$ e $V$ per un nuovo token alla volta, aggiunge la nuova voce $K$/$V$ e legge tutta la cronologia in cache. Limitato dalla larghezza di banda della memoria (caricamento dei pesi del modello e degli stati KV crescenti a ogni passaggio). Latenza tra token / velocità di streaming dell’output.

Come spiegato nell’articolo tecnico NVIDIA su Prefill, Decode e caching KV, la fase Prefill elabora simultaneamente tutti i token di input. Poiché l’intero prompt è noto in anticipo, le GPU possono raggruppare le proiezioni in operazioni su matrici dense, saturando le unità di calcolo e riempiendo la KV cache in un solo passaggio. Prompt lunghi, come i contesti con più documenti nelle pipeline di generazione aumentata dal recupero (RAG), aumentano direttamente il lavoro di Prefill e il tempo al primo token.

Emesso il primo token di output, l’inferenza entra nella fase Decode. Ogni passaggio elabora un solo nuovo token per sequenza, aggiunge i suoi vettori Key e Value alla cache e legge tutti i tensori $K$ e $V$ memorizzati dalla memoria ad alta larghezza di banda della GPU (HBM) per calcolare l’attenzione. Poiché l’intensità aritmetica per passaggio è bassa mentre le letture dalla memoria crescono con la lunghezza della sequenza e la dimensione del batch, la decodifica diventa presto limitata dalla larghezza di banda della memoria e dalla VRAM disponibile anziché dai FLOPs di calcolo grezzi.

Quanta memoria usa una KV cache?

L’occupazione di memoria di una KV cache cresce linearmente con la lunghezza della sequenza, la dimensione del batch, il numero di livelli e il numero di teste di attenzione Key-Value.

A partire dalle forme dei tensori usate nelle cache Transformer standard, dove ogni livello memorizza un tensore Key e uno Value con dimensioni relative a batch, teste KV, lunghezza della sequenza e dimensione della testa, la dimensione della cache per sequenza si esprime simbolicamente così:

bytes ≈ 2 × layers × kv_heads × head_dim × seq_len × bytes_per_element

Ogni termine della formula è una leva che architetti dei modelli e ingegneri dell’inferenza usano per tenere sotto controllo la memoria:

  • 2: tiene conto della memorizzazione dei tensori sia Key (K) sia Value (V).
  • layers: numero totale di blocchi decoder Transformer nel modello.
  • kv_heads e head_dim: numero di teste di attenzione Key/Value per livello e dimensione di ogni testa.
  • seq_len: totale dei token in cache, ossia token del prompt più token generati fino a quel momento.
  • bytes_per_element: precisione numerica dei tensori memorizzati, per esempio 2 byte per FP16/BF16, 1 byte per formati a 8 bit o frazioni inferiori con quantizzazione della KV cache a 4 bit come NVFP4.

Ridurre kv_heads: MHA, MQA e GQA

Nella classica Multi-Head Attention (MHA), ogni testa Query ha una propria testa Key e una propria testa Value (kv_heads = query_heads). Con l’espansione delle finestre di contesto e l’aumento delle dimensioni dei batch, memorizzare proiezioni K e V separate per ogni testa è diventato un importante collo di bottiglia di memoria.

Due modifiche architetturali riducono direttamente kv_heads nella formula precedente:

Oltre a GQA, i motori di inferenza riducono bytes_per_element quantizzando i tensori K e V in cache in formati a precisione inferiore, così da far entrare nella memoria GPU contesti più lunghi o batch simultanei più grandi.

Come gestiscono la memoria della KV cache i motori di serving su larga scala?

I motori di serving in produzione gestiscono la memoria della KV cache suddividendo i tensori contigui in blocchi di dimensione fissa, così che le GPU possano ospitare più richieste simultanee senza sprecare VRAM.

Nelle implementazioni più semplici, un runtime di serving prealloca una porzione contigua di memoria GPU dimensionata sulla lunghezza massima possibile della sequenza di ogni richiesta. Poiché le lunghezze effettive dell’output non sono note in anticipo, la preallocazione causa una grave frammentazione interna ed esterna della memoria: gli spazi riservati restano inutilizzati mentre le nuove richieste non possono essere eseguite.

Kwon et al. hanno affrontato questo collo di bottiglia con PagedAttention in Efficient Memory Management for Large Language Model Serving with PagedAttention (arXiv:2309.06180), il meccanismo alla base di vLLM. Ispirato alla memoria virtuale e al paging dei sistemi operativi:

  • Blocchi non contigui di dimensione fissa: PagedAttention divide la KV cache di ogni richiesta in blocchi di dimensione fissa che contengono Key e Value per un numero definito di token, associati tramite una tabella di corrispondenza tra blocchi logici e fisici.
  • Allocazione su richiesta: nuovi blocchi fisici vengono allocati solo quando si generano nuovi token, eliminando praticamente la frammentazione interna.
  • Condivisione dei prefissi a livello di blocco: quando più richieste condividono un prefisso del prompt identico, come un prompt di sistema o esempi few-shot comuni, le rispettive tabelle di blocchi logici possono puntare agli stessi blocchi KV fisici in memoria, usando la copia su scrittura solo quando le sequenze divergono.

Nella valutazione dello studio su vLLM (arXiv:2309.06180), questa gestione KV a pagine ha raggiunto un throughput 2–4× superiore a FasterTransformer e Orca a latenza comparabile, facendo entrare batch notevolmente più grandi nella stessa memoria GPU. Gli stack di serving moderni, tra cui vLLM e il sistema di riutilizzo della KV cache di NVIDIA TensorRT-LLM, si basano sulla gestione KV a blocchi per riutilizzare i prefissi condivisi e massimizzare la concorrenza.

Qual è la differenza tra KV cache e prompt caching?

Una KV cache è il meccanismo di inferenza sottostante, specifico per richiesta, che riutilizza gli stati di attenzione tra i passaggi di decodifica. Il prompt caching è invece una funzione di serving e API tra richieste che conserva e riutilizza questi blocchi KV tra chiamate distinte con un prefisso identico.

Poiché gli sviluppatori incontrano spesso entrambi i termini quando ottimizzano latenza e costi, è utile distinguere ciò che avviene in una singola chiamata di generazione da ciò che avviene tra più richieste API:

Aspetto KV cache (decodifica nella richiesta) Prompt caching (riutilizzo dei prefissi tra richieste)
Ambito Una singola richiesta, tra i passaggi di generazione dei token. Più richieste in un breve intervallo di conservazione.
Cosa evita Ricalcolare i token precedenti nella stessa risposta mentre si decodifica ogni nuovo token. Ripetere il Prefill sui prefissi statici del prompt ripetuti tra chiamate distinte.
Ciclo di vita Creata durante Prefill, estesa durante Decode, liberata o rimossa al termine della richiesta. Conservata in storage a blocchi (memoria GPU o host), con una politica di rimozione e TTL per le chiamate future.
Visibilità in fattura Integrata nell’inferenza standard; non fatturata come voce separata. Spesso offerta dai fornitori API con prezzi scontati per i token di input in cache e TTFT inferiore.

L’attenzione causale impone una regola rigorosa al prompt caching: il prefisso deve corrispondere esattamente fin dal primissimo token. Poiché i tensori Key e Value di ogni token dipendono dalla sequenza esatta dei token precedenti, modificare anche un solo timestamp, ID utente o istruzione all’inizio del prompt invalida gli stati $K$ e $V$ in cache di tutti i token successivi.

Per le regole di conservazione, le soglie minime di token e le strutture di sconto dei singoli fornitori, consulta la guida completa su cos’è il prompt caching e come viene tariffato.

Indicazioni pratiche per sviluppatori e team

Capire il comportamento della KV cache si traduce direttamente in una migliore architettura dei prompt, latenza più stabile e minori spese infrastrutturali:

  1. Mantieni i contenuti statici all’inizio del prompt: colloca istruzioni di sistema, schemi degli strumenti e documenti di riferimento condivisi prima di input utente dinamici o metadati specifici della richiesta. Un prefisso identico permette sia alle API ospitate sia ai motori locali come vLLM o TensorRT-LLM di riutilizzare blocchi KV in cache anziché eseguire passaggi Prefill ridondanti.
  2. Osserva come la lunghezza del contesto influisce sulla concorrenza nelle GPU gestite in proprio: poiché la KV cache cresce linearmente con seq_len, raddoppiare la lunghezza media del contesto dimezza il numero di richieste simultanee che la memoria GPU può contenere. Per i modelli a pesi aperti gestiti in proprio, privilegia architetture con Grouped-Query Attention (GQA) e runtime che supportano allocazione KV a pagine e quantizzazione KV.
  3. Riduci il contesto superfluo anche con finestre di contesto enormi: fornire cronologie enormi a ogni turno aumenta la latenza di Prefill, la pressione sulla memoria KV e i costi dei token. Combina il caching dei prefissi con recupero mirato e riepiloghi periodici: sono strategie fondamentali trattate nella guida su come ridurre i costi delle API IA e nel confronto dei prezzi delle API IA.

Domande frequenti

Una KV cache cambia la qualità dell’output del modello?

Una KV cache standard mantenuta nella precisione di inferenza nativa del modello, come FP16 o BF16, è matematicamente equivalente a ricalcolare il prefisso a ogni passaggio, perché la mascheratura causale impedisce ai token futuri di modificare i vettori Key e Value passati. Compromessi sulla qualità emergono solo applicando una quantizzazione con perdita della KV cache, come la compressione a 8 o 4 bit, oppure rimuovendo token più vecchi per risparmiare memoria.

Perché una KV cache non può riutilizzare i token a metà di un prompt se l’inizio è cambiato?

In un Transformer causale, la rappresentazione di ogni token incorpora l’attenzione su tutti i token precedenti nella sequenza. Se cambiano i primi dieci token di un prompt, cambieranno anche gli stati nascosti e quindi le proiezioni Key e Value dall’undicesimo token in poi. Poiché questi stati dipendono dal prefisso esatto che li precede, i motori di serving possono riutilizzare blocchi KV in cache tra richieste solo quando il prefisso corrisponde dall’inizio.

La KV cache contiene i pesi del modello?

No. I pesi del modello sono parametri statici appresi durante l’addestramento e condivisi da tutte le richieste servite dalla GPU. La KV cache contiene tensori di attivazione dinamici specifici della richiesta, ossia le proiezioni Key e Value dei token attualmente nel contesto. Durante l’inferenza, la memoria GPU totale deve contenere i pesi statici del modello, le attivazioni temporanee del runtime e le KV cache dinamiche di tutte le richieste attive nel batch.

Perché generare altri token di output rallenta quando la concorrenza è elevata?

Durante la fase Decode, ogni passaggio di un nuovo token richiede la lettura dei tensori Key e Value memorizzati per tutti i token precedenti di ogni richiesta attiva nel batch. Con l’aumentare delle lunghezze delle sequenze e delle richieste simultanee, cresce il volume di dati KV trasferiti dalla memoria GPU a ogni passaggio, saturando la larghezza di banda e costringendo lo scheduler a limitare la dimensione del batch o ad attendere le letture dalla memoria.

Fonti

Scritto da

Chunyu Cai

Chunyu Cai è l’amministratore delegato di Lotook, LLC e un imprenditore che scrive di modelli di IA, agenti e di come cambiano il lavoro quotidiano. Chunyu Cai ha conseguito un master presso Imperial College London.