English · Español
Lab 01 — Kernel ingenuo de softmax fusionado (correcto, lento)¶
Objetivo: escribir la versión ingenua en CUDA C de softmax fusionado sobre la fila de logits de ~600 formas del MiniGPT gramatical. Confirmar corrección contra NumPy. Situar el punto en el roofline de la GPU. No tunear — eso lo hace el siguiente lab.
Tiempo estimado: 2–4 horas.
Prerrequisito:
lab/00-hello-cuda.mdcompleto. Directoriosrc/minikernel/existe conBLUEPRINT.mdrevisado.
Lo que produces¶
Un directorio experiments/24-naive-kernel/ y código de soporte en src/minikernel/:
src/minikernel/softmax_naive.cu— el kernel.src/minikernel/softmax_naive.py— lanzador Python (carga el kernel víacupy.RawKernel).src/minikernel/dispatch.py— borrador inicial: un dispatcher que elige kernel CUDA vs fallback NumPy. Fallback en CPU =np.exp(x - x.max()) / s.tests/test_softmax_naive.py— test de equivalencia numérica (1e-5 abs vsnp.softmax).experiments/24-naive-kernel/bench.py— cronometraje de una sola configuración.experiments/24-naive-kernel/manifest.json.experiments/24-naive-kernel/README.md.
El operador¶
Softmax por filas con el truco de estabilidad numérica (restar el máximo), sobre entradas de forma (B, V) con \(V = 600\) (vocab del MiniGPT gramatical por §A13). Para la Fase 24, \(B\) recorre \(\{1, 8, 64, 512, 4096\}\).
TODOs¶
Bloque A — escribir el kernel ingenuo¶
- Según
theory/02§"Version 1: Naive": un thread por cada par (fila, columna-stride). Cada thread vuelve a leer la fila para calcular max y suma. Derrochador por diseño. - Usa entradas fp32, acumulador fp32, salidas fp32. Sin precisión mixta todavía (el lab 02 la explora).
- Maneja \(V\) que no sea potencia de 2: protege con
if (col < V). - Lanza con
grid = (B,),block = (V_padded,)dondeV_padded = next_pow_2(V) = 1024.
Bloque B — test de corrección¶
-
tests/test_softmax_naive.py: genera logits aleatorios connp.random.default_rng(42), forma(64, 600). Compara la salida del kernel CUDA connp.exp(x - x.max(axis=-1, keepdims=True)) / np.exp(x - x.max(axis=-1, keepdims=True)).sum(axis=-1, keepdims=True)conatol=1e-5. - Skipif cuando no se detecte CUDA; en ese caso, el test ejercita el fallback NumPy vía
dispatch.pyy aserta concordancia consigo mismo (chequeo de cordura de que el fallback existe). - Ejecuta en CPU (portátil de Borja) — el dispatch devuelve la ruta NumPy; el test corre.
- Ejecuta en GPU en la nube — el dispatch devuelve la ruta CUDA; el test corre.
Bloque C — bench¶
-
bench.py: barrido \(B \in \{1, 8, 64, 512, 4096\}\). Para cada uno, cronometra 100 lanzamientos (tras 3 calentamientos). Registra la mediana. - Calcula el ancho de banda HBM alcanzado: bytes movidos por fila × \(B\) × lanzamientos / tiempo.
- Calcula la fracción del pico HBM (de tu consulta del device en la fase 23).
- Esperado: 1–5% del pico. Malo — pero la línea base. El lab 02 sube desde aquí.
Bloque D — situar en el roofline¶
- Calcula \(I = \text{FLOPs} / \text{bytes}\) para este kernel con \(B = 64, V = 600\), fp32. Esperado \(\approx 1\) FLOP/byte (limitado por memoria).
- Dibuja un punto en un esqueleto de roofline (arrastrar al lab 03).
Bloque E — manifest¶
{
"experiment": "24-naive-kernel",
"date": "YYYY-MM-DD",
"seed": 42,
"gpu": {"model": null, "compute_capability": null},
"kernel": {"name": "softmax_naive", "dtype": "fp32", "V": 600},
"results": {
"B_sweep": [1, 8, 64, 512, 4096],
"median_us_per_launch": [null, null, null, null, null],
"achieved_bandwidth_gbs": [null, null, null, null, null],
"fraction_of_hbm_peak": [null, null, null, null, null],
"correctness": "passed | failed"
}
}
Restricciones¶
- El fallback NumPy funciona en CPU. La máquina de Borja no tiene CUDA; el dispatcher debe rutar a la ruta NumPy sin errores.
- No tunees. Sin SMEM, sin reducción paralela, sin online softmax. Solo el kernel ingenuo de 3 pasadas. La idea es tener una línea base lenta pero correcta.
- Testea solo en fp32. fp16 vive en el lab 02.
- Usa semilla 42 para todas las entradas aleatorias.
Condiciones de parada¶
Hecho cuando:
- El kernel CUDA corre en la GPU en la nube, la salida coincide con
np.softmaxa 1e-5. - El fallback NumPy funciona en el portátil de Borja, la salida coincide con
np.softmaxa 1e-7. - Barrido de bench a lo largo de \(B\) registrado; un punto dibujado en un roofline-stub.
manifest.jsoncommiteado.tests/test_softmax_naive.pyen verde en ambos entornos (CUDA + CPU).
Escollos¶
- La suma desborda o subdesborda. Sin el truco
- max, \(\exp(x)\) desborda para \(x > 88\) en fp32. Con el truco, \(\exp(x - m) \leq 1\). Usa el truco desde el principio; el lab lo especifica. - NaN en la salida. Casi siempre: fila de todo
-info gradientes todo-cero. Para entradas aleatorias inicializadas esto no debería pasar, pero un 0/0 en la pasada de normalización es la pista. Protege cons = max(s, 1e-30). - El dispatcher elige CUDA silenciosamente cuando no hay CUDA instalado. Detecta al importar, fija un flag a nivel de módulo, rutea en consecuencia.
- Pérdida de mantisa fp32 en la suma. El orden de la suma importa; el kernel ingenuo del lab 01 puede que no coincida con NumPy a 1e-7 (solo a 1e-5). Documéntalo si es así.
Cuándo consultar solutions/¶
Tras cumplir todas las condiciones de parada. La referencia muestra el kernel ingenuo canónico y el dispatcher.
Siguiente lab: lab/02-tuned-kernel.md.