Skip to content

English · Español

Break — Op in-place sobre un tensor que requiere grad; muestra la rotura del grafo de autograd

Una operación in-place (add_, mul_, relu_) sobre un tensor con requires_grad=True rompe la cinta de autograd. PyTorch a veces lo detecta y lanza un error claro; a veces produce gradientes incorrectos en silencio. Lo causamos a propósito en el mini-GPT y vemos ambas variantes.


Síntoma que verá Borja

Dos implementaciones de la conexión residual del mini-GPT dentro de un bloque transformer:

  • Ejecución A (control):

    h = h + self.attn(self.ln1(h))   # out-of-place
    h = h + self.ffn(self.ln2(h))    # out-of-place
    

  • Ejecución B (break):

    h.add_(self.attn(self.ln1(h)))   # in-place
    h.add_(self.ffn(self.ln2(h)))    # in-place
    

Las salidas del forward son numéricamente idénticas (la op in-place escribe los mismos valores). Pero al hacer loss.backward():

  • Ejecución A: se completa con normalidad. Los gradientes fluyen. El entrenamiento continúa.
  • Ejecución B (variante 1, PyTorch ≥ 1.5 con torch.autograd.set_detect_anomaly(True)): lanza
RuntimeError: one of the variables needed for gradient computation has been
modified by an inplace operation: [torch.FloatTensor [1, 8, 64]], which is
output 0 of NativeLayerNormBackward, is at version 2; expected version 0
instead.
  • Ejecución B (variante 2, sin detección de anomalías): puede computar en silencio gradientes erróneos (si un tensor guardado era la entrada de la op in-place) o crashear con un error menos informativo.

Si la detección de anomalías está desactivada y la op in-place resulta sobrescribir un tensor cuyo valor de forward el grafo de autograd guardó, el backward computa el gradiente erróneo usando el valor post-sobrescritura. El entrenamiento continúa pero converge a un mínimo distinto (erróneo). La precisión en test es peor que la baseline en 3-10%, pero la ejecución en sí no lanza error.

El break, mecánicamente

Sustituye h = h + ... por h.add_(...) en src/minigpt/block.py. O cambia F.relu(x) a F.relu_(x). O cambia x.exp() a x.exp_(). Cualquiera de éstas es el break.

La versión mínima:

# In `src/minigpt/block.py`, replace:
h = h + self.attn(self.ln1(h))
# With:
h.add_(self.attn(self.ln1(h)))

Por qué esto enseña el concepto

El autograd de PyTorch funciona guardando los valores exactos del tensor del forward que el backward necesita. Por ejemplo:

  • NativeLayerNormBackward guarda la entrada del LN (porque el gradiente de LN depende de ella).
  • La siguiente op (self.attn) lee esta salida de LN y produce una salida de atención.
  • La suma residual (h + attn_output) va seguida de otro LN (self.ln2), que guarda su entrada — que es la suma residual.

Si la suma residual se calcula in-place como h.add_(attn_output), PyTorch actualiza el almacenamiento subyacente de h. Pero:

  • El tensor guardado por el LN anterior también apuntaba al almacenamiento de h (porque PyTorch guarda referencias, no copias, por eficiencia de memoria).
  • La actualización in-place cambia el valor guardado.
  • Durante el backward, cuando el backward() del LN anterior lee su entrada guardada, obtiene el valor nuevo, no el original.

PyTorch detecta esto mediante un contador de versión sobre el almacenamiento de cada tensor. Cada op in-place incrementa el contador. Cuando el backward lee un tensor guardado, comprueba el contador contra el valor en el momento de guardar. Discrepancia → RuntimeError.

Sin detección de anomalías, la comprobación de versión sigue ocurriendo en el backward, pero sólo para ops específicas que comprueban explícitamente. Algunas ops (raras) no comprueban; si te topas con una, obtienes gradientes erróneos en silencio.

La lección:

  1. Las ops in-place son una optimización (ahorran memoria reutilizando almacenamiento).
  2. Son inseguras para cualquier tensor cuyo valor de forward sea necesario en el backward.
  3. La red de seguridad de PyTorch atrapa la mayoría de los casos pero no todos.
  4. La disciplina: prefiere ops out-of-place salvo que hayas auditado que ningún tensor necesario para el backward depende del almacenamiento.

Escalera de diagnóstico que Borja debería recorrer

  1. Primera comprobación: el mensaje de error (si la detección de anomalías está activada). Nombra la variable, la versión esperada vs obtenida, y apunta a la op originadora. Éste es el diagnóstico más rápido.
  2. Segunda comprobación: activa la detección de anomalías si no está activada: torch.autograd.set_detect_anomaly(True) al principio del script de entrenamiento.
  3. Tercera comprobación: busca en el codebase ops con sufijo _ (guion bajo) sobre tensores con requires_grad. El grep es mecánico.
  4. Cuarta comprobación (si los errores son silenciosos): compara los valores de gradiente entre la Ejecución A y la Ejecución B. Diferirán.
  5. Diagnóstico: una op in-place sobre un tensor que está guardado para el backward por algún nodo aguas arriba.

Reproductor

# Control
just phase-25-train inplace=false

# Break (with anomaly detection: clear error)
just phase-25-train inplace=true detect_anomaly=true

# Break (silent variant)
just phase-25-train inplace=true detect_anomaly=false

# Compare gradients
just phase-25-grad-compare experiments/25-A experiments/25-B

Cascada de pistas

  1. (Suave) "El error menciona 'version 2; expected version 0'. ¿Qué cambia la versión de un tensor?"
  2. (Media) "Busca cualquier llamada a método *_ (sufijo guion bajo) en el código del modelo."
  3. (Directa) "La suma residual es h.add_(...). PyTorch guarda el valor pre-suma de h para el backward del LayerNorm aguas arriba; tu suma in-place lo sobrescribe."

Fix

Reemplaza h.add_(x) por h = h + x. O h = h.clone(); h.add_(x) si necesitas específicamente la mutación in-place por alguna razón aguas abajo (raro).

En general, nunca uses ops in-place sobre tensores con requires_grad=True salvo que tengas una razón muy específica y hayas auditado el grafo.

Cuándo el in-place SÍ es seguro

  • Paso del optimizador. param.data.add_(grad, alpha=-lr) — el step() del optimizador está fuera del grafo de autograd (los gradientes ya se han computado). Dentro del cuerpo de optimizer.step(), in-place es correcto y ahorra memoria.
  • Activaciones durante inferencia. with torch.no_grad(): desactiva el tracking de grad; in-place es seguro.
  • Un tensor fresco sin historial de autograd. Un torch.zeros(...) está bien mutarlo.

El peligro es: in-place sobre un tensor del que depende el grafo de autograd. El arreglo no es "nunca uses add_" — es "usa add_ con conocimiento".

Lo que este break NO es

  • No es un bug de precisión numérica.
  • No es un bug arquitectónico.
  • No es un bug de hiperparámetros.

Es un peligro del grafo de autograd. Específico de PyTorch (y otros frameworks con autograd dinámico similar). No es un peligro en NumPy (sin autograd) o en frameworks "compila-luego-ejecuta" (los gradientes se derivan simbólicamente).

Por qué éste es el break relevante para el grammar-tutor §A13

Cuando implementes el agente tutor de gramática de la Fase 32 en PyTorch, te sentirás tentado a usar ops in-place porque el modelo es pequeño y la memoria parece gratis. La tentación es especialmente alta en el código de manejo de acción / observación que escribirás fresco. Este break es la advertencia: no cuesta nada a escala §A13 usar ops out-of-place; te cuesta una sesión de debugging cada seis meses usar ops in-place descuidadamente.

Cross-refs

  • theory/04-autograd-tape-walk-mini-gpt.md — la cinta que el break corrompe.
  • theory/02-autograd-engine.md — cómo funcionan los tensores guardados.
  • Fase 8 — el autograd hecho a mano que escribiste antes de PyTorch; este break también lo rompería.