Skip to content

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.md completo. Directorio src/minikernel/ existe con BLUEPRINT.md revisado.


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ía cupy.RawKernel).
  • src/minikernel/dispatch.pyborrador 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 vs np.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,) donde V_padded = next_pow_2(V) = 1024.

Bloque B — test de corrección

  • tests/test_softmax_naive.py: genera logits aleatorios con np.random.default_rng(42), forma (64, 600). Compara la salida del kernel CUDA con np.exp(x - x.max(axis=-1, keepdims=True)) / np.exp(x - x.max(axis=-1, keepdims=True)).sum(axis=-1, keepdims=True) con atol=1e-5.
  • Skipif cuando no se detecte CUDA; en ese caso, el test ejercita el fallback NumPy vía dispatch.py y 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:

  1. El kernel CUDA corre en la GPU en la nube, la salida coincide con np.softmax a 1e-5.
  2. El fallback NumPy funciona en el portátil de Borja, la salida coincide con np.softmax a 1e-7.
  3. Barrido de bench a lo largo de \(B\) registrado; un punto dibujado en un roofline-stub.
  4. manifest.json commiteado.
  5. tests/test_softmax_naive.py en 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 -inf o 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 con s = 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.