Dimensionar para agentes: 275 tokens de entrada por cada uno de salida, y por qué el cuello de botella sigue siendo el decode

Séptimo artículo del track operativo de la capa de control. El cuarto trató la convivencia entre tráfico humano y tráfico agéntico sobre una flota fija. Este trata la pregunta anterior: de qué tamaño tiene que ser la flota, y de qué tamaño el gateway que hay delante. Verificado contra LiteLLM 1.102.0 (commit del 11 de septiembre de 2026) y vLLM 0.29.0, con datos de carga publicados entre mayo y septiembre de 2026.

TL;DR

Hasta este año, dimensionar para agentes era extrapolar el perfil de un chat y añadir margen. En 2026 se publicaron las primeras caracterizaciones a escala de producción y el perfil resultó ser otro. Estas son las siete conclusiones.

El prompt mediano de una llamada agéntica es de 68.000 tokens, de los cuales 63.000 llegan cacheados. La medición cubre 13,5 millones de sesiones, 3,2 millones de usuarios, 760,5 millones de llamadas al modelo y 44,9 billones de tokens de prompt en una semana de junio de 2026. El ratio mediano de entrada a salida supera 275 a 1. Cualquier cálculo hecho sobre un perfil de 800 tokens de prompt y 250 de salida describe otro sistema.

La carga es dominante en prefill contando tokens y dominante en decode contando tiempo. Los tokens realmente nuevos que hay que prefillear en cada turno son solo entre 1,5 y 7,3 veces la salida, no 275. Con el prefix cache funcionando, el reparto del tiempo de ejecución va del 91 % al 98,6 % en decode. Se dimensiona por ancho de banda de memoria y por capacidad de KV cache, no por FLOPs de prefill.

El riesgo de dimensionado no es la media, es el colapso del prefix cache. El acierto mediano dentro de un turno es del 98 %, cae al 55 % en la frontera entre turnos y al 8 % cuando el cliente cambia de modelo. Con contexto recuperado dinámicamente, el reuso de prefijo baja al 1 %. Un fallo convierte 142.000 tokens de prefijo en prefill real. La afinidad de sesión por réplica deja de ser una optimización y pasa a ser un requisito de capacidad.

La concurrencia intra-turno medida en producción es de 1,15 en la mediana y 1,4 en el percentil 90. El 93 % de los lotes de herramientas contienen una sola invocación. La imagen del agente que abre veinte llamadas en paralelo no aparece en los datos. El multiplicador real viene del ciclo de trabajo y de que el 87 % de las llamadas las inicia el agente, no la persona.

El gateway se dimensiona por worker, y cada worker es un universo. Un vCPU y 4 GiB por worker, con requests iguales a limits. Los contadores de límites, las cachés y los cooldowns viven en memoria del worker salvo que haya Redis, así que un límite de 100 peticiones por minuto con cuatro workers y tres réplicas permite 1.200. El pool de Prisma también es por proceso: el número de conexiones abiertas contra Postgres es workers por connection_limit.

El coste de CPU que crece con la carga agéntica es el contador de tokens. Serializa el catálogo de herramientas entero en cada petición y lo tokeniza. El propio proyecto midió 46, 53 y 100 ms para prompts de 50K, 75K y 100K tokens, y la operación pasa por un limitador de concurrencia de cuatro por worker. Con agentes que mandan catálogos largos y contextos de seis cifras, ese límite es el techo antes que la CPU.

El autoescalado por memoria no funciona en esta pieza y la sonda de arranque tarda cinco minutos. El chart oficial deja deliberadamente sin objetivo la memoria porque el motor de consultas de Prisma nunca devuelve el máximo alcanzado. Queda la CPU al 60 %, con un startupProbe de 300 segundos que hace llegar tarde a cualquier réplica nueva. Para agentes hay dos métricas mejores, y están en /metrics.

Estás aquí: la aritmética que va antes de comprar

El capacity planning clásico parte de un SLO de chat y cruza dos presupuestos, el de VRAM y el de tiempo. Ese método sigue siendo correcto. Lo que cambia con agentes son las entradas: la longitud del prompt, el número de llamadas por unidad de trabajo humano, el reparto entre prefill y decode, y la definición misma de SLO. Este artículo sustituye esas entradas por cifras medidas y arrastra el resultado hasta dos números: réplicas de gateway y GPUs.

Queda fuera el reparto del tráfico una vez montado, que ya cubrieron la convivencia de humanos y agentes y el enrutado por prefijo.

La analogía: el taller de reparación y el mostrador

Un taller de reparación de electrodomésticos dimensiona su plantilla contando averías, no llamadas. La diferencia importa porque la persona que trae la lavadora ocupa el mostrador tres minutos, mientras que el técnico que la desmonta vuelve a consultar la ficha de la máquina catorce veces a lo largo de la tarde. Si el jefe de taller dimensiona por visitas al mostrador, se queda corto en técnicos y le sobra personal de atención.

La plataforma de inferencia tiene la misma asimetría. La persona manda un turno. El agente manda quince llamadas por ese turno, y cada una arrastra el expediente completo de lo que lleva hecho. El mostrador es el gateway y los técnicos son las GPUs, y el error habitual consiste en dimensionar los dos con el mismo contador.

Hay un segundo detalle que la analogía recoge bien. El técnico no lee la ficha entera cada vez: la tiene abierta sobre el banco. Eso es el prefix cache. El día que alguien le cambia de banco entre consultas, vuelve a buscarla desde el archivo, y el trabajo se multiplica sin que haya aumentado el número de averías.

Parte 1. El perfil real de la carga

Durante dos años, cualquier cálculo de capacidad para agentes se hizo sobre supuestos. En 2026 aparecieron tres trabajos que miden el asunto, y conviene leerlos juntos porque miden andamiajes distintos y sus cifras no son promediables.

Las tres fuentes

La caracterización a escala la publicó un equipo de investigación sobre telemetría de GitHub Copilot en producción (arXiv 2608.00101, 30 de julio de 2026): 13,5 millones de sesiones, 3,2 millones de usuarios, 95,1 millones de turnos de usuario, 760,5 millones de llamadas al modelo y 44,9 billones de tokens de prompt, en una semana de junio de 2026. Es la muestra más grande publicada hasta la fecha.

La caracterización por bancos de pruebas la firma un equipo de arquitectura (arXiv 2605.26297, 25 de mayo de 2026), que instrumenta cinco de ellos (ADE-Bench, DABStep, GAIA, SWE-bench Pro y Terminal-Bench 2.0) y mide el reparto interno de tiempo.

La medición de producción sobre un andamiaje distinto la publicó el propio proyecto vLLM junto a AgentX (blog de vLLM, 8 de septiembre de 2026).

Las tres discrepan en turnos por sesión: 15 llamadas en la mediana de Copilot, 43 turnos en AgentX, 62,4 en SWE-bench Pro con Qwen. No es contradicción, son andamiajes con bucles distintos. La lección de dimensionado es que el número de llamadas por unidad de trabajo humano depende del cliente que se conecte, y por eso hay que medirlo en casa antes de fijar el tamaño.

Las cifras que sí se pueden usar

MagnitudValor medidoFuente
Prompt mediano por llamada68K tokens, de ellos 63K cacheados2608.00101
Ratio entrada:salida (mediana)por encima de 275:12608.00101
Sesión mediana3 turnos de usuario, 15 llamadas, 4,2 min2608.00101
Sesión media6,1 turnos, 40,6 llamadas, 62,6 min2608.00101
Llamadas por turno (mediana)4,52608.00101
Llamadas iniciadas por el agente87 %2608.00101
Rango entre perfiles de usuario50× (23K a 1,1M tokens por turno)2608.00101
Contexto de entrada mediano142K tokens; salida 444 tokens por turnoAgentX
Contexto acumulado en SWE-bench Promedia 68,7 a 80,1K, máximo 146 a 166K2605.26297
Tokens de razonamiento sobre la salida29,0 % a 67,6 %2605.26297

Dos observaciones operativas que salen de ahí y que no aparecen en ninguna guía de dimensionado.

La primera: los agentes que fallan acumulan hasta 1,8 veces más contexto medio que los que terminan bien (2605.26297). El coste no correlaciona con el éxito, correlaciona con la dificultad y con el atasco. Un dimensionado hecho sobre la tarea que sale bien se queda corto.

La segunda: el rango entre perfiles de usuario es de 50 a 1. Un usuario que solo conversa gasta 23.000 tokens por turno; uno que deja al agente en bucle largo gasta 1,1 millones. Segmentar por perfil de uso rinde más que cualquier enrutado por dificultad, porque la varianza está en las personas, no en las peticiones.

Lo que hay que medir antes de calcular

Tres cifras propias, tomadas de una semana de tráfico real, valen más que cualquier tabla de este artículo:

  1. Llamadas al modelo por hora de trabajo humano, por perfil de usuario.
  2. Distribución de longitud de prompt (mediana y percentil 95), no la media.
  3. Tasa de acierto de prefix cache agregada, con la serie temporal, no el número.

Las tres se sacan de lo que el gateway ya guarda. En LiteLLM_SpendLogs están los tokens por petición y el identificador de clave y de equipo; en proxy_server_request está el bloque tools de cada llamada; y en /metrics de vLLM están vllm:prefix_cache_queries y vllm:prefix_cache_hits.

Parte 2. Prefill, decode y la trampa del cache

Aquí está el hallazgo que corrige la intuición de casi todo el mundo, incluida la mía cuando empecé este artículo.

Contando tokens, la carga agéntica es abrumadoramente de entrada: 275 a 1. La conclusión aparentemente obvia sería dimensionar por capacidad de prefill, es decir por FLOPs. Es falsa.

Lo que hay que prefillear en cada turno no es el prompt entero, sino lo que se añadió desde el turno anterior. Ese añadido, medido, está entre 1,5 y 7,3 veces la longitud de la salida (2605.26297). El resto lo sirve el prefix cache. Y el reparto real del tiempo de ejecución del modelo queda así: entre el 91,0 % y el 98,6 % en decode, entre el 1,4 % y el 9,0 % en prefill (2605.26297).

La tasa de acierto que sostiene ese reparto es alta y está medida en tres sitios: teórica del 87,9 % al 99,3 % y empírica del 84,6 % al 99,5 % (2605.26297); por encima del 96 % en producción (AgentX); y una mediana del 98 % dentro de un turno en Copilot (2608.00101).

Dónde se cae

El mismo trabajo de Copilot mide la degradación, y es el dato más accionable del artículo:

  • 98 % de acierto mediano dentro de un turno. El detalle importa: la primera llamada de un turno acierta en torno al 45 %, la segunda sube al 86 % y a partir de la tercera se estabiliza entre el 92 % y el 94 %.
  • 55 % en la frontera entre turnos, con huecos de inactividad de 4,1 minutos de mediana.
  • 8 % cuando el cliente cambia de modelo.

Y hay un cuarto régimen peor: con contexto recuperado dinámicamente, es decir cuando el andamiaje inyecta resultados de búsqueda o de RAG que cambian en cada iteración, el reuso de prefijo cae al 1 % o menos (arXiv 2608.15127, 15 de agosto de 2026).

Un fallo de cache con un contexto de 142.000 tokens convierte un prefill de unos pocos miles de tokens en uno de seis cifras. El trabajo se multiplica por un factor de entre 20 y 60 sin que haya cambiado el número de peticiones. Esa es la diferencia entre una flota que cumple y una que se atasca a media tarde.

De ahí salen dos reglas de dimensionado:

  1. Sin afinidad de sesión hacia la misma réplica, el dimensionado calculado no vale. El mecanismo concreto de LiteLLM es session_affinity, que no está activo por defecto y que ya se trató en el artículo del enrutado por prefijo. Sin él, el cálculo hay que rehacerlo suponiendo régimen de prefill, que es entre 20 y 60 veces más caro.
  2. Cambiar de modelo a mitad de sesión cuesta el cache entero. Cualquier política de enrutado por dificultad que mueva una conversación viva de un grupo de modelos a otro paga un 8 % de acierto en la siguiente llamada. Si se quiere enrutar por dificultad, que sea al principio de la sesión.

El co-batching, que es el otro efecto que nadie mide

Mezclar en el mismo lote una petición corta con peticiones de contexto largo degrada la latencia entre tokens de la corta. La cifra medida: una petición larga en el lote sube el tiempo por token un 38,3 %; dos lo suben un 79,7 % (2608.15127). Es la razón técnica de por qué separar pools de humanos y de agentes mejora la experiencia humana más de lo que sugiere el reparto de capacidad, y es también la razón de no subir --max-num-seqs por encima de lo que permite el presupuesto de KV.

Parte 3. El presupuesto de KV, con los números del caso agéntico

La fórmula no cambia respecto al capacity planning clásico, pero las entradas sí, y el resultado sorprende:

$$\text{KV}_{\text{bytes/token}} = 2 \times n_{\text{capas}} \times n_{\text{kv heads}} \times d_{\text{head}} \times \text{bytes}_{\text{elem}}$$

Para una geometría de clase 70B (80 capas, 8 cabezas KV con atención agrupada, dimensión de cabeza 128, elementos de 2 bytes) salen 0,3125 MiB por token. Con el contexto mediano agéntico de 142.000 tokens:

$$142.000 \times 0{,}3125\ \text{MiB} \approx 43{,}3\ \text{GiB por secuencia}$$

Para una geometría de clase 30B (48 capas, mismo resto) salen 0,1875 MiB por token, es decir unos 26,0 GiB por secuencia.

Conviene detenerse en lo que significa. Una H100 de 80 GB con los pesos de un modelo de 70B en FP8 ocupados no tiene 43 GiB libres para una sola secuencia. La conclusión es dura y hay que escribirla: servir contexto agéntico real de clase 70B exige paralelismo tensorial de 2 como mínimo y KV cache cuantizado. Y el número de secuencias simultáneas que caben, que es lo que fija --max-num-seqs, sale de dividir el KV disponible entre esa cifra, no de una heurística de chat.

$$\text{secuencias} \approx \frac{\text{VRAM}_{\text{KV disponible}}}{\text{KV}_{\text{bytes/token}} \times \text{contexto máximo}}$$

Con FP8 en el KV, el divisor se reduce a la mitad. Es la palanca de capacidad más barata que existe en este perfil de carga, y su coste en calidad se valida con evals, no se supone.

Parte 4. De personas a peticiones concurrentes

Aquí es donde la mayoría de los cálculos se van de largo, porque se asume que un agente equivale a muchas peticiones simultáneas. Los datos dicen otra cosa.

La concurrencia de llamadas dentro de un turno, medida en producción, es de 1,15 en la mediana y 1,4 en el percentil 90, y el 93 % de los lotes de herramientas contienen una sola invocación (2608.00101). El paralelismo intra-turno es pequeño. Los subagentes aparecen en el 44 % de las sesiones de AgentX, con una mediana de 4 despliegues entre las que los usan, así que hay cola larga, pero no es el régimen habitual.

Lo que sí multiplica es el ciclo de trabajo. La sesión media dura 62,6 minutos y contiene 40,6 llamadas. A 444 tokens de salida y una interactividad de unos 60 tokens por segundo, cada llamada ocupa unos 7,4 segundos de decode, lo que da entre 5 y 7 minutos activos de esos 62,6.

$$C = D \times \rho \times A$$

donde $D$ son las personas trabajando, $\rho$ el ciclo de trabajo y $A$ la amplificación. Con $\rho \approx 0{,}10$ a $0{,}12$ derivado de las cifras anteriores, veinte desarrolladores dan entre 2 y 2,4 secuencias concurrentes en estado estacionario. El pico, no la media, es lo que hay que dimensionar.

La amplificación

Tres factores publicados por separado:

  • Fallos de herramienta: el 9 % de los turnos los dispara, con una amplificación de cómputo de 4 veces (2608.00101).
  • Subagentes: el 44 % de las sesiones, mediana de 4 despliegues (AgentX).
  • Varianza interna: la misma tarea del mismo agente varía hasta 30 veces en coste dentro de una misma traza (2608.15127).

El producto de los dos primeros da $(1 + 0{,}09 \times 3) \times (1 + 0{,}44 \times 3) \approx 2{,}9$. Nadie publica ese producto como cifra única, así que va marcado como derivación propia, pero un factor de amplificación de 3 es la provisión de diseño razonable y encaja con la varianza medida.

Para los veinte desarrolladores del ejemplo: entre 6 y 7 secuencias concurrentes de diseño, con contextos de 142K. Con 43,3 GiB de KV por secuencia en clase 70B, eso son unos 300 GiB de KV cache, es decir cuatro H100 dedicadas solo a KV con los pesos aparte. Con KV en FP8, la mitad. Con un modelo de clase 30B, un tercio.

Ese cálculo, hecho con el perfil de chat de 800 tokens de prompt, habría dado una sola GPU. Esa es la distancia entre los dos regímenes.

Parte 5. El gateway, que también se dimensiona

El proxy no consume GPU, pero tiene su propia aritmética y es la parte que más sorpresas da en producción, porque casi todo su estado es por proceso.

Un worker es un universo

El valor por defecto de --num_workers es 1 (litellm/constants.py:57). El arranque habitual es uvicorn directo con uvloop (proxy_cli.py:1477), y existen alternativas con gunicorn, hypercorn y granian.

Lo que vive en memoria de cada worker, y por tanto se multiplica:

  • La caché en memoria, limitada a 200 elementos con TTL de 600 segundos (litellm/caching/in_memory_cache.py:27). Con muchos deployments, los cooldowns y los contadores compiten por ese mismo espacio y se desalojan antes de caducar.
  • La caché de autenticación de claves, con TTL de 60 segundos (proxy_server.py:1650), ajustable con general_settings.user_api_key_cache_ttl.
  • Los contadores de límites y presupuestos cuando no hay Redis (parallel_request_limiter_v3.py, rutas local_only=True).
  • Los cooldowns del router, con DEFAULT_COOLDOWN_TIME_SECONDS = 5 (constants.py:75).
  • Un planificador propio de tareas periódicas por worker (proxy_server.py:9674), coordinado entre pods con un lock en Redis.

La consecuencia práctica: un límite de 100 peticiones por minuto con cuatro workers y tres réplicas permite hasta 1.200 sin Redis. Con Redis, el limitador v3 registra siete scripts Lua al arrancar (parallel_request_limiter_v3.py:608) y los límites pasan a ser globales.

Un detalle del limitador v3 que importa con carga agéntica: LITELLM_TPM_TOKEN_RESERVATION_ENABLED viene activado por defecto y reserva tokens estimados antes de la llamada, lo que añade una ida y vuelta a Redis por petición. Y hay un asyncio.Lock por instancia que serializa las comprobaciones atómicas de lote (:665), documentado por el propio código como riesgo de percentil 99 bajo contención.

Si Redis cae, no fallan peticiones: hay un cortacircuitos con umbral de 5 fallos y recuperación de 60 segundos (constants.py:456), y el proxy degrada a memoria local. Lo que se pierde en silencio es la globalidad de los límites.

Postgres

El pool también es por proceso. Los valores por defecto son database_connection_pool_limit = 10 y database_connection_pool_timeout = 60 (proxy_cli.py:62), inyectados como parámetros de la URL de Prisma. Un pod con cuatro workers abre cuarenta conexiones. Tres pods, ciento veinte. Es la vía rápida a agotar max_connections de Postgres sin haber servido tráfico, y la razón por la que el chart oficial ofrece PgBouncer en el propio pod con maxDbConnections: 20 y maxClientConn: 1000.

En el camino de la petición solo están la autenticación, que va cacheada 60 segundos, y el rate limiting. El gasto se difiere. La cola de SpendLogs está acotada a 64 MB y al desbordar descarta los elementos más antiguos con un error en el log (utils.py:6704). El intervalo de escrituras es proxy_batch_write_at, con valor por defecto de 10 segundos más una dispersión aleatoria de hasta 5 (constants.py:1719, proxy_server.py:9704); la guía de producción recomienda subirlo a 60.

Con Postgres lento, la cola crece hasta esos 64 MB y luego pierde datos de facturación sin devolver ningún error al cliente. Es el modo de fallo silencioso que hay que alertar.

El contador de tokens, que es la trampa agéntica

Esta es la pieza que convierte un dimensionado correcto en uno insuficiente, y no aparece en ninguna guía.

Antes de admitir una petición, el proxy cuenta tokens. Con carga de chat el coste es despreciable. Con carga agéntica no lo es, por dos motivos que se suman.

El primero es la longitud del prompt. El propio proyecto midió el coste del conteo de admisión en 46, 53 y 100 ms para prompts de 50K, 75K y 100K tokens (benchmarks, consultado el 12 de septiembre de 2026), y publicó que con un contador escrito en Rust esas cifras bajan a 4,9, 6,8 y 10,2 ms.

El segundo es el catálogo de herramientas. La función que prepara las definiciones serializa el catálogo entero a un pseudo-TypeScript y lo tokeniza (litellm_core_utils/token_counter.py:934), más nueve tokens fijos. El coste es lineal en el tamaño del catálogo y se paga en cada petición. Ese es exactamente el patrón de un agente conectado a un gateway MCP: catálogo largo, reenviado en cada turno. La factura en tokens de ese catálogo ya se trató en el artículo del gateway MCP; aquí aparece su otra cara, que es CPU del proxy.

Y encima hay un techo explícito. El conteo se ejecuta en hilo con un limitador de capacidad cuyo valor por defecto es 4 por worker (token_counter.py:340, constants.py:407). Con prompts de 100K, cuatro conteos concurrentes de 100 ms significan un techo teórico de unas 40 admisiones por segundo por worker antes de que el limitador empiece a encolar, y eso sin haber llamado todavía al modelo.

Lo que se cachea es el tokenizador, con un LRU de 64 entradas (utils.py:2206), no el resultado del conteo.

Tres palancas, en orden de retorno: subir TOKEN_COUNTER_MAX_CONCURRENT_COUNTS si sobra CPU, reducir el catálogo de herramientas expuesto por clave, y vigilar la cifra de overhead del gateway en /metrics para saber si esto está pasando.

Streaming y admisión

No hay límite de conexiones por defecto. --limit_concurrency existe pero es opcional, y devuelve 503. El control de admisión propio se activa solo si se define general_settings.max_in_flight_requests_per_worker (middleware/admission_control_middleware.py:285), con max_queued_requests_per_worker y admission_queue_timeout_seconds en 1,0 por defecto, rechazando con 503 y cabecera retry-after: 1.

Para agentes, esa cabecera importa: un cliente agéntico que reintenta sin respetarla multiplica la carga justo cuando el gateway está diciendo que no puede más. La combinación de reintentos del gateway ya la calculó el artículo cuarto y llegaba a 45 llamadas por turno en el peor caso.

El timeout de petición por defecto es de 6.000 segundos (constants.py:512). La guía de producción recomienda 600. Con tareas agénticas largas, dejarlo a 6.000 significa que una conexión colgada ocupa un hueco durante hora y media.

Recursos y autoescalado

El chart oficial recomienda 1 vCPU y 4 GiB por worker, con requests iguales a limits (helm/litellm-helm/values.yaml:235), y la documentación de producción sugiere un worker por pod escalando horizontalmente. El benchmark de alto rendimiento del propio proyecto, en cambio, usa cuatro workers por pod con PgBouncer delante.

El HPA viene con targetCPUUtilizationPercentage: 60. Y hay un comentario en el chart que merece leerse dos veces: la memoria se deja deliberadamente sin objetivo porque el consumo residente del motor de consultas de Prisma es una marca de máximo que nunca se devuelve, de modo que un HPA por memoria subiría réplicas y no las bajaría nunca (values.yaml:260).

El otro problema es el arranque. El startupProbe tiene failureThreshold: 30 con periodo de 10 segundos, es decir hasta 300 segundos antes de considerar el pod listo (values.yaml:109). Un HPA que reacciona al 60 % de CPU y tarda cinco minutos en aportar capacidad llega tarde a cualquier ráfaga agéntica.

La alternativa, opcional en el chart, es escalar por métricas de Prometheus: targetRequestsPerSecond y targetTokensPerSecond sobre rate(litellm_proxy_total_requests_metric_total[1m]) y rate(litellm_total_tokens_metric_total[1m]). El propio chart avisa de que la métrica de tokens va por detrás con respuestas en streaming largas, porque los tokens se contabilizan al completar. Para agentes, eso es casi siempre.

Parte 6. Qué mirar: las métricas que sirven y las que se rompieron

En el gateway

MétricaPara qué
litellm_in_flight_requestsSaturación real por worker. Es la métrica de escalado
litellm_overhead_latency_metricCoste del proxy por petición; sube con el conteo de tokens
litellm_overhead_with_guardrails_latency_metricLo mismo incluyendo guardrails
litellm_request_queue_time_secondsLlegada hasta pre-call, incluye autenticación y encolado ASGI
litellm_deployment_cooled_downRéplicas fuera del pool
litellm_service_latencyLatencia de Redis y de la base de datos, por servicio

El endpoint /health/backlog devuelve in_flight_requests, admitted_requests, queued_requests y rejected_requests (health_endpoints/_health_endpoints.py:1871).

Lo que no expone: la profundidad de la cola de SpendLogs ni sus bytes, las conexiones de Prisma en uso frente al límite, ni la profundidad de la cola del worker de logging. Los tres son modos de fallo reales y hay que instrumentarlos por fuera.

En el motor

Nombres verificados en la documentación de métricas de vLLM 0.29.0:

  • vllm:num_requests_running, vllm:num_requests_waiting
  • vllm:time_to_first_token_seconds, vllm:e2e_request_latency_seconds, vllm:request_queue_time_seconds, vllm:inter_token_latency_seconds
  • vllm:prefix_cache_queries, vllm:prefix_cache_hits, vllm:kv_cache_usage_perc

Y las que rompen paneles al actualizar:

  • vllm:time_in_queue_requests queda duplicada por vllm:request_queue_time_seconds.
  • El indicador directo de tasa de acierto de prefix cache desapareció. Ahora se calcula como rate(vllm:prefix_cache_hits) / rate(vllm:prefix_cache_queries).
  • vllm:num_requests_swapped y vllm:cpu_cache_usage_perc quedan obsoletas con el intercambio a CPU.
  • vllm:gpu_cache_usage_perc pasó a vllm:kv_cache_usage_perc, lo que rompe también cualquier HPA que la use.

El SLO que hay que perseguir

Un percentil 95 de tiempo hasta el primer token no describe la experiencia de nadie cuando la unidad de trabajo son 43 turnos. Las tareas de codificación superan con frecuencia los diez minutos y las de investigación llegan a horas (2608.15127).

Dos indicadores sirven mejor:

  1. Percentil 90 de latencia de tarea completa, medida por sesión, no por petición. Sale de agrupar LiteLLM_SpendLogs por identificador de sesión.
  2. Percentil 90 de interactividad, en tokens por segundo por usuario. Es la que usa AgentX, con valores entre 58,3 y 74,2 en sus corridas.

Para autoescalar el motor, el ejemplo oficial de production-stack usa vllm:num_requests_waiting con umbral 5, sondeo cada 15 segundos y enfriamiento de 360. Con carga agéntica conviene añadir vllm:kv_cache_usage_perc como segundo disparador, porque con 26 a 43 GiB de KV por secuencia la presión de memoria llega antes que la cola.

Parte 7. Las palancas del motor, ordenadas por retorno

Verificado contra vLLM 0.29.0, publicado el 9 de septiembre de 2026.

--long-prefill-token-threshold 512. La medición de AgentX le atribuye +93 % en tokens por GPU y segundo, con una mejora de 2,3 veces en el percentil 90, atacando el bloqueo de cabecera de cola que produce un prefill largo delante de peticiones cortas. Es la palanca de mayor retorno documentada para este perfil.

KV cache en FP8. Divide por dos el presupuesto de memoria por secuencia, que es el recurso limitante. Se valida con evals.

--max-num-queued-reqs y --max-num-queued-tokens. Nuevos en 0.29. Son el control de admisión en el motor, y son el mecanismo correcto para proteger el SLO en lugar de dejar que la cola crezca sin límite.

max_num_batched_tokens a la baja. Valores altos mejoran el tiempo hasta el primer token; valores bajos mejoran la latencia entre tokens. En un régimen donde el 91 % del tiempo es decode, la latencia entre tokens es lo que se percibe. El prefill troceado está activado por defecto siempre que es posible en el motor V1.

Afinidad de sesión en el gateway. No es del motor, pero es la que sostiene todo lo anterior. Sin ella, ese 98 % de acierto se convierte en el reparto aleatorio entre réplicas.

Dos avisos de versión: el Model Runner V2 pasa a ser el predeterminado en 0.29 y el V1 queda obsoleto con retirada prevista para la 0.32; y el arranque con python -m vllm.entrypoints.openai.api_server está obsoleto.

El cálculo completo, de principio a fin

Caso: veinte desarrolladores, andamiaje de codificación, modelo de clase 30B en la flota, contexto máximo de 128K, KV en FP8.

1. Concurrencia de estado estacionario. $C = 20 \times 0{,}11 \times 3 \approx 6{,}6$ secuencias.

2. Presupuesto de KV por secuencia. 0,1875 MiB por token en BF16, la mitad en FP8, es decir unos 0,094 MiB. A 128K de contexto: unos 12,0 GiB por secuencia. Para 6,6 secuencias con margen, redondeando a 8: 96 GiB de KV.

3. GPUs. Los pesos de un 30B en FP8 ocupan unos 30 GB. Sobre H100 de 80 GB con gpu_memory_utilization de 0,90 quedan unos 72 GB utilizables por tarjeta. Con paralelismo tensorial de 2, los pesos se reparten: unos 15 GB por GPU, de modo que quedan unos 57 GB de KV por GPU y unos 114 GB en la réplica. Eso cubre los 96 GiB calculados, y el TP=2 además suma ancho de banda de memoria, que es el recurso que domina el decode. Dos H100, una réplica, TP=2. Conviene ver el contraste: en una sola H100 los pesos enteros dejan unos 42 GB de KV, es decir tres secuencias y media, menos de la mitad de lo necesario. Con una segunda réplica para disponibilidad y ventanas de mantenimiento: cuatro GPUs.

4. Peticiones por segundo en el gateway. 20 personas, sesión media de 40,6 llamadas en 62,6 minutos, ciclo de trabajo incluido: del orden de 0,2 a 0,3 peticiones por segundo de media, con picos de 2 a 3.

5. Workers del gateway. El techo no lo pone el throughput sino el conteo de tokens: cuatro conteos concurrentes por worker, a 100 ms con prompts largos. Con picos de 3 peticiones por segundo y prompts de seis cifras, dos réplicas de un worker cada una cubren el caso con margen para el rolling update, a 1 vCPU y 4 GiB cada una. Postgres verá 20 conexiones.

6. Lo que hace falta encender. Afinidad de sesión, control de admisión en el gateway y en el motor, --long-prefill-token-threshold, y Redis para que los límites sean globales.

El mismo cálculo con el perfil de chat habría dado una GPU y un worker. La distancia no está en el margen de seguridad, está en las entradas.

Un config.yaml de referencia

model_list:
  - model_name: agentes-30b
    litellm_params:
      model: hosted_vllm/qwen3-coder-30b
      api_base: http://vllm-agentes.inferencia.svc:8000/v1
      rpm: 600
    model_info:
      input_cost_per_token: 0.00000018
      output_cost_per_token: 0.00000072

router_settings:
  routing_strategy: simple-shuffle
  # sin esto, el cálculo de capacidad de este artículo no vale
  optional_pre_call_checks: ["session_affinity"]
  num_retries: 2
  allowed_fails: 3
  cooldown_time: 30

general_settings:
  # control de admisión: por debajo de esto la cola crece sin límite
  max_in_flight_requests_per_worker: 24
  max_queued_requests_per_worker: 48
  admission_queue_timeout_seconds: 1.0
  # la cola de gasto descarta a los 64 MB; menos intervalo, menos backlog
  proxy_batch_write_at: 60
  enable_drain_endpoint: true
  drain_endpoint_token: os.environ/DRAIN_TOKEN
  database_connection_pool_limit: 10

litellm_settings:
  # 6000 s por defecto: una conexión colgada ocupa hueco hora y media
  request_timeout: 600
  callbacks: ["langfuse_otel", "prometheus"]
  cache: true
  cache_params:
    type: redis
    host: os.environ/REDIS_HOST
    namespace: litellm

Variables de entorno que acompañan:

NUM_WORKERS=1
TOKEN_COUNTER_MAX_CONCURRENT_COUNTS=8   # default 4; subir solo si sobra CPU
LITELLM_TPM_TOKEN_RESERVATION_ENABLED=true
PROXY_CONFIG_RELOAD_INTERVAL_SECONDS=30

Y del lado del motor:

vllm serve Qwen/Qwen3-Coder-30B \
  --tensor-parallel-size 2 \
  --max-model-len 131072 \
  --kv-cache-dtype fp8 \
  --long-prefill-token-threshold 512 \
  --max-num-queued-reqs 64 \
  --gpu-memory-utilization 0.90

Checklist

  1. Medir tres cifras propias durante una semana: llamadas por hora de trabajo humano y por perfil, distribución de longitud de prompt, y serie temporal de acierto de prefix cache.
  2. Calcular el KV por secuencia con la geometría real del modelo, tomada de su config.json, no de una tabla.
  3. Fijar --max-num-seqs desde ese presupuesto, no desde una heurística de chat.
  4. Encender la afinidad de sesión antes de aceptar el número de GPUs calculado.
  5. Encender el control de admisión en las dos capas y comprobar que el cliente respeta retry-after.
  6. Bajar request_timeout de 6.000 a 600.
  7. Subir proxy_batch_write_at a 60 y alertar sobre el crecimiento de la cola de gasto por fuera, porque no hay métrica.
  8. Dejar el HPA por CPU al 60 %, nunca por memoria, y asumir los 300 segundos del startupProbe al planificar el margen.
  9. Vigilar litellm_in_flight_requests y litellm_overhead_latency_metric; si el overhead sube con el tamaño del catálogo de herramientas, el culpable es el contador de tokens.
  10. Repasar los paneles por los renombres de métricas de vLLM antes de actualizar.
  11. Revisar que ninguna política de enrutado mueva una sesión viva entre grupos de modelos.
  12. Rehacer el cálculo cuando entre un perfil de usuario nuevo: el rango entre perfiles es de 50 a 1.

Trampas y cosas que no son lo que parecen

El ratio de 275 a 1 no significa que el sistema sea de prefill. Los tokens nuevos por turno son entre 1,5 y 7,3 veces la salida. El tiempo está entre el 91 % y el 98,6 % en decode.

El acierto de prefix cache agregado oculta el problema. Un 90 % agregado puede ser 98 % intra-turno y 55 % en fronteras. Hay que mirar la serie, no el número, y segmentar por tipo de petición.

La tasa de acierto de vLLM cuenta como acierto el reprefill tras un desalojo. El contador existe internamente y no se expone, así que el número publicado es optimista en régimen de presión de memoria.

Los agentes que fallan cuestan más que los que aciertan, hasta 1,8 veces más contexto. Dimensionar sobre la tarea que sale bien deja corto el cálculo.

El pool de base de datos es por proceso, no por pod. Cuatro workers son cuarenta conexiones.

El HPA por memoria no funciona con esta pieza, y no por un fallo de configuración: el motor de consultas de Prisma no devuelve la memoria.

El startupProbe de 300 segundos convierte cualquier autoescalado en reactivo tardío. Para picos agénticos, mejor sobreprovisionar que confiar en el escalado.

Subir --max-num-seqs para “aprovechar la GPU” degrada la latencia percibida: dos peticiones largas en el lote suben el tiempo por token un 79,7 %.

El contador de tokens tiene un limitador de cuatro por worker y no aparece en ninguna guía de dimensionado. Con catálogos de herramientas grandes y contextos largos es el primer techo que se toca.

Las cifras de este artículo vienen de tres andamiajes distintos y no son promediables. Quince, cuarenta y tres y sesenta y dos turnos por sesión son tres sistemas, no tres medidas del mismo.

Cierre

La aritmética del dimensionado no ha cambiado. Lo que ha cambiado son las entradas, y ahora hay mediciones públicas a escala en lugar de intuiciones. Tres cifras resumen el cambio de régimen: 68.000 tokens de prompt mediano, 87 % de llamadas iniciadas por el agente y no por la persona, y un reparto de tiempo que es decode en más del 90 % mientras el prefix cache acierte.

De ahí sale un orden de trabajo que no es el habitual. Primero, la afinidad de sesión, porque sin ella el resto del cálculo describe otro sistema. Segundo, el presupuesto de KV con la geometría real del modelo, que es lo que fija cuántas secuencias caben. Tercero, el gateway, donde el techo no es el throughput sino un contador de tokens con cuatro hilos por worker.

Y una advertencia sobre las cifras propias. El rango entre perfiles de usuario medido es de 50 a 1. Cualquier plataforma que no segmente su medición por perfil está promediando dos poblaciones distintas y sacando un número que no describe a ninguna de las dos.

Ver también

Fuentes