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

un

ospite
1 / ?
torna alle lezioni

I Gradienti Locali si Moltiplicano

Forward & Backward Kernels


Il Forward Pass

Il forward pass di ANDREA-120M fa passare l'input attraverso una sequenza di operazioni:


x = embed(token_ids)         # embedding dei token
for layer in 12_layers:
x = x + attn(LN(x))      # sottostrato attention
x = x + mlp(LN(x))       # sottostrato MLP
logits = LN(x) @ embed.T     # proiezione di output condivisa
loss   = cross_entropy(logits, targets)

Ogni operazione legge tensori in ingresso e produce tensori in uscita. Il forward pass termina in un singolo scalare: la cross-entropy loss per questo batch.


Il Backward Pass

L'addestramento aggiorna i pesi nella direzione che diminuisce la loss. Per ottenere le direzioni di aggiornamento, il motore ha bisogno di:


dL/dW per ogni W apprendibile nel modello

La regola della catena lo fornisce. Per una catena loss = f(g(h(x))):


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

Ogni fattore è un gradiente locale: come l'output di un'operazione cambia quando il suo input cambia di una piccola quantità. Moltiplicando i gradienti locali all'indietro attraverso il grafo, si propaga il segnale di perdita a ogni peso.


Differenziazione in Modalità Reverse

Backprop calcola i gradienti in ordine inverso: partendo da dL/dlogits = 1, poi procedendo all'indietro attraverso l'entropia crociata, poi la proiezione di output, poi la normalizzazione del layer, poi dodici blocchi transformer, poi gli embedding. A ogni passo, si moltiplica il gradiente in arrivo per il Jacobiano locale.


La modalità reverse è efficiente quando l'output è un singolo scalare (la perdita) e ci sono molti input (i pesi). Un singolo passaggio all'indietro produce gradienti per ogni peso nel modello. La modalità forward richiederebbe un passaggio per peso; per ANDREA-120M con ~120M pesi, la modalità forward è impraticabile.

Perché la Modalità Reverse

ANDREA-120M ha ~120M pesi e produce una singola perdita scalare per step di training. Confronta la differenziazione automatica in modalità reverse contro forward-mode. Indica (1) quale modalità produce tutti i gradienti dei pesi in un singolo passaggio backward; (2) quanti passaggi forward-mode sarebbero necessari per calcolare tutti i 120M gradienti dei pesi; (3) quale modalità usa ANDREA e perché.

Ogni Operazione Forward Ha un Gemello Backward

La Disciplina di Accoppiamento

microgpt_cuda.cu include due kernel CUDA per ogni operazione: uno che calcola l'output forward, uno che calcola i gradienti degli input dati i gradienti degli output. L'accoppiamento è uno-a-uno:


Kernel forwardKernel backwardOperazione
k_embed_fwdk_embed_bwdRicerca embedding dei token
k_layernorm_fwdk_layernorm_bwdNormalizzazione layer
k_attn_qkv_fwdk_attn_qkv_bwdProiezioni Q, K, V
k_attn_fwdk_attn_bwdAttenzione scaled dot-product
k_attn_out_fwdk_attn_out_bwdProiezione di output W_O
k_mlp_fwdk_mlp_bwdMLP (con GELU)
k_residual_addk_residual_add_bwdConnessione residua
k_loss_fwdk_loss_bwdPerdita cross-entropy

Otto coppie di operazioni coprono l'intero transformer. Più alcuni kernel di utilità: k_grad_norm_partial, k_grad_norm_final, k_grad_scale per il clipping dei gradienti (vedi attività 75).


Il Compito di un Kernel Backward

Dato il gradiente che arriva dai layer successivi (grad_output), un kernel backward calcola:


1. grad_input: il gradiente rispetto al tensore di input dell'operazione. Questo viene passato ulteriormente all'indietro.

2. grad_weight: il gradiente rispetto ai parametri apprendibili nell'operazione. Questo va nello stato dell'ottimizzatore.


Entrambi vengono calcolati in un singolo lancio di kernel. I thread CUDA cooperano su tile del tensore di gradiente in parallelo.


Tensori Salvati

Il calcolo all'indietro spesso necessita di valori dal passaggio forward. Ad esempio, k_layernorm_bwd necessita della media e varianza calcolate durante il forward; k_mlp_bwd necessita del pre-attivazione GELU. Il motore di training li memorizza in buffer dedicati durante il forward, poi li legge durante l'indietro.


Costo in memoria: approssimativamente la stessa forma dell'output forward per ogni tensore salvato. Per ANDREA-120M con batch=8, seq=1024, d_model=768, un tensore salvato è 8 × 1024 × 768 × 4 bytes = 25 MB. Attraverso 12 layer & più tensori salvati per layer, le attivazioni dominano la VRAM durante l'addestramento (~5-10 GB su una scheda da 24 GB).

Tracciamento di un Passo Backward

ANDREA-120M completa un forward pass attraverso un blocco transformer. Traccia cosa succede durante il backward pass attraverso lo stesso blocco (struttura pre-norm: `x = x + Attention(LN(x))` poi `x = x + MLP(LN(x))`). Nomina i kernel backward nell'ordine in cui si attivano, & indica con quale kernel forward si accoppiano. Copri almeno 4 kernel.

Dove Risiedono i Gradienti in Memoria

Un Tensore di Gradiente per Ogni Tensore di Peso

Ogni tensore di peso apprendibile in ANDREA-120M ha un tensore di gradiente corrispondente della stessa forma. Per ogni blocco:


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]

Più gli embedding dei token, embedding di posizione e una normalizzazione finale del layer. La memoria totale del buffer dei gradienti corrisponde alla memoria dei pesi: ~120M float, ~480 MB a FP32, ~240 MB a FP16.


Accumulo Attraverso Microbatch

Il batch_size = 8 di ANDREA entra in VRAM a FP16. Batch effettivi più grandi richiedono accumulo di gradienti: eseguire più passaggi forward+backward su batch piccoli, sommando i gradienti nello stesso buffer, poi eseguire un passo dell'ottimizzatore.


for microbatch in range(n_microbatches):
forward(microbatch)
backward()           # AGGIUNGE ai buffer dei gradienti, non sovrascrive
scale_grads(1.0 / n_microbatches)  # media tra i microbatch
optimizer_step()
zero_grads()             # reset per il prossimo step di training

I kernel di backward usano una semantica +=, non =. Ogni chiamata aggiunge contributi di gradiente al buffer esistente; il buffer mantiene la somma cumulativa fino a quando zero_grads() lo azzera.


Lo Stato dell'Ottimizzatore

AdamW (attività 73) mantiene due buffer aggiuntivi per peso: primo momento m & secondo momento v. Memoria totale durante l'addestramento:


pesi:       1× conteggio pesi
gradienti:  1× conteggio pesi
Adam m:     1× conteggio pesi
Adam v:     1× conteggio pesi
attivazioni salvate: ~2-4× a seconda di strati & batch
──────────────────────────────────────────
totale:      ~6-8× conteggio pesi

ANDREA-120M a FP16: ~240 MB × 4 buffer (peso, grad, m, v) + ~5-10 GB attivazioni = ~10-12 GB totali. Ben al di sotto del limite di 24 GB della RTX 4090. ANDREA-12M addestrato in 1.4 GB; il ridimensionamento parametri 10× porta ~10× memoria.

Dimensionamento Buffer Gradiente

ANDREA-120M ha ~120.000.000 pesi e usa accumulazione di gradienti su 4 microbatch per step di training. Calcola: (a) dimensione del buffer dei gradienti in MB a FP16; (b) memoria totale per pesi + gradienti + Adam m + Adam v a FP16; (c) quante chiamate separate `forward()` + `backward()` vengono eseguite per step di training. Mostra i tuoi calcoli.

Controllo Completo di Memoria & Precisione

Cosa Costano i Framework Generici

PyTorch & JAX rendono autograd comodo: scrivi codice Python, ottieni gradienti automaticamente. Il costo: un layer di dispatch generico tra il tuo codice & CUDA. Ogni operazione passa attraverso l'overhead dell'interprete Python, contabilità del framework, & selezione dinamica del kernel. Per addestrare un piccolo modello linguistico su una GPU, quell'overhead conta.


Costi concreti che ANDREA evita:


1. Latenza dell'interprete Python. Ogni operazione PyTorch attraversa il confine Python/C++. Per ~100 lanci di kernel per step di training a ~9 step/min, ciò significa ~900 attraversamenti di confine al minuto. La dispatch a livello C elimina questo.


2. Imprevedibilità dell'allocatore del framework. L'allocatore di caching di PyTorch offre un buon throughput in media ma memoria di picco imprevedibile. Il motore di training di ANDREA pre-alloca ogni buffer all'avvio; nessuna riallocazione durante il training, nessuna frammentazione, nessun OOM a sorpresa allo step 100K.


3. Selezione di kernel generici. PyTorch seleziona i kernel a runtime tramite euristiche. ANDREA seleziona i kernel a compile time, ottimizzati per le dimensioni delle tile del tensor core RTX 4090.


4. Idraulica a precisione mista. Il percorso cuBLAS FP16 di ANDREA-120M e gli esperimenti tensor core FP8 E4M3 di ANDREA richiedono un controllo preciso su quali tensori risiedono a quale precisione. I framework generici espongono questo controllo attraverso API stratificate; le scritture CUDA personalizzate lo implementano direttamente.


Il Compromesso

Costi del CUDA personalizzato: più codice da scrivere, più bug da trovare, nessun ecosistema comunitario. Il file microgpt_cuda.cu di ANDREA è ~6000 righe di CUDA scritto a mano che ha richiesto mesi per il debug. Ogni nuova operazione richiede la scrittura di un kernel forward, un kernel backward e test.


Cosa guadagna ANDREA:


- Riproducibilità completa. Il pipeline di training è un binario C più un proxy Python. Nessuna deriva di versione tra i rilasci di PyTorch, nessuna incompatibilità di versione CUDA con le wheel del framework.

- Riprese bit-exact. SIGTERM attiva la scrittura di un checkpoint che cattura ogni tensore esattamente come lo vede la GPU. La ripresa riprende la stessa traiettoria di loss del run.

- Memoria prevedibile. ANDREA-120M addestrato per 200K step senza OOM. La memoria è stata contabilizzata all'avvio del motore.

- Accesso diretto all'hardware. Dimensioni dei tile del tensor core, impostazioni FP8 E4M3, copie di memoria asincrone: tutto direttamente indirizzabile in CUDA, opaco nei framework generici.


Riproducibilità Come Missione

La sezione 9 del whitepaper ANDREA elenca lo stack completo di riproducibilità:


Motore di addestramento: microgpt/microgpt_cuda.cu
Proxy di addestramento: microgpt/training_proxy.py
Configurazioni esperimento: experiments/ANDREA-*-TRAIN.json
Pipeline dati: scripts/pull-hermes3.py, scripts/prep-megachat.py
Dashboard: scripts/live-loss-dashboard.html
Specifica Bandit: docs/FIREHOSE-BANDIT.md
Documentazione modello: docs/ANDREA.md

Requisito hardware: una GPU NVIDIA con ≥8 GB VRAM (RTX 3060 o superiore). Chiunque può riprodurre ANDREA-12M da questi artefatti. Il percorso CUDA personalizzato è parte del motivo: nessuna versione del framework congelata, nessuna sorpresa di dipendenze tra cinque anni.


Segnali & Checkpoint

Il ciclo di addestramento CUDA risponde a due segnali POSIX:


- SIGTERM: scrive un checkpoint immediato, poi esce. Utilizzato quando si ferma l'addestramento in modo pulito.

- SIGUSR1: scrive un checkpoint immediato, continua l'addestramento. Utilizzato durante la pivotatura di polish in v3 per catturare lo stato senza interrompere l'esecuzione.


Formato checkpoint: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. Contatore di step, conteggio pesi, poi pesi seguiti dai momenti di Adam. Riprende bit-exattamente. Il proxy archivia .samples.json & .state.json separatamente su polish; .loss.json non viene mai archiviato (accumula la storia completa dell'addestramento).

Perché non PyTorch

ANDREA avrebbe potuto usare l'autograd di PyTorch invece di scrivere `microgpt_cuda.cu` a mano. Dai due motivi di ingegneria distinti per cui ANDREA ha scelto CUDA custom. Un motivo dovrebbe riferirsi al controllo di memoria o precisione; l'altro dovrebbe riferirsi a riproducibilità, dipendenze dal framework o manutenzione a lungo termine.