Decodificación especulativa, salida temprana y cuantización: una decodificación de LLM más rápida.
Cómo la decodificación especulativa y la salida temprana reducen la latencia de inferencia de los LLM, cuándo la aceptación preliminar resulta beneficiosa, y cómo se combinan con la cuantización y la paginación de caché KV.
Fuentes: **Foundations of LLM Inference Optimization: Speculative Decoding and Early Exit | Part 3D**
La inferencia de LLM se lleva a cabo en dos fases. Prefill procesa todo el prompt de forma paralela y puede mantener ocupada una GPU. Decode emite un token a la vez, y cada token depende del anterior, por lo que el hardware pasa la mayor parte del tiempo en espera. Esa dependencia secuencial es la razón por la cual la latencia interactiva sigue siendo alta, incluso cuando los FLOPs parecen abundantes en teoría.
Prefill phase:
Input: [token_1, token_2, ..., token_512] → all 512 tokens processed in parallel
Matrix shape: [batch, 512, 4096]
GPU utilization: high — large matrix, full Tensor Core throughput
Decode phase (one step):
Input: [token_513] → one token processed
Matrix shape: [batch, 1, 4096]
GPU utilization: low - tiny matrix, most CUDA cores idle
Por qué la decodificación se encuentra en el camino crítico
En cada paso de decodificación, el modelo realiza consultas contra una caché KV en constante crecimiento. El trabajo por paso aumenta con la longitud del contexto, y en el caso de las conversaciones no se puede procesar en lotes libremente, ya que los SLA de latencia limitan cuánto tiempo puede esperar una solicitud. La aritmética muestra cuán poco del chip utiliza realmente un solo paso:
One attention head, decode step:
Query Q: [8, 1, 128] → 8 × 128 = 1,024 elements
Keys K: [8, 2048, 128] → 8 × 2048 × 128 = 2M elements
Values V: [8, 2048, 128] → same
Matrix multiply (QKᵀ) per head:
Shape: [8, 1, 128] × [8, 128, 2048] → [8, 1, 2048]
FLOPs: 8 × 1 × 128 × 2048 ≈ 2.1M FLOPs per head
For 32 heads: ≈ 67M FLOPs per layer
For 32 layers: ≈ 2.1B FLOPs total per decode step
A100 peak at BF16: ~312 TFLOPS = 312 × 10¹² FLOPs/sec
Time to execute 2.1B FLOPs at 100% utilization:
2.1 × 10⁹ / 312 × 10¹² ≈ 0.0067 ms
Actual observed decode latency per step: ~10–30ms
Effective compute utilization: < 0.1%
Una utilización efectiva inferior al 0.1% significa que la decodificación está limitada por el tráfico de memoria y la cantidad de pasos, no por el cálculo bruto. Hay dos formas de abordar esto: generar más tokens aceptados por paso costoso, o realizar menos trabajo por paso. La decodificación especulativa hace lo primero, mientras que la salida anticipada hace lo segundo.
Decodificación especulativa: boceto primero, luego verificación
Un pequeño modelo de boceto propone varios tokens futuros. El modelo objetivo los verifica a todos en una sola pasada paralela, mantiene el prefijo más largo que coincide y realiza un nuevo muestreo al primer rechazo. Cada token de boceto aceptado es uno por el cual no se pagó el costo completo de un paso del modelo objetivo.
Without speculative decoding:
Generate 5 tokens: 5 × 30ms = 150ms
With speculative decoding (K=4):
Draft 4 tokens: 4 × 1.5ms = 6ms
1 target verification pass: ~35ms (slightly longer than decode,
processes K+1=5 positions)
Expected accepted tokens per round:
(1 - 0.80^5) / (1 - 0.80) ≈ 3.36 tokens
Time per round: 6ms + 35ms = 41ms
Time per token: 41ms / 3.36 ≈ 12.2ms
Speedup: 30ms → 12.2ms ≈ 2.5×
La aceleración depende por completo de la frecuencia con la que el borrador y el objetivo coinciden. El texto sencillo y de baja entropía acepta borradores largos; un token inesperado acorta rápidamente la ronda. Aquí está el bucle en su forma conceptual (las implementaciones reales también gestionan la caché KV en ambos modelos):
import torch
import torch.nn.functional as F
def speculative_decode(target_model, draft_model, input_ids,
max_new_tokens, K=4, temperature=1.0):
"""
Conceptual speculative decoding loop.
Real implementations handle KV cache management across both models.
"""
generated = input_ids.clone()
while generated.shape[1] - input_ids.shape[1] < max_new_tokens:
# --- Draft phase ---
draft_tokens = []
draft_probs = []
draft_input = generated.clone()
for _ in range(K):
with torch.no_grad():
draft_logits = draft_model(draft_input).logits[:, -1, :]
q = F.softmax(draft_logits / temperature, dim=-1)
token = torch.multinomial(q, num_samples=1)
draft_tokens.append(token)
draft_probs.append(q)
draft_input = torch.cat([draft_input, token], dim=1)
# --- Verify phase: one target forward pass over all K+1 positions ---
verify_input = torch.cat([generated] + draft_tokens, dim=1)
with torch.no_grad():
target_logits = target_model(verify_input).logits
# target_logits[:, -K-1:, :] covers all K draft positions + bonus
# --- Accept/reject ---
accepted = 0
for i in range(K):
p = F.softmax(target_logits[:, -(K+1)+i, :] / temperature, dim=-1)
q = draft_probs[i]
token = draft_tokens[i]
# Acceptance probability
accept_prob = torch.min(
torch.ones_like(p.gather(1, token)),
p.gather(1, token) / (q.gather(1, token) + 1e-9)
)
if torch.rand(1) < accept_prob:
generated = torch.cat([generated, token], dim=1)
accepted += 1
else:
# Sample corrected token and stop this round
corrected_dist = F.relu(p - q)
corrected_dist = corrected_dist / corrected_dist.sum(dim=-1, keepdim=True)
corrected_token = torch.multinomial(corrected_dist, num_samples=1)
generated = torch.cat([generated, corrected_token], dim=1)
break
else:
# All K accepted - take bonus token
bonus_logits = target_logits[:, -1, :]
p_bonus = F.softmax(bonus_logits / temperature, dim=-1)
bonus_token = torch.multinomial(p_bonus, num_samples=1)
generated = torch.cat([generated, bonus_token], dim=1)
return generated
Elige un borrador de la misma familia cuando sea posible, como uno más pequeño destilado o cuantizado, y mide la tasa de aceptación con tu propio tráfico en lugar de con ejemplos de blogs públicos.
Cuando los borradores divergen
Si la distribución del borrador se aleja de la del objetivo, la tasa de aceptación cae y pagas el costo del borrador casi sin obtener beneficio alguno. Monitorea continuamente la tasa de aceptación y vuelve al método de decodificación simple cuando esta disminuya.
Una rápida verificación con números redondos: supongamos que un paso objetivo cuesta 10 ms y el borrador propone 5 tokens, de los cuales en promedio se aceptan unos 3 (60%). El costo por token aceptado es menor en comparación con los pasos de un solo token, incluso después de los costos adicionales del borrador, siempre y cuando la tasa de aceptación siga siendo alta. Si la tasa de aceptación baja a aproximadamente un token, el sistema deja de ser rentable. Esa sensibilidad es la razón por la cual los paneles de control son superiores a las anécdotas.
Salida anticipada: omite capas profundas en tokens fáciles
Algunas arquitecturas permiten que un token abandone la red en una capa intermedia una vez que la confianza es alta, lo que ahorra recursos de procesamiento en puntuación, artículos y otros tokens predecibles.
Layer distribution of exits:Exit at layers 1–8 (very easy tokens like punctuation, articles): 15%
Exit at layers 9–16 (medium tokens, common continuations): 35%
Exit at layers 17–24 (harder tokens, named entities, numbers): 30%
Exit at layers 25–32 (full computation required): 20%Weighted average layers executed:
0.15 × 6 + 0.35 × 12 + 0.30 × 20 + 0.20 × 32
= 0.90 + 4.20 + 6.00 + 6.40
= 17.5 layers averageSpeedup vs always running 32 layers:
32 / 17.5 ≈ 1.83×
A diferencia de la decodificación especulativa, la salida anticipada sí modifica los resultados al omitir capas, por diseño. Compromete la calidad en favor de la velocidad y depende en gran medida de cuán bien esté calibrada la confianza, por lo que es necesario presupuestar el delta de precisión y medirlo.
Apilamiento con cuantización, agrupamiento y caché KV
La decodificación especulativa modifica el bucle de propuesta de tokens al añadir un segundo modelo; la salida anticipada cambia la profundidad dentro de un mismo modelo. Ambas enfrentan cuellos de botella relacionados de maneras diferentes, y a veces pueden combinarse con cuantización, agrupamiento continuo y paginación del caché KV:
Combined stack example:
Target: 7B model, BF16, FlashAttention, PagedAttention
→ Model: ~14 GB, memory-efficient attention, paged KV cache
Draft: 70M model, INT4, FlashAttention
→ Model: ~35 MB, near-zero memory overhead
Speculative decode:
→ with K=4, α=0.80
→ ~2.5× token generation speedup on long outputs
Full stack speedup vs FP32 no-optimization baseline:
- Quantization: 2–2.3× throughput
- FlashAttention: 20–40% attention latency reduction
- Speculative decoding: 2–2.5× decode speedup (on eligible requests)
Combined: 5–8× improvement in end-to-end tokens/second
El apilamiento requiere atención especial. Las longitudes de los borradores interactúan con las suposiciones de un programador de lotes continuo, y un programador deficiente fragmenta los lotes y perjudica la utilización. Las políticas de eliminación de la caché KV necesitan ajuste para que las conversaciones largas no generen sobrecarga. El caché de prompts en la fase de prellenado también es importante, de lo contrario se “arregla la decodificación” pero se sigue desperdiciando recursos en el prellenado. Además, combinar la salida temprana con cuantización agresiva puede aumentar rápidamente las alucinaciones. Un diseño de servicio coherente es mejor que aplicar un método propuesto en un artículo científico directamente en el flujo principal.
Cuándo resulta útil cada técnica
La decodificación especulativa es útil cuando la concordancia entre borradores es alta y los pasos objetivo dominan la latencia. Falla cuando los borradores rara vez coinciden o cuando la sobrecarga de los borradores anula los beneficios. La salida temprana es útil cuando muchos tokens son fáciles de procesar y el presupuesto de precisión lo permite; falla con tokens difíciles o cuando la confianza en los resultados no está bien calibrada.
Un ejemplo concreto: un modelo objetivo de 7 mil millones de parámetros para chats, con respuestas de 50 a 150 tokens. Prellenar un prompt del sistema con 2 mil tokens es un proceso pesado, pero ocurre raramente por turno, mientras que los pasos de decodificación son los que causan la demora que perciben los usuarios. Reducir la cantidad de pasos de decodificación mediante aceptaciones especulativas, o la carga por paso mediante salida anticipada, es lo que realmente sienten los usuarios.
¿Es importante esto para modelos pequeños en el dispositivo? Por lo general, menos que para modelos grandes. ¿Puede ejecutarse el borrador en CPU? A veces; hay que medirlo. Como orden de prioridades, primero solucione los problemas de agrupamiento y caché, luego añada la decodificación especulativa, y finalmente la salida anticipada si su stack lo permite.
Integración en la inferencia de LLM en producción
- Consiga primero los valores de latencia p95 y calidad de la versión básica antes de cambiar nada.
- Coloca el modelo en borrador junto con el objetivo en el mismo nodo. Los borradores en hosts diferentes generan fluctuaciones de red que pueden anular los beneficios, y dos modelos más la caché KV pueden agotar la memoria de una máquina que antes albergaba cómodamente uno solo.
- Registra histogramas de aceptación, longitud promedio aceptada, deltas de evaluación de calidad, utilización de GPU y latencia p95.
- Observa el equilibrio entre CPU y GPU cuando los borradores se ejecutan en dispositivos diferentes.
- Evalúa la calidad en pruebas factuales mediante el método A/B. Antes de un despliegue global, prueba prompts fijos en pruebas de QA factuales, de programación y de rechazo, y compara las tasas de resultados idénticos en tokens cuando se configure la decodificación especulativa para lograr una distribución exacta. Para una salida temprana, monitorea la tasa de aciertos en tareas calificadas y las preferencias humanas cuando estén disponibles.
- Coloca cada optimización detrás de una bandera de funcionalidad con un interruptor de apagado que permita volver a la decodificación estándar, y practica el proceso de reversión.
- Revisar todo después de realizar cambios en el tokenizador o en el ajuste fino.
Un decodificador especulativo implementado correctamente debería coincidir con la distribución objetivo, pero hay que verificarlo mediante pruebas emparejadas en lugar de confiar ciegamente en ello. Utilizar un borrador no relacionado, ignorar cómo la temperatura afecta la aceptación, o declarar victoria basándose únicamente en pruebas de rendimiento sin controles de calidad son las formas habituales en que esto falla, y cada una de ellas es medible.
Los equipos de la plataforma deben encargarse de estas optimizaciones en la capa de servicio para que cada equipo de aplicaciones no invente su propio método de selección de borradores. En lugar de modificar código en las aplicaciones, coordinen con los responsables del servicio, expongan encabezados o atributos de seguimiento que indiquen si se aplicó decodificación especulativa o salida anticipada, y etiqueten los costos por token según la optimización para que el departamento financiero pueda ver los beneficios.
El cuello de botella que queda
Una vez que la decodificación se vuelva más rápida, los usuarios podrían seguir esperando por las llamadas a las herramientas, la recuperación de datos o el renderizado de Markdown en el lado del cliente. El límite restante puede ser el ancho de banda de la memoria, la red o el cliente, y no los FLOPs; por lo tanto, realice un seguimiento de extremo a extremo y realice un perfilamiento antes de aplicar todos los trucos descritos en los artículos. La decodificación especulativa tampoco reemplaza una buena recuperación de datos ni prompts efectivos; solo hace que los siguientes tokens sean más económicos cuando el modelo ya sabe lo que quiere decir.
La decodificación secuencial representa el costo estructural de la generación autoregresiva. La decodificación especulativa y la salida anticipada reducen ese costo en condiciones que se pueden medir, por lo que deben implementarse como cualquier otra función de producción: con métricas, indicadores, posibilidades de reversión y un responsable claro dentro del equipo de la plataforma de servicio.
Leer más
- Optimización de la inferencia de LLM: prefill, decodificación y LLMOps empresariales: Cuellos de botella entre prefill y decodificación, loteo continuo, FlashAttention, cuantización, PagedAttention, decodificación especulativa, prefill por bloques y servicio desagregado.
- Loteo continuo y prefill por bloques en vLLM y SGLang: Los lotes estáticos de LLM desperdician la GPU en datos de relleno. La programación a nivel de iteración y el prefill por bloques mantienen fluidas las cadenas de decodificación mientras los nuevos prompts siguen progresando.