English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

invitado
1 / ?

Los Gradientes Locales se Multiplican

Kernels Forward y Backward


La Pasada Hacia Adelante

La pasada hacia adelante de ANDREA-120M recorre la entrada a través de una secuencia de operaciones:


x = embed(token_ids)         # incrustaciones de tokens
for layer in 12_layers:
x = x + attn(LN(x))      # subcapa de atención
x = x + mlp(LN(x))       # subcapa MLP
logits = LN(x) @ embed.T     # proyección de salida compartida
loss   = cross_entropy(logits, targets)

Cada operación lee tensores de entrada y produce tensores de salida. La pasada hacia adelante termina en un escalar único: la pérdida de entropía cruzada para este lote.


La Pasada Hacia Atrás

El entrenamiento actualiza los pesos en la dirección que disminuye la pérdida. Para obtener las direcciones de actualización, el motor necesita:


dL/dW para cada W entrenable en el modelo

La regla de la cadena lo proporciona. Para una cadena loss = f(g(h(x))):


dL/dx = (dL/df) * (df/dg) * (dg/dh) * (dh/dx)

Cada factor es un gradiente local: cómo cambia la salida de una operación cuando su entrada cambia por una pequeña cantidad. Multiplicar gradientes locales hacia atrás a través del grafo propaga la señal de pérdida a cada peso.


Diferenciación en Modo Reverso

Backprop calcula gradientes en orden reverso: comenzando desde dL/dlogits = 1, luego retrocediendo a través de la entropía cruzada, luego la proyección de salida, luego la normalización de capa, luego doce bloques transformer, luego las incrustaciones. En cada paso, multiplica el gradiente entrante por el Jacobiano local.


El modo reverso es eficiente cuando la salida es un escalar único (la pérdida) y hay muchas entradas (los pesos). Una pasada hacia atrás produce gradientes para cada peso en el modelo. El modo forward necesitaría una pasada por peso; para ANDREA-120M con ~120M pesos, el modo forward es inviable.

Por qué Modo Reverso

ANDREA-120M tiene ~120M pesos y produce una única pérdida escalar por paso de entrenamiento. Compara la diferenciación automática en modo reverso contra el modo forward. Indica (1) qué modo produce todos los gradientes de pesos en una única pasada hacia atrás; (2) cuántas pasadas en modo forward serían necesarias para calcular todos los 120M gradientes de pesos; (3) qué modo usa ANDREA y por qué.

Cada Op Forward Obtiene Su Gemelo Backward

La Disciplina de Emparejamiento

microgpt_cuda.cu incluye dos kernels CUDA para cada operación: uno que calcula la salida forward, uno que calcula los gradientes de entrada dados los gradientes de salida. El emparejamiento es uno a uno:


Kernel forwardKernel backwardOperación
k_embed_fwdk_embed_bwdBúsqueda de embedding de token
k_layernorm_fwdk_layernorm_bwdNormalización de capa
k_attn_qkv_fwdk_attn_qkv_bwdProyecciones Q, K, V
k_attn_fwdk_attn_bwdAtención producto punto escalado
k_attn_out_fwdk_attn_out_bwdProyección de salida W_O
k_mlp_fwdk_mlp_bwdMLP (con GELU)
k_residual_addk_residual_add_bwdConexión residual
k_loss_fwdk_loss_bwdPérdida de entropía cruzada

Ocho pares de operaciones cubren el transformer completo. Más unos pocos kernels de utilidad: k_grad_norm_partial, k_grad_norm_final, k_grad_scale para el recorte de gradientes (ver actividad 75).


El Trabajo de un Kernel Backward

Dado el gradiente que fluye desde las capas posteriores (grad_output), un kernel backward calcula:


1. grad_input: el gradiente con respecto al tensor de entrada de la operación. Esto se pasa más hacia atrás.

2. grad_weight: el gradiente con respecto a los parámetros entrenables en la operación. Esto va al estado del optimizador.


Ambos se computan en un solo lanzamiento de kernel. Los hilos CUDA cooperan en teselas del tensor de gradiente en paralelo.


Tensores Guardados

La computación hacia atrás a menudo necesita valores del pase hacia adelante. Por ejemplo, k_layernorm_bwd necesita la media y varianza computadas durante el forward; k_mlp_bwd necesita la pre-activación GELU. El motor de entrenamiento los almacena en buffers dedicados durante el forward, luego los lee durante el backward.


Costo de memoria: aproximadamente la misma forma que la salida del forward para cada tensor guardado. Para ANDREA-120M con batch=8, seq=1024, d_model=768, un tensor guardado es 8 × 1024 × 768 × 4 bytes = 25 MB. A través de 12 capas & múltiples tensores guardados por capa, las activaciones dominan la VRAM durante el entrenamiento (~5-10 GB en una tarjeta de 24 GB).

Trazado de Un Paso de Backward

ANDREA-120M completa un pase forward a través de un bloque transformer. Traza qué sucede durante el pase backward a través del mismo bloque (en estructura pre-norm: `x = x + Attention(LN(x))` luego `x = x + MLP(LN(x))`). Nombra los kernels backward en el orden en que se ejecutan, & indica con qué kernel forward se empareja cada uno. Cubre al menos 4 kernels.

Dónde Viven los Gradientes en Memoria

Un Tensor de Gradiente por Tensor de Peso

Cada tensor de peso entrenable en ANDREA-120M tiene un tensor de gradiente correspondiente de forma idéntica. Para cada bloque:


W_Q       [768, 768]     ↔   grad_W_Q       [768, 768]
W_K       [768, 768]     ↔   grad_W_K       [768, 768]
W_V       [768, 768]     ↔   grad_W_V       [768, 768]
W_O       [768, 768]     ↔   grad_W_O       [768, 768]
W_1       [768, 3072]    ↔   grad_W_1       [768, 3072]
W_2       [3072, 768]    ↔   grad_W_2       [3072, 768]
LN1.gamma [768]          ↔   grad_LN1.gamma [768]
LN1.beta  [768]          ↔   grad_LN1.beta  [768]
LN2.gamma [768]          ↔   grad_LN2.gamma [768]
LN2.beta  [768]          ↔   grad_LN2.beta  [768]

Más token embeddings, position embeddings y una layer norm final. La memoria total del buffer de gradientes coincide con la memoria de pesos: ~120M floats, ~480 MB en FP32, ~240 MB en FP16.


Acumulación a Través de Micro-lotes

El batch_size = 8 de ANDREA cabe en VRAM en FP16. Lotes efectivos más grandes requieren acumulación de gradientes: ejecutar múltiples pases forward+backward en lotes pequeños, sumando gradientes en el mismo buffer, luego realizar un paso del optimizador.


for microbatch in range(n_microbatches):
forward(microbatch)
backward()           # AÑADE a los buffers de gradientes, no sobrescribe
scale_grads(1.0 / n_microbatches)  # promedia entre microbatches
optimizer_step()
zero_grads()             # reinicia para el siguiente paso de entrenamiento

Los kernels de backward usan semántica +=, no =. Cada llamada añade contribuciones de gradiente al buffer existente; el buffer mantiene la suma acumulada hasta que zero_grads() lo limpia.


El Estado del Optimizador

AdamW (actividad 73) mantiene dos buffers más por peso: primer momento m y segundo momento v. Memoria total durante el entrenamiento:


pesos:    1× conteo de pesos
gradientes:  1× conteo de pesos
Adam m:     1× conteo de pesos
Adam v:     1× conteo de pesos
acts guardados: ~2-4× dependiendo de capas & lote
──────────────────────────────────────────
total:      ~6-8× conteo de pesos

ANDREA-120M en FP16: ~240 MB × 4 buffers (peso, grad, m, v) + ~5-10 GB activaciones = ~10-12 GB total. Cómodamente por debajo del límite de 24 GB de la RTX 4090. ANDREA-12M entrenado en 1.4 GB; el escalado de parámetros 10× trae ~10× memoria.

Dimensionamiento de Buffers de Gradiente

ANDREA-120M tiene ~120,000,000 pesos y usa acumulación de gradientes a través de 4 micro-lotes por paso de entrenamiento. Calcula: (a) tamaño del buffer de gradientes en MB a FP16; (b) memoria total para pesos + gradientes + Adam m + Adam v a FP16; (c) cuántas llamadas separadas a `forward()` + `backward()` se ejecutan por paso de entrenamiento. Muestra tu aritmética.

Control total de memoria y precisión

Qué cuestan los frameworks genéricos

PyTorch y JAX hacen que autograd sea conveniente: escribe código Python, obtén gradientes automáticamente. El costo: una capa de despacho genérica entre tu código y CUDA. Cada operación pasa por sobrecarga del intérprete Python, contabilidad del framework y selección dinámica de kernels. Para entrenar un modelo de lenguaje pequeño en una GPU, esa sobrecarga importa.


Costos concretos que ANDREA evita:


1. Latencia del intérprete de Python. Cada operación de PyTorch cruza el límite Python/C++. Para ~100 lanzamientos de kernels por paso de entrenamiento a ~9 pasos/min, eso son ~900 cruces de límite por minuto. El despacho a nivel C elimina esto.


2. Impredecibilidad del asignador del framework. El asignador de caché de PyTorch da buen rendimiento promedio pero memoria máxima impredecible. El motor de entrenamiento de ANDREA pre-asigna cada buffer al inicio; no reasignación durante el entrenamiento, no fragmentación, no OOM sorpresa en el paso 100K.


3. Selección genérica de kernels. PyTorch elige kernels en tiempo de ejecución vía heurísticas. ANDREA elige kernels en tiempo de compilación, ajustados a los tamaños de tesela del tensor core de RTX 4090.


4. Fontanería de precisión mixta. La ruta cuBLAS FP16 de ANDREA-120M y los experimentos de núcleos tensoriales FP8 E4M3 de ANDREA requieren un control preciso sobre qué tensores viven en qué precisión. Los frameworks genéricos exponen este control a través de APIs en capas; las escrituras personalizadas de CUDA lo escriben directamente.


El Compromiso

Costos de CUDA personalizado: más código para escribir, más errores para encontrar, sin ecosistema comunitario. El archivo microgpt_cuda.cu de ANDREA tiene ~6000 líneas de CUDA escrito a mano que tomó meses depurar. Cada nueva operación requiere escribir un kernel forward, un kernel backward y pruebas.


Lo que gana ANDREA:


- Reproducibilidad total. El pipeline de entrenamiento es un binario C más un proxy de Python. Sin deriva de versiones entre lanzamientos de PyTorch, sin desajustes de versión de CUDA con las ruedas del framework.

- Reanudaciones bit-exactas. SIGTERM activa la escritura de un checkpoint que captura cada tensor exactamente como lo ve la GPU. La reanudación retoma la misma trayectoria de pérdida en la que estaba la ejecución.

- Memoria predecible. ANDREA-120M entrenado durante 200K pasos sin OOM. La memoria se contabilizó al inicio del motor.

- Acceso directo al hardware. Tamaños de teselas del tensor core, configuraciones FP8 E4M3, copias de memoria asíncronas: todo directamente direccionable en CUDA, opaco en frameworks genéricos.


Reproducibilidad Como Misión

La sección 9 del whitepaper de ANDREA lista la pila completa de reproducibilidad:


Motor de entrenamiento: microgpt/microgpt_cuda.cu
Proxy de entrenamiento: microgpt/training_proxy.py
Configuraciones de experimentos: experiments/ANDREA-*-TRAIN.json
Pipeline de datos: scripts/pull-hermes3.py, scripts/prep-megachat.py
Panel de control: scripts/live-loss-dashboard.html
Especificación de Bandit: docs/FIREHOSE-BANDIT.md
Documentación del modelo: docs/ANDREA.md

Requisito de hardware: una GPU NVIDIA con ≥8 GB de VRAM (RTX 3060 o mejor). Cualquiera puede reproducir ANDREA-12M a partir de estos artefactos. El camino CUDA personalizado es parte de por qué: no hay congelamientos de versión de framework, no hay sorpresas de dependencias dentro de cinco años.


Señales & Checkpoints

El bucle de entrenamiento CUDA responde a dos señales POSIX:


- SIGTERM: escribe un checkpoint inmediato, luego sale. Se usa al detener el entrenamiento de manera limpia.

- SIGUSR1: escribe un checkpoint inmediato, continúa el entrenamiento. Usado durante el pivote de pulido en v3 para capturar el estado sin interrumpir la ejecución.


Formato de checkpoint: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. Contador de pasos, conteo de pesos, luego pesos seguidos de momentos de Adam. Reanuda bit-exactamente. El proxy archiva .samples.json & .state.json por separado en pulido; .loss.json nunca se archiva (acumula el historial completo del entrenamiento).

Por Qué No PyTorch

ANDREA podría haber usado el autograd de PyTorch en lugar de escribir `microgpt_cuda.cu` a mano. Da dos razones de ingeniería distintas por las que ANDREA eligió CUDA personalizado. Una razón debe referirse al control de memoria o precisión; la otra debe referirse a reproducibilidad, dependencias de framework o mantenimiento a largo plazo.