La caché KV es la base de datos invisible que consume memoria en la GPU durante la decodificación de LLM.
La caché KV consume silenciosamente la memoria de la GPU durante la inferencia de LLM: prefill vs decode, cálculos KV, GQA y MQA, PagedAttention, políticas de cuantización y expulsión.
Fuentes: The KV Cache: AI’s Unseen Database Dominating GPU Memory
El verdadero cuello de botella en la inferencia de LLM
Durante la inferencia, el procesamiento en GPU suele quedar inactivo mientras el ancho de banda de la memoria alimenta un cache clave–valor que crece constantemente. Cada nuevo token agrega tensores K/V que deben leerse nuevamente en pasos posteriores. Las implementaciones que ignoran este crecimiento del cache chocan con limitaciones de contexto y latencia mucho antes de que se agoten los FLOPs.
Detrás de escena: cómo funciona el cache clave–valor
La atención necesita claves y valores para cada token anterior. Al almacenarlos en caché se evita volver a calcular todo el prefijo en cada paso. El compromiso es un consumo de memoria lineal con respecto a la longitud de la secuencia × capas × cabezas × dimensión.
Prefill vs decodificación
Prefill procesa el prompt y llena el cache. Decodificación agrega un token a la vez, leyendo todo el cache en cada paso. El Prefill requiere mucho procesamiento; la decodificación depende mucho del ancho de banda.
import torch
import torch.nn as nn
# Configuration representing a simplified LLM layer
batch_size = 1
num_heads = 8
head_dim = 64
hidden_dim = 512
# Projection layers for Keys and Values
W_k = nn.Linear(hidden_dim, num_heads * head_dim)
W_v = nn.Linear(hidden_dim, num_heads * head_dim)
# --- 1. PREFILL PHASE ---
# The prompt has 5 tokens. We process them simultaneously.
prompt_len = 5
prompt_embeddings = torch.randn(batch_size, prompt_len, hidden_dim)
# Project to K and V, then reshape to: [Batch, Heads, SeqLen, HeadDim]
kv_cache_k = W_k(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)
kv_cache_v = W_v(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)
print(f"Prefill complete. Cache K shape: {kv_cache_k.shape}")
# Output: [1, 8, 5, 64] (5 tokens cached)
# --- 2. DECODING PHASE ---
# We generate 3 subsequent tokens, one by one.
for step in range(3):
# Input is only the single, newly generated token (SeqLen = 1)
new_token_embedding = torch.randn(batch_size, 1, hidden_dim)
# Project only the new token's K and V
new_k = W_k(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)
new_v = W_v(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)
# Append the new token's representations to our historical cache
# This prevents recalculating the past prompt_len + step tokens
kv_cache_k = torch.cat([kv_cache_k, new_k], dim=2)
kv_cache_v = torch.cat([kv_cache_v, new_v], dim=2)
print(f"Decoding Step {step + 1} | Cache K shape: {kv_cache_k.shape}")
# Step 1 Output: [1, 8, 6, 64]
# Step 2 Output: [1, 8, 7, 64]
# Step 3 Output: [1, 8, 8, 64]
def calculate_kv_cache_gb(batch_size: int, seq_len: int, layers: int, hidden_dim: int, precision_bytes: int = 2) -> float:
# 2 represents the key and value states
total_bytes = batch_size * seq_len * layers * hidden_dim * 2 * precision_bytes
return total_bytes / (1024 ** 3) # Convert to Gigabytes
# Llama 2 70B architecture parameters (assuming standard MHA)
llama_70b_gb = calculate_kv_cache_gb(
batch_size=32,
seq_len=8192,
layers=80,
hidden_dim=8192
)
print(f"KV Cache Size: {llama_70b_gb:.2f} GB")
# Output: KV Cache Size: 320.00 GB
+---------------------------------------------------------------+
| GPU VRAM (80 GB) |
+------------------------------+--------------------------------+
| Static Model Weights (Fixed) | KV Cache (Dynamic / Variable) |
| [==========================] | [=======================> ] |
| FP16 Weights (e.g., ~35GB/GPU| Grows linearly with: |
| across split tensor setups) | Batch Size * Sequence Length |
+------------------------------+--------------------------------+
MHA (1:1 Ratio) GQA (Grouped Ratio) MQA (Many:1 Ratio)
[Q1][Q2][Q3][Q4] [Q1][Q2] [Q3][Q4] [Q1][Q2][Q3][Q4]
| | | | \ / \ / \ | / /
[K1][K2][K3][K4] [ K1 ] [ K2 ] [ K1 ]
[V1][V2][V3][V4] [ V1 ] [ V2 ] [ V1 ]
# A simple simulation of PagedAttention block mapping
class VirtualPageTable:
def __init__(self, block_size: int):
self.block_size = block_size
self.logical_to_physical = {} # Maps (request_id, block_index) -> physical_block_id
self.free_blocks = list(range(1000)) # Pool of physical memory blocks
def allocate_block_for_token(self, request_id: int, token_index: int) -> int:
# Determine which logical block the token falls into
logical_block_idx = token_index // self.block_size
key = (request_id, logical_block_idx)
# Allocate a new physical block only when the previous one is full
if key not in self.logical_to_physical:
physical_block = self.free_blocks.pop(0)
self.logical_to_physical[key] = physical_block
return self.logical_to_physical[key]
# Instantiating a table with a block size of 4 tokens
table = VirtualPageTable(block_size=4)
# Token 5 belongs to logical block 1 (tokens 4-7). We allocate physical space on demand.
physical_id = table.allocate_block_for_token(request_id=42, token_index=5)
print(f"Logical token mapped to physical block: {physical_id}")
FP16/BF16 Value: [0.12345678] (16 bits of precision)
↓ Quantization via Scaling Factor (e.g., scale = 127.0)
INT8 Value: [15] (8 bits of precision, 50% memory saved)
Sequence: [T1] [T2] [T3] [T4] [T5] [T6] [T7]
└───── Window Size (4) ─────┘
(T1 and T2 are evicted from active memory)
# Example: Initializing a vLLM engine with memory optimizations
from vllm import LLMEngine, EngineArgs
# Define engine arguments optimized for memory management
engine_args = EngineArgs(
model="meta-llama/Llama-3-8B-Instruct",
# 1. Quantization: Reduces KV cache footprint
quantization="fp8",
# 2. Paged KV Cache: Eliminates physical fragmentation
gpu_memory_utilization=0.90,
# 3. Dynamic Batching: Prevents static padding overhead
max_num_seqs=256
)
# Initialize the engine with built-in memory guardrails
engine = LLMEngine.from_engine_args(engine_args)
Mecánicas de anexión
Las implementaciones almacenan buffers K/V por capa y escriben la nueva fila en cada paso. Los errores aquí corrompen silenciosamente el mecanismo de atención; es mejor utilizar kernels probados en la práctica.
La matemática de la memoria preocupante
Bytes ≈ 2 (K y V) × capas × kv_heads × head_dim × seq_len × bytes_per_element. Un contexto largo en modelos grandes puede consumir más memoria que los pesos propiamente dichos.
Técnicas de optimización
Rediseño arquitectónico: MHA, GQA, MQA
La atención de consulta agrupada y la de múltiples consultas reducen el número de heads KV, lo que hace que la caché sea más pequeña para la misma longitud de secuencia.
PagedAttention
Trate los datos KV como memoria virtual paginada: asigne bloques según sea necesario, reduzca la fragmentación y permita un mayor agrupamiento de datos.
Cuantización de KV
Almacene las entradas de caché con menor precisión cuando lo permitan los presupuestos de calidad.
Ventana deslizante / expulsión
Descarte o comprima los tokens alejados cuando la tarea tolere un rango de atención limitado.
Matriz de decisiones para producción
Elija modelos GQA/MQA cuando haya limitaciones de memoria; active la atención paginada para alta concurrencia; cuantifique solo KV después de la evaluación; utilice ventanas para chats que requieran contexto local.
Cierre
El caché KV es la base de datos invisible del decodificación autoregresiva. Óptimícelo explícitamente: arquitectura, paginación, precisión, expulsión; de lo contrario, se pagarán precios más altos en términos de GPUs y latencia.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplique el tamaño del caché puede perder rendimiento bajo cargas multiinquilino.
Documente el máximo contexto por SKU para que el producto no solicite silenciosamente 128k en una tarjeta provista para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar en lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar en lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar con lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un kernel “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
Documente el contexto máximo por SKU para que el producto no pueda solicitar silenciosamente 128k en una tarjeta configurada para 8k.
Al trabajar en lotes, recuerde que los pasos de decodificación deben sincronizarse con la secuencia más larga; la política de relleno y el procesamiento continuo en lotes son tan importantes como el ancho de banda bruto.
Mida tanto los tokens por segundo como los bytes KV por token. Un núcleo “más rápido” que duplica el espacio de caché puede perder rendimiento bajo cargas multiinquilino.
La asimetría entre el relleno previo y la decodificación también afecta al autoescalado: los aumentos en la longitud de las solicitudes sobrecargan el procesamiento previo, mientras que las generaciones largas afectan negativamente el ancho de banda. Diseñe grupos de tamaño para ambos escenarios.
Compare las afirmaciones de los proveedores sobre el “contexto largo” solicitando información sobre los bytes KV a la longitud máxima, con su tipo de dato y tamaño de lote, en lugar de tokens publicitarios.
Las políticas de expulsión deben tener en cuenta las características del producto: resumir los segmentos eliminados es mejor que olvidarlos silenciosamente para los agentes de soporte que se refieren a turnos anteriores.
Los programadores de procesamiento continuo en lotes que ignoran la fragmentación de KV siguen sufriendo problemas de memoria insuficiente; los diseños basados en páginas y las herramientas de desfragmentación deben incluirse en la misma actualización.
La asimetría en el relleno previo y la decodificación también afecta al autoescalado: los picos en la longitud de las instrucciones sobrecargan el procesamiento del relleno previo, mientras que las generaciones largas agotan el ancho de banda. Se necesitan grupos de tamaño para ambos casos.
Compare las afirmaciones de los proveedores sobre el “contexto largo” solicitando la cantidad de bytes KV a la longitud máxima con su tipo de dato y tamaño de lote, no tokens publicitarios.
Las políticas de eliminación deben tener en cuenta las características del producto: resumir los segmentos descartados es mejor que olvidarlos silenciosamente para los agentes de soporte que se refieren a turnos anteriores.
Los programadores de lotes continuos que ignoran la fragmentación de KV siguen causando agotamiento de memoria; los diseños basados en páginas y las funciones de desfragmentación deben incluirse en la misma actualización.
La asimetría en el relleno previo y la decodificación también afecta al autoescalado: los picos en la longitud de las instrucciones sobrecargan el procesamiento del relleno previo, mientras que las generaciones largas agotan el ancho de banda. Se necesitan grupos de tamaño para ambos casos.
Compare las afirmaciones de los proveedores sobre el “contexto largo” solicitando los bytes KV a la longitud máxima con su tipo de dato y tamaño de lote, no tokens publicitarios.
Las políticas de expulsión deben tener en cuenta las características del producto: resumir los segmentos eliminados es mejor que el olvido silencioso para los agentes de soporte que citan interacciones anteriores.
Los programadores de lotes continuos que ignoran la fragmentación de KV siguen causando agotamiento de memoria; los diseños basados en páginas y los mecanismos de desfragmentación deben incluirse en la misma actualización.
La asimetría entre el relleno previo y la decodificación también afecta al escalado automático: los picos en la longitud de las instrucciones sobrecargan el procesamiento de relleno previo, mientras que las generaciones largas sobrecargan el ancho de banda. Se necesitan grupos de tamaño para ambos casos.
Compare las afirmaciones de los proveedores sobre el “contexto largo” solicitando los bytes KV a la longitud máxima con su tipo de dato y tamaño de lote, no tokens publicitarios.
Las políticas de expulsión deben tener en cuenta las características del producto: resumir los segmentos eliminados es mejor que el olvido silencioso para los agentes de soporte que citan interacciones anteriores.
Lecturas relacionadas
- Inferencia LLM de extremo a extremo: Prefill, Decodificación, Cache KV y Muestreo — Una explicación preparada para pizarras blancas sobre el tokenizador a través del streaming: por qué el prefill está limitado por los cálculos, la decodificación por el ancho de banda, y cómo GQA, el paginado y la especulación modifican los costos operativos.
- Decodificación especulativa y salida temprana: Acelerando la decodificación autoregresiva — Los métodos de redactar primero y verificar después, junto con las salidas por capas basadas en la confianza, reducen la latencia de decodificación cuando lo permiten los presupuestos de aceptación y precisión.