La GPU como máquina física: el modelo Roofline
poder predecir, antes de medir, qué limita un workload (cómputo, ancho de banda o capacidad de memoria) y por qué. Esta es la lente con la que verás todo el curso: cuando entrenes, sirvas o escribas kernels, siempre estarás peleando contra uno de estos tres límites.
1.1Por qué empezamos por aquí
La mayoría de la gente trata la GPU como una caja que "va más o menos rápido". Un ingeniero de percentil top razona al revés: dada una operación, sé qué la limita y por tanto qué la aceleraría. Sin esto, optimizar es prueba y error; con esto, es deducción.
Tres recursos finitos, siempre los mismos:
- Cómputo — operaciones aritméticas por segundo (FLOP/s). En tu RTX 5090, los Tensor Cores de 5ª generación hacen el grueso del trabajo de matrices.
- Ancho de banda de memoria — bytes/s que puedes mover entre la VRAM (GDDR7) y los registros/caches de los SM. En la 5090: 1.792 GB/s.
- Capacidad de memoria — cuántos bytes caben en la VRAM. En la 5090: 32 GB. (Este límite no afecta la velocidad sino qué cabe; lo tratamos en L3.)
1.2Anatomía de la RTX 5090 (Blackwell GB202)
Datos que conviene memorizar porque los usarás en cálculos:
| Propiedad | Valor | Para qué lo usas |
|---|---|---|
| CUDA cores | 21.760 | paralelismo de propósito general |
| Tensor Cores | 5ª gen, FP4/FP6/FP8/NVFP4/MXFP4 nativo | matmuls de baja precisión (el corazón de los LLM) |
| VRAM | 32 GB GDDR7 | qué modelos/batches caben |
| Ancho de banda | 1.792 GB/s (bus 512-bit) | techo de los workloads memory-bound |
| L2 cache | 96 MB (vs 72 MB en 4090) | reduce presión de banda en accesos repetidos (atención) |
| Compute capability | sm_120 | qué wheels/kernels necesitas (L2) |
| Interconexión | PCIe 5.0 (no NVLink) | no hay tensor-parallel eficiente entre dos 5090 |
| TDP | ~575 W | térmica/energía |
Dos consecuencias prácticas que la mayoría ignora:
- No hay NVLink. Dos 5090 se comunican por PCIe, que es ~10× más lento que el NVLink de una H100. Por eso partir un modelo grande entre dos 5090 (tensor parallelism) rinde mal; lo que sí funciona es correr réplicas independientes del modelo, una por GPU. Tenlo presente si algún día añades una segunda tarjeta.
- El L2 de 96 MB explica por qué a veces el throughput sube más de lo que predice solo el ratio de ancho de banda: parte de los accesos repetidos (p.ej. en atención) se sirven desde caché, no desde VRAM.
1.3El modelo Roofline (la herramienta central)
La idea, formalizada por Williams et al. (2009), es sencilla y potente. Para cualquier kernel define:
- Intensidad aritmética
I = FLOPs / bytes_movidos(operaciones por byte leído/escrito de VRAM). - Rendimiento alcanzable
P = min( P_peak , I × BW )dondeP_peakes el techo de cómputo (FLOP/s) yBWel ancho de banda (bytes/s).
Interpretación:
- Si
Ies baja (mueves muchos bytes por cada FLOP), estás memory-bound:P ≈ I × BW. Acelerar el cómputo no ayuda; lo que ayuda es mover menos bytes (cuantización, fusión de kernels, mejor caché). - Si
Ies alta (muchos FLOPs por byte), estás compute-bound:P ≈ P_peak. Lo que ayuda es más FLOP/s (mejor precisión en Tensor Cores, mejor utilización). - El punto donde se cruzan (
I_ridge = P_peak / BW) es la "cresta" del tejado: por debajo, mandas tú la memoria; por encima, manda el cómputo.
Regla mental para LLMs: el decode autoregresivo (generar token a token) es memory-bound — por eso los tok/s dependen casi linealmente del ancho de banda, y por eso la GDDR7 de la 5090 importa tanto. El prefill (procesar el prompt en paralelo) es compute-bound. El batching sube la intensidad aritmética del decode (reutilizas los mismos pesos para varias secuencias), moviéndolo hacia compute-bound: por eso el throughput agregado crece con la concurrencia aunque la latencia por petición no mejore.
1.4Derivación: el techo de tok/s en decode
Vamos a derivar el límite teórico de generación para un modelo denso, porque lo compararás con tu medición real en L4.
En decode, para generar un token nuevo el modelo lee todos sus pesos una vez (cada peso participa en exactamente un producto). Por tanto, ignorando el KV-cache y el overhead:
tiempo_por_token ≈ bytes_de_pesos_leídos / ancho_de_banda
tok/s_teórico ≈ ancho_de_banda / (num_parámetros × bytes_por_parámetro)
Ejemplo (modelo 8B en FP16, batch=1):
bytes_por_token = 8e9 parámetros × 2 bytes = 1.6e10 bytes
tok/s_teórico = 1.792e12 / 1.6e10 ≈ 112 tok/s
Ejemplo (mismo 8B en 4-bit, batch=1):
bytes_por_token = 8e9 × 0.5 bytes = 4e9 bytes
tok/s_teórico = 1.792e12 / 4e9 ≈ 448 tok/s
Esto explica de un plumazo por qué cuantizar a 4-bit multiplica los tok/s: mueves 4× menos bytes por token, y el decode es memory-bound. (En la práctica medirás algo menor por overhead de kernels, KV-cache y dequantización; el gap es tu "eficiencia de serving", que aprenderás a cerrar en el Nivel 1.)
Por eso un Qwen3-8B Q4 en llama.cpp ronda ~186–213 tok/s reales (no 448): el límite teórico es el techo, no la promesa.
1.5Laboratorio L1.1 — Medir el ancho de banda real de tu tarjeta
Antes de creer el dato de catálogo (1.792 GB/s), mídelo. Una operación trivialmente memory-bound (copiar un tensor grande) te dará el ancho de banda efectivo.
1# lab_l1_bandwidth.py — mide el ancho de banda efectivo de la VRAM
2import torch, time
3
4assert torch.cuda.is_available()
5dev = "cuda"
6# Tensor de 1 GB en fp32 (256M elementos × 4 bytes). Lo bastante grande para
7# saturar la memoria y que el overhead de lanzamiento del kernel sea despreciable.
8n = 256 * 1024 * 1024
9x = torch.empty(n, dtype=torch.float32, device=dev)
10y = torch.empty_like(x)
11
12# Warmup: las primeras llamadas pagan compilación/alocación de la caché de CUDA.
13for _ in range(10):
14 y.copy_(x)
15torch.cuda.synchronize() # esperar a que la GPU termine; si no, medimos el lanzamiento async, no el trabajo
16
17iters = 100
18t0 = time.perf_counter()
19for _ in range(iters):
20 y.copy_(x) # copy = 1 lectura + 1 escritura del tensor completo
21torch.cuda.synchronize()
22dt = time.perf_counter() - t0
23
24bytes_moved = iters * x.numel() * x.element_size() * 2 # ×2: leemos x y escribimos y
25gbps = bytes_moved / dt / 1e9
26print(f"Ancho de banda efectivo: {gbps:.1f} GB/s") # esperable ~1500-1750 GB/s en una 5090 sanaLíneas no triviales explicadas:
torch.cuda.synchronize(): las llamadas CUDA son asíncronas; sin sincronizar,perf_countermediría solo el tiempo de encolar el trabajo, no de ejecutarlo. Es el error de medición nº1 en GPU.- El warmup: la primera vez que se ejecuta un kernel, CUDA compila/cachea y aloca buffers. Si lo incluyes en la medición, subestimas el rendimiento.
× 2:copy_lee el origen y escribe el destino → mueve dos veces el tamaño del tensor.
Si obtienes mucho menos de ~1.500 GB/s, sospecha del driver (ver L2: driver <575 deja la 5090 al nivel de una 4090) o de térmica/power limit.
1.6Ejercicios (con solución)
E1. Deriva el tamaño del KV-cache de un modelo con n_layers=32, n_kv_heads=8, head_dim=128, para seq_len=32768, batch=1, en FP16. ¿Y en FP8?
Solución
KV-cache = 2 (K y V) × n_layers × n_kv_heads × head_dim × seq_len × batch × bytes
FP16: 2 × 32 × 8 × 128 × 32768 × 1 × 2 = 2 × 32 × 8 × 128 × 32768 × 2
= 4.398e9 bytes ≈ 4.1 GiB
FP8 : la mitad ≈ 2.05 GiB
Moraleja: a contextos largos, el KV-cache —no los pesos— es lo que llena la VRAM. Por eso --kv-cache-dtype fp8 (Nivel 1) duplica el contexto máximo.
E2. Un modelo de 14B en FP16, batch=1. ¿Memory-bound o compute-bound en decode? ¿Cuál es su tok/s teórico en la 5090?
Solución
Decode batch=1 = memory-bound siempre (intensidad aritmética ~1). tok/s ≈ 1.792e12 / (14e9 × 2) ≈ 64 tok/s. Coincide con mediciones reales de Qwen3-14B FP16.E3. ¿Por qué el throughput agregado con 32 peticiones concurrentes es mucho mayor que 32× el de una sola, aunque cada petición no vaya más rápida?
Solución
Con batching, los pesos se leen **una vez** y se reutilizan para las 32 secuencias → la intensidad aritmética sube ~32× → el decode pasa de memory-bound a compute-bound, donde el techo es mucho más alto. La latencia por petición no baja (sigues generando token a token), pero el trabajo útil por byte leído se multiplica. Es la base de la "continuous batching" de vLLM.1.7Trampas comunes
- Medir sin
synchronize()→ números absurdamente altos. - Olvidar el warmup → números bajos.
- Asumir que "más TFLOPs = más tok/s" en decode → falso, es memory-bound.
- Creer que dos 5090 sirven un modelo grande el doble de rápido → PCIe mata el tensor-parallel; usa réplicas.
1.8Referencias (opcionales, el contenido está arriba)
- CS336 (Stanford) Lecture 5 "GPUs" y Lecture 10 "Inference" — YouTube, playlist Spring 2025.
- Williams, Waterman, Patterson, "Roofline: An Insightful Visual Performance Model" (CACM 2009).
- NVIDIA RTX 5090 technical brief (specs Blackwell).