Lokalne gradienty się mnożą
Przepływ forward
Przepływ forward ANDREA-120M przeprowadza wejście przez sekwencję operacji:
x = embed(token_ids) # osadzenia tokenów
for layer in 12_layers:
x = x + attn(LN(x)) # podwarstwa uwagi
x = x + mlp(LN(x)) # podwarstwa MLP
logits = LN(x) @ embed.T # wspólna projekcja wyjściowa
loss = cross_entropy(logits, targets)
Każda operacja odczytuje tensory wejściowe i produkuje tensory wyjściowe. Przepływ w przód kończy się pojedynczą skalą: stratą entropii krzyżowej dla tej partii.
Przepływ wsteczny
Trening aktualizuje wagi w kierunku zmniejszającym stratę. Aby uzyskać kierunki aktualizacji, silnik potrzebuje:
dL/dW dla każdego uczalnego W w modelu
Reguła łańcuchowa to umożliwia. Dla łańcucha loss = f(g(h(x))):
dL/dx = (dL/df) * (df/dg) * (dg/dh) * (dh/dx)
Każdy czynnik to lokalny gradient: jak wyjście jednej operacji zmienia się, gdy jej wejście zmieni się o małą wartość. Mnożenie lokalnych gradientów wstecz przez graf propaguje sygnał straty do każdej wagi.
Dywergencjacja w trybie odwrotnym
Backprop oblicza gradienty w kolejności odwrotnej: zaczynając od dL/dlogits = 1, potem idąc wstecz przez entropię krzyżową, potem projekcję wyjściową, potem normalizację warstw, potem dwanaście bloków transformera, potem osadzenia. Na każdym kroku mnożymy przychodzący gradient przez lokalną Jacobiana.
Tryb odwrotny jest efektywny, gdy wyjście to pojedyncza skalarna wartość (strata) & istnieje wiele wejść (wagi). Jeden wsteczny przebieg produkuje gradienty dla każdej wagi w modelu. Tryb prosty wymagałby jednego przejścia na wagę; dla ANDREA-120M z ~120M wagami, tryb prosty jest niewykonalny.
Dlaczego tryb odwrotny
Każda operacja Forward otrzymuje bliźniaczą Backward
Dyscyplina parowania
microgpt_cuda.cu dostarcza dwa jądra CUDA dla każdej operacji: jedno obliczające wyjście forward, jedno obliczające gradienty wejścia dane gradienty wyjścia. Parowanie jest jeden-do-jednego:
| Jądro forward | Jądro backward | Operacja |
|---|---|---|
k_embed_fwd | k_embed_bwd | Wyszukiwanie osadzenia tokenu |
k_layernorm_fwd | k_layernorm_bwd | Normalizacja warstwowa |
k_attn_qkv_fwd | k_attn_qkv_bwd | Projekcje Q, K, V |
k_attn_fwd | k_attn_bwd | Skalowana uwaga iloczynowa |
k_attn_out_fwd | k_attn_out_bwd | Projekcja wyjściowa W_O |
k_mlp_fwd | k_mlp_bwd | MLP (z GELU) |
k_residual_add | k_residual_add_bwd | Połączenie resztkowe |
k_loss_fwd | k_loss_bwd | Strata entropii krzyżowej |
Osiem par operacji obejmuje pełny transformer. Plus kilka kernelów pomocniczych: k_grad_norm_partial, k_grad_norm_final, k_grad_scale do obcinania gradientów (patrz aktywność 75).
Zadanie kernela wstecznego
Biorąc pod uwagę gradient płynący z późniejszych warstw (grad_output), kernel wsteczny oblicza:
1. grad_input: gradient z względem tensora wejściowego operacji. Jest przekazywany dalej wstecz.
2. grad_weight: gradient z względem uczalnych parametrów w operacji. Trafia do stanu optymalizatora.
Oba są obliczane w jednym uruchomieniu kernela. Wątki CUDA współpracują równolegle na kafelkach tensora gradientu.
Zapisane tensory
Obliczenia wsteczne często potrzebują wartości z przejścia forward. Na przykład, k_layernorm_bwd potrzebuje średniej i wariancji obliczonych podczas forward; k_mlp_bwd potrzebuje pre-aktywacji GELU. Silnik treningowy przechowuje je w dedykowanych buforach podczas forward, a następnie odczytuje podczas backward.
Koszt pamięci: mniej więcej taki sam kształt jak wyjście forward dla każdego zapisanego tensora. Dla ANDREA-120M z batch=8, seq=1024, d_model=768, jeden zapisany tensor to 8 × 1024 × 768 × 4 bajty = 25 MB. Na 12 warstwach & wielu zapisanych tensorach na warstwę, aktywacje dominują VRAM podczas treningu (~5-10 GB na karcie 24 GB).
Śledzenie jednego kroku wstecznego
Gdzie w pamięci przechowywane są gradienty
Jeden tensor gradientu na tensor wag
Każdy uczalny tensor wag w ANDREA-120M ma odpowiadający mu tensor gradientu o identycznym kształcie. Dla każdego bloku:
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, position embeddings, & a final layer norm. The gradient buffer total memory matches the weight memory: ~120M floats, ~480 MB at FP32, ~240 MB at FP16.
Akumulacja w Poprzek Mikrowiątek
Rozmiar partii ANDREA = 8 mieści się w VRAM przy FP16. Większe efektywne partie wymagają akumulacji gradientów: uruchom wiele przejść forward+backward na małych partiach, sumując gradienty do tego samego bufora, a następnie wykonaj jeden krok optymalizatora.
for microbatch in range(n_microbatches):
forward(microbatch)
backward() # DODAJE do buforów grad, nie nadpisuje
scale_grads(1.0 / n_microbatches) # uśrednij przez mikrobatch'e
optimizer_step()
zero_grads() # zresetuj dla następnego kroku treningowego
Jądra backward używają semantyki +=, nie =. Każde wywołanie dodaje wkłady gradientu do istniejącego bufora; bufor przechowuje bieżącą sumę, dopóki zero_grads() go nie wyczyści.
Stan Optymalizatora
AdamW (aktywność 73) przechowuje dwa dodatkowe bufory na wagę: pierwszy moment m & drugi moment v. Całkowita pamięć w czasie treningu:
wagi: 1× liczba wag
gradienty: 1× liczba wag
Adam m: 1× liczba wag
Adam v: 1× liczba wag
zapisane akty: ~2-4× w zależności od warstw i batcha
──────────────────────────────────────────
łącznie: ~6-8× liczba wag
ANDREA-120M w FP16: ~240 MB × 4 bufory (wagi, grad, m, v) + ~5-10 GB aktywacje = ~10-12 GB łącznie. Komfortowo poniżej limitu 24 GB RTX 4090. ANDREA-12M trenowane w 1,4 GB; 10× skalowanie parametrów przynosi ~10× pamięci.
Rozmiar buforów gradientów
Pełna Kontrola Nad Pamięcią & Precyzją
Jaki Jest Koszt Ogólnych Frameworków
PyTorch & JAX czynią autograd wygodnym: pisz kod Python, otrzymuj gradienty automatycznie. Koszt: ogólna warstwa dyspozycyjna między twoim kodem & CUDA. Każda operacja przechodzi przez narzut interpretera Python, księgowość frameworka & dynamiczny wybór jądra. Podczas trenowania małego modelu językowego na jednej GPU, ten narzut ma znaczenie.
Konkretne koszty, których unika ANDREA:
1. Opóźnienie interpretera Pythona. Każda operacja PyTorch przekracza granicę Python/C++. Przy ~100 uruchomieniach kernelów na krok treningowy i ~9 krokach/min, to ~900 przekroczeń granicy na minutę. Dispatch na poziomie C eliminuje to.
2. Nieprzewidywalność alokatora frameworka. Alokator buforujący PyTorch daje dobrą przepustowość średnio, ale nieprzewidywalne szczytowe zużycie pamięci. Silnik treningowy ANDREA pre-alokuje każdy bufor przy starcie; brak realokacji podczas treningu, brak fragmentacji, brak niespodziewanych OOM na kroku 100K.
3. Wybór generycznych kernelów. PyTorch wybiera kernele w czasie rzeczywistym za pomocą heurystyk. ANDREA wybiera kernele w czasie kompilacji, dostrojone do rozmiarów kafelków tensor core RTX 4090.
4. Rury precyzji mieszanej. Ścieżka cuBLAS FP16 w ANDREA-120M oraz eksperymenty z tensor core'ami FP8 E4M3 w ANDREA wymagają precyzyjnej kontroli nad tym, które tensory znajdują się w jakiej precyzji. Ogólne frameworki udostępniają tę kontrolę poprzez warstwowe API; niestandardowe zapisy CUDA implementują to bezpośrednio.
Kompromis
Koszty niestandardowego CUDA: więcej kodu do napisania, więcej błędów do znalezienia, brak ekosystemu społecznościowego. Plik microgpt_cuda.cu w ANDREA to ~6000 linii ręcznie napisanego CUDA, który wymagał miesięcy debugowania. Każda nowa operacja wymaga napisania jądra forward, jądra backward oraz testów.
Co zyskuje ANDREA:
- Pełna reprodukowalność. Potok treningowy to jeden binarny plik C plus jeden proxy Python. Brak dryfu wersji między wydaniami PyTorch, brak niezgodności wersji CUDA z kołami frameworka.
- Dokładne na poziomie bitu wznowienia. SIGTERM wyzwala zapis punktu kontrolnego, który przechwyci każdy tensor dokładnie tak, jak widzi go GPU. Wznowienie kontynuuje tę samą trajektorię straty, na której znajdował się przebieg.
- Przewidywalne zużycie pamięci. ANDREA-120M trenowane przez 200K kroków bez OOM. Pamięć została rozliczona przy uruchomieniu silnika.
- Bezpośredni dostęp do sprzętu. Rozmiary kafelków tensor core, ustawienia FP8 E4M3, asynchroniczne kopiowanie pamięci: wszystko bezpośrednio adresowalne w CUDA, nieprzejrzyste w generycznych frameworkach.
Reprodukowalność jako misja
Sekcja 9 whitepaperu ANDREA wymienia pełny stos reprodukowalności:
Silnik treningowy: microgpt/microgpt_cuda.cu
Proxy treningowy: microgpt/training_proxy.py
Konfiguracje eksperymentów: experiments/ANDREA-*-TRAIN.json
Rurociąg danych: scripts/pull-hermes3.py, scripts/prep-megachat.py
Panel informacyjny: scripts/live-loss-dashboard.html
Specyfikacja Bandita: docs/FIREHOSE-BANDIT.md
Dokumentacja modelu: docs/ANDREA.md
Wymaganie sprzętowe: jedna karta NVIDIA GPU z ≥8 GB VRAM (RTX 3060 lub lepsza). Każdy może odtworzyć ANDREA-12M z tych artefaktów. Niestandardowa ścieżka CUDA jest częścią tego dlaczego: brak zamrożenia wersji frameworka, brak niespodzianek z zależnościami za pięć lat.
Sygnały & Checkpoints
Pętla treningowa CUDA reaguje na dwa sygnały POSIX:
- SIGTERM: zapisz natychmiastowy checkpoint, a następnie wyjdź. Używane podczas czysty zatrzymywania treningu.
- SIGUSR1: zapisz natychmiastowy punkt kontrolny, kontynuuj trening. Używany podczas polerowania pivotu w v3 do przechwytywania stanu bez przerywania uruchomienia.
Format punktu kontrolnego: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. Licznik kroków, liczba wag, następnie wagi po których idą momenty Adama. Wznawia bitowo dokładnie. Proxy archiwizuje .samples.json & .state.json oddzielnie podczas polerowania; .loss.json nigdy nie jest archiwizowany (gromadzi pełną historię treningu).