Saltar al contenido
AI•3 min de lectura

El error de mi propio benchmark

Publicamos el primer barrido de concurrencia y los números eran demasiado buenos: 10 usuarios simultáneos, todos rápidos, sin un solo fallo. Antes de celebrar, revisamos una cosa: el TTFT era idéntico en todas las peticiones. Cuando 10 usuarios distintos obtienen exactamente el mismo tiempo, no estás midiendo 10 usuarios: estás midiendo uno.

El bug era de mi propio harness. Esta es la historia y las reglas que sacamos de ella.

Qué medía (mal) el harness

El test de carga mandaba el mismo prompt a todos:

# load_test.py (versión con el bug, simplificado)
for i in range(concurrency):
    requests.append(post(url, payload=SAME_PROMPT))   # ← todos el mismo texto

Con eso, el servidor veía 10 peticiones con el mismo prefijo. Y con --enable-prefix-caching, el prefijo se calcula una vez y se reutiliza: las 10 peticiones compartían la caché KV. O sea: medíamos la memoria de una sesión, no de diez.

El síntoma estaba a la vista en el CSV crudo:

# decode_c10.csv (10 filas)
ttft_ms: 2489.2 2301.8 2372.0 2432.4 2568.6 2465.8 2408.7 2330.9 2556.0 2467.7
prompt_tokens: 15254 (idéntico en las 10)

Diez TTFT distintos pero todos alrededor de 2,4 s, con el mismo prompt de 15.254 tokens: eso es caché compartida, no concurrencia real.

El fix

Marcador único al inicio del prompt, para que cada sesión tenga su propio prefijo:

# load_test.py (corregido): --session-mode unique
prompt = f"[session {uuid4()}] {base_prompt}"

Un detalle de método: el marcador va al principio. Si lo pones al final, el prefijo común sigue siendo igual y la caché se seguiría compartiendo.

El resultado honesto

Con sesiones únicas, los números cambian de historia:

Escenario (10 usuarios, 16K)tok/s p50TTFT p50
Prefijo compartido (lo que medía antes)73,63.773 ms
Sesiones distintas (lo real)15,99.652 ms

El caso "bonito" no es falso: es un caso de uso distinto (varios turnos de la misma conversación, que es lo normal en un agente). Pero presentarlo como "10 usuarios concurrentes" era engañoso. Lo correcto es decir cuál es cuál —y eso hacemos de aquí en adelante. → Prefix caching: la misma carga, 3× más rápido

La segunda lección: los percentiles

El harness calculaba percentiles con índice discreto:

idx = int(len(vals) * p / 100)

Con 5 o 10 muestras, el "p90" es el máximo de la muestra, no un percentil. No está mal como orden de magnitud, pero no lo llames percentil. Para percentiles de verdad: N ≥ 100 e interpolación.

Las reglas del benchmark honesto

  1. Calienta y descarta las primeras corridas (la primera paga compilación y carga).
  2. N ≥ 100 si vas a hablar de percentiles.
  3. Sesiones únicas para medir concurrencia; prefijo compartido para medir caché. Y di cuál estás midiendo.
  4. Un cambio por vez: si cambias prompt y concurrencia, no sabes qué movió el número.
  5. Mira el crudo, no el resumen. El bug se descubrió porque el TTFT era sospechosamente idéntico.
  6. Escribe el escenario en el nombre del archivo (c10-16k-shared.csv) para no confundirlos seis horas después.

Lo que me llevo

Un benchmark propio es código, y el código tiene bugs. El sesgo más común no es mentir: es medir el mejor caso sin darte cuenta y creer que es el caso normal. La defensa es barata: mira los datos crudos y pregúntate si el resultado es demasiado bueno.

El marco completo de medición está en Medir inferencia sin engañarte y en la guía Inference Engineering.

¿Qué bug has encontrado en tu propio benchmark? Cuéntame en los comentarios: los ajenos se ven rápido, los propios duelen.

Referencias

> Más posts