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

un

visitante
1 / ?

Gradientes Locais se Multiplicam

Kernels Forward & Backward


A Passada Forward

A passada forward da ANDREA-120M leva a entrada através de uma sequência de operações:


x = embed(token_ids)         # incorporações de tokens
for layer in 12_layers:
x = x + attn(LN(x))      # subcamada de atenção
x = x + mlp(LN(x))       # subcamada MLP
logits = LN(x) @ embed.T     # projeção de saída compartilhada
loss   = cross_entropy(logits, targets)

Cada operação lê tensores de entrada e produz tensores de saída. A passagem forward termina em um único escalar: a perda de entropia cruzada para este lote.


A Passagem Backward

O treinamento atualiza os pesos na direção que diminui a perda. Para obter as direções de atualização, o engine precisa de:


dL/dW para cada W aprendível no modelo

A regra da cadeia fornece isso. Para uma cadeia loss = f(g(h(x))):


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

Cada fator é um gradiente local: como a saída de uma operação muda quando sua entrada muda por uma pequena quantidade. Multiplicar gradientes locais para trás pelo grafo propaga o sinal de perda para cada peso.


Diferenciação em Modo Reverso

O backprop calcula gradientes em ordem reversa: começando de dL/dlogits = 1, depois percorrendo para trás pela entropia cruzada, depois projeção de saída, depois normalização de camada, depois doze blocos transformer, depois embeddings. Em cada passo, multiplique o gradiente de entrada pelo Jacobiano local.


O modo reverso é eficiente quando a saída é um escalar único (a perda) & há muitas entradas (os pesos). Uma passada para trás produz gradientes para cada peso no modelo. O modo forward precisaria de uma passada por peso; para ANDREA-120M com ~120M pesos, o modo forward é inviável.

Por que Modo Reverso

ANDREA-120M tem ~120M pesos e produz uma única perda escalar por passo de treinamento. Compare a diferenciação automática em modo reverso contra modo forward. Declare (1) qual modo produz todos os gradientes de pesos em uma única passada reversa; (2) quantas passadas forward seriam necessárias para calcular todos os 120M gradientes de pesos; (3) qual modo ANDREA usa & por quê.

Todo Op Forward Recebe Um Gêmeo Backward

A Disciplina de Pareamento

microgpt_cuda.cu envia dois kernels CUDA para cada operação: um que computa a saída forward, um que computa gradientes de entrada dados gradientes de saída. O pareamento é um-para-um:


Kernel forwardKernel backwardOperação
k_embed_fwdk_embed_bwdBusca de embedding de token
k_layernorm_fwdk_layernorm_bwdNormalização de camada
k_attn_qkv_fwdk_attn_qkv_bwdProjeções Q, K, V
k_attn_fwdk_attn_bwdAtenção por produto escalar
k_attn_out_fwdk_attn_out_bwdProjeção de saída W_O
k_mlp_fwdk_mlp_bwdMLP (com GELU)
k_residual_addk_residual_add_bwdConexão residual
k_loss_fwdk_loss_bwdPerda de entropia cruzada

Oito pares de operações cobrem o transformer completo. Mais alguns kernels utilitários: k_grad_norm_partial, k_grad_norm_final, k_grad_scale para clipping de gradientes (veja atividade 75).


O Trabalho de um Kernel Backward

Dado o gradiente que flui das camadas posteriores (grad_output), um kernel backward computa:


1. grad_input: o gradiente em relação ao tensor de entrada da operação. Este é passado adiante para trás.

2. grad_weight: o gradiente em relação aos parâmetros aprendíveis na operação. Este vai para o estado do otimizador.


Ambos são computados em um único lançamento de kernel. Threads CUDA cooperam em tiles do tensor de gradiente em paralelo.


Tensores Salvos

O cálculo backward frequentemente precisa de valores do forward pass. Por exemplo, k_layernorm_bwd precisa da média e variância computadas durante o forward; k_mlp_bwd precisa da pré-ativação GELU. O motor de treinamento armazena estes em buffers dedicados durante o forward, depois os lê durante o backward.


Custo de memória: aproximadamente o mesmo formato que a saída do forward para cada tensor salvo. Para ANDREA-120M com batch=8, seq=1024, d_model=768, um tensor salvo é 8 × 1024 × 768 × 4 bytes = 25 MB. Em 12 camadas & múltiplos tensores salvos por camada, as ativações dominam a VRAM durante o treinamento (~5-10 GB em uma placa de 24 GB).

Rastreando Um Passo de Backward

ANDREA-120M completa um forward pass através de um bloco transformer. Rastreie o que acontece durante o backward pass através do mesmo bloco (em estrutura pre-norm: `x = x + Attention(LN(x))` então `x = x + MLP(LN(x))`). Nomeie os kernels backward na ordem em que disparam, & indique qual kernel forward cada um emparelha. Cubra pelo menos 4 kernels.

Onde os Gradientes Vivem na Memória

Um Tensor de Gradiente Por Tensor de Peso

Todo tensor de peso aprendível no ANDREA-120M tem um tensor de gradiente correspondente de forma idêntica. Para cada bloco:


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]

Mais token embeddings, position embeddings e uma layer norm final. A memória total do buffer de gradientes corresponde à memória dos pesos: ~120M floats, ~480 MB em FP32, ~240 MB em FP16.


Acumulação Através de Microbatches

O batch_size = 8 da ANDREA cabe na VRAM em FP16. Batches efetivos maiores requerem acumulação de gradientes: execute múltiplas passadas forward+backward em batches pequenos, somando gradientes no mesmo buffer, depois dê um passo do otimizador.


for microbatch in range(n_microbatches):
forward(microbatch)
backward()           # ADICIONA aos buffers de gradientes, não sobrescreve
scale_grads(1.0 / n_microbatches)  # média entre microbatches
optimizer_step()
zero_grads()             # reinicia para o próximo passo de treinamento

Os kernels de backward usam semântica +=, não =. Cada chamada adiciona contribuições de gradiente ao buffer existente; o buffer mantém a soma acumulada até que zero_grads() o limpe.


O Estado do Otimizador

AdamW (atividade 73) mantém dois buffers adicionais por peso: primeiro momento m & segundo momento v. Memória total durante o treinamento:


weights:    1× contagem de pesos
gradients:  1× contagem de pesos
Adam m:     1× contagem de pesos
Adam v:     1× contagem de pesos
ativs. salvas: ~2-4× dependendo de camadas & lote
──────────────────────────────────────────
total:      ~6-8× contagem de pesos

ANDREA-120M em FP16: ~240 MB × 4 buffers (peso, grad, m, v) + ~5-10 GB ativações = ~10-12 GB total. Confortavelmente abaixo do limite de 24 GB da RTX 4090. ANDREA-12M treinado em 1.4 GB; o escalonamento de parâmetros 10× traz ~10× memória.

Dimensionando Buffers de Gradiente

O ANDREA-120M possui ~120.000.000 pesos & usa acumulação de gradientes em 4 micro-lotes por etapa de treinamento. Calcule: (a) tamanho do buffer de gradientes em MB em FP16; (b) memória total para pesos + gradientes + Adam m + Adam v em FP16; (c) quantas chamadas separadas de `forward()` + `backward()` são executadas por etapa de treinamento. Mostre sua aritmética.

Controle Total de Memória & Precisão

O Custo dos Frameworks Genéricos

PyTorch & JAX tornam o autograd conveniente: escreva código Python, obtenha gradientes automaticamente. O custo: uma camada de despacho genérica entre seu código & CUDA. Toda operação passa por overhead do interpretador Python, bookkeeping do framework, & seleção dinâmica de kernels. Para treinar um pequeno modelo de linguagem em uma GPU, esse overhead importa.


Custos concretos que o ANDREA evita:


1. Latência do interpretador Python. Cada operação PyTorch atravessa a fronteira Python/C++. Para ~100 lançamentos de kernel por passo de treinamento a ~9 passos/min, isso são ~900 travessias de fronteira por minuto. O despacho em nível C elimina isso.


2. Imprevisibilidade do alocador do framework. O alocador de cache do PyTorch oferece bom throughput em média, mas memória máxima imprevisível. O motor de treinamento do ANDREA pré-aloca todos os buffers na inicialização; sem realocação durante o treinamento, sem fragmentação, sem OOMs surpresa no passo 100K.


3. Seleção genérica de kernel. O PyTorch escolhe kernels em tempo de execução via heurísticas. O ANDREA escolhe kernels em tempo de compilação, ajustados aos tamanhos de tile do tensor core RTX 4090.


4. Infraestrutura de precisão mista. O caminho cuBLAS FP16 do ANDREA-120M e os experimentos de tensor core FP8 E4M3 do ANDREA requerem controle preciso sobre quais tensores vivem em qual precisão. Frameworks genéricos expõem esse controle através de APIs em camadas; escritas personalizadas em CUDA o escrevem diretamente.


O Tradeoff

Custos do CUDA personalizado: mais código para escrever, mais bugs para encontrar, sem ecossistema comunitário. O microgpt_cuda.cu do ANDREA tem ~6000 linhas de CUDA escrito à mão que levou meses para depurar. Cada nova operação requer escrever um kernel forward, um kernel backward e testes.


O que o ANDREA ganha:


- Reprodutibilidade total. O pipeline de treinamento é um binário C mais um proxy Python. Sem derivação de versão entre lançamentos do PyTorch, sem incompatibilidades de versão CUDA com wheels do framework.

- Retomadas bit-exatas. SIGTERM aciona a escrita de um checkpoint que captura cada tensor exatamente como a GPU o vê. O retomar pega a mesma trajetória de perda em que a execução estava.

- Memória previsível. ANDREA-120M treinado por 200K passos sem OOMs. A memória foi contabilizada na inicialização do engine.

- Acesso direto ao hardware. Tamanhos de tile do tensor core, configurações FP8 E4M3, cópias de memória assíncronas: tudo diretamente acessível no CUDA, opaco em frameworks genéricos.


Reprodutibilidade Como Missão

A seção 9 do whitepaper ANDREA lista a pilha completa de reprodutibilidade:


Motor de treinamento: microgpt/microgpt_cuda.cu
Proxy de treinamento: microgpt/training_proxy.py
Configurações de experimento: experiments/ANDREA-*-TRAIN.json
Pipeline de dados: scripts/pull-hermes3.py, scripts/prep-megachat.py
Painel: scripts/live-loss-dashboard.html
Especificação do Bandit: docs/FIREHOSE-BANDIT.md
Documentação do modelo: docs/ANDREA.md

Requisito de hardware: uma GPU NVIDIA com ≥8 GB de VRAM (RTX 3060 ou melhor). Qualquer pessoa pode reproduzir o ANDREA-12M a partir desses artefatos. O caminho CUDA personalizado é parte do motivo: sem congelamentos de versão de framework, sem surpresas de dependências daqui a cinco anos.


Sinais & Checkpoints

O loop de treinamento CUDA responde a dois sinais POSIX:


- SIGTERM: grava um checkpoint imediato, depois sai. Usado ao parar o treinamento de forma limpa.

- SIGUSR1: escreve um checkpoint imediato, continua o treinamento. Usado durante o pivot de polimento no v3 para capturar o estado sem interromper a execução.


Formato do checkpoint: [int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]. Contador de passos, contagem de pesos, depois pesos seguidos pelos momentos de Adam. Retoma bit-exatamente. O proxy arquiva .samples.json & .state.json separadamente no polimento; .loss.json nunca é arquivado (ele acumula o histórico completo do treinamento).

Por Que Não PyTorch

ANDREA poderia ter usado o autograd do PyTorch em vez de escrever `microgpt_cuda.cu` à mão. Dê duas razões de engenharia distintas pelas quais ANDREA escolheu CUDA customizado. Uma razão deve referenciar controle de memória ou precisão; a outra deve referenciar reprodutibilidade, dependências de framework ou manutenção de longo prazo.