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":

  1. Draft Stage: Un modelo pequeño y rápido (1-3B params) genera K tokens especulativos (típicamente K=4 a K=8)
  2. Verification Stage: El modelo grande valida estos K tokens en paralelo, utilizando el autoregressive transformer original
  3. 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
  4. 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

  1. Clonar vLLM: git clone https://github.com/lm-sys/vllm
  2. Ejecutar benchmark localmente con tus modelos favoritos
  3. Medir reject rate del draft model en tu dominio
  4. A/B test: con vs. sin SD en producción
  5. 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