NICOGIS TECH WEEKLY · DEEP DIVE · 23 OTTOBRE 2026
Cache Engineering for AI Agents: KV Cache, Position Encoding, Prefill Optimization e Inference Serving
Un agente che esegue centinaia di tool call non consuma risorse soltanto quando genera token. Ogni osservazione allunga la traiettoria, può aumentare lo stato di attenzione da conservare e rende più costoso il successivo prefill. Il problema diventa: quale stato computazionale conservare, riutilizzare, eliminare o riposizionare senza compromettere la correttezza?
Questo deep dive parte dalla matematica dei transformer, analizza il paper Cache Engineering for Agents e arriva a un laboratorio riproducibile con vLLM e C#/.NET 10. Distingueremo rigorosamente il prefix caching già disponibile nei server di inferenza dalle operazioni sperimentali drop e repos proposte da AKV.
1. La traiettoria di un agente è diversa da una chat
In una chat tradizionale il contesto aumenta con messaggi relativamente brevi. In un agente, una singola tool response può introdurre migliaia di token: log, file sorgente, risultati di ricerca, JSON, documenti. Un ciclo LLM → tool → observation → LLM ripetuto decine o centinaia di volte amplifica il problema.
System + task + tools
|
v
Agent LLM ----> Tool A ----> Observation A
| |
+----------------------------+
|
+-------------> Tool B ----> Observation B
| |
+----------------------------+
|
v
Next prefill / decode
È necessario separare quattro grandezze: token della cronologia logica, token realmente elaborati durante il prefill, byte di KV cache allocati in GPU e byte trasferiti fra GPU, CPU e storage. Una API commerciale può fatturare token cached senza esporre la KV cache fisica. Un inference server self-hosted può riutilizzare blocchi KV senza cambiare il testo del prompt.
2. KV cache: matematica e dimensionamento
In un transformer autoregressivo, per ogni layer si calcolano le proiezioni Q = XWq, K = XWk e V = XWv. La self-attention causale è, schematicamente, softmax(QKᵀ / sqrt(d) + mask) V. Durante il decode i token precedenti non cambiano: ricalcolarne ogni volta key e value sarebbe uno spreco. La KV cache conserva queste rappresentazioni per riutilizzarle.
Per L layer, Hkv KV head, dimensione D per head, N token e B byte per elemento, una stima è:
M_KV ≈ 2 × L × Hkv × D × N × B
Il fattore due rappresenta K e V. Consideriamo 32 layer, 8 KV head, dimensione 128, BF16 (2 byte), 32.768 token:
2 × 32 × 8 × 128 × 32768 × 2 = 4,294,967,296 byte ≈ 4 GiB per sequenza
Otto sequenze indipendenti di quella lunghezza richiederebbero circa 32 GiB di sola KV cache, prima di pesi del modello, buffer, frammentazione e altre allocazioni. GQA e MQA riducono il numero di KV head; la quantizzazione KV riduce i byte per elemento ma introduce vincoli di precisione e compatibilità. La dimensione massima di contesto pubblicizzata dal modello non coincide con la capacità di servire contemporaneamente molte richieste a quella lunghezza.
3. Prefill e decode: due workload differenti
| Fase | Attività | Collo di bottiglia tipico | Metriche |
|---|---|---|---|
| Prefill | Elabora il prompt e costruisce KV | Compute e lunghezza input | Prefill latency, TTFT |
| Decode | Genera token incrementali | Bandwidth, letture KV, batching | ITL, output token/s |
| Scheduler | Distribuisce risorse fra richieste | Memoria, queueing, preemption | P95/P99, queue time |
Il Time to First Token osservato dal client include rete, eventuale coda, prefill e prima emissione. Ridurre il prefill può migliorare il TTFT senza incidere proporzionalmente sulla durata totale di una risposta lunga. Il continuous batching permette di introdurre nuove richieste mentre altre sono in decode; il chunked prefill suddivide prompt lunghi per condividere meglio la GPU.
4. RoPE: perché eliminare token non risolve il problema delle posizioni
Le Rotary Position Embeddings (RoPE) codificano la posizione ruotando query e key: q′p = R(p)qp e k′p = R(p)kp. Per ogni coppia di coordinate, R(p) è una rotazione il cui angolo dipende dalla posizione e dalla frequenza. Il prodotto scalare fra query e key incorpora così una relazione posizionale.
Supponiamo di avere una traiettoria di 100.000 token e di eliminare dalla cache gli stati di 80.000 token intermedi. La cache attiva è più piccola, ma le posizioni dei token rimasti possono essere ancora molto elevate. Indice logico della traiettoria, numero di elementi KV attivi e indice RoPE non sono la stessa cosa.
Nel caso RoPE standard, un key già ruotato alla posizione p può essere idealmente trasportato alla posizione q applicando:
k′q = R(q) · R(p)^(-1) · k′p
È una proprietà della composizione delle rotazioni. Non implica però che un runtime generico possa spostare arbitrariamente tutti gli stati KV senza conseguenze: gli hidden state dipendono dalle informazioni che erano visibili quando sono stati calcolati, e varianti di RoPE possono avere requisiti differenti.
Errore concettuale frequente: ricollocare un key nella dimensione posizionale non equivale a ricalcolare il token con una storia di attenzione diversa.
5. Prefix caching e PagedAttention non sono sinonimi
Il prefix caching evita di ripetere il prefill quando due richieste condividono lo stesso prefisso tokenizzato. È particolarmente efficace con system prompt, istruzioni sugli strumenti e documenti statici. La somiglianza semantica non basta: serve l'identità dei token e degli altri fattori rilevanti per la cache.
Request A: [system][tools][document][question A]
Request B: [system][tools][document][question B]
+----- shared prefix -----+
cached KV
In vLLM l'Automatic Prefix Caching usa hash dei blocchi che dipendono dai token e dal prefisso precedente. La gestione comprende reference count ed eviction. La documentazione descrive anche un cache_salt opzionale per isolare gruppi di fiducia, riducendo il rischio di canali laterali temporali fra tenant.
PagedAttention affronta invece l'allocazione: divide la KV cache in blocchi fisici non necessariamente contigui, mappati a una sequenza logica. Riduce sprechi di memoria e facilita la condivisione. Prefix caching è una tecnica di riutilizzo; PagedAttention è una tecnica di gestione della memoria. Possono essere combinate.
SGLang utilizza strutture radix per il riuso dei prefissi. Nei modelli ibridi, che combinano full attention, sliding-window attention e stati ricorrenti, la semantica del riuso cambia a seconda della componente: un checkpoint ricorrente non è intercambiabile con un normale blocco KV.
6. Il paper Cache Engineering for Agents: append, drop, repos
Il lavoro Cache Engineering for Agents propone di gestire la cache direttamente, senza riscrivere continuamente la traiettoria testuale dell'agente. Le operazioni fondamentali sono:
| Operazione | Effetto | Obiettivo |
|---|---|---|
append(x) | Aggiunge KV dei nuovi token | Proseguire l'esecuzione |
drop(P) | Esclude selettivamente KV dalla futura attenzione | Ridurre contesto attivo e memoria |
repos | Modifica gli encoding posizionali dei key conservati | Compattare le posizioni senza re-prefill completo |
La differenza rispetto alla summarization è decisiva. Se si sostituisce un tratto del prompt con un riassunto, il prefisso cambia e può essere necessario ricalcolare stati successivi. Se il runtime supporta drop, può escludere porzioni della cache dalle attenzioni future senza necessariamente rigenerare gli stati conservati.
Context engineering:
[A][B][C][D][E] -> summarize -> [A][S][E]
prefix modified / possible re-prefill
Cache engineering:
[A][B][C][D][E] -> drop KV(B,C,D) -> active [A][E]
retained states may be reused
-> repos -> adjusted position encodings
Negli esperimenti degli autori, la policy tool-response rolling-drop ottiene +1,7 punti di score medio e −27,7% di costo medio rispetto alla summarization a soglia. AKV riporta fino a 1,2× di throughput di output e fino a 1,5× di miglioramento relativo della latenza P95 nel proprio setup. Sono risultati sperimentali specifici, non prestazioni garantite di qualsiasi inference server.
7. History dependence: stessi token, cache diversa
Questo è il passaggio più sottile del paper. Immaginiamo due richieste che contengono gli stessi token A, B e C. Nel primo caso C è stato elaborato quando B era visibile; nel secondo B era già stato escluso con drop. Il vettore KV di C può essere diverso perché è diversa la storia dell'attenzione. Di conseguenza la sola identità dei token non determina più lo stato della cache.
Il serving engine AKV, costruito su SGLang, utilizza una struttura radix estesa con nodi che rappresentano le operazioni di drop, gestione dell'eviction consapevole dei riferimenti e segment-batch attention per ricostruire stati coerenti. Il riposizionamento richiede inoltre versioni dei key con encoding posizionali appropriati. È un problema di provenienza dello stato, non semplicemente di cache hit.
8. Inference serving: scheduling, eviction e offloading
| Tecnica | Beneficio | Trade-off |
|---|---|---|
| Prefix caching | Evita prefill duplicato | Memoria per blocchi riutilizzabili |
| PagedAttention | Riduce frammentazione | Gestione tabelle di blocchi |
| Chunked prefill | Migliora coesistenza con decode | Scheduling più complesso |
| KV quantization | Meno byte/token | Possibili effetti numerici |
| CPU offloading | Maggiore capacità effettiva | Trasferimenti e latenza |
| Disaggregated prefill/decode | Worker specializzati | Trasferimento KV fra worker |
| AKV drop/repos | Controllo del contesto attivo | Runtime specializzato e semantica complessa |
In un sistema disaggregato il costo di trasferimento della KV cache può diventare di primo ordine. Un risparmio nel calcolo non garantisce quindi un miglioramento end-to-end. Occorre misurare anche traffico GPU-GPU, GPU-CPU, congestione di rete, code e preemption.
9. Laboratorio: vLLM e client C#/.NET 10
Obiettivo: confrontare richieste con prefisso invariato e prefisso variabile, misurando TTFT e latenza totale. Il laboratorio verifica il prefix caching standard di vLLM, non implementa le operazioni AKV drop/repos. Per replicare AKV è necessario il repository degli autori e l'infrastruttura SGLang modificata.
9.1 Prerequisiti e server
Occorrono una GPU NVIDIA supportata, ambiente CUDA/driver compatibile, vLLM e .NET SDK 10. Il modello seguente è relativamente piccolo ma richiede comunque VRAM adeguata. Verificare le opzioni effettive con vllm serve --help, perché possono cambiare fra versioni.
vllm serve Qwen/Qwen2.5-1.5B-Instruct \ --host 0.0.0.0 --port 8000 \ --enable-prefix-caching --max-model-len 8192 # In un altro terminale: dotnet new console -n KvCacheBench --framework net10.0 cd KvCacheBench
9.2 Program.cs completo, senza pacchetti esterni
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.Json;
var endpoint = Environment.GetEnvironmentVariable("LLM_URL")
?? "http://localhost:8000";
var model = Environment.GetEnvironmentVariable("LLM_MODEL")
?? "Qwen/Qwen2.5-1.5B-Instruct";
using var http = new HttpClient {
BaseAddress = new Uri(endpoint.TrimEnd('/') + "/"),
Timeout = TimeSpan.FromMinutes(5)
};
// Il prefisso lungo rimane identico nei casi A e B.
var shared = string.Join("\n", Enumerable.Range(0, 100)
.Select(i => $"Regola {i:D3}: descrivi trade-off " +
"fra KV cache, prefill, decode e scheduling GPU."));
Console.WriteLine("case,iteration,ttft_ms,total_ms,output_chars");
foreach (var testCase in new[] { "A", "B", "C" }) {
for (int i = 0; i < 14; i++) {
var prefix = testCase == "C"
? $"Variante {i:D4}\n" + shared : shared;
var payload = new {
model,
stream = true,
temperature = 0,
max_tokens = 120,
messages = new object[] {
new { role = "system", content =
"Rispondi in italiano, in modo tecnico e conciso." },
new { role = "user", content = prefix +
$"\n\nSpiega il concetto {i:D2} di KV cache." }
}
};
using var req = new HttpRequestMessage(
HttpMethod.Post, "v1/chat/completions") {
Content = new StringContent(JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json")
};
var watch = Stopwatch.StartNew();
using var res = await http.SendAsync(req,
HttpCompletionOption.ResponseHeadersRead);
res.EnsureSuccessStatusCode();
await using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
double? ttft = null;
int chars = 0;
while (await reader.ReadLineAsync() is { } line) {
if (!line.StartsWith("data: ", StringComparison.Ordinal))
continue;
var data = line[6..];
if (data == "[DONE]") break;
using var doc = JsonDocument.Parse(data);
if (!doc.RootElement.TryGetProperty("choices", out var choices)
|| choices.GetArrayLength() == 0) continue;
var choice = choices[0];
if (!choice.TryGetProperty("delta", out var delta)
|| !delta.TryGetProperty("content", out var content)
|| content.ValueKind != JsonValueKind.String) continue;
var fragment = content.GetString();
if (string.IsNullOrEmpty(fragment)) continue;
ttft ??= watch.Elapsed.TotalMilliseconds;
chars += fragment.Length;
}
watch.Stop();
if (i >= 2) Console.WriteLine(string.Join(",",
testCase, (i - 2).ToString(),
ttft?.ToString("F2", CultureInfo.InvariantCulture) ?? "",
watch.Elapsed.TotalMilliseconds.ToString("F2",
CultureInfo.InvariantCulture), chars.ToString()));
}
}
Il programma esegue 2 richieste di warm-up e 12 richieste misurate per ciascun caso. A e B condividono il prefisso; C lo altera prima del blocco comune. Le richieste sono sequenziali per isolare il riuso, quindi non costituiscono un test di concorrenza. L'ordine A-B-C può favorire il warm-up della cache: ripetere i test invertendo l'ordine e riavviando il server fra configurazioni.
9.3 Esecuzione e protocollo
# Linux / macOS export LLM_URL=http://localhost:8000 export LLM_MODEL=Qwen/Qwen2.5-1.5B-Instruct dotnet run -c Release > results.csv # PowerShell $env:LLM_URL = "http://localhost:8000" $env:LLM_MODEL = "Qwen/Qwen2.5-1.5B-Instruct" dotnet run -c Release | Out-File results.csv
Ripetere con prefix caching abilitato e disabilitato, mantenendo identici modello, precisione, GPU e limiti di contesto. Eseguire più repliche indipendenti, annotare la versione del runtime e acquisire anche le metriche server. Il ttft_ms misura il primo frammento di contenuto nello stream, non il primo byte di rete. output_chars non equivale ai token generati: per token/s usare i contatori del server o un tokenizer compatibile.
10. Metriche e interpretazione corretta
| Metrica | Significato | Attenzione |
|---|---|---|
| TTFT | Tempo al primo token visibile | Include rete, coda e prefill |
| ITL / TPOT | Tempo fra token successivi | Dipende dal batching |
| P50/P95/P99 | Distribuzione delle latenze | Servono campioni sufficienti |
| Prefix cache hit rate | Quota di prefisso riutilizzata | Definizione runtime-specifica |
| KV cache usage | Pressione sulla cache | Non è tutta la VRAM usata |
| Output token/s | Throughput di generazione | Distinguere per richiesta e aggregato |
| Cost per correct task | Costo per task verificato | Richiede un valutatore esterno |
vLLM espone metriche Prometheus su /metrics; nomi e disponibilità dipendono dalla versione. Fra le famiglie rilevanti: utilizzo KV, query e hit del prefix cache, richieste attive, TTFT e latenza end-to-end. Un hit rate su finestra temporale va calcolato usando variazioni dei contatori nella stessa finestra, non confrontando valori cumulativi di intervalli differenti.
La metrica decisiva per gli agenti non è il solo costo/token: costo per task completato correttamente. Una policy aggressiva può ridurre il contesto attivo e peggiorare l'accuratezza. La valutazione deve includere task fissi, verificatori deterministici, errori di tool calling e regressioni funzionali.
11. Architettura di produzione: cache non significa memoria durevole
ASP.NET Core / Agent API
|
AuthN / AuthZ / Tenant
|
Agent Orchestrator ---- Durable state / checkpoint
| |
Model Gateway Memory / tool results
|
Cache policy + routing
|
+------+-------+
| |
GPU worker A GPU worker B
KV cache KV cache
| |
+------+-------+
|
Metrics / tracing / audit
Lo stato durevole dell'agente descrive decisioni, tool call, risultati e checkpoint necessari al recovery. La KV cache è un'accelerazione computazionale, spesso volatile. Se un worker GPU si arresta, l'agente deve poter ripartire dalla traiettoria persistita anche quando tutta la cache è andata persa.
Per il multi-tenant, attenzione al riuso di prefissi fra gruppi di fiducia: differenze di latenza possono rivelare indirettamente informazioni. Isolamento della cache, autorizzazioni e politiche di retention devono essere coerenti. Inoltre una tool response malevola rimane non attendibile anche se è già stata memorizzata nella cache.
12. Conclusioni
Il Cache Engineering sposta l'ottimizzazione degli agenti dal solo testo del prompt allo stato computazionale dell'attenzione. Prefix caching e PagedAttention sono oggi strumenti concreti di ottimizzazione del serving; le operazioni drop e repos di AKV rappresentano una direzione di ricerca più avanzata che richiede un runtime specializzato.
Le tre regole operative sono: misurare separatamente prefill, decode e occupazione KV; non riutilizzare stati senza rispettarne la storia; non confondere mai cache GPU e persistenza dell'agente. Il miglioramento più importante non è il costo per token, ma il costo per task corretto e verificabile.
Fonti tecniche e scientifiche
- Cache Engineering for Agents — paper (2026)
- AKV — codice degli autori
- vLLM — Automatic Prefix Caching
- vLLM — documentazione e serving
- PagedAttention — Hugging Face
- Rotary Embeddings — Hugging Face
- Transformers — Cache Explanation
- SGLang — Unified Radix Cache
Nota metodologica: i numeri AKV sono risultati sperimentali degli autori. Il laboratorio proposto verifica il prefix caching standard, non replica AKV. Controllare la versione delle CLI e la compatibilità GPU prima dell'esecuzione.