Phase 17 - Lesson 22

Pruebas de carga en APIs de LLM — Por que mienten k6 y Locust

Los probadores de carga tradicionales no fueron diseñados para respuestas en streaming, longitudes de salida variables, métricas a nivel de token o saturación de GPU. Dos trampas afectan a la mayoría de los equipos. La trampa del GIL: la medición a nivel de token de Locust ejecuta la tokenización bajo el GIL de Python, lo que compite con la generación de solicitudes bajo alta concurrencia; el retraso de la tokenización infla entonces la latencia entre tokens reportada — tu cliente es el cuello de botella, no el servidor. La trampa de la uniformidad de prompts: prompts idénticos en un bucle prueban un solo punto en la distribución de tokens; el tráfico real tiene una longitud variable y diversas coincidencias de prefijo. LLMPerf corrige esto con --mean-input-tokens + --stddev-input-tokens. Mapeo de herramientas en 2026: especializadas en LLM (GenAI-Perf, LLMPerf, LLM-Locust, guidellm) para precisión a nivel de token; k6 v2026.1.0 + k6 Operator 1.0 GA (septiembre de 2025) — conscientes de streaming, distribuido nativo de Kubernetes a través de CRDs TestRun/PrivateLoadZone, ideal para puertas de enlace de CI/CD; Vegeta para saturación a tasa constante en Go; Locust 2.43.3 solo con la extensión LLM-Locust para streaming. Patrones de carga: estado estable (steady-state), rampa (ramp), pico (spike, prueba de autoescalado), remojo (soak, fugas de memoria).

Tipo: Build Lenguajes: Python (stdlib, generador de prompts realistas de juguete + colector de latencia) Prerrequisitos: Phase 17 · 08 (Inference Metrics), Phase 17 · 03 (GPU Autoscaling) Tiempo: ~75 minutos

Objetivos de Aprendizaje

  • Explicar los dos antipatrones (trampa del GIL, trampa de la uniformidad de prompts) que hacen que los probadores de carga genéricos mientan al evaluar APIs de LLM.
  • Elegir una herramienta para un propósito específico: LLMPerf (ejecución de benchmark), k6 + extensión de streaming (puerta de enlace de CI), guidellm (sintético a gran escala), GenAI-Perf (referencia de NVIDIA).
  • Diseñar cuatro patrones de carga (estable, rampa, pico, remojo) y nombrar el modo de fallo que detecta cada uno.
  • Construir una distribución de prompts realista utilizando la media + desviación estándar de tokens de entrada en lugar de una longitud fija.

El Problema

Evaluaste tu endpoint de LLM con k6 con 500 usuarios concurrentes. Aguantó. Lo lanzaste a producción. En producción, con 200 usuarios reales, el servicio colapsó — el TTFT P99 explotó, las GPUs se saturaron.

Ocurrieron dos cosas. Primero, k6 envió 500 prompts idénticos — tu coalescencia de solicitudes (request-coalescing) y almacenamiento en caché de prefijos hicieron que pareciera que estabas manejando 500 decodificaciones concurrentes cuando en realidad estabas manejando una sola. Segundo, k6 no realiza un seguimiento de la latencia entre tokens (inter-token latency) en respuestas de streaming de la forma en que lo experimenta el usuario final; ve una conexión HTTP, no 500 tokens llegando en intervalos variables.

Las pruebas de carga para LLMs son una disciplina propia.

El Concepto

La trampa del GIL (Locust)

Locust utiliza Python y ejecuta la tokenización del lado del cliente bajo el GIL. Bajo alta concurrencia, el tokenizador se encola detrás de la generación de solicitudes. La latencia entre tokens reportada incluye el retraso de la tokenización en el cliente. Piensas que el servidor es lento; pero es el arnés de pruebas.

Solución: la extensión LLM-Locust mueve la tokenización a procesos independientes, o utiliza un arnés en lenguaje compilado (k6, LLMPerf usando tokenizers.rs).

La trampa de la uniformidad de prompts

Todos los probadores de carga conocidos te permiten configurar un solo prompt. En una prueba en bucle de 10,000 iteraciones, se envía exactamente el mismo prompt cada vez. El servidor ve el mismo prefijo en todo momento — la tasa de aciertos de la caché de prefijos se aproxima al 100%, la tasa de procesamiento parece excelente.

Solución: realiza un muestreo a partir de una distribución de prompts. LLMPerf utiliza --mean-input-tokens 500 --stddev-input-tokens 150 — longitudes diversas, contenido diverso.

Cuatro patrones de carga

  1. Steady-state — RPS constante durante 30-60 min. Detecta: regresiones de rendimiento de línea base.
  2. Ramp — incrementa linealmente el RPS de 0 al objetivo durante 15 min. Detecta: punto de ruptura de capacidad, anomalías de calentamiento (warm-up).
  3. Spike — RPS repentino de 3-10 veces durante 2 min y luego regresa. Detecta: latencia de autoescalado, saturación de colas, impacto del inicio en frío (cold-start).
  4. Soak — estado estable durante 4-8 horas. Detecta: fugas de memoria, desviación de pool de conexiones (connection-pool drift), desbordamiento de observabilidad.

Mapeo de herramientas para 2026

LLMPerf (Anyscale) — Python pero con tokenización basada en Rust. Prompts con media/desviación estándar. Consciente de streaming. La mejor opción por defecto para pruebas de rendimiento.

NVIDIA GenAI-Perf — Referencia de NVIDIA. Utiliza el cliente Triton; cobertura integral de métricas. Ten en cuenta que su ITL excluye el TTFT; el de LLMPerf lo incluye. Ambas herramientas producen un TPOT diferente para el mismo servidor.

LLM-Locust (TrueFoundry) — Extensión de Locust que soluciona la trampa del GIL. DSL familiar de Locust + métricas de streaming.

guidellm — benchmarking sintético a gran escala.

k6 v2026.1.0 + k6 Operator 1.0 GA (septiembre de 2025):

  • El propio k6 (Go, compilado, sin GIL) añadió métricas conscientes de streaming.
  • El k6 Operator utiliza los CRDs TestRun / PrivateLoadZone para pruebas distribuidas nativas de Kubernetes.
  • El mejor para puertas de enlace de CI/CD y pruebas de SLA.

Vegeta — Go, más simple que k6. Saturación HTTP a tasa constante. No es consciente de LLM pero es bueno para pruebas de pasarela (gateway) o límites de tasa.

Locust 2.43.3 stock — tiene la trampa del GIL para LLMs. Solo con la extensión LLM-Locust.

Puerta de enlace de SLA en CI

Ejecuta k6 en el PR con:

  • 30-50 iteraciones, cada una con el RPS de línea base.
  • Puerta: TTFT P50/P95, 5xx < 5%, TPOT por debajo del umbral.
  • Rompe el build ante cualquier violación.

Distribución de prompts realista

Constrúyela a partir de muestras de tráfico reales (si las tienes) o de distribuciones publicadas (por ejemplo, prompts de ShareGPT para chat, HumanEval para código). Pásale la media + desviación estándar a LLMPerf. Evita los bucles con un solo prompt a toda costa.

Números que debes recordar

  • k6 Operator 1.0 GA: septiembre de 2025.
  • k6 v2026.1.0: métricas conscientes de streaming.
  • Ejecución típica de LLMPerf: 100-1000 solicitudes con concurrencia X.
  • Puerta de enlace de CI típica: 30-50 iteraciones por PR.
  • Cuatro patrones: estable, rampa, pico, remojo.

Úsalo

code/main.py simula una prueba de carga con una distribución de prompts realista, mide el TPOT efectivo y demuestra la trampa del prompt uniforme.

Entrégalo

Esta lección produce outputs/skill-load-test-plan.md. Dados el volumen de trabajo y el SLA, elige la herramienta y diseña los cuatro patrones de carga.

Ejercicios

  1. Ejecuta code/main.py. Compara la distribución uniforme versus la realista: ¿dónde está la diferencia?
  2. Escribe el script de k6 para una puerta de enlace de CI: TTFT P95 < 800 ms con 100 concurrentes, tiempo de ejecución de 5 minutos.
  3. Tu prueba de remojo (soak) muestra un crecimiento de memoria de 50 MB/hora. Nombra tres causas y la instrumentación para elegir entre ellas.
  4. Prueba de pico de 10 RPS a 100 RPS. ¿Cuál es el tiempo de recuperación esperado si la pila de producción Karpenter + vLLM está instalada (Phase 17 · 03 + 18)?
  5. GenAI-Perf reporta TPOT=6ms; LLMPerf reporta TPOT=11ms en el mismo servidor. Explica.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
LLMPerf "el arnés de LLM" Herramienta de benchmark de Anyscale, consciente de streaming
GenAI-Perf "herramienta de NVIDIA" Arnés de referencia de NVIDIA
LLM-Locust "Locust para LLMs" Extensión de Locust que soluciona la trampa del GIL
guidellm "benchmark sintético" Herramienta de generación sintética a gran escala
k6 Operator "k6 para K8s" k6 distribuido basado en CRDs
GIL trap "sobrecarga del cliente Python" El retraso de la tokenización infla la latencia reportada
Prompt-uniformity trap "la mentira del prompt único" El bucle con el mismo prompt golpea la caché, inflando el rendimiento (throughput)
Steady-state "carga constante" RPS plano durante N minutos
Ramp "subida lineal" De 0 al objetivo durante la duración establecida
Spike "prueba de ráfaga" Multiplicador repentino y luego reversión
Soak "prueba larga" Horas de ejecución para detectar fugas

Leituras Adicionais

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).