A0.5 — KV cache y el costo del contexto
Outcome del módulo: entender el mecanismo que hace que el contexto largo sea caro (no solo cuánto contexto entra, sino cuánto cuesta procesarlo por token), y usar prompt caching desde el backend para cortar costo y latencia en cargas que repiten un prefijo.
Este módulo profundiza el “por qué” detrás de A0 Concepto 1 (costo por tokens) y Concepto 2 (context window). Ahí se dijo “más tokens de input = más costo y más latencia, aunque el modelo aguante”. Acá se explica de dónde sale ese costo.
Concepto 1 — Qué es el KV cache y por qué existe
Idea núcleo: para generar cada token nuevo, el modelo compara ese token contra todos los anteriores; los vectores (key y value) de los tokens anteriores no cambian, así que se calculan una vez y se guardan en memoria de la GPU en vez de recalcularse en cada paso.
Entender (capa 1)
Texto: en el paso de atención, el token más nuevo compara contra cada token previo usando dos vectores por token anterior: un key y un value (resúmenes numéricos que el modelo computa para ese token en cada capa). Recalcular esos vectores en cada paso sería trabajo puro desperdiciado, porque una vez procesado un token sus key/value quedan fijos. El KV cache los guarda la primera vez y en el paso siguiente el modelo solo computa el key/value del token nuevo y lee el resto del cache. El cache guarda vectores, no el texto original — por eso podés tener un out-of-memory aunque el modelo mismo entre con espacio de sobra: lo que crece es el cache, no los pesos.
Visual:
flowchart LR
N[token nuevo] --> AT[paso de atencion]
KV[KV cache<br/>keys y values de tokens previos] --> AT
AT --> OUT[siguiente token]
N -->|se computa 1 vez y se guarda| KV
Ejemplo (mal → bien conceptual):
# mal: recomputar keys/values de TODOS los tokens en cada paso
# trabajo por token crece con el largo del input -> desperdicio cuadratico
# bien: KV cache
# paso 1: computa y guarda key/value de cada token del prompt (una vez)
# paso N: computa solo el token nuevo, lee el resto del cache
Fijar (capa 2)
Nota atómica:
- Front: ¿qué guarda el KV cache y por qué existe?
- Back: guarda los vectores key y value de cada token ya procesado, en cada capa. Existe para no recalcular esos vectores en cada paso de generación — una vez procesado un token, sus key/value no cambian.
Feynman: “es una libreta de apuntes. La primera vez que leés cada palabra anotás su resumen. Para escribir la siguiente palabra no volvés a leer todo el libro: mirás tus apuntes.”
Aplicar (capa 3)
Estimá el cache de un request tuyo: contá los tokens de input (system prompt + historial) con count_tokens del proveedor, y notá que el cache crece linealmente con ese número. Duplicar el contexto duplica el cache.
Límites
- El cache vive en la GPU del proveedor; como usuario de una API no lo administrás directo, pero pagás sus efectos (latencia y costo por token de contexto).
- Guarda vectores, no texto: no es “memoria” del modelo entre requests. Cada llamada por API es stateless salvo que reenvíes el historial ([[a0-fundamentos-llm]] Concepto 2).
Concepto 2 — Prefill vs decoding: por qué el contexto largo es caro
Idea núcleo: generar tiene dos fases con cuellos distintos; la fase que domina el costo del contexto largo (decoding) está limitada por ancho de banda de memoria, no por cómputo — por eso achicar el cache no solo ahorra memoria, también acelera.
Entender (capa 1)
Texto: la generación corre en dos fases. En prefill el modelo lee todo el input de una, en paralelo, y construye los key/value de cada token en el cache en una sola pasada — mantiene ocupadas las unidades de cálculo, es compute-bound. En decoding el modelo produce la salida de a un token: cada token nuevo corre un paso de atención contra todo el cache, o sea lee cada key/value guardado desde la memoria de la GPU antes de emitir el siguiente token, y repite esa lectura por cada token que produce. El límite acá es qué tan rápido se mueve el cache desde la memoria hacia las unidades de cálculo — es memory-bound. Un cache más grande = más datos cruzando el bus de memoria por token = generación más lenta y más cara, aunque quepa cómodo en memoria.
Visual:
flowchart TD
IN[input completo] -->|prefill: 1 pasada, compute-bound| CACHE[KV cache lleno]
CACHE -->|decoding: relee TODO el cache por token| T1[token 1]
CACHE --> T2[token 2]
CACHE --> T3[token N]
T1 -.memory-bound.-> BUS[ancho de banda de memoria = cuello]
T2 -.-> BUS
T3 -.-> BUS
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué un request puede ir lento aunque el cache entre cómodo en memoria?
- Back: porque decoding es memory-bound: en cada token se relee todo el cache desde la memoria de la GPU. El costo sigue cuánto cache se barre por token, no cuánto espacio ocupa. Cache más grande = más tráfico de memoria por token = más lento.
Feynman: “prefill es leer el libro entero una vez de corrido. Decoding es que, para escribir cada palabra nueva, tenés que hojear otra vez todo lo que llevás. Cuanto más grueso el fajo, más tardás en cada palabra.”
Aplicar (capa 3)
Compará latencia de una misma tarea con contexto corto vs contexto largo (mismo modelo, mismo output esperado). El tiempo por token de salida sube con el contexto de entrada aunque el output sea idéntico — ese delta es el barrido del cache.
Límites
- La partición prefill/decoding es del proveedor; no la controlás, pero explica por qué “meter todo el doc por las dudas” cuesta latencia real, no solo tokens.
- Regla práctica: mandá al modelo solo lo necesario. El costo escala lineal con los tokens de contexto aunque el modelo tenga ventana de sobra.
Concepto 3 — La ecuación del tamaño del cache
Idea núcleo: el tamaño del cache es un producto de números fijos del modelo por dos que vos controlás — tokens de contexto y requests simultáneos — y crece lineal con ambos.
Entender (capa 1)
Texto: el tamaño del cache es:
2 × capas × kv_heads × head_dim × bytes_por_numero × tokens × batch
- El 2 cubre key y value.
- capas: cada capa guarda su propio cache.
- kv_heads: cuántos juegos de key/value guarda cada capa.
- head_dim: tamaño de cada vector.
- bytes_por_numero: espacio de cada valor guardado (16 bits = 2 bytes es lo típico).
- tokens: el largo del contexto (una entrada por token).
- batch: cuántos requests se sirven a la vez.
Los primeros cinco los fija la arquitectura del modelo. Los dos últimos son tuyos: el cache crece lineal con los tokens (duplicar contexto duplica el cache) y lineal con el batch (servir más usuarios a la vez lo escala igual de rápido). Ejemplo de referencia [verificar contra specs del modelo]: un Llama 3 70B (80 capas, 8 kv_heads, head_dim 128, 2 bytes) a 128.000 tokens de contexto para un solo request da ~40 GB — casi una tarjeta de 80 GB entera por un request largo.
Visual:
flowchart LR
FIJO[fijos del modelo<br/>capas · kv_heads · head_dim · bytes] --> SIZE[tamano del cache]
TOK[tokens de contexto<br/>lo controlas vos] --> SIZE
BATCH[batch / usuarios simultaneos<br/>lo controlas vos] --> SIZE
Fijar (capa 2)
Nota atómica:
- Front: ¿qué dos factores de la ecuación del cache controla quien construye la app, y cómo escalan?
- Back: los tokens de contexto y el batch (usuarios simultáneos). Ambos escalan el cache de forma lineal: duplicar cualquiera de los dos duplica el cache.
Feynman: “el modelo trae un costo fijo por token, como el peso de un ladrillo. Vos decidís cuántos ladrillos (tokens de contexto) y cuántas paredes a la vez (usuarios). El montón crece derecho con las dos cosas.”
Aplicar (capa 3)
Tomá un modelo que uses y buscá sus specs (capas, kv_heads, head_dim). Calculá el cache por token, después multiplicá por un contexto realista de tu app. Compará cache de 8K vs 128K tokens: es exactamente 16×.
Límites
- Los números fijos varían por modelo; verificá specs reales, no asumas los del ejemplo.
- La ecuación es para modelos self-hosted; en una API no ves los GB, pero el mismo escalado lineal se refleja en precio y latencia.
Concepto 4 — Técnicas para achicar el cache
Idea núcleo: cada optimización ataca un término de la ecuación o el desperdicio alrededor; se diferencian en cuánto piden a cambio, desde casi gratis hasta un verdadero riesgo de perder información.
Entender (capa 1)
Texto: las técnicas caen en dos grupos. Unas cambian la arquitectura (se deciden al entrenar el modelo): reducen la huella de cada token. Otras operan sobre un modelo ya servido: cómo se guarda o se administra el cache.
| Técnica | Ataca | Costo / tradeoff |
|---|---|---|
| Grouped-query attention (GQA) | kv_heads | casi gratis, default hoy (varios query heads comparten un kv_head) |
| Multi-query attention (MQA) | kv_heads | ahorra más, pero baja calidad y desestabiliza entrenamiento |
| Multi-head latent attention (DeepSeek) | bytes/token | comprime keys/values a un espacio latente; +trabajo en cada lectura |
| Quantization 8/4 bit | bytes/número | 8bit ~gratis (<1% accuracy); 4bit pierde en retrieval multi-dato |
| Eviction (tirar tokens) | tokens | gamble real: el token que tirás hoy puede ser el que necesitás después |
| Paged attention (vLLM) | fragmentación | ~gratis, sube throughput 2-3×; corta desperdicio de 60-80% a <4% |
| Prefix / prompt caching | recomputo del prefijo | ~gratis en calidad; comparte el cache de un prefijo repetido |
Detalle de dos que importan por su tradeoff:
- Eviction mantiene una ventana de los tokens recientes más unos pocos del arranque (esos primeros tokens actúan de ancla y absorben mucha atención, estabilizan la salida). El problema es estructural: si un token importa depende de una pregunta que todavía no llegó; una vez tirado, el modelo genera como si nunca hubiera estado. Se ve en tareas de retrieval: maneja bien un chat casual y después falla al buscar un dato enterrado en medio de un documento largo.
- Paged attention parte el cache en bloques chicos de tamaño fijo repartidos en memoria (idea prestada de los sistemas operativos), con una tabla que mapea cada request a sus bloques. Al empacar más apretado, la fragmentación cae drásticamente.
Visual:
flowchart TD
ARCH[cambian la arquitectura<br/>se entrena con ellas] --> GQA[GQA / MQA: menos kv_heads]
ARCH --> LAT[latent attention: comprime keys/values]
SERV[operan sobre modelo servido] --> QUANT[quantization: menos bytes/numero]
SERV --> EVICT[eviction: menos tokens<br/>riesgo de perder info]
SERV --> PAGED[paged + prefix caching: administran mejor el cache]
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué eviction es un gamble y paged attention no?
- Back: eviction descarta tokens según una apuesta sobre cuáles no vas a necesitar; si te equivocás, esa información se pierde para siempre en esa generación. Paged attention no descarta nada: solo cambia cómo se guarda el cache (bloques repartidos en vez de un bloque contiguo), así que recorta desperdicio sin tocar la calidad.
Feynman: “quantization es escribir tus apuntes en letra más chica: entra más, se lee casi igual. Eviction es arrancar hojas viejas para hacer lugar: rápido, hasta que necesitás justo la hoja que arrancaste.”
Aplicar (capa 3)
Para un modelo que uses, averiguá si trae GQA (casi todos los modernos sí) y cuántos kv_heads tiene vs query heads. La razón entre ambos es el factor por el que GQA ya te está achicando el cache.
Límites
- GQA y latent attention se deciden al entrenar: no las activás vos, aplican al elegir modelo.
- Casi todas rinden recién en contexto largo y alta concurrencia. En contexto corto el cache es chico y muchas de estas técnicas resuelven un problema que todavía no apareció.
Concepto 5 — Prompt caching: la palanca que sí controlás
Idea núcleo: prefix caching (lo que las APIs venden como prompt caching) reusa el cache de un prefijo repetido entre requests; para aprovecharlo, ordenás el prompt de estable a variable y ponés el input que cambia al final.
Entender (capa 1)
Texto: como el cache vive en bloques compartibles (Concepto 4, paged attention), dos requests que empiezan con el mismo texto pueden apuntar a los mismos bloques físicos mientras cada uno tiene su continuación privada. Esa es la base de prefix caching. El ahorro es grande en cualquier carga que repita un prefijo — un agente que manda el mismo system prompt de miles de tokens en cada llamada. Los proveedores cobran los tokens cacheados a una fracción de los frescos (lecturas ~0.1× del precio de input; escrituras ~1.25× con TTL corto [verificar contra pricing vigente]). OpenAI y Anthropic reportan reducciones de costo y latencia de 50-90% en cache hit.
Regla dura: el cache es un match de prefijo. Cualquier byte que cambie en el prefijo invalida todo lo que viene después. El orden de render es tools → system → messages. Entonces: contenido estable primero (system prompt congelado, lista de tools determinística), contenido volátil (timestamps, IDs por request, la pregunta que varía) después del último punto de cache.
Visual:
flowchart LR
PREFIJO[prefijo estable<br/>system + tools + ejemplos] -->|cache_control aca| CACHE[bloques cacheados<br/>lectura ~0.1x]
CACHE --> VAR[input variable<br/>la pregunta de este request]
VAR --> RESP[respuesta]
Ejemplo (mal → bien):
# mal: dato volatil dentro del system prompt -> invalida el cache en cada request
system = f"Sos un asistente. Fecha actual: {datetime.now()}. {instrucciones_largas}"
# el timestamp cambia el prefijo cada vez -> cache_read_input_tokens siempre 0
# bien: prefijo congelado, dato volatil despues, punto de cache al final del prefijo estable
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[
{"type": "text", "text": instrucciones_largas,
"cache_control": {"type": "ephemeral"}}, # TTL 5 min por defecto
],
messages=[{"role": "user", "content": pregunta_de_este_request}],
)
# verificar el hit:
print(resp.usage.cache_read_input_tokens) # >0 = lo leyo del cache
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué un
datetime.now()en el system prompt rompe el prompt caching? - Back: porque el cache es un match de prefijo exacto por bytes. Un timestamp cambia el prefijo en cada request, así que no hay prefijo repetido para reusar y cada llamada paga precio completo (
cache_read_input_tokensqueda en 0).
Feynman: “el cache reconoce el arranque del prompt como una huella. Si el arranque es idéntico, entra por la puerta rápida. Cambiale una coma al principio y ya es una huella nueva: vuelve a pagar la entrada completa.”
Aplicar (capa 3)
Tomá un prompt tuyo con prefijo grande y repetido. Mandalo dos veces seguidas (dentro del TTL) con cache_control en el último bloque del prefijo estable. Mirá cache_read_input_tokens en el segundo request: si es 0, buscá el invalidador silencioso (timestamp, UUID, JSON sin ordenar, set de tools que varía).
Límites
- TTL corto (5 min por defecto en Anthropic; hay opción de 1h a mayor costo de escritura). Loops rápidos aprovechan el cache; scouts espaciados por horas no — el prefijo ya expiró.
- Prefijo mínimo cacheable depende del modelo [verificar]: prefijos más cortos no cachean y no dan error, solo
cache_creation_input_tokens: 0. - OPSEC / seguridad: compartir cache entre usuarios abrió side-channels de timing que pueden filtrar información sobre los prompts de otros. Si un producto tuyo comparte cache entre tenants con data sensible, no asumas aislamiento total por diseño.
- El cache es por modelo: cambiar de modelo a mitad de conversación invalida todo el cache.
Build step
Instrumentá una llamada tuya que repita un prefijo grande:
- Estructurá el prompt prefijo-estable → variable: system prompt e instrucciones fijas primero, input que cambia al final.
- Poné
cache_controlen el último bloque del prefijo estable. - Mandá el request dos veces dentro del TTL y logueá
cache_creation_input_tokens(primera) ycache_read_input_tokens(segunda). - Confirmá que la segunda llamada leyó del cache. Si no, diffeá los bytes del prefijo entre ambos requests para cazar el invalidador.
import anthropic
client = anthropic.Anthropic()
def llamar(pregunta: str):
r = client.messages.create(
model="claude-opus-4-8",
max_tokens=512,
system=[
{"type": "text", "text": INSTRUCCIONES_FIJAS, # prefijo estable
"cache_control": {"type": "ephemeral"}},
],
messages=[{"role": "user", "content": pregunta}], # variable, al final
)
print("write:", r.usage.cache_creation_input_tokens,
"read:", r.usage.cache_read_input_tokens)
return r
llamar("primera pregunta") # write > 0, read == 0
llamar("segunda pregunta") # write == 0, read > 0 <- cache hit
Checklist de dominio A0.5
Marcás cuando podés explicar (Feynman) + aplicar cada uno:
- Qué guarda el KV cache y por qué evita recomputar (keys/values fijos por token)
- Prefill vs decoding, y por qué el contexto largo es memory-bound (relee todo el cache por token)
- La ecuación del cache y los dos factores que controlás (tokens de contexto, batch)
- Las técnicas para achicarlo por tradeoff (GQA ~gratis · quantization · eviction = gamble · paged/prefix caching)
- Prompt caching: prefijo estable primero, verificar hit con
cache_read_input_tokens, y los límites (TTL, mínimo cacheable, side-channel de timing)
Salida verificable del módulo: una llamada instrumentada que muestra cache_read_input_tokens > 0 en la segunda pasada, y la capacidad de explicar por qué mover un dato volátil al system prompt lo llevaría a 0.