Lokale Gradienten multiplizieren sich
Der Forward-Pass
ANDREA-120M's Forward-Pass führt den Input durch eine Sequenz von Operationen:
x = embed(token_ids) # Token-Einbettungen
for layer in 12_layers:
x = x + attn(LN(x)) # Attention-Unterschicht
x = x + mlp(LN(x)) # MLP-Unterschicht
logits = LN(x) @ embed.T # Gebündelte Ausgabprojektion
loss = cross_entropy(logits, targets)
Jede Operation liest Eingabetensoren und erzeugt Ausgabetensoren. Der Forward-Pass endet in einem einzelnen Skalar: dem Cross-Entropy-Verlust für diesen Batch.
Der Backward-Pass
Das Training aktualisiert die Gewichte in der Richtung, die den Verlust verringert. Um Aktualisierungsrichtungen zu erhalten, benötigt der Engine:
dL/dW für jedes lernbare W im Modell
Die Kettenregel liefert dies. Für eine Kette loss = f(g(h(x))):
dL/dx = (dL/df) * (df/dg) * (dg/dh) * (dh/dx)
Jeder Faktor ist ein lokaler Gradient: wie sich die Ausgabe einer Operation ändert, wenn sich ihr Eingang um einen kleinen Betrag ändert. Das Multiplizieren der lokalen Gradienten rückwärts durch das Diagramm propagiert das Verlustsignal zu jedem Gewicht.
Reverse-Mode-Differenzierung
Backprop berechnet Gradienten in umgekehrter Reihenfolge: beginnend bei dL/dlogits = 1, dann rückwärts durch Cross-Entropy, dann Output-Projektion, dann Layer Norm, dann zwölf Transformer-Blöcke, dann Embeddings. Bei jedem Schritt multipliziert man den eingehenden Gradienten mit dem lokalen Jakobianer.
Reverse-Mode ist effizient, wenn die Ausgabe ein einzelner Skalar (der Verlust) ist & es viele Eingaben gibt (die Gewichte). Ein rückwärtsgerichteter Durchgang erzeugt Gradienten für jedes Gewicht im Modell. Forward-Mode würde einen Durchgang pro Gewicht benötigen; für ANDREA-120M mit ~120M Gewichten ist Forward-Mode undurchführbar.
Warum Reverse-Mode
Jede Forward-Operation bekommt ein Backward-Zwillingspaar
Die Pairing-Disziplin
microgpt_cuda.cu liefert zwei CUDA-Kernel für jede Operation: einen, der die Forward-Ausgabe berechnet, einen, der Eingabegradienten gegeben Ausgabegradienten berechnet. Die Zuordnung ist eins-zu-eins:
| Forward-Kernel | Backward-Kernel | Operation |
|---|---|---|
k_embed_fwd | k_embed_bwd | Token-Embedding-Suche |
k_layernorm_fwd | k_layernorm_bwd | Layer-Normalisierung |
k_attn_qkv_fwd | k_attn_qkv_bwd | Q-, K-, V-Projektionen |
k_attn_fwd | k_attn_bwd | Skalierte Dot-Product-Attention |
k_attn_out_fwd | k_attn_out_bwd | Ausgabeprojektion W_O |
k_mlp_fwd | k_mlp_bwd | MLP (mit GELU) |
k_residual_add | k_residual_add_bwd | Residualverbindung |
k_loss_fwd | k_loss_bwd | Kreuzentropie-Verlust |
Acht Operationenpaare decken den vollständigen Transformer ab. Plus einige Hilfskerne: k_grad_norm_partial, k_grad_norm_final, k_grad_scale für Gradienten-Clipping (siehe Aktivität 75).
Die Aufgabe eines Backward-Kerns
Gegeben den Gradienten, der aus späteren Schichten einfließt (grad_output), berechnet ein Backward-Kern:
1. grad_input: der Gradient bezüglich des Eingabetensors der Operation. Dieser wird weiter rückwärts weitergegeben.
2. grad_weight: der Gradient bezüglich der lernbaren Parameter in der Operation. Dieser geht in den Optimizer-Zustand.
Beide werden in einem einzigen Kernel-Launch berechnet. CUDA-Threads arbeiten parallel an Tiles des Gradient-Tensors zusammen.
Gespeicherte Tensoren
Die Rückwärtsberechnung benötigt oft Werte aus dem Vorwärts-Pass. Zum Beispiel benötigt k_layernorm_bwd den Mittelwert & die Varianz, die während des Vorwärts-Passes berechnet wurden; k_mlp_bwd benötigt die GELU-Präaktivierung. Der Trainings-Engine speichert diese während des Vorwärts-Passes in dedizierten Buffern und liest sie während des Rückwärts-Passes.
Speicherkosten: ungefähr dieselbe Form wie die Forward-Ausgabe für jeden gespeicherten Tensor. Für ANDREA-120M mit batch=8, seq=1024, d_model=768 beträgt ein gespeicherter Tensor 8 × 1024 × 768 × 4 bytes = 25 MB. Über 12 Schichten & mehrere gespeicherte Tensoren pro Schicht dominieren Aktivierungen den VRAM während des Trainings (~5-10 GB auf einer 24 GB Karte).
Tracing eines Backward-Schritts
Wo Gradienten im Speicher leben
Ein Gradient-Tensor pro Gewichtstensor
Jeder lernbare Gewichtstensor in ANDREA-120M hat einen passenden Gradiententensor gleicher Form. Für jeden Block:
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]
Plus Token-Embeddings, Positions-Embeddings & eine finale Layer-Norm. Der Gesamtspeicher des Gradientenpuffers entspricht dem Gewichtsspeicher: ~120M Floats, ~480 MB bei FP32, ~240 MB bei FP16.
Akkumulation über Mikrobatches
ANDREA's batch_size = 8 passt in VRAM bei FP16. Größere effektive Batches erfordern Gradientenakkumulation: Mehrere Forward+Backward-Pässe auf kleinen Batches ausführen, Gradienten in denselben Puffer summieren, dann einen Optimizer-Schritt ausführen.
for microbatch in range(n_microbatches):
forward(microbatch)
backward() # FÜGT zu Grad-Puffern hinzu, überschreibt nicht
scale_grads(1.0 / n_microbatches) # Mittelwert über Microbatches
optimizer_step()
zero_grads() # Zurücksetzen für nächsten Trainingsschritt
Backward-Kernel verwenden +=-Semantik, nicht =. Jeder Aufruf fügt Gradientenbeiträge zum bestehenden Puffer hinzu; der Puffer hält die laufende Summe, bis zero_grads() ihn löscht.
Der Optimizer-Zustand
AdamW (Aktivität 73) speichert zwei weitere Puffer pro Gewicht: ersten Moment m & zweiten Moment v. Gesamter Trainingszeit-Speicher:
Gewichte: 1× Gewichtsanzahl
Gradienten: 1× Gewichtsanzahl
Adam m: 1× Gewichtsanzahl
Adam v: 1× Gewichtszahl
gespeicherte Aktivierungen: ~2-4× je nach Schichten & Batch
──────────────────────────────────────────
gesamt: ~6-8× Gewichtszahl
ANDREA-120M bei FP16: ~240 MB × 4 Puffer (Gewichte, Gradienten, m, v) + ~5-10 GB Aktivierungen = ~10-12 GB insgesamt. Bequem unter der 24-GB-Grenze der RTX 4090. ANDREA-12M trainiert in 1,4 GB; die 10× Parameter-Skalierung bringt ~10× Speicher.
Größe der Gradientenpuffer
Vollständige Kontrolle über Speicher & Präzision
Was generische Frameworks kosten
PyTorch & JAX machen Autograd bequem: Python-Code schreiben, Gradienten automatisch erhalten. Der Preis: eine generische Dispatch-Schicht zwischen Ihrem Code & CUDA. Jede Operation geht durch Python-Interpreter-Overhead, Framework-Buchhaltung & dynamische Kernel-Auswahl. Beim Training eines kleinen Sprachmodells auf einer GPU ist dieser Overhead relevant.
Konkrete Kosten, die ANDREA vermeidet:
1. Python-Interpreter-Latenz. Jeder PyTorch-Operator überschreitet die Python/C++-Grenze. Bei ~100 Kernel-Starts pro Trainingsschritt und ~9 Schritten/Minute sind das ~900 Grenzüberschreitungen pro Minute. Dispatch auf C-Ebene eliminiert dies.
2. Unvorhersehbare Framework-Allocator. Der Caching-Allocator von PyTorch liefert durchschnittlich gute Durchsatzraten, aber unvorhersehbare Spitzen im Speicherverbrauch. Der Trainings-Engine von ANDREA reserviert jeden Buffer beim Start vorab; keine Neuzuweisung während des Trainings, keine Fragmentierung, keine überraschenden OOMs beim Schritt 100K.
3. Generische Kernel-Auswahl. PyTorch wählt Kerne zur Laufzeit über Heuristiken aus. ANDREA wählt Kerne zur Kompilierzeit, abgestimmt auf RTX 4090 Tensor-Core-Tile-Größen.
4. Gemischte-Präzisions-Plumbing. Der FP16 cuBLAS-Pfad von ANDREA-120M & die FP8 E4M3 Tensor-Core-Experimente von ANDREA erfordern präzise Kontrolle darüber, welche Tensoren in welcher Präzision leben. Generische Frameworks stellen diese Kontrolle über geschichtete APIs bereit; benutzerdefinierte CUDA-Implementierungen schreiben sie direkt.
Der Tradeoff
Kosten von benutzerdefiniertem CUDA: mehr Code zum Schreiben, mehr Bugs zum Finden, kein Community-Ökosystem. ANDREA's microgpt_cuda.cu umfasst ~6000 Zeilen handgeschriebenes CUDA, das Monate zum Debuggen brauchte. Jede neue Operation erfordert das Schreiben eines Forward-Kernels, eines Backward-Kernels & Tests.
Was ANDREA gewinnt:
- Vollständige Reproduzierbarkeit. Der Trainings-Pipeline besteht aus einem C-Binary plus einem Python-Proxy. Kein Versionsdrift bei PyTorch-Releases, keine CUDA-Versionen-Inkompatibilitäten mit Framework-Wheels.
- Bit-exakte Fortsetzungen. SIGTERM löst ein Checkpoint-Schreiben aus, das jeden Tensor genau so erfasst, wie die GPU ihn sieht. Die Fortsetzung nimmt den gleichen Loss-Verlauf auf, auf dem der Lauf war.
- Vorhersehbares Gedächtnis. ANDREA-120M für 200K Schritte trainiert ohne OOMs. Das Gedächtnis wurde beim Engine-Start berücksichtigt.
- Direkter Hardwarezugriff. Tensor-Core-Tile-Größen, FP8 E4M3-Einstellungen, asynchrone Speicherkopien: alles direkt in CUDA adressierbar, undurchsichtig in generischen Frameworks.
Reproduzierbarkeit als Mission
Der ANDREA-Whitepaper-Abschnitt 9 listet den vollständigen Reproduzierbarkeits-Stack auf:
Training-Engine: microgpt/microgpt_cuda.cu
Training-Proxy: microgpt/training_proxy.py
Experiment-Konfigurationen: experiments/ANDREA-*-TRAIN.json
Daten-Pipeline: scripts/pull-hermes3.py, scripts/prep-megachat.py
Dashboard: scripts/live-loss-dashboard.html
Bandit-Spezifikation: docs/FIREHOSE-BANDIT.md
Modelldokumentation: docs/ANDREA.md
Hardwareanforderung: eine NVIDIA GPU mit ≥8 GB VRAM (RTX 3060 oder besser). Jeder kann ANDREA-12M aus diesen Artefakten reproduzieren. Der benutzerdefinierte CUDA-Pfad ist Teil des Grundes: keine Framework-Versionen einfrieren, keine Abhängigkeitsüberraschungen in fünf Jahren.
Signale & Checkpoints
Die CUDA-Trainings-Schleife reagiert auf zwei POSIX-Signale:
- SIGTERM: Schreibe einen sofortigen Checkpoint, dann beende. Wird verwendet, um das Training sauber zu stoppen.
- SIGUSR1: Schreibt einen sofortigen Checkpoint, fährt mit dem Training fort. Wurde während des Polish-Pivots in v3 verwendet, um den Zustand ohne Unterbrechung des Laufs zu erfassen.
Checkpoint-Format: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. Schrittzähler, Anzahl der Gewichte, dann Gewichte gefolgt von Adam-Momenten. Fährt bit-genau fort. Der Proxy archiviert .samples.json & .state.json separat bei Polish; .loss.json wird nie archiviert (es akkumuliert die vollständige Trainingshistorie).