Skip to content

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 β_peak para 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:

  1. Q = x @ W_Q, K = x @ W_K, V = x @ W_V — tres proyecciones de entrada, \(O(T d^2)\) cada una.
  2. S = Q @ K.T / sqrt(d_k)\(O(T^2 d_k)\).
  3. A = softmax(S + mask)\(O(T^2)\) elementwise.
  4. out_h = A @ V\(O(T^2 d_v)\).
  5. 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, escribe attention_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_timed las 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:

  1. ¿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}}\)).
  2. ¿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).
  3. 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:

  1. Los seis archivos están commiteados.
  2. perf.png muestra la pila esperada para los cuatro valores de \(T\).
  3. roofline.png muestra cada op como un punto sobre el roofline de la Fase 1.
  4. README.md responde 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?