Tu benchmark de referencia (entregable de C0)
producir un número base reproducible de TU máquina —throughput de serving y de training— que usarás como punto de comparación en todos los niveles siguientes. Sin baseline no hay medición; sin medición no hay "percentil top". Esta lección cierra el Checkpoint C0.
4.1Qué vas a medir y por qué
Tres números, cada uno una cara distinta de tu hardware:
- Throughput de serving (decode) — tok/s generando, memory-bound. Lo comparas con el techo teórico de L1.4.
- Throughput agregado con concurrencia — cómo escala con batching (continuous batching de vLLM). Demuestra el efecto de L1·E3.
- Throughput de training (QLoRA) — tok/s de fine-tuning, para dimensionar cuánto tarda un experimento del Nivel 2.
La gracia no es el número en sí, sino que lo puedas regenerar con un comando y que sepas explicar por qué es el que es.
4.2Laboratorio L4.1 — Benchmark de serving con vLLM
vLLM trae un script oficial de benchmark (benchmark_serving.py) que simula carga concurrente realista. Lo usamos contra un servidor local.
Paso 1 — levantar el servidor:
1# Terminal A: servidor vLLM con Qwen3-8B
2docker run --gpus all -p 8000:8000 --ipc=host \
3 vllm/vllm-openai:latest \
4 --model Qwen/Qwen3-8B \
5 --dtype auto \
6 --max-model-len 8192 \
7 --gpu-memory-utilization 0.90--gpu-memory-utilization 0.90: vLLM reserva el 90% de la VRAM para pesos + KV-cache. No bajes de 0.7: la prefix-cache necesita ~30% del espacio. Si tienes OOM, baja primero--max-model-len, no esto.--ipc=host: vLLM usa memoria compartida entre procesos worker; sin esto, falla en Docker.--dtype auto: usa la precisión nativa del checkpoint (BF16 para Qwen3-8B).
Paso 2 — lanzar el benchmark:
1# Terminal B: clona vLLM solo para el script de benchmark
2git clone https://github.com/vllm-project/vllm.git
3cd vllm/benchmarks
4
5# descarga un dataset de prompts reales (ShareGPT) para carga realista
6wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json
7
8python benchmark_serving.py \
9 --backend vllm \
10 --model Qwen/Qwen3-8B \
11 --dataset-name sharegpt \
12 --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
13 --num-prompts 200 \
14 --request-rate inf \
15 --save-result --result-dir ./results--request-rate inf: envía las 200 peticiones de golpe → mide throughput máximo (saturación), no latencia interactiva.- El JSON de salida trae:
output_throughput(tok/s agregado),mean_ttft_ms(time-to-first-token),mean_tpot_ms(time-per-output-token).
Paso 3 — barrer concurrencia para ver el efecto del batching (L1·E3):
1for rate in 1 4 16 64; do
2 python benchmark_serving.py --backend vllm --model Qwen/Qwen3-8B \
3 --dataset-name sharegpt --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
4 --num-prompts 200 --request-rate $rate \
5 --save-result --result-dir ./results --result-filename rate_$rate.json
6doneGrafica output_throughput vs request-rate: verás cómo el throughput agregado sube con la concurrencia mientras el tpot por petición apenas cambia. Esa curva es la prueba empírica del roofline de L1.
4.3Laboratorio L4.2 — Benchmark de training con Unsloth (QLoRA)
Mide tok/s de fine-tuning y VRAM pico. No buscamos entrenar bien todavía (eso es el Nivel 2), solo medir velocidad.
1# lab_l4_train_bench.py — 100 pasos de QLoRA sobre Qwen3-4B, midiendo tok/s y VRAM
2import time, torch
3from unsloth import FastLanguageModel
4from datasets import load_dataset
5from trl import SFTTrainer, SFTConfig
6
7max_seq = 2048
8model, tokenizer = FastLanguageModel.from_pretrained(
9 model_name="unsloth/Qwen3-4B-Base", # versión 4-bit lista de Unsloth
10 max_seq_length=max_seq,
11 load_in_4bit=True, # QLoRA: base en 4-bit
12)
13model = FastLanguageModel.get_peft_model(
14 model, r=16, lora_alpha=16,
15 target_modules=["q_proj","k_proj","v_proj","o_proj",
16 "gate_proj","up_proj","down_proj"],
17 use_gradient_checkpointing="unsloth", # estira VRAM (ver L3.4)
18)
19
20ds = load_dataset("yahma/alpaca-cleaned", split="train[:2000]")
21def fmt(ex):
22 return {"text": f"### Instrucción:\n{ex['instruction']}\n### Respuesta:\n{ex['output']}"}
23ds = ds.map(fmt)
24
25trainer = SFTTrainer(
26 model=model, tokenizer=tokenizer, train_dataset=ds,
27 args=SFTConfig(
28 per_device_train_batch_size=2,
29 gradient_accumulation_steps=4,
30 max_steps=100,
31 learning_rate=2e-4,
32 logging_steps=10,
33 optim="paged_adamw_8bit", # paged optimizer (ver L3.4)
34 max_seq_length=max_seq,
35 output_dir="./out_bench",
36 ),
37)
38
39torch.cuda.reset_peak_memory_stats()
40t0 = time.perf_counter()
41stats = trainer.train()
42dt = time.perf_counter() - t0
43
44peak_gb = torch.cuda.max_memory_allocated() / 1e9
45# tokens procesados ≈ steps × batch × grad_accum × seq_len
46tokens = 100 * 2 * 4 * max_seq
47print(f"Training: {tokens/dt:.0f} tok/s | VRAM pico: {peak_gb:.1f} GB | {dt:.1f}s/100 steps")Líneas no triviales:
load_in_4bit=True+unsloth/Qwen3-4B-Base: QLoRA real, base cuantizado a 4-bit NF4.use_gradient_checkpointing="unsloth": recomputa activaciones en backward; sin esto, el batch×seq que cabe es mucho menor (L3.4).optim="paged_adamw_8bit": estados del optimizador en 8-bit + paginados a RAM en picos → evita OOM.reset_peak_memory_stats()+max_memory_allocated(): forma fiable de medir VRAM pico real de PyTorch.
4.4Empaquetar todo en make benchmark
El entregable de C0 es que esto se regenere con un comando. Estructura mínima de tu repo ai-mastery:
ai-mastery/
├── README.md # specs de tu máquina + tus números + checklist C0..C5
├── Dockerfile.blackwell # de N0·L2
├── Makefile
└── nivel0/
├── lab_l1_bandwidth.py
├── lab_l3_vram.py
├── lab_l4_train_bench.py
└── bench_serving.sh # el barrido de 4.2
1# Makefile
2benchmark: bandwidth vram serving training
3bandwidth:
4 python nivel0/lab_l1_bandwidth.py
5vram:
6 python nivel0/lab_l3_vram.py
7serving:
8 bash nivel0/bench_serving.sh
9training:
10 python nivel0/lab_l4_train_bench.pyEn el README registra una tabla como esta (rellénala con TUS números):
1## Máquina de referencia
2- GPU: RTX 5090 32GB | Driver: 5xx.xx | CUDA 12.8 | torch 2.11 | vLLM 0.11.x
3- Ancho de banda medido: ____ GB/s (catálogo 1792)
4- Serving Qwen3-8B @8K: decode ____ tok/s | TTFT ____ ms | agregado@64 ____ tok/s
5- Techo teórico decode 8B BF16: 112 tok/s → mi eficiencia: ____ %
6- Training Qwen3-4B QLoRA: ____ tok/s | VRAM pico ____ GB4.5CHECKPOINT C0 — criterio de aprobado
Marca C0 como pasado solo cuando, en un segundo pase (reconstruido sin copiar):
-
make benchmarkregenera tus tres números en una máquina/contenedor limpio. - El README documenta specs, versiones y los números, incluyendo el % de eficiencia (tok/s real ÷ techo teórico de L1.4).
- Sabes explicar el gap: por qué tu decode real no llega al techo teórico (overhead de kernels, KV-cache, dequant, no estar saturando el batch).
- Sabes decir, para 3 modelos cualesquiera que te propongan, si caben en tu 5090 y con qué técnica (usando la calculadora de L3).
Rúbrica: estás en Nivel 3 (reproductor) del eje fundamentos cuando puedes regenerar los números desde cero y explicar cada uno. Ese es el aprobado de C0.
Subproducto público opcional: repo blackwell-stack + post "Qué tok/s saca de verdad una RTX 5090 y por qué no llega al techo teórico". (Hay mucha demanda de benchmarks honestos de la 5090.)
4.6Cierre del Nivel 0
Ya tienes: la lente del roofline (L1), un entorno sano y depurable (L2), el presupuesto de VRAM internalizado (L3) y un baseline reproducible (L4). Con esto, el Nivel 1 (serving world-class) deja de ser "configurar vLLM a ciegas" y pasa a ser "cerrar el gap entre mi número real y el techo teórico, conscientemente".