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

un

Gast
1 / ?

Lokale Gradienten multiplizieren sich

Forward & Backward Kernels


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

ANDREA-120M hat ~120M Gewichte & produziert einen einzelnen skalaren Loss pro Trainingsschritt. Vergleichen Sie Reverse-Mode Automatic Differentiation mit Forward-Mode. Nennen Sie (1) welcher Modus alle Gewichtsgradienten in einem einzigen Backward-Pass produziert; (2) wie viele Forward-Mode-Pässe benötigt würden, um alle 120M Gewichtsgradienten zu berechnen; (3) welchen Modus ANDREA verwendet & warum.

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-KernelBackward-KernelOperation
k_embed_fwdk_embed_bwdToken-Embedding-Suche
k_layernorm_fwdk_layernorm_bwdLayer-Normalisierung
k_attn_qkv_fwdk_attn_qkv_bwdQ-, K-, V-Projektionen
k_attn_fwdk_attn_bwdSkalierte Dot-Product-Attention
k_attn_out_fwdk_attn_out_bwdAusgabeprojektion W_O
k_mlp_fwdk_mlp_bwdMLP (mit GELU)
k_residual_addk_residual_add_bwdResidualverbindung
k_loss_fwdk_loss_bwdKreuzentropie-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

ANDREA-120M schließt einen Forward-Pass durch einen Transformer-Block ab. Verfolgen Sie, was während des Backward-Pass durch denselben Block passiert (in Pre-Norm-Struktur: `x = x + Attention(LN(x))` dann `x = x + MLP(LN(x))`). Nennen Sie die Backward-Kernel in der Reihenfolge, in der sie ausgelöst werden, & geben Sie an, welcher Forward-Kernel jeder mit gepaart ist. Decken Sie mindestens 4 Kernel ab.

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

ANDREA-120M hat ~120.000.000 Gewichte & verwendet Gradientenakkumulation über 4 Mikrobatches pro Trainingsschritt. Berechnen Sie: (a) Gradientenpuffergröße in MB bei FP16; (b) Gesamtspeicher für Gewichte + Gradienten + Adam m + Adam v bei FP16; (c) wie viele separate `forward()` + `backward()`-Aufrufe pro Trainingsschritt ausgeführt werden. Zeigen Sie Ihre Rechenoperationen.

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).

Warum nicht PyTorch

ANDREA hätte PyTorchs Autograd statt des handschriftlichen `microgpt_cuda.cu` verwenden können. Nennen Sie zwei unterschiedliche ingenieurtechnische Gründe, warum ANDREA benutzerdefiniertes CUDA gewählt hat. Ein Grund sollte sich auf Speicher- oder Präzisionskontrolle beziehen; der andere auf Reproduzierbarkeit, Framework-Abhängigkeiten oder langfristige Wartung.