English · Español
03 — Tres fallos diseñados: anatomía de cómo se ve cada uno¶
Tres bugs intencionales. Cada uno tiene una firma visual específica en el dashboard. Aprenderlas aquí, en frío, hace que en la Fase 26 o 28 las reconozcas en caliente.
El montaje pedagógico¶
experiments/19-break-it/ contiene tres sub-experimentos. Cada uno toma la config sana de la Fase 18 y la corrompe de exactamente una manera. Borja:
- Ejecuta los tres. Cada uno produce un
dashboard.html. - Examina los dashboards. Para cada uno, escribe un diagnóstico en
borja-diagnoses.mdantes de mirar qué break se aplicó (los nombres de directorio01,02,03son deliberadamente no informativos). - Revela los breaks (
solutions/03-three-failures-ref.mdlista cuál es cuál). - Puntúa 3/3, ⅔ o ⅓.
Esta página documenta cómo debería verse cada break, para que el laboratorio sea interpretable. Léela después de escribir los diagnósticos, no antes.
Fallo 1: mala inicialización (Xavier × 100)¶
La corrupción¶
Multiplicar todos los pesos iniciales de nn.Linear y nn.Embedding por 100×. Matemáticamente: en lugar de \(W \sim \mathcal{N}(0, \sigma_\text{Xavier}^2)\), usa \(W \sim \mathcal{N}(0, (100 \sigma_\text{Xavier})^2)\).
Qué pasa por dentro¶
Forward pass: con magnitud de entrada \(\|x\| \sim O(1)\) y \(W\) inflado 100×, la salida de la primera capa tiene magnitud \(\sim 100\). A lo largo del stack de dos capas (cada capa con normalización, pero la normalización solo puede compensar escala, no la magnitud del residual stream sin normalizar), las magnitudes se acumulan. Para cuando llegan al LM head, los logits pre-softmax están en el rango \(\pm 10^3\). softmax de tales logits es one-hot; log de \(\sim 0\) en el denominador del softmax → \(\log(0) = -\infty\) → NaN.
Tiempo hasta NaN: típicamente 5-30 pasos.
Firma en el dashboard¶
- El Panel 4 (activaciones) es el primero en chillar. La magnitud de activación de la capa 0 es 50-200× el baseline sano desde el paso 0. La salida final de LN está en los centenares. Rojo brillante, inmediato.
- El Panel 3 (norma de gradiente pre-clip) le sigue. Los gradientes de
lossrespecto aweightsson proporcionales a las magnitudes de activación al cuadrado (vía la regla de la cadena); la norma pre-clip llega a \(10^4\)-\(10^6\) en 5 pasos. Post-clip se queda fijada en el umbral de clip (1.0). - El Panel 1 (loss) se va a NaN en 10-30 pasos. Tras ese punto, todos los paneles posteriores muestran NaN; la ejecución muere efectivamente.
Pista diagnóstica¶
Si el panel 4 muestra la magnitud de activación de la capa 0 ya muy por encima de lo sano en el paso 0 (antes de que ocurra entrenamiento alguno), el init está mal. Un init sano produce activaciones del mismo orden en el paso 0 que en el paso 100; el primer forward pass ya codifica si el init es razonable.
Por qué este es un bug del mundo real¶
Casos del mundo real: alguien copia-pega un Kaiming init que asume ReLU dentro de un stack que usa GELU y se olvida de reajustar la varianza. O alguien implementa Xavier con 1/fan_in en lugar de 2/(fan_in + fan_out). La exageración 100× aquí es para hacer la lección inequívoca; las versiones del mundo real son 2-5× y producen una versión más sutil del mismo dashboard.
Fallo 2: sin warmup (warmup_steps = 0)¶
La corrupción¶
Establece warmup = 0 en el schedule. El primer paso del optimizador en global_step = 0 usa LR = lr_max directamente (o lo que cosine_schedule(0, T, 0, lr_max, lr_min) evalúe a — habitualmente lr_max).
Qué pasa por dentro¶
En el paso 0, los \(m_0, v_0\) de AdamW están sin inicializar (cero). El primer gradiente \(g_0\) de los parámetros aleatoriamente inicializados tiene una varianza que aún no está bien aproximada por \(v_0\), por lo que \(\hat v_0\) está salvajemente sesgado. El paso de update:
Cuando \(\hat v_0\) se subestima (porque \(v_0 = 0\) y la corrección de sesgo divide por \((1 - 0.95)\)), el divisor es demasiado pequeño, el paso demasiado grande. El modelo salta a una mala región del espacio de parámetros.
Típicamente, la pérdida baja ligeramente (el paso sí mejoró), sufre un spike fuerte en el paso 5-20 (los gradientes de la mala región son salvajes), luego se recupera despacio durante ~200 pasos.
Firma en el dashboard¶
- El Panel 3 (norma de gradiente pre-clip) hace spike en el paso 1-5. A menudo 10-50× el pico sano. Post-clip se queda fijado en 1.0 durante unos pasos.
- El Panel 1 (curva de pérdida) muestra una forma de V al principio. La pérdida baja, hace spike, baja, se normaliza. El spike es pequeño en términos absolutos pero visible en el eje log.
- El Panel 2 (LR) es la pistola humeante. Empieza en
lr_maxen lugar de 0. Si no se te ocurre mirar el panel 2, podrías atribuir el problema a malos datos o mal init; el panel 2 hace el diagnóstico inmediato. - Los paneles 4, 5, 6 muestran excursiones transitorias pero se asientan en regiones sanas hacia el paso 200.
Pista diagnóstica¶
La combinación de "spike en el Panel 3 en el paso 1-5" + "el Panel 2 empieza en lr_max" es indicadora única de warmup ausente. Bad-init produce excursiones persistentes de magnitud en el Panel 4; máscara-rota no produce spike en el Panel 3 (los gradientes son sanos para la tarea fácil).
Por qué este es un bug del mundo real¶
Casos del mundo real: alguien usa una librería de schedules que no tiene warmup por default. O warmup_steps está configurado pero la variable se lee después de construir el optimizador (así usa el default = 0). El Panel 2 del dashboard pilla esto de un vistazo.
Fallo 3: máscara causal rota (mask = ones, no tril)¶
La corrupción¶
Reemplaza la máscara causal triangular inferior con una máscara entera de true:
# Healthy:
causal_mask = np.tril(np.ones((L, L), dtype=bool))
# Broken:
causal_mask = np.ones((L, L), dtype=bool)
Cada token puede ahora atender a tokens futuros. El entrenamiento se vuelve una tarea más fácil: predecir \(y_l\) desde \(x_0, x_1, \ldots, x_L\) (incluyendo \(x_{l+1} = y_l\) en la entrada).
Qué pasa por dentro¶
En tiempo de entrenamiento, el modelo "hace trampas" — en la posición \(l\) la propia entrada contiene \(x_{l+1} = y_l\). El modelo aprende a copiar del futuro, lo cual es trivial. La pérdida de entrenamiento baja a casi 0 en 100-200 pasos.
En tiempo de validación, la misma máscara rota está en uso. La pérdida de val también parece baja. La máscara está rota tanto en train como en val — así que el bug es consistente, simplemente erróneo.
Pero: el modelo no ha aprendido a predecir desde el pasado. Ha aprendido un mapeo identidad. Cuando se despliega para generación (Fase 21), genera basura — en tiempo de inferencia, los tokens futuros no existen, la máscara es materialmente distinta y el modelo no tiene ni idea de qué hacer.
Firma en el dashboard (¡sutil!)¶
- El Panel 1 (curvas de pérdida) muestra train acercándose a 0 rápido. En 100 pasos, la pérdida de entrenamiento está por debajo de 0.1. Si tu modelo tiene ~103k parámetros y el corpus cientos de ejemplos, el modelo no puede memorizar legítimamente el corpus tan rápido — salvo que esté resolviendo una tarea trivial.
- La pérdida de val sigue de cerca a la de train (ambas acercándose a 0). Esta es la parte sospechosa. El overfitting real muestra train ≪ val; la máscara rota muestra train ≈ val, ambas muy pequeñas. El modelo no está haciendo overfitting al training set; está resolviendo una tarea más fácil que se aplica igual de bien a val.
- El Panel 6 (cabezas muertas) a menudo muestra conteos altos de cabezas muertas. Por qué: cuando la máscara permite atención al futuro, muchas cabezas pueden resolverse trivialmente con "atiende a la posición +1, copia". Solo una cabeza necesita hacer esto; las otras son redundantes y mueren efectivamente.
- Los paneles 2, 3, 4, 5 se ven mayormente normales. Esta es la trampa — cinco de seis paneles parecen bien.
Pista diagnóstica¶
La firma es panel 1 + panel 6 juntos: pérdida demasiado baja, conteo de cabezas muertas anormalmente alto. Cualquiera por sí sola es ambigua. Juntos, "o el modelo es demasiado bueno, o está resolviendo la tarea equivocada" y "muchas cabezas son redundantes", es el diagnóstico de máscara rota.
La otra heurística: si la pérdida parece demasiado buena para ser cierta, lo es. Calcula la entropía del corpus. La pérdida del modelo no puede bajar por debajo de esa entropía salvo que algo esté mal.
Por qué este es un bug del mundo real¶
Casos del mundo real: alguien activa lógica de KV-cache que asume una convención de máscara distinta. O una implementación de atención donde la máscara se aplica en el sitio equivocado (aditiva -inf vs multiplicativa 0). Este es uno de los bugs de atención más comunes en código publicado; el currículo lo convierte en lección de la Fase 19 porque diagnosticarlo más tarde, en la Fase 21 o más allá, es mucho más caro.
Cómo difieren los tres dashboards rotos de un vistazo¶
| Panel | Sano | Fallo 1 (init) | Fallo 2 (warmup) | Fallo 3 (mask) |
|---|---|---|---|---|
| 1 Loss | Descenso suave | NaN al paso 30 | Forma de V al paso 5 | Se acerca a 0 (sospechosamente) |
| 2 LR | Warmup luego cosenoidal | Da igual (NaN) | Empieza en lr_max | Sano |
| 3 Grad norm | Estable, a veces clipeado | Explota inmediatamente | Spike al paso 1-5 | Sano |
| 4 Activaciones | Estables | Ya enormes al paso 0 | Spikes transitorias | Sano |
| 5 Spectral | Estable | Explota | Excursión breve | Sano |
| 6 Dead | ~5% | Efectivamente todas (post-NaN) | Sano tras recuperarse | Altas en atención |
El algoritmo de diagnóstico es: qué panel chilla primero y qué forma tiene el chillido.
Resumen en un párrafo¶
Tres breaks diseñados producen cada uno una firma única en el dashboard. Bad init: el Panel 4 ya muestra magnitudes de activación enormes en el paso 0; la pérdida hace NaN en 30 pasos. Sin warmup: el Panel 2 empieza en lr_max en lugar de 0, el Panel 3 hace spike en el paso 1-5, la pérdida se recupera. Máscara rota: los Paneles 1 y 6 juntos — la pérdida es sospechosamente baja y el conteo de cabezas muertas inusualmente alto. Interioriza estas tres firmas y te protegerán a lo largo de todas las fases de entrenamiento futuras del currículo.
Siguiente: lab/00-instrument-hooks.md.