⚡ Inferencia

Cómo un LLM genera texto paso a paso, qué optimizaciones existen y a qué velocidad puedes esperar que funcione en tu hardware.

Un token cada vez

Un LLM genera un token cada vez. No es como una imagen que aparece entera. Es como alguien que escribe letra por letra: cada nueva letra depende de todo lo que ha escrito antes.

Mensaje original: "La capital de Francia es"
Iteración 1: prompt = "La capital de Francia es" → genera "París"
Iteración 2: prompt = "La capital de Francia es París" → genera "."
Iteración 3: prompt = "La capital de Francia es París." → genera "Es"
Iteración 4: prompt = "La capital de Francia es París. Es" → genera "una"
Iteración 5: prompt = "La capital de Francia es París. Es una" → genera "ciudad"

Cada iteración = forward pass completo del modelo.

El coste O(N²)

Sin ninguna optimización:

Paso 1: Forward([t1, t2, t3, t4, t5])    → t6
Paso 2: Forward([t1, t2, t3, t4, t5, t6]) → t7
...
Paso N: Forward([t1 ... t_{M+N}])        → t_{M+N+1}

Donde M = tokens de entrada, N = tokens de salida.

FLOPs totales ≈ Σ_{i=1}^{N} (M + i) × O(d_model²)
              ≈ N × (M + N/2) × O(d_model²)
              ≈ O(N × (M+N))   ← CUADRÁTICO en tokens generados

💡 Ejemplo: M=50 tokens de prompt, N=100 tokens generados, d_model=4096. FLOPs totales = 100 × (50+50) × 4096² = 167 GFLOP. Pero el 98% del tiempo es mover memoria, no computar. Tiempo real ≈ 14 GB (pesos) / 1008 GB/s × 100 pasos ≈ 1.4 segundos.

Speculative Decoding

Técnica para acelerar el bucle autoregresivo usando un modelo pequeño (draft) para generar K tokens rápidamente, luego el modelo grande (target) los verifica en paralelo.

1. Draft (modelo pequeño, rápido): genera K tokens especulativos
2. Verify (modelo grande): procesa los K tokens en UN forward pass
   (paralelo, porque la atención puede ver todos a la vez)
3. Si todos son aceptados → ahorro de K-1 forward passes
   Si alguno es rechazado → retroceder y regenerar

Ahorro típico: 2-3× en velocidad.

La optimización que lo cambia todo

El problema: en cada paso del bucle, recalculamos las mismas cosas una y otra vez. Los tokens del prompt original no cambian, pero los procesamos cada vez.

La solución: guardar (cachear) las matrices K y V de cada token. Cuando generamos un nuevo token, solo procesamos ese token nuevo y reutilizamos lo guardado de los anteriores.

❌ Sin KV Cache

Paso 1: [A, B, C] → D

Paso 2: [A, B, C, D] → E

Paso 3: [A, B, C, D, E] → F

✅ Con KV Cache

Paso 1: [A, B, C] → D → guardar

Paso 2: [D] → E → guardar K_D, V_D

Paso 3: [E] → F → guardar K_E, V_E

Prefill y Decode

Fase 1: Prefill — procesar el prompt (paralelo)

Entrada: [t1, t2, ..., tM] (todos a la vez)
- Forward completo para todos los tokens
- Atención: matriz M×M
- Se genera el primer token de salida
- Se guarda K_i, V_i para todos los tokens en KV Cache

Coste: ~1 forward pass de tamaño M (rápido, ~50 ms para 100 tokens)

Fase 2: Decode — generar tokens uno a uno (serial)

Entrada: [t_{M+k}] (solo el nuevo token)
- Forward de UN SOLO token
- Q_{nuevo}: se calcula para el token nuevo
- K_{nuevo}, V_{nuevo}: se calculan y se añaden a la cache
- Atención: Q_{nuevo} mira a [K_cache + K_{nuevo}]
- FFN para el token nuevo

Coste: ~1 forward pass de tamaño 1 (rápido, ~5 ms por token)

🟢 Ahorro: Para M=50, N=100 tokens: sin KV Cache → ~10,050 unidades; con KV Cache → 150 unidades. Ahorro: ~98%.

El coste oculto: VRAM

La KV Cache ocupa mucha VRAM, proporcional a:

KV_Cache_bytes = 2 × n_layers × d_model × n_tokens × bytes_por_param

Para LLaMA 7B, 4096 tokens, FP16:
  = 2 × 32 × 4096 × 4096 × 2
  = 2.147 GB

Para 128k tokens (LLaMA 3.1 8B):
  = 2 × 32 × 4096 × 131072 × 2
  = 68.7 GB   ← más que el propio modelo (16 GB en FP16)

Técnicas de compresión de KV Cache

TécnicaFactorCómo
GQA8 queries comparten 1 par KV
KV Cache quantizationGuardar KV en INT8
Budget forcingVariableSolo guardar los últimos N tokens
H2O (Heavy Hitter)Solo guardar tokens con alta atención
MLA (DeepSeek)~10×Comprimir KV en espacio latente

Ancho de banda → tokens/s

La velocidad a la que un LLM genera texto depende casi exclusivamente del ancho de banda de memoria de tu GPU, no de lo rápida que sea haciendo cálculos.

💡 Es como una fábrica embotellando agua: la línea de embotellado es rapidísima (TFLOPS), pero el agua llega por una tubería estrecha (ancho de banda). El cuello de botella es la tubería, no la embotelladora.

La fórmula

tokens/s ≈ BW_memoria / (n_params × bytes_por_param)

Tabla para modelo 7B (batch=1):

GPUBW (GB/s)FP16Q4_K_MINT8
RTX 4090100872 t/s240 t/s144 t/s
RTX 408071651 t/s170 t/s102 t/s
RTX 407050436 t/s120 t/s72 t/s
RTX 406027219 t/s65 t/s39 t/s
RTX 309093667 t/s223 t/s134 t/s
A1002039146 t/s486 t/s291 t/s
H1003352239 t/s798 t/s479 t/s
DDR5-4800 (CPU)503.6 t/s12 t/s7.1 t/s

⚠️ Las velocidades reales suelen ser 60-80% de la teórica por overhead de atención, softmax y normalización. llama.cpp logra ~70-80% de eficiencia.

Qué significan estas velocidades: 240 t/s → respuesta instantánea (como ChatGPT); 50 t/s → cómodo; 10 t/s → visiblemente lento; 2 t/s → doloroso.

¿Por qué el ancho de banda y no los TFLOPS?

Modelo 7B FP16: 14 GB de pesos
GPU RTX 4090:   1008 GB/s, 82 TFLOPS

Por token:
  Leer pesos de VRAM: 14 GB / 1008 GB/s = 13.9 ms
  Operaciones: 14 GFLOP / 82 TFLOPS = 0.17 ms

  → 98.8% del tiempo es I/O de memoria
  → 1.2% es cómputo real

Si la GPU fuera 10× más rápida en cómputo:
  → Velocidad pasaría de 72 t/s a 73 t/s  (casi nada)

Batch Size > 1 (throughput)

En servidores de producción, se usa batch size > 1 para atender a varios usuarios simultáneamente:

Batch=1:  14 GB de pesos → 1 usuario     → 72 t/s
Batch=4:  14 GB de pesos → 4 usuarios    → 4 × 50 t/s = 200 t/s total
Batch=16: 14 GB de pesos → 16 usuarios   → 16 × 30 t/s = 480 t/s total

Calculadora de Velocidad

Selecciona un modelo, una GPU y una precisión para calcular los tokens/s estimados y ver el cuello de botella.