🧠 Teoria11 minuti di lettura

La prima risposta è lenta, le altre volano: cosa conserva davvero la KV cache

Per un modello da 13 miliardi di parametri la cache di un singolo token occupa 800 KB, e una richiesta da 2048 token arriva a 1,6 GB. Da quella memoria nasce lo sconto del 90% sul prompt caching, e anche la regola che quasi nessuno applica: la cache è un prefisso, e un byte cambiato in testa la butta via tutta.

AS

Alessandro Saiani

Human in the Loop

La prima risposta è lenta, le altre volano: cosa conserva davvero la KV cache

La scena la conosci. Apri una sessione, dai in pasto all'agente un file lungo o un CLAUDE.md corposo, fai la prima domanda, e aspetti. Qualche secondo in cui non succede niente, poi il testo comincia a scorrere. Fai la seconda domanda sullo stesso materiale e la risposta parte quasi subito.

La spiegazione comoda è "si è scaldato". Quella vera è che fra la prima e la seconda domanda il modello ha smesso di rifare un lavoro che aveva già fatto, e quel lavoro ha un nome, una dimensione in gigabyte e, da un paio d'anni, un prezzo di listino. Capire cosa viene conservato spiega tre cose in un colpo solo: perché la prima risposta è lenta, perché un agente che gira tutto il giorno costa meno dei token che consuma, e perché l'ordine in cui scrivi il prompt decide la bolletta.

Cosa viene conservato davvero

Dentro il meccanismo di attenzione, ogni token viene proiettato in tre vettori: query, chiave e valore. La query è quella del token che il modello sta elaborando adesso; chiavi e valori sono quelli di tutti i token che vengono prima, e servono a decidere a cosa guardare e cosa portarsi dietro.

Il punto è che chiavi e valori di un token, una volta calcolati, non cambiano mai più. Il token numero 40 avrà per sempre gli stessi vettori, in ogni layer, per tutto il resto della generazione. Ricalcolarli a ogni nuovo token generato sarebbe ripetere lo stesso conto migliaia di volte, quindi non si fa: si tengono da parte. Quella scorta di chiavi e valori è la KV cache.

Attenzione a non pensarla come una cache di testo. Non contiene le tue frasi: contiene numeri, tanti, uno strato per ogni layer del modello. Ed è per questo che occupa così tanto spazio.

Due fasi, due colli di bottiglia diversi

Il momento migliore per capire la differenza è il paper che nel 2023 ha reso praticabile servire modelli grandi a molti utenti insieme, quello che ha introdotto PagedAttention e il sistema vLLM. Gli autori separano nettamente due fasi.

La prima è la fase di prompt, il prefill: il modello prende tutto quello che gli hai dato e calcola chiavi e valori per ogni token. Tutti i token sono già noti, quindi il lavoro si può spalmare in parallelo. Testuale: "the computation of the prompt phase can be parallelized using matrix-matrix multiplication operations. Therefore, this phase can efficiently use the parallelism inherent in GPUs".

La seconda è la generazione: un token alla volta, ognuno dipendente dal precedente. Qui non c'è parallelismo da sfruttare, e il conto diventa "matrix-vector multiplication, which is less efficient". Gli autori sono netti sulla conseguenza: "this sequential generation process makes the workload memory-bound". Il limite non è la potenza di calcolo, è quanto in fretta si riesce a muovere memoria.

Ecco la risposta alla domanda di partenza. La prima risposta è lenta perché deve macinare l'intero prompt prima di poter dire qualsiasi cosa, anche se lo fa nel modo che alla GPU riesce meglio. Le successive partono subito perché quel lavoro è già fatto e messo da parte: resta solo la parte lenta per token, che però è corta.

Ottocento kilobyte per token

Quanto spazio serve? Il paper fa il conto esplicito su un modello aperto da 13 miliardi di parametri: "the KV cache of a single token demands 800 KB of space", che viene da 2 vettori (chiave e valore) per 5120 dimensioni per 40 layer per 2 byte. Una singola richiesta lunga 2048 token occupa così 1,6 GB.

Fermiamoci un secondo su questo numero, perché è controintuitivo. Non stiamo parlando dei pesi del modello, che sono fissi e condivisi fra tutti. Stiamo parlando della memoria che serve per ogni conversazione aperta. Su una scheda con 80 GB, e con i pesi che ne occupano già una fetta, il numero di conversazioni che ci stanno dentro contemporaneamente si conta in decine, non in migliaia.

C'è un secondo dato che spiega perché è nata un'intera linea di ricerca su questo: nei sistemi precedenti a vLLM, solo il 20,4-38,2% della memoria riservata alla KV cache conteneva davvero stati di token. Il resto era frammentazione e spazio prenotato per generazioni che sarebbero potute arrivare e magari non arrivavano.

Tieni insieme i due fatti e capisci tutto quello che viene dopo: quella memoria è cara, è contesa, e nessuno può permettersi di tenerla occupata per te a tempo indeterminato.

Dallo spreco di memoria allo sconto in bolletta

Se il lavoro di prefill è costoso e il suo risultato non può restare in memoria per sempre, la conseguenza commerciale è naturale: il fornitore te lo tiene per un po', se gli dici quale pezzo tenere, e in cambio ti fa pagare molto meno quando lo riusi. È il prompt caching.

I numeri, presi dalla documentazione ufficiale dell'API di Claude e non dai riassunti:

OperazioneCosto rispetto all'input normale
Scrittura in cache, durata 5 minuti1,25×
Scrittura in cache, durata 1 ora
Lettura da cache0,1×

Sulla durata la pagina è precisa: "By default, the cache has a 5-minute lifetime. The cache is refreshed for no additional cost each time the cached content is used." Ogni volta che leggi, il timer riparte senza pagare. In una sessione di lavoro continuo la cache da cinque minuti resta calda da sola, e quella da un'ora serve solo se fra una richiesta e l'altra passano davvero decine di minuti.

Da qui esce il famoso 90% di risparmio, che non è uno slogan ma aritmetica: leggere costa un decimo. E da qui esce anche la soglia che quasi nessuno calcola. Scrivere in cache costa più del normale, quindi il caching conviene solo se poi leggi: con la durata breve il pareggio arriva alla seconda richiesta (1,25 + 0,1 contro 2), con quella lunga alla terza (2 + 0,2 contro 3). Sotto quella soglia stai solo pagando un sovrapprezzo.

La regola che governa tutto: è un prefisso

Qui sta la parte che conta per chi costruisce agenti, ed è anche quella che viene saltata più spesso.

La cache non è un insieme di pezzi indipendenti che puoi marcare a piacere. È un prefisso: vale dall'inizio del prompt fino al punto che hai marcato, e la chiave è fatta dai byte esatti di quel tratto. Se cambia un byte in mezzo, tutto quello che viene dopo è da rifare.

E l'ordine non lo decidi tu. La documentazione lo scrive esplicitamente: "Cache prefixes are created in the following order: tools, system, then messages. This order forms a hierarchy where each level builds upon the previous ones." Con la conseguenza, altrettanto esplicita: "Changes at each level invalidate that level and all subsequent levels."

Tradotto in pratica, ecco cosa ti costa caro senza che nessuno te lo dica:

  • Aggiungere o togliere un tool a metà conversazione. I tool stanno in posizione zero, prima di tutto il resto. Cambiarli invalida l'intera cache, compresa tutta la storia della conversazione.
  • Mettere la data di oggi nel system prompt. Un datetime.now() in testa significa che ogni richiesta ha un prefisso diverso, e nessuna cache viene mai riusata. Vale per un ID di sessione, per un UUID, per il nome dell'utente.
  • Serializzare senza ordinare. Un json.dumps senza sort_keys, o l'iterazione di un set, producono byte diversi a parità di contenuto. La cache non lo sa: vede due prefissi diversi.
  • Costruire l'elenco dei tool a seconda dell'utente. Ogni utente diventa un prefisso a sé, e non si condivide niente.

Rovesciando la regola si ottiene il modo giusto di impaginare un prompt: quello che non cambia mai va all'inizio, quello che cambia a ogni richiesta va in fondo. È anche il motivo per cui un CLAUDE.md scritto bene conviene due volte: dice all'agente come lavorare, e siccome sta in testa e resta identico, è la parte che si paga un decimo.

La parte onesta

Il primo limite è il più fastidioso: il caching fallisce in silenzio. Non c'è un errore, non c'è un avviso, le risposte continuano ad arrivare identiche. Cambia solo il conto a fine mese. L'unico modo per accorgersene è guardare i campi che l'API restituisce a ogni risposta: se cache_read_input_tokens resta a zero su richieste che condividono lo stesso inizio, qualcosa a monte sta cambiando i byte. Vale la pena saperlo anche per leggere le fatture: input_tokens è solo la parte non cachata, e il totale del prompt è la somma dei tre campi.

Il secondo è una trappola vera. Esiste una lunghezza minima sotto la quale il caching non parte, ed è diversa da modello a modello: 512 token su Opus 5, 1.024 su Sonnet 5 e Opus 4.8, 2.048 su Opus 4.7, 4.096 su Opus 4.6 e su Haiku 4.5. Non è una scala che migliora di generazione in generazione: un prompt da 3.000 token viene cachato sul modello più recente e non viene cachato sul modello piccolo che magari usi proprio per risparmiare. La documentazione lo dice senza giri di parole: "Shorter prompts cannot be cached, even if marked with cache_control. Any requests to cache fewer than this number of tokens will be processed without caching, and no error is returned."

Il terzo: i punti di marcatura sono al massimo quattro per richiesta. Non puoi spezzettare il prompt in dieci sezioni sperando che ognuna si salvi per conto suo.

Il quarto riguarda chi insegue i modelli nuovi. Le cache sono legate al modello: cambiarlo significa ripartire a freddo, e questo va messo nel conto delle migrazioni, insieme a tutto il resto di cui avevo scritto sul ritmo dei rilasci.

Un'ultima nota di misura: la pagina ufficiale non promette percentuali di riduzione della latenza. Dice che il caching riduce tempi e costi, e quantifica solo i secondi in termini di moltiplicatori. I numeri tipo "meno 85% di latenza" che girano nei blog non vengono da lì.

Cosa farne stasera

Tre cose concrete, in ordine di quanto rendono.

Guarda dove metti le cose che cambiano. Apri il codice che costruisce il prompt e chiediti, riga per riga, se quel pezzo sarà identico alla prossima richiesta. Tutto ciò che è stabile va in alto: istruzioni, documentazione, esempi, definizioni dei tool. Tutto ciò che cambia va in fondo: la domanda, il timestamp, l'ID. Una sola riga spostata può fare la differenza fra pagare il pieno e pagare un decimo.

Verifica invece di sperare. Fai due richieste identiche e leggi cache_read_input_tokens sulla seconda. Se è zero, hai un invalidatore, e conviene trovarlo adesso: è il tipo di regressione che entra con una modifica innocua e resta in produzione per mesi.

Fai il conto della soglia. Se il pezzo stabile è più corto del minimo del tuo modello, o se lo riusi una volta sola, il caching non ti serve: stai solo pagando il sovrapprezzo di scrittura. È lo stesso ragionamento che vale per ogni ottimizzazione di costo per token: prima misuri, poi decidi.

Torniamo alla scena dell'inizio. Quei secondi di attesa prima della prima risposta non sono un difetto, sono il modello che prende appunti. Tutto quello che puoi fare, come chi scrive il prompt, è assicurarti che gli appunti restino validi il più a lungo possibile.


Fonti:

  1. Kwon et al. — Efficient Memory Management for Large Language Model Serving with PagedAttention (arXiv 2309.06180)
  2. Documentazione Claude — Prompt caching
  3. Vaswani et al. — Attention Is All You Need (arXiv 1706.03762)