Saltar al contenido
AI•4 min de lectura

Mide tu vLLM: TTFT, tokens/s y concurrencia sin engañarte

Un servidor que "funciona" no dice nada. La pregunta de un engineer es cuánto: cuánto tarda el primer token, cuántos tokens por segundo, cuántas personas a la vez. Este capítulo te deja midiendo tu vLLM con los mismos scripts que usé en mi PoC, y con la lección que aprendí equivocándome.

Los 4 números que importan

MétricaQué mideQué significa para el usuario
TTFTTiempo hasta el primer tokenQué tan "instantánea" se siente la respuesta
Decode tok/sVelocidad de generaciónFluidez; arriba de ~30 tok/s se lee cómodo
Prefill tok/sVelocidad procesando el promptCuánto cuesta mandar contexto enorme
Throughput agregadoTokens/segundo de TODOS los usuariosLa capacidad real del servidor

Y una regla de lectura: mira p50 y p90, no promedios. El promedio esconde al usuario que espera el doble.

1 usuario: benchmark_openai.py

Este script mide un request de principio a fin contra cualquier endpoint OpenAI-compatible:

# terminal del pod
python benchmark_openai.py \
  --base-url http://localhost:8000/v1 \
  --model qwen3-coder-30b \
  --prompt "$(cat prompt_largo.txt)" \
  --api-key $VLLM_API_KEY \
  --out results_1user.csv

El CSV que sale tiene una fila por request con estas columnas:

ttft_ms,total_s,tok_s,prompt_tokens,output_tokens,ok,error
2541.2,3.31,120.3,15269,93,True,

Esa fila es real, de mi PoC: 15.269 tokens de prompt procesados y 93 de respuesta en 3,31 segundos, con un TTFT de 2.541 ms. Déjala con ese TTFT tan feo un momento, porque es la trampa número uno de este capítulo y la destapamos en la sección del gateway.

Nota que el prompt es largo (15K tokens). Si mides con "hola", todo es prefill instantáneo y los números son ruido. Usa un prompt del tamaño de tu workload real.

Muchos usuarios: load_test.py

Un benchmark de 1 usuario no responde la pregunta del negocio: ¿cuántos aguantan a la vez?

# terminal del pod
python load_test.py \
  --base-url http://localhost:8000/v1 \
  --model qwen3-coder-30b \
  --concurrency 10 \
  --context-tokens 16000 \
  --max-tokens 300 \
  --api-key $VLLM_API_KEY \
  --session-mode unique

Mis resultados reales en la L40S (a través del gateway), para que tengas un piso de referencia:

EscenarioTiempo totalTTFT p50tok/s por usuarioAgregado
1 usuario, 16K3,3 s2.541 ms120,3120 tok/s
5 usuarios, 16K9,1 s5.435 ms27,3166 tok/s
10 usuarios, 16K (sesiones distintas)16,1 s9.652 ms15,9199 tok/s
10 usuarios, 16K (prefijo compartido)5,2 s3.773 ms73,6740 tok/s

La lectura: el throughput agregado sube con la concurrencia (199 tok/s), pero el tok/s individual baja (15,9 por usuario). La GPU no hace magia, reparte. Si tu SLO es "40 tok/s por persona en hora pico", esa tabla te dice cuántos caben.

El bug que infla tus números

La fila con 740 tok/s es trampa y avance de trampa a la vez. Mi primer load_test.py mandaba el mismo prompt a todos los usuarios. Con prefix caching activo, vLLM procesó ese contexto UNA vez y las 10 sesiones lo reutilizaron: estaba midiendo la memoria de una sola sesión multiplicada por magia.

El fix fue --session-mode unique (un marcador distinto por sesión), y de ahí salió la fila honesta de 16,1 s. El post-mortem completo está en El error de mi propio benchmark.

El matiz que lo hace interesante: en un equipo real los agentes SÍ comparten prefijo (el mismo system prompt gigante). Por eso el escenario "prefijo compartido" no es trampa si tu workload es así; es trampa cuando mides para decidir capacidad y tu workload real no comparte nada.

La trampa de la red

Ese TTFT de 2.541 ms con 1 usuario era absurdo para una L40S. Al medir por tramo encontré esto:

TramoTiempo
El modelo responde56 ms
El gateway añade15 ms
La red pública hasta el pod~1.000 ms

El 94% de la espera era internet, no el sistema. Si mides contra la URL pública del pod, estás midiendo a RunPod, no a vLLM. Para comparar hardware, mide por túnel SSH o dentro del pod. El caso completo, con la metodología, está en El cuello de botella no era la GPU: era la red.

Checkpoint y el error típico

Checkpoint: un CSV tuyo de 1 usuario y uno de 10, con tu prompt real. Ahora puedes responder la pregunta que abre este capítulo con números propios.

El error típico: benchmark con prompt de 10 tokens y un solo run. Prompt corto = mides el arranque de Python, no la GPU. Un solo run = mides la suerte. Tres corridas mínimo, prompt del tamaño real, y mira p90.

Para la teoría de por qué percentiles y no promedios, tengo Medir inferencia sin engañarte. Y vLLM expone todo esto también en /metrics formato Prometheus: curl localhost:8000/metrics | grep -i ttft.

Siguiente capítulo

Tu servidor sirve. Ahora lo convertimos en un servicio de equipo: llaves por persona, presupuestos, logs por request y la conversación de costos que hay que tener antes de migrar a producción.

¿Qué números te dieron tus 10 usuarios concurrentes? Ese es el dato que decide todo lo que viene. Déjalo en los comentarios.


> Más posts