GRATÍCULAinstrumento de maestría
BancoRTX 5090 · GB202
Rev2026.06
Entrar
N1 · Serving world-class/L1

Métricas, PagedAttention y continuous batching

Objetivo de maestría

saber exactamente qué medir en serving y por qué el throughput escala con la concurrencia. Sin esto, "optimizar vLLM" es tocar flags al azar. Con esto, cada flag tiene una hipótesis detrás.


1.1Las tres métricas que lo dicen todo

Un servidor LLM no tiene "una velocidad". Tiene tres números que a menudo van en direcciones opuestas:

  • TTFT (Time To First Token) — latencia desde que llega la petición hasta el primer token. La domina el prefill (procesar el prompt). Es lo que siente el usuario como "se ha quedado pensando".
  • TPOT (Time Per Output Token), a veces ITL (inter-token latency) — tiempo entre tokens una vez empezada la generación. La domina el decode (memory-bound, N0·L1). Es la "velocidad de escritura" percibida.
  • Throughput agregado — tokens/s sumando todas las peticiones concurrentes. Es lo que importa para coste/capacidad, no para latencia individual.

La tensión clave: optimizar throughput suele empeorar TTFT/TPOT individuales y viceversa. Un sistema servido para máximo throughput (batches grandes) hace esperar más a cada petición; uno servido para mínima latencia (batch=1) desperdicia la GPU. Tu trabajo es elegir el punto según el caso de uso.

Regla: para un chatbot interactivo priorizas TTFT y TPOT; para procesar 100k documentos de noche priorizas throughput y te da igual la latencia individual.


1.2PagedAttention: por qué vLLM existe

El KV-cache (N0·L1·E1) crece con cada token generado y es enorme. El enfoque naïve reserva un bloque contiguo de memoria por petición dimensionado al máximo posible de longitud → desperdicia muchísima VRAM (fragmentación interna y reserva especulativa).

PagedAttention (Kwon et al., SOSP 2023) aplica la idea de la memoria virtual paginada de los sistemas operativos: parte el KV-cache en bloques de tamaño fijo (p.ej. 16 tokens) que no tienen por qué ser contiguos en memoria. Una tabla de bloques mapea "posición lógica → bloque físico", igual que una page table mapea direcciones virtuales a físicas.

Consecuencias prácticas que tienes que entender:

  • Casi cero desperdicio: solo se aloca el KV-cache que realmente se usa, bloque a bloque.
  • Sharing: dos peticiones con el mismo prefijo (mismo system prompt) pueden compartir los bloques físicos de ese prefijo → base del prefix caching (L2).
  • Más peticiones simultáneas en la misma VRAM → más throughput.

Esta es la razón por la que vLLM rinde más que una implementación ingenua: no por kernels más rápidos, sino por gestionar la memoria del KV-cache como un SO gestiona la RAM.


1.3Continuous batching: por qué el throughput escala

El batching estático clásico espera a juntar N peticiones, las procesa juntas y no admite nuevas hasta terminar las N. Problema: las peticiones tienen longitudes distintas; las cortas terminan y su hueco en el batch queda ocioso hasta que termina la más larga.

Continuous batching (también "in-flight batching") opera a nivel de iteración (cada paso de decode), no de petición: en cuanto una secuencia termina, su slot se libera y entra una nueva petición de la cola en el mismo batch, sin esperar a que terminen las demás. La GPU nunca queda ociosa mientras haya cola.

Conexión con el roofline (N0·L1): al mantener el batch lleno, los pesos del modelo se leen una vez y se reutilizan para muchas secuencias → la intensidad aritmética del decode sube → el decode pasa de memory-bound a compute-bound → el throughput agregado crece con la concurrencia aunque el TPOT por petición apenas cambie. Esto es exactamente lo que vas a medir.


1.4Laboratorio L1.1 — La curva de batching (prueba empírica)

Reusa el servidor vLLM de N0·L4. Vamos a trazar throughput y latencia vs concurrencia para ver el efecto.

bash
1# Servidor (terminal A): Qwen3-8B
2docker run --gpus all -p 8000:8000 --ipc=host vllm/vllm-openai:latest \
3  --model Qwen/Qwen3-8B --dtype auto --max-model-len 8192 --gpu-memory-utilization 0.90
python
1# lab_n1l1_batching_curve.py — mide throughput y latencia a varias concurrencias
2import asyncio, time, statistics, httpx
3
4URL = "http://localhost:8000/v1/completions"
5MODEL = "Qwen/Qwen3-8B"
6PROMPT = "Explica en detalle cómo funciona la fotosíntesis."  # prompt fijo: aísla el efecto del batching
7
8async def one_request(client):
9    t0 = time.perf_counter()
10    r = await client.post(URL, json={
11        "model": MODEL, "prompt": PROMPT,
12        "max_tokens": 256, "temperature": 0.0,  # determinista: comparaciones limpias
13    }, timeout=120)
14    dt = time.perf_counter() - t0
15    n_out = r.json()["usage"]["completion_tokens"]
16    return dt, n_out
17
18async def run_at_concurrency(c):
19    async with httpx.AsyncClient() as client:
20        t0 = time.perf_counter()
21        # lanza 'c' peticiones a la vez, repetidas hasta tener una muestra estable
22        results = await asyncio.gather(*[one_request(client) for _ in range(c)])
23        wall = time.perf_counter() - t0
24    lats = [d for d, _ in results]
25    total_out = sum(n for _, n in results)
26    agg_tps = total_out / wall                # throughput agregado real
27    per_req_tps = statistics.mean(n / d for d, n in results)
28    return c, agg_tps, per_req_tps, statistics.mean(lats)
29
30async def main():
31    print(f"{'conc':>5} {'agg_tok/s':>10} {'per_req_tok/s':>13} {'lat_s':>7}")
32    for c in (1, 2, 4, 8, 16, 32, 64):
33        c, agg, per, lat = await run_at_concurrency(c)
34        print(f"{c:5d} {agg:10.0f} {per:13.1f} {lat:7.2f}")
35
36asyncio.run(main())

Líneas no triviales explicadas:

  • temperature=0.0: salida determinista → cuando compares configuraciones (L2–L4) los cambios se deben a la config, no al sampling.
  • asyncio.gather(*[...]): dispara las c peticiones simultáneamente; sin async dispararías en serie y no habría batching que medir.
  • Diferencia clave del print: agg_tok/s (sube fuerte con la concurrencia) vs per_req_tok/s (casi plano). Esa divergencia es continuous batching + roofline en acción.

Qué deberías ver: agg_tok/s crece de forma marcada de conc=1 a conc≈32 y luego se aplana (saturas el compute o el KV-cache); per_req_tok/s apenas se mueve; lat_s sube un poco con la concurrencia. Guarda la tabla: es evidencia para tu lab notebook y parte de entender C1.


1.5Cómo leer el gap (tu eficiencia de serving)

Recupera tu techo teórico de decode de N0·L1.4 (Qwen3-8B BF16 ≈ 112 tok/s por petición). Compara con tu per_req_tok/s a conc=1. El cociente es tu eficiencia. El gap viene de:

  • Overhead de lanzamiento de kernels y de Python.
  • Lectura del KV-cache además de los pesos.
  • No saturar el batch (a conc=1 desperdicias la GPU).

El throughput agregado a conc alta puede superar con creces el techo "por petición" porque ahí ya no es memory-bound. Entender por qué tu número es el que es —no solo cuál es— es lo que separa al Nivel 3 del Nivel 2 de la rúbrica.


1.6Ejercicios

E1. A partir de tu tabla, ¿a qué concurrencia se aplana el throughput agregado? ¿Qué recurso crees que satura ahí (compute o KV-cache)? Verifica mirando la VRAM con nvitop durante el barrido.

E2. Repite el barrido con max_tokens=1024. ¿Cómo cambia la curva y por qué? (Pista: secuencias más largas → KV-cache mayor → menos peticiones simultáneas caben.)

E3. Explica con tus palabras, para tu lab notebook, por qué dos usuarios con el mismo system prompt podrían compartir memoria en vLLM. (Conecta con prefix caching de L2.)

1.7Trampas comunes

  • Medir latencia con peticiones en serie y creer que es "la velocidad del servidor".
  • Confundir throughput agregado con velocidad por usuario.
  • Olvidar temperature=0 al comparar configuraciones.

1.8Referencias

  • Kwon et al., "Efficient Memory Management for LLM Serving with PagedAttention" (SOSP 2023).
  • CS336 L10 "Inference". Docs vLLM: "Architecture", "Optimization".