Un modelo, dos comportamientos
Mide la latencia de un LLM en serio y verás dos números que no encajan.
El primero es cuánto tarda en empezar a responder. El segundo es cuánto tarda entre cada palabra una vez que arranca. Con un prompt largo, el modelo digiere cientos de tokens casi de golpe. Después se arrastra generando de uno en uno.
No es un fallo de tu servidor. Es que la inferencia tiene dos fases con física distinta: prefill y decode. Confundirlas es la razón por la que muchos equipos optimizan la métrica equivocada.
Qué hace cada fase
En prefill, el modelo procesa todo tu prompt de una vez. Lee los tokens de entrada en paralelo, calcula sus estados internos y rellena la caché de claves y valores (la KV cache). Al final de esta fase produce el primer token de la respuesta.
En decode, el modelo genera la respuesta token a token. Cada nuevo token depende del anterior, así que no hay paralelismo posible dentro de una misma secuencia: token 1, luego token 2, luego token 3. En cada paso reutiliza la KV cache que construyó en prefill y le añade una fila.
La distinción práctica es sencilla. Prefill escala con el tamaño de tu prompt. Decode escala con el tamaño de tu respuesta. Y cada una golpea un cuello de botella diferente del hardware.
Por qué prefill vuela y decode se arrastra
La clave es la intensidad aritmética: cuántas operaciones haces por cada byte que mueves desde la memoria de la GPU.
Prefill procesa muchos tokens a la vez, así que carga los pesos del modelo una vez y los aprovecha para toda la tanda. Hace mucho cálculo por cada lectura de memoria. Está limitado por la potencia de cómputo (compute-bound): la GPU va a tope de FLOPs y es feliz.
Decode genera un solo token por paso. Para producirlo tiene que volver a leer los pesos del modelo enteros desde memoria… para hacer una cantidad ridícula de cálculo con ellos. Está limitado por el ancho de banda de memoria (memory-bound): la GPU pasa la mayor parte del tiempo esperando datos, no calculando.
Por eso un modelo grande puede tragar un prompt de 2.000 tokens en una fracción de segundo y luego tardar lo mismo en generar 100 tokens de respuesta. No procesa 20 veces más rápido el prompt porque sea mágico: lo hace en paralelo. La respuesta, por definición, es secuencial.
| Prefill | Decode | |
|---|---|---|
| Qué procesa | Todo el prompt de golpe | Un token por paso |
| Paralelismo | Alto (todos los tokens a la vez) | Ninguno dentro de la secuencia |
| Cuello de botella | Cómputo (FLOPs) | Ancho de banda de memoria |
| Escala con | Longitud del prompt | Longitud de la respuesta |
| Métrica que domina | TTFT | TPOT |
| Palanca principal | Contexto más corto, prompt caching | Batching, cuantización, decodificación especulativa |
Las dos métricas que de verdad importan
Si mides la inferencia con un solo número, te estás engañando. Hay dos, y corresponden a las dos fases.
TTFT (time to first token) es cuánto tarda en aparecer la primera palabra. Lo dominan el prefill y el tamaño de tu prompt. Es lo que hace que una interfaz de chat se sienta viva o muerta.
TPOT (time per output token) es el tiempo entre tokens una vez que ha arrancado. Lo domina el decode. Determina la velocidad de “tecleo” que percibe la persona y el coste de respuestas largas.
Medirlas por separado es trivial con cualquier cliente que soporte streaming:
import time
from openai import OpenAI
client = OpenAI()
start = time.perf_counter()
first_token_at = None
tokens = 0
stream = client.chat.completions.create(
model="tu-modelo",
messages=[{"role": "user", "content": prompt_largo}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if not delta:
continue
if first_token_at is None:
# Fin del prefill: acaba de llegar el primer token
first_token_at = time.perf_counter()
tokens += 1
end = time.perf_counter()
ttft = first_token_at - start # coste del prefill
tpot = (end - first_token_at) / max(tokens - 1, 1) # coste medio del decode
print(f"TTFT: {ttft*1000:.0f} ms | TPOT: {tpot*1000:.1f} ms/token")
Si tu TTFT es alto, el problema está en el prompt: demasiado largo, sin cachear, o un modelo demasiado grande para el trabajo. Si tu TPOT es alto, el problema está en el decode: pocas peticiones por lote, sin cuantizar, o generando más de lo necesario.
Qué palanca mueves según la fase
Aquí es donde la distinción deja de ser teórica y te ahorra dinero.
Para bajar el TTFT actúas sobre el prefill. Acorta el contexto: menos instrucciones repetidas, RAG que recupere lo justo y no medio corpus. Usa prompt caching para que la parte fija del prompt (system, ejemplos, documentos estables) no se recompute en cada petición. Es la optimización con mejor relación esfuerzo/resultado cuando reutilizas el mismo preámbulo miles de veces.
Para bajar el TPOT actúas sobre el decode. El batching continuo agrupa varias peticiones para que esa lectura carísima de los pesos se reparta entre muchos usuarios a la vez; es lo que hace rentable servir un LLM. La cuantización reduce cuántos bytes hay que mover por token, que es exactamente el cuello de botella del decode. Y la decodificación especulativa genera varios tokens por paso con un modelo pequeño y los verifica con el grande, atacando de frente el carácter secuencial de esta fase.
Un patrón revelador: mucha gente intenta arreglar respuestas lentas comprando GPUs con más FLOPs. Si el problema es el decode, malgastas el dinero. El decode no necesita más cómputo; necesita más ancho de banda o mover menos bytes. Diagnostica la fase antes de firmar la factura.
En resumen
Un LLM no tiene una velocidad, tiene dos. El prefill es paralelo, limitado por cómputo y se mide con el TTFT. El decode es secuencial, limitado por memoria y se mide con el TPOT.
Antes de optimizar nada, separa esos dos números. Un TTFT alto y un TPOT alto son problemas distintos con soluciones distintas, y tratarlos como uno solo es la forma más rápida de gastar tiempo y hardware en la mitad equivocada del problema.
La próxima vez que alguien te diga que “el modelo va lento”, la primera pregunta correcta es: ¿lento en arrancar o lento en escribir?