English · Español
Lab 03 — Perfil de rendimiento de atención¶
Objetivo: medir dónde se gasta el tiempo en scaled dot-product attention sobre el i5-8250U de Borja para longitudes de secuencia 64, 128, 256 y 512. Identificar el softmax memory-bound y dejar una referencia anticipada a la Fase 27 (Flash Attention).
Tiempo estimado: 60–90 minutos.
Requisito previo: labs 00–02 commiteados; experimento de roofline de la Fase 1 commiteado (necesitas
β_peakpara tu máquina).
Qué produces¶
Un directorio experiments/15-attention-perf/ que contiene:
perf.py— script de medición.results.json— tiempos por operación y por longitud de secuencia.perf.png— gráfico de barras apiladas: tiempo dedicado a \(QK^\top\), softmax, \(AV\), proyecciones (de entrada/salida).roofline.png— superpón tu rendimiento de atención medido sobre el roofline de la Fase 1.manifest.json.README.md.
Contexto¶
El forward de atención tiene seis operaciones matmul/elementwise:
Q = x @ W_Q,K = x @ W_K,V = x @ W_V— tres proyecciones de entrada, \(O(T d^2)\) cada una.S = Q @ K.T / sqrt(d_k)— \(O(T^2 d_k)\).A = softmax(S + mask)— \(O(T^2)\) elementwise.out_h = A @ V— \(O(T^2 d_v)\).out = concat @ W_O— \(O(T d^2)\).
Para secuencias largas (\(T \gg d_k\)), las operaciones 2 y 4 dominan (escalan como \(T^2\)). Para secuencias cortas, 1 y 5 dominan (escalan como \(T d^2\), con una constante mayor). El softmax (op 3) está siempre memory-bound — 1–2 FLOPs por byte movido.
La Flash Attention de la Fase 27 ataca el tráfico de memoria en 2 + 3 + 4 mediante tiling y no materializar \(A\). Aquí no lo arreglamos; lo medimos para preparar la fase futura.
TODOs¶
Bloque A — forward instrumentado¶
- En
perf.py, escribeattention_forward_timed(x, mha)que devuelva un dict{"qkv_proj": t, "scores": t, "softmax": t, "av": t, "out_proj": t, "total": t}con tiempos por operación en segundos. - Usa
time.perf_counter_ns(). Envuelve cada op con start/stop; fuerza la evaluación (numpy es eager, así que esto funciona directamente). - Ejecuta una pasada de calentamiento antes de medir.
- Cada medición debe agregar al menos 50ms de trabajo (ejecuta varias iteraciones y divide).
Bloque B — barrido de longitud de secuencia¶
- Longitudes de secuencia:
T ∈ {64, 128, 256, 512}. - Fijo:
d_model = 64, n_heads = 4, d_head = 16. - Para cada \(T\), ejecuta
attention_forward_timedlas iteraciones necesarias para obtener números estables. Registra medias y (opcionalmente) std. - Guarda todos los resultados en
results.json.
Bloque C — barras apiladas¶
- Cuatro barras (una por \(T\)). Cada barra apilada: qkv_proj + scores + softmax + av + out_proj.
- eje x: \(T\). eje y: tiempo por forward (ms).
- Anota la componente dominante por \(T\).
- Guárdalo como
perf.png.
Observaciones esperadas:
- En \(T = 64\): dominan las proyecciones de entrada (qkv_proj + out_proj).
- En \(T = 512\): dominan las ops \(T^2\) (scores, av, softmax).
- El softmax nunca es la componente más pequeña proporcionalmente — aunque sea "solo" elementwise, su tráfico de memoria es pesado.
Bloque D — colocación en el roofline¶
- Calcula la intensidad aritmética para cada una de las cinco ops en cada \(T\):
qkv_proj: \(2 T d^2\) FLOPs sobre \(4 T d + 4 d^2\) bytes → \(I = T d / (2 T + 2 d)\).scores: \(2 T^2 d_k\) FLOPs sobre \(8 T d_k + 4 T^2\) bytes → \(I = T d_k / (4 d_k + 2 T)\).softmax: \(\sim 5 T^2\) FLOPs sobre \(8 T^2\) bytes → \(I \approx 0.6\) FLOP/byte.av: misma forma que scores.out_proj: misma forma que qkv_proj.- Calcula los GFLOPS medidos por op (FLOPs / tiempo).
- Sobre el gráfico de roofline de la Fase 1, superpón cada punto (intensidad, GFLOPS). Color por tipo de op.
- Guárdalo como
roofline.png.
Bloque E — redactar¶
En README.md, responde:
- ¿Qué op es memory-bound en la máquina de Borja, y para qué \(T\)? El softmax siempre. Scores y av para $T < $ algún umbral (calcula el umbral a partir de \(T d_k > 4(d_k + T) \cdot I_{\text{crit}}\)).
- ¿Cuál es la brecha entre el rendimiento medido y el techo del roofline para el kernel softmax? Calcula la ratio. Debería ser < 10% — el softmax se queda muy por debajo del techo memory-bound porque está mal vectorizado para fp32 en numpy (frente a un kernel BLAS afinado).
- Anticipo: ¿cómo cambiaría Flash Attention el panorama del roofline? Dos frases. (Flash Attention no cambia los FLOPs de las ops, solo los bytes movidos — al evitar materializar \(A\). Sube las ops \(T^2\) por el roofline reduciendo su tráfico de memoria.)
Bloque F — manifest¶
{
"experiment": "15-attention-perf",
"date": "YYYY-MM-DD",
"seed": 0,
"versions": { "python": "3.11.x", "numpy": "X.Y.Z" },
"depends_on": "experiments/01-roofline/manifest.json",
"config": {
"d_model": 64,
"n_heads": 4,
"T_sweep": [64, 128, 256, 512]
},
"hardware": {
"cpu_model": "Intel Core i5-8250U",
"cpu_governor_at_run": "performance"
},
"results_summary": {
"softmax_GFLOPS_at_T_512": null,
"softmax_pct_of_total_at_T_512": null,
"scores_GFLOPS_at_T_512": null,
"phase_27_motivation_pct_memory": null
}
}
Restricciones¶
- Sin PyTorch. Solo numpy.
- Governor
performance. Igual que en lab 01-01-memcpy-bandwidth. - Ejecuciones frías/calientes. Solo calientes — queremos números repetibles, no tiempos de fallos de página de la primera iteración.
- Sin optimización guiada por perfilado. No afines el código. El objetivo es medir la implementación ingenua; el ajuste es la Fase 27.
Condiciones de parada¶
Hecho cuando:
- Los seis archivos están commiteados.
perf.pngmuestra la pila esperada para los cuatro valores de \(T\).roofline.pngmuestra cada op como un punto sobre el roofline de la Fase 1.README.mdresponde a las tres preguntas del Bloque E con referencia a números concretos.
Trampas¶
- Confusión memcpy / view de numpy. Cuando midas un trozo como
Q @ K.T, la transpuesta es gratis (solo es una view) pero el matmul escribe en un nuevo buffer. Asegúrate de no estar midiendo solo la operación view por accidente. - Hyperthreading. El i5-8250U tiene 4 cores / 8 hilos. El BLAS de numpy usa los 8 por defecto. Por consistencia, o bien fija
OMP_NUM_THREADS=4(cores reales) o documenta que estás usando 8. - Calor de caché entre tamaños. Mayor \(T\) significa mayor working set, lo que reventará la caché. Esto es realista — no intentes "arreglarlo".
- Resolución de
time.perf_counter. En Linux es de resolución de nanosegundos, pero el planificador del kernel puede interrumpirte. Ejecutar cada medición 100+ veces y tomar la mediana es más robusto que la media.
Cuándo consultar solutions/¶
Cuando los seis archivos estén commiteados. Solución en solutions/03-attention-perf-ref.md.
Fin de los labs de la Fase 15. Escribe PHASE_15_REPORT.md, rellena learners/borja/phase-15/reflections.md.
Esta es la fase central de derivación del currículo. La reflexión aquí importa — escríbela despacio. ¿Qué cuajó? ¿Qué queda borroso? ¿Qué fue más duro de lo esperado?