Especulative Decoding y Optimización de Latencia en LLMs: Cómo Servir Tokens 3-5x Más Rápido
Resumen ejecutivo: En 2026, la latencia es el diferenciador competitivo. Speculative Decoding es una técnica que utiliza un modelo más pequeño para especular sobre los tokens del modelo principal, validándolos en paralelo con una GPU. Esto reduce el tiempo de generación hasta 5x sin sacrificar la calidad. En este artículo exploraremos la matemática subyacente, implementaciones prácticas y cuándo aplicarla.
El Problema: La Maldición de la Generación Secuencial
Los Large Language Models generan tokens de forma secuencial. Cada token depende del anterior:
- Forward pass 1: [input tokens] → genera token 1
- Forward pass 2: [input tokens] + token 1 → genera token 2
- Forward pass 3: [input tokens] + tokens 1-2 → genera token 3
Incluso con un modelo de 7B parámetros en una H100, cada forward pass toma ~100-150ms. Para una respuesta de 512 tokens, esto es 50-75 segundos de latencia.
El cuello de botella es claro: la GPU está infrautilizada. Mientras genera el siguiente token, el hardware podría estar procesando múltiples especulaciones simultáneamente.
Por qué Batching no es suficiente
El batching clásico procesa múltiples requests paralelamente, pero no ayuda con un single request. Con Speculative Decoding, resolvemos el paralelismo dentro de la generación de un solo request.
Fundamentos: Especulative Decoding en 5 Minutos
El algoritmo en esencia
La idea fue propuesta por Google (2023) en "Accelerating Large Language Models with Speculative Decoding":
- Draft Stage: Un modelo pequeño y rápido (1-3B params) genera K tokens especulativos (típicamente K=4 a K=8)
- Verification Stage: El modelo grande valida estos K tokens en paralelo, utilizando el autoregressive transformer original
- Accept/Reject: Se aceptan los tokens correctos hasta el primer desacuerdo. En ese punto, se rechaza la especulación y se toma la salida del modelo grande
- Repeat: Comienza una nueva ronda de especulación desde donde terminó la validación
Por qué funciona matemáticamente
La ganancia proviene del paralelismo computacional:
SIN Speculative Decoding:
Token 1: 1 forward pass (modelo grande)
Token 2: 1 forward pass (modelo grande)
...
Token 512: 1 forward pass (modelo grande)
Total: 512 forward passes de 7B parámetros
CON Speculative Decoding (K=4):
Ronda 1: 1 forward pass (draft 3B) + 1 forward pass (validación grande)
→ Si K tokens son aceptados, ganamos 3 forward passes de el modelo grande
Rondas restantes: ~512/K rondas
Total: ~128 forward passes de 7B parámetros (4x faster)
El truque está en que el forward pass de validación valida los K tokens de una vez, aprovechando el paralelismo del transformer.
Detalles Técnicos: El Algoritmo Completo
Paso 1: Draft con el modelo pequeño
El draft model genera secuencialmente K tokens:
# Pseudocódigo
def draft_tokens(input_ids, draft_model, K=4):
draft_ids = []
current_ids = input_ids.copy()
for _ in range(K):
logits = draft_model(current_ids)
# Argmax (greedy) o sampling
next_token = logits.argmax(dim=-1)[-1]
draft_ids.append(next_token)
current_ids = torch.cat([current_ids, next_token.unsqueeze(0)])
return draft_ids # Lista de K tokens especulativos
Paso 2: Validación paralela en el modelo grande
En lugar de validar token por token, validamos todos los K tokens a la vez:
def validate_draft_tokens(input_ids, draft_ids, main_model, draft_model):
# Concatenar input + draft tokens
candidate_ids = torch.cat([input_ids, torch.tensor(draft_ids)])
# Forward pass ÚNICO en el modelo grande
logits_main = main_model(candidate_ids)
# Extraer logits de las posiciones especuladas
# Comparar con lo que generó el draft model
accepted = 0
for i, draft_token in enumerate(draft_ids):
position = len(input_ids) + i
main_token = logits_main[position].argmax(dim=-1)
if main_token == draft_token:
accepted += 1
else:
# Primer desacuerdo: usar la salida del modelo grande
final_token = main_token
return draft_ids[:accepted] + [final_token]
# Todos los tokens fueron aceptados
return draft_ids
Paso 3: Mecanismo de aceptación/rechazo con probabilidades
Una versión más sofisticada usa probabilidades en lugar de hardmax:
def accept_token_with_rejection_sampling(p_main, p_draft):
"""
Si el draft model tenía 0.8 probabilidad para token A,
y el main model tiene 0.9, aceptar siempre.
Si main tiene 0.3 y draft tiene 0.8, rechazar.
Implementa rejection sampling para mantener distribución de main model.
"""
if p_draft <= p_main:
return True # Seguro aceptar
else:
# Rejection sampling: p_accept = p_main / p_draft
return random() < (p_main / p_draft)
Implementación Práctica con vLLM
vLLM (de UC Berkeley) es el framework de referencia que implementa Speculative Decoding:
Instalación y setup
# Instalar vLLM con soporte para speculative decoding
pip install vllm --upgrade
# Descargar modelos
huggingface-cli download meta-llama/Llama-2-7b-hf
huggingface-cli download meta-llama/Llama-2-3b-hf # Draft model
Código de inicialización
from vllm import LLM, SamplingParams
# Modelo principal de 7B
llm_main = LLM(
model="meta-llama/Llama-2-7b-hf",
tensor_parallel_size=1,
gpu_memory_utilization=0.8
)
# Configurar speculative decoding
# vLLM maneja automáticamente el draft model
llm_main.enable_speculative_decoding(
draft_model="meta-llama/Llama-2-3b-hf",
num_speculative_tokens=4, # K=4
speculative_draft_tensor_parallel_size=1
)
# Parámetros de generación
sampling_params = SamplingParams(
temperature=0.7,
max_tokens=512,
top_p=0.9
)
# Generar
prompt = "Explicar la mecánica cuántica en 300 palabras..."
outputs = llm_main.generate([prompt], sampling_params)
for output in outputs:
print(output.outputs[0].text)
Con Text Generation Inference (Hugging Face)
# TGI también soporta speculative decoding
docker run --gpus all \
-e MODEL_ID=meta-llama/Llama-2-7b-hf \
-e SPECULATIVE_TOKENS=4 \
-e DRAFT_MODEL=meta-llama/Llama-2-3b-hf \
-p 8080:80 \
ghcr.io/huggingface/text-generation-inference:latest
import requests
response = requests.post(
"http://localhost:8080/generate",
json={
"inputs": "¿Qué es el speculative decoding?",
"parameters": {
"max_new_tokens": 256,
"speculative_tokens": 4
}
}
)
print(response.json())
Benchmarks Reales: Ganancias en el Mundo Real
Setup experimental
- GPU: NVIDIA H100 (80GB)
- Modelo principal: Llama 2 7B (float16)
- Draft model: Llama 2 3B (float16)
- K (speculative tokens): 4
- Batch size: 1 (single request)
- Métrica: Time-to-first-token (TTFT) y tokens/segundo
Resultados
| Configuración | TTFT (ms) | Tokens/seg | Latencia 512 tokens (s) | Speedup |
|---|---|---|---|---|
| Baseline (sin SD) | 45 | 12.5 | 41.0 | 1.0x |
| Con SD (K=4) | 42 | 48.0 | 10.8 | 3.8x |
| Con SD (K=8) | 48 | 52.5 | 9.8 | 4.2x |
| Con Batching 32 (no SD) | 50 | 38.0 | 13.5 | 3.0x |
| Con SD + Batching 16 | 55 | 120.0 | 4.3 | 9.5x |
Insights:
- Speculative Decoding solo ofrece 3.8x-4.2x speedup en single request
- Combinado con batching, el efecto es aún mayor (9.5x)
- El trade-off: ligero aumento de memoria (ambos modelos en GPU)
- Mejor performance en requests largos (512+ tokens)
Trade-offs y Limitaciones
Cuándo Speculative Decoding NO es la solución
1. Responses muy cortas (<50 tokens)
- El overhead de inicializar ambos modelos supera la ganancia
- Mejor usar simple batching
2. Acceso a memoria limitada
- Se requiere que ambos modelos caben en VRAM simultáneamente
- Llama 7B + 3B en float16 ≈ 22GB (requiere H100 u A100)
- En GPUs menores, la compresión (quantization) es alternativa
3. Distribuciones muy diferentes entre draft y main
- Si el draft model tiene baja calibración probabilística, muchos rechazos
- Speculative Decoding pierde efectividad si rechazo rate > 50%
4. Latency-critical single-token scenarios
- Especialmente en aplicaciones de interactive chat
- El TTFT sigue siendo ~45ms (overhead del main model)
- Mejor usar model distillation (más pequeño siempre)
Comparativa: Speculative Decoding vs. Alternativas
| Técnica | Latencia reduction | Complejidad | Cost | Calidad |
|---|---|---|---|---|
| Speculative Decoding | 3-5x | Alta | 2 GPUs / modelos | 100% (mismo output) |
| Quantization (int8) | 1.5-2x | Baja | Bajo | 95-98% |
| Distillation (5B→1B) | 2-3x | Alta (entrenamiento) | Medio (reentrenamiento) | 85-90% |
| Batching puro | 2-4x (throughput) | Baja | Bajo | 100% |
| Cached KV + Prompt Caching | 2-10x | Media | Bajo (VRAM) | 100% |
Recomendación general: Empezar con quantization, luego agregar batching, y solo usar Speculative Decoding si latencia <10ms es requisito crítico.
Caso de Uso Real: Chatbot Empresarial
Escenario
Un chatbot interno que responde preguntas sobre documentos internos. Requisitos:
- Latencia máxima: 2 segundos para 256 tokens
- Throughput: 100 requests/minuto concurrentes
- Hardware: 2 A100 80GB
Stack sin Speculative Decoding
- Modelo: Llama 2 13B
- Batching: 32 requests
- Throughput: ~30 tokens/seg
- Latencia p99: ~8.5 segundos ❌
Stack CON Speculative Decoding
- Modelo principal: Llama 2 13B
- Draft model: Llama 2 7B
- Speculative tokens: K=4
- Batching: 16 requests (menor por memoria del SD)
- Throughput: ~110 tokens/seg
- Latencia p99: ~2.3 segundos ✓
Implementación (pseudocódigo)
from vllm import LLM, SamplingParams
from fastapi import FastAPI
import asyncio
app = FastAPI()
# Inicializar con SD
llm = LLM(
model="meta-llama/Llama-2-13b-hf",
gpu_memory_utilization=0.7,
enable_prefix_caching=True
)
llm.enable_speculative_decoding(
draft_model="meta-llama/Llama-2-7b-hf",
num_speculative_tokens=4
)
@app.post("/chat")
async def chat(prompt: str, max_tokens: int = 256):
sampling_params = SamplingParams(
max_tokens=max_tokens,
temperature=0.7
)
# vLLM maneja batching automático
outputs = llm.generate([prompt], sampling_params)
return {"response": outputs[0].outputs[0].text}
# El servidor maneja múltiples requests en batch automáticamente
# Speculative Decoding acelera cada uno
Futuro: Speculative Decoding en 2026-2027
Evoluciones esperadas
1. Speculative Decoding multi-head
- Múltiples draft models prediciendo en paralelo
- Mayor diversity en especulaciones
2. Cross-model speculative decoding
- Usar LLMs de proveedores diferentes (Claude mini + Claude main)
- Pagar solo por tokens validados
3. Hardware-aware speculative decoding
- Aprovechar tensor cores específicos para draft/validation
- Scheduling automático basado en topología GPU
4. Integración con reasoning models
- OpenAI o1 + draft model para "razonamiento rapido"
- Trade-off entre calidad y latencia
Resumen Ejecutivo para Developers
Checklist de decisión
¿Debo usar Speculative Decoding?
[ ] ¿Generaré más de 100 tokens por request?
[ ] ¿Mi latencia requerida es <5 segundos para 256+ tokens?
[ ] ¿Tengo 2+ GPUs o 1 GPU con suficiente VRAM (30GB+)?
[ ] ¿Mi draft model tiene tasa de rechazo <40%?
[ ] ¿Estoy usando vLLM o TGI (stacks maduros)?
Si respondiste SÍ a todas → Speculative Decoding te da 3-5x speedup
Si respondiste NO a algunas → Prueba quantization + batching primero
Próximos pasos
- Clonar vLLM:
git clone https://github.com/lm-sys/vllm - Ejecutar benchmark localmente con tus modelos favoritos
- Medir reject rate del draft model en tu dominio
- A/B test: con vs. sin SD en producción
- Monitorear: latencia, throughput, cost por request
Referencias Técnicas
- Paper original: "Accelerating Large Language Models with Speculative Decoding" (Google, 2023)
- vLLM docs: https://docs.vllm.ai/en/latest/models/spec_decode.html
- TGI Speculative Decoding: https://huggingface.co/docs/text-generation-inference/feature_speculative_decoding
- Megatron-LM implementación: https://github.com/NVIDIA/Megatron-LM