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

un

gość
1 / ?
powrót do lekcji

Lokalne gradienty się mnożą

Forward & Backward Kernels


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

ANDREA-120M ma ~120M wag & produkuje pojedynczą skalarną stratę na krok treningowy. Porównaj automatyczne różniczkowanie w trybie odwrotnym z trybem prostym. Stwierdź (1) który tryb produkuje wszystkie gradienty wag w jednym przejściu wstecznym; (2) ile przejść w trybie prostym byłoby potrzebnych do obliczenia wszystkich 120M gradientów wag; (3) który tryb używa ANDREA & dlaczego.

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 forwardJądro backwardOperacja
k_embed_fwdk_embed_bwdWyszukiwanie osadzenia tokenu
k_layernorm_fwdk_layernorm_bwdNormalizacja warstwowa
k_attn_qkv_fwdk_attn_qkv_bwdProjekcje Q, K, V
k_attn_fwdk_attn_bwdSkalowana uwaga iloczynowa
k_attn_out_fwdk_attn_out_bwdProjekcja wyjściowa W_O
k_mlp_fwdk_mlp_bwdMLP (z GELU)
k_residual_addk_residual_add_bwdPołączenie resztkowe
k_loss_fwdk_loss_bwdStrata 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

ANDREA-120M kończy forward pass przez jeden blok transformera. Śledź, co dzieje się podczas backward pass przez ten sam blok (w strukturze pre-norm: `x = x + Attention(LN(x))` potem `x = x + MLP(LN(x))`). Nazwij jądra wsteczne w kolejności ich uruchomienia & podaj, z którym jądrem forward każde z nich się paruje. Obejrzyj co najmniej 4 jądra.

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

ANDREA-120M ma ~120 000 000 wag & używa akumulacji gradientów w 4 mikropartach na krok treningowy. Oblicz: (a) rozmiar bufora gradientów w MB przy FP16; (b) całkowita pamięć dla wag + gradientów + Adam m + Adam v przy FP16; (c) ile oddzielnych wywołań `forward()` + `backward()` odbywa się na krok treningowy. Pokaż obliczenia.

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

Dlaczego nie PyTorch

ANDREA mógł użyć autogradu PyTorcha zamiast ręcznego pisania `microgpt_cuda.cu`. Podaj dwa różne powody inżynierskie, dla których ANDREA wybrał niestandardowy CUDA. Jeden powód powinien odnosić się do kontroli pamięci lub precyzji; drugi powinien odnosić się do powtarzalności, zależności frameworka lub długoterminowej konserwacji.