¿Por qué la inferencia es tan lenta?

Un modelo de 70 mil millones de parámetros puede procesar miles de tokens por segundo durante el entrenamiento, distribuido en un clúster de GPUs procesando secuencias completas en paralelo. Pero en tiempo de inferencia, ese mismo modelo genera aproximadamente de 30 a 50 tokens por segundo por solicitud. Esa es una desaceleración dramática, y la razón no es la que la mayoría de la gente asume. No es que la GPU sea demasiado débil o que el modelo sea demasiado grande en algún sentido abstracto. El cuello de botella es mucho más específico, y entenderlo es la clave de cada técnica de optimización en este track.

Durante el entrenamiento, ya conocemos la secuencia objetivo completa (el texto de referencia del que el modelo está aprendiendo). Una técnica llamada teacher forcing nos permite alimentar la secuencia completa al modelo de una vez: todas las posiciones se calculan en un solo pase hacia adelante, y la pérdida para cada token se calcula en paralelo. El modelo lee, digamos, 2,048 tokens y produce 2,048 predicciones simultáneamente. Los miles de núcleos de la GPU se mantienen ocupados.

La inferencia es fundamentalmente diferente. Los modelos de lenguaje son autorregresivos : cada token depende de todos los tokens anteriores. Generamos el token 1, lo añadimos al contexto, generamos el token 2, lo añadimos, y así sucesivamente. No hay forma de evitar esta dependencia secuencial. Y aquí está la clave: durante esta generación secuencial, la GPU está mayormente inactiva . El cuello de botella no es el cómputo — es el ancho de banda de memoria . Cada paso de generación de token requiere leer todo el conjunto de pesos del modelo desde la memoria de la GPU, pero solo realiza una cantidad mínima de cómputo con esos pesos. La GPU pasa la mayor parte de su tiempo esperando a que lleguen los datos de la memoria, no haciendo cálculos.

💡 Piénsalo como una fábrica con 10,000 trabajadores (los núcleos de la GPU) pero con una sola puerta estrecha al almacén (ancho de banda de memoria). Durante el entrenamiento, cada trabajador recibe una gran caja de piezas para ensamblar (muchos tokens por carga de pesos). Durante la decodificación, cada trabajador recibe un solo tornillo (un token por carga de pesos) — terminan al instante y luego se quedan inactivos esperando la siguiente entrega a través de esa puerta estrecha.

Este track trata sobre entender y solucionar ese cuello de botella. Comenzaremos aquí cuantificando exactamente por qué la decodificación está limitada por la memoria, y luego recorreremos las técnicas clave: reutilización de cómputo con KV caching (artículo 2), compresión de pesos del modelo mediante cuantización (artículo 3), servicio de múltiples solicitudes con batching continuo (artículo 4), especulación anticipada con modelos borrador (artículo 5), y hacer la atención misma más rápida (artículo 6).

Prefill vs Decodificación: Dos fases muy diferentes

Cuando un usuario envía un prompt a un modelo de lenguaje, la inferencia en realidad ocurre en dos fases distintas que se comportan de manera completamente diferente desde la perspectiva de la GPU. Entender esta división es esencial, porque la estrategia de optimización para cada fase es diferente.

La fase de prefill procesa todo el prompt del usuario de una vez. Todos los tokens del prompt se conocen de antemano, así que podemos calcular la atención para todas las posiciones en una sola multiplicación de matrices por lotes. Si el prompt tiene 1,000 tokens, la GPU procesa los 1,000 en un solo pase, llenando cada núcleo con trabajo útil. Esta fase está limitada por el cómputo — las unidades aritméticas de la GPU son el cuello de botella, no el ancho de banda de memoria. La GPU está ocupada, y ese es el régimen para el cual fue diseñada.

La fase de decodificación genera tokens de uno en uno. Cada paso produce exactamente un nuevo token, lo que significa que leemos todo el conjunto de pesos del modelo desde la memoria de alto ancho de banda (HBM) solo para realizar las multiplicaciones matriz-vector de un solo token. Esta fase está limitada por el ancho de banda de memoria — la GPU termina su pequeña cantidad de aritmética casi instantáneamente y luego espera a que llegue el siguiente lote de pesos desde la memoria.

Podemos cuantificar esta diferencia usando la intensidad aritmética — la relación entre cómputo y tráfico de memoria:

$$\text{Arithmetic Intensity} = \frac{\text{FLOPs}}{\text{Bytes Transferred}}$$

Esta métrica nos dice si una carga de trabajo está limitada por el cómputo (alta intensidad — muchos FLOPs por byte cargado) o limitada por la memoria (baja intensidad — pocos FLOPs por byte cargado). Para un tratamiento más profundo de cómo esto se conecta con los límites del hardware, consulta el modelo roofline en el track de GPUs.

Durante el prefill con $n$ tokens de prompt, cada matriz de pesos que cargamos de la memoria se multiplica por una matriz de entrada de $n$ filas. Realizamos $O(n)$ FLOPs por cada byte de pesos que transferimos, así que la intensidad aritmética escala linealmente con $n$. Con un prompt de 1,000 tokens, hacemos 1,000 veces más trabajo útil por byte cargado que con un solo token. Eso nos empuja firmemente al régimen limitado por cómputo donde los TFLOPS de la GPU son el factor limitante.

Durante la decodificación, $n = 1$. Cargamos cada matriz de pesos para multiplicarla por un solo vector. Hacemos $O(1)$ FLOPs por byte transferido — la intensidad aritmética es tan baja como puede ser. El enorme cómputo paralelo de la GPU queda inactivo mientras las lecturas de memoria dominan el tiempo real.

Hagamos esto concreto. Consideremos un modelo de 7B parámetros almacenado en FP16 (2 bytes por parámetro = 14 GB de pesos). Generar un solo token requiere leer los 14 GB completos desde la HBM hasta las unidades de cómputo de la GPU. En una NVIDIA A100, el ancho de banda de HBM es aproximadamente 2 TB/s. Así que el tiempo mínimo por token es:

$$t_{\text{token}} = \frac{14 \times 10^9 \text{ bytes}}{2 \times 10^{12} \text{ bytes/s}} = 7 \text{ ms}$$

Eso es aproximadamente 140 tokens por segundo — el máximo teórico para batch size 1. Pero la A100 tiene 312 TFLOPS de cómputo FP16. ¿Cuánto estamos usando realmente? Una sola multiplicación matriz-vector para un token en una capa produce del orden de unos pocos miles de millones de FLOPs en todo el modelo, mientras que la GPU podría realizar 312 billones por segundo. Estamos usando una fracción minúscula del cómputo disponible. La GPU está hambrienta de datos.

💡 Es exactamente por esto que el batching ayuda tanto durante la decodificación. Si procesamos 32 solicitudes simultáneamente, cargamos los pesos de la memoria una vez pero realizamos 32 multiplicaciones matriz-vector (efectivamente una multiplicación matriz-matriz). Eso multiplica la intensidad aritmética por 32, empujándonos de vuelta hacia el régimen limitado por cómputo. Obtenemos 32 veces el rendimiento con aproximadamente la misma latencia por token — una de las optimizaciones más importantes en el servicio de inferencia, que cubriremos en el artículo 4.

¿Dónde se va el tiempo?

Ahora que sabemos que la decodificación está limitada por el ancho de banda de memoria, rastreemos exactamente qué sucede durante un solo paso de decodificación y veamos dónde se gasta el tiempo. Para cada nuevo token, el modelo debe ejecutar lo siguiente:

  • Paso 1: Cargar el embedding del token recién generado (pequeño — una sola búsqueda de vector).
  • Paso 2: Para cada capa del transformer: cargar las matrices de pesos de proyección Q/K/V desde la HBM, calcular la atención entre el nuevo token y todos los tokens anteriores (leyendo la KV cache), cargar los pesos de la proyección de salida, cargar los pesos del FFN (dos matrices grandes) y calcular el FFN.
  • Paso 3: Cargar la cabeza del modelo de lenguaje (frecuentemente una gran matriz de proyección de vocabulario), calcular los logits sobre todo el vocabulario, aplicar softmax , y muestrear el siguiente token.

El costo dominante es el paso 2, repetido en cada capa. Un modelo de 7B tiene alrededor de 32 capas, y las matrices de pesos de cada capa deben leerse desde la HBM. Un modelo de 70B tiene alrededor de 80 capas con dimensiones ocultas más grandes, multiplicando la transferencia de datos proporcionalmente. El tiempo por token está abrumadoramente determinado por qué tan rápido podemos transmitir esos pesos desde la HBM hasta la SRAM en el chip de la GPU, donde ocurre la aritmética real.

Esto nos da una fórmula notablemente limpia para estimar la latencia de decodificación con batch size 1:

$$t_{\text{token}} \approx \frac{2P}{B_{\text{mem}}}$$

Desglosemos cada símbolo:

  • $P$ — el número de parámetros en el modelo (por ejemplo, $7 \times 10^9$ para un modelo de 7B). Esto determina la cantidad total de datos de pesos.
  • $2P$ — el tamaño del modelo en bytes, asumiendo almacenamiento en FP16 (2 bytes por parámetro). Si cuantizamos a INT8, esto se convierte en $1P$; para INT4, se convierte en $0.5P$. El factor de 2 es específico de la precisión — es lo único que la cuantización cambia en esta fórmula, razón por la cual la cuantización es una optimización de inferencia tan poderosa (artículo 3).
  • $B_{\text{mem}}$ — el ancho de banda de memoria de la GPU en bytes por segundo. Para una A100 SXM esto es aproximadamente $2 \times 10^{12}$ bytes/s (2 TB/s). Para una H100 SXM es aproximadamente $3.35 \times 10^{12}$ bytes/s (3.35 TB/s).

¿Por qué esta fórmula es solo aproximada? Porque ignora el cómputo de atención (que lee la KV cache — proporcional a la longitud de la secuencia, no al tamaño del modelo), la sobrecarga de lanzamiento de kernels y otros costos menores. Pero para secuencias cortas a medianas con batch size 1, la carga de pesos domina y esta fórmula es sorprendentemente precisa.

Repasemos los casos límite:

  • Modelo 7B en A100 (2 TB/s): $t = \frac{14 \times 10^9}{2 \times 10^{12}} = 7$ ms por token, o aproximadamente 140 tokens/s. Suficientemente rápido para uso interactivo, pero estamos desperdiciando más del 99% de los 312 TFLOPS de la GPU.
  • Modelo 70B en A100 (2 TB/s): $t = \frac{140 \times 10^9}{2 \times 10^{12}} = 70$ ms por token, o aproximadamente 14 tokens/s. Aún utilizable pero notablemente lento, y los 80 GB completos de HBM están casi llenos solo con los pesos del modelo.
  • Modelo 70B en H100 (3.35 TB/s): $t = \frac{140 \times 10^9}{3.35 \times 10^{12}} \approx 42$ ms por token, o aproximadamente 24 tokens/s. La mejora de $1.7\times$ en ancho de banda de la H100 sobre la A100 se traduce casi directamente en una aceleración de $1.7\times$ — exactamente lo que esperarías de una carga de trabajo limitada por memoria.
  • Modelo 70B cuantizado a INT4 en A100: $t = \frac{35 \times 10^9}{2 \times 10^{12}} \approx 17.5$ ms por token, o aproximadamente 57 tokens/s. La cuantización de FP16 a INT4 reduce el numerador por $4\times$, cuadruplicando el rendimiento. Esta es la palanca más importante en la optimización de inferencia.
📌 Estos son máximos teóricos — los sistemas reales siempre son más lentos. El cómputo de atención crece con la longitud de la secuencia y no está capturado en esta fórmula. Las lecturas de la KV cache añaden tráfico de memoria. La sobrecarga de lanzamiento de kernels, las normalizaciones de capas y las transferencias de memoria no solapadas contribuyen. El rendimiento real típico es del 50-70% del máximo teórico de esta fórmula.

La tabla a continuación calcula los tokens por segundo máximos teóricos para varios tamaños de modelo y GPUs, usando la fórmula anterior:

import json, js

# Model sizes in billions of parameters
models = [
    ("1.3B",   1.3e9),
    ("7B",     7e9),
    ("13B",   13e9),
    ("70B",   70e9),
    ("405B", 405e9),
]

# GPUs: name, bandwidth in bytes/s, HBM capacity in GB
gpus = [
    ("A100 (2.0 TB/s)",  2.0e12,  80),
    ("H100 (3.35 TB/s)", 3.35e12, 80),
    ("H200 (4.8 TB/s)",  4.8e12, 141),
]

rows = []
for model_name, params in models:
    model_bytes_fp16 = params * 2  # FP16: 2 bytes per param
    model_gb = model_bytes_fp16 / 1e9
    row = [model_name, f"{model_gb:.0f} GB"]
    for gpu_name, bw, hbm_gb in gpus:
        if model_gb > hbm_gb:
            row.append("OOM")
        else:
            t_ms = (model_bytes_fp16 / bw) * 1000  # ms per token
            tok_s = 1000 / t_ms
            row.append(f"{tok_s:.0f} tok/s")
    rows.append(row)

js.window.py_table_data = json.dumps({
    "headers": ["Model", "FP16 Size"] + [g[0] for g in gpus],
    "rows": rows
})

print("Theoretical max decode throughput (batch_size=1, FP16)")
print("Formula: tok/s = bandwidth / (2 * params)")
print()
print("Key insight: throughput is purely a function of model size")
print("and memory bandwidth. Compute (TFLOPS) barely matters here.")
💡 Observa cómo el modelo de 405B ni siquiera cabe en una sola A100 o H100 en FP16. Esta es la razón por la que el paralelismo tensorial (dividir el modelo entre múltiples GPUs) es obligatorio para modelos grandes, y por la que la cuantización (artículo 3) es tan atractiva — un modelo de 70B cuantizado a INT4 ocupa solo 35 GB, cabe cómodamente en una sola GPU de 80 GB mientras también cuadruplica el rendimiento de decodificación.

¿Por qué no podemos simplemente paralelizar?

Si la decodificación es tan dolorosamente secuencial, ¿por qué no podemos paralelizarla de la forma en que el entrenamiento paraleliza a través de la secuencia? Después de todo, las GPUs son máquinas paralelas — seguramente podemos hacerlo mejor que procesar un token a la vez.

La respuesta está en la dependencia autorregresiva . Durante el entrenamiento, todos los tokens objetivo son conocidos (teacher forcing), así que calcular la predicción en la posición 500 no requiere esperar la predicción en la posición 499 — todos se calculan a partir de la referencia. Pero durante la inferencia, la entrada a la posición 500 es la predicción de la posición 499. No podemos saber qué alimentar al paso $t$ hasta que el paso $t-1$ haya terminado. Esta es una verdadera dependencia de datos que ninguna cantidad de paralelismo de hardware puede romper.

Entonces, ¿qué podemos paralelizar? Tres dimensiones quedan abiertas:

  • Dimensión de batch: servir a múltiples usuarios simultáneamente. Si llegan 32 solicitudes, podemos procesarlas juntas agrupando sus productos matriz-vector en un solo producto matriz-matriz. Esto no hace más rápida ninguna solicitud individual, pero mejora dramáticamente el rendimiento (tokens por segundo en todas las solicitudes) porque amortizamos el costo de carga de pesos entre múltiples tokens. Esta es la idea central del batching continuo (artículo 4).
  • Dimensión del modelo (paralelismo tensorial): dividir las matrices de pesos de un modelo entre múltiples GPUs. Cada GPU tiene una porción de cada capa, y se comunican resultados parciales a través de interconexiones rápidas (NVLink). Esto reduce la carga de memoria por GPU, lo que permite que modelos más grandes se sirvan, y puede mejorar la latencia si la comunicación entre GPUs es suficientemente rápida. La compensación es la sobrecarga de sincronización.
  • Dimensión especulativa: en lugar de generar un token a la vez, usar un modelo borrador pequeño y rápido para adivinar múltiples tokens por adelantado, y luego verificarlos todos en paralelo con el modelo grande. Si las adivinanzas son correctas (lo cual sucede frecuentemente para tokens predecibles), obtenemos múltiples tokens por el costo de un solo pase hacia adelante del modelo grande. Esto es la decodificación especulativa (artículo 5).

Pero la generación secuencial de tokens para una sola solicitud sigue siendo la restricción fundamental. No podemos generar el token 50 hasta que hayamos generado el token 49. Esta es la razón por la que la optimización de inferencia existe como campo — estamos tratando de exprimir el máximo rendimiento de hardware masivamente paralelo que está encadenado a una carga de trabajo secuencial.

Cada técnica en este track ataca el problema desde un ángulo diferente. El diagrama a continuación las mapea al cuello de botella que abordan:

  • KV caching (artículo 2): evitar cómputo redundante almacenando en caché los resultados intermedios de tokens anteriores, para que cada paso de decodificación solo calcule la contribución del nuevo token.
  • Cuantización (artículo 3): reducir el numerador de $t_{\text{token}} = 2P / B_{\text{mem}}$ disminuyendo los bytes por parámetro. INT4 significa $0.5P$ en lugar de $2P$ — una aceleración de $4\times$ en el régimen limitado por memoria.
  • Batching continuo (artículo 4): aumentar la intensidad aritmética procesando muchas solicitudes por carga de pesos, pasando del régimen limitado por memoria al limitado por cómputo.
  • Decodificación especulativa (artículo 5): romper la restricción de un-token-a-la-vez adivinando por adelantado y verificando en paralelo.
  • Atención eficiente (artículo 6): reducir el costo de memoria y cómputo del mecanismo de atención en sí, que crece con la longitud de la secuencia y se convierte en un cuello de botella secundario para contextos largos.
💡 En la práctica, los sistemas de servicio en producción combinan casi todas estas técnicas simultáneamente. Una configuración típica podría usar un modelo cuantizado a INT4 con KV caching, batching continuo de más de 64 solicitudes concurrentes, paralelismo tensorial en 2-4 GPUs, y FlashAttention para los kernels de atención. Cada técnica se combina con las demás.

Con el cuello de botella ahora claramente identificado — el ancho de banda de memoria durante la decodificación secuencial — estamos listos para abordar la primera y más fundamental optimización: reutilizar el cómputo entre pasos de decodificación con la KV cache, que es el tema del siguiente artículo.

Quiz

Pon a prueba tu comprensión del cuello de botella de inferencia y por qué la decodificación es fundamentalmente diferente del entrenamiento.

Durante la decodificación autorregresiva con batch_size=1, ¿cuál es el cuello de botella principal?

¿Por qué la fase de prefill está limitada por el cómputo mientras que la fase de decodificación está limitada por la memoria?

Un modelo de 13B parámetros en FP16 se sirve en una GPU con 3 TB/s de ancho de banda de memoria. ¿Cuál es el rendimiento máximo teórico aproximado de decodificación con batch_size=1?

¿Por qué no podemos paralelizar la generación de tokens para una sola solicitud de la misma forma en que el entrenamiento paraleliza a través de la secuencia?