microgpt - treinamento e inferência de GPT implementados em 200 linhas de Python puro
(karpathy.github.io)- Projeto artístico publicado por karpathy. Implementa todo o algoritmo do GPT em um único arquivo de 200 linhas, sem dependências externas
- A diferença para LLMs de produção é apenas de escala e eficiência; o núcleo é o mesmo, e entender este código significa entender a essência algorítmica do GPT
- Inclui dataset, tokenizador, motor de autograd, arquitetura Transformer semelhante ao GPT-2, otimizador Adam e também o loop de treino e inferência
- Como resultado de 10 anos de trabalho de simplificação de LLMs em projetos anteriores como micrograd, makemore e nanogpt, concentra a essência do GPT na forma mínima que já não pode mais ser simplificada
- Treina com 32.000 nomes para gerar novos nomes plausíveis, realizando todos os cálculos diretamente com autograd em nível escalar
- O processo de treinamento é composto por cálculo da perda → retropropagação → atualização com Adam e pode ser executado em cerca de 1 minuto
Visão geral do microgpt
- microgpt é um script Python de 200 linhas que implementa de forma completa o processo de treino e inferência de um modelo GPT
- Sem bibliotecas externas, inclui dataset, tokenizador, autograd, modelo, otimizador e loop de treinamento
- Integra projetos anteriores como micrograd, makemore e nanogpt em um único arquivo
- Uma implementação que deixa apenas o núcleo algorítmico, em um nível de “não dá mais para simplificar”
- O código completo está disponível no GitHub Gist, na página web e no Google Colab
Composição do dataset
- O combustível dos grandes modelos de linguagem é um fluxo de dados de texto; em produção usam-se páginas da web da internet, mas no microgpt é usado um exemplo simples com 32.000 nomes, um por linha
- Cada nome é tratado como um "documento", e o objetivo do modelo é aprender padrões estatísticos dos dados para gerar novos documentos semelhantes
- Após o treinamento, o modelo "alucina" novos nomes plausíveis como "kamon", "karai" e "vialan"
- Do ponto de vista do ChatGPT, a conversa com o usuário também é apenas um "documento com formato estranho"; ao inicializar o documento com um prompt, a resposta do modelo corresponde a uma conclusão estatística de documento
Tokenizador
- Como redes neurais operam com números, e não com caracteres, é necessário converter o texto em uma sequência de IDs inteiros de tokens e depois restaurá-lo
- Tokenizadores de produção como o tiktoken (usado no GPT-4) operam em blocos de caracteres por eficiência, mas o tokenizador mais simples atribui um inteiro a cada caractere único do dataset
- As letras minúsculas de a a z são ordenadas e cada caractere recebe um ID pelo índice; o valor inteiro em si não tem significado, e cada token é um símbolo discreto distinto
- Adiciona-se o token especial BOS (Beginning of Sequence) para indicar que "um novo documento começou/terminou", e "emma" é encapsulado como
[BOS, e, m, m, a, BOS] - O vocabulário final tem 27 itens (26 letras minúsculas + 1 BOS)
Diferenciação automática (Autograd)
- O treinamento de redes neurais exige gradientes: para cada parâmetro, é preciso saber "se eu aumentar um pouco este valor, a perda sobe ou desce, e quanto?"
- O grafo computacional tem muitas entradas (parâmetros do modelo e tokens de entrada), mas converge para uma única saída escalar, a perda (loss)
- A retropropagação (Backpropagation) começa na saída e percorre o grafo no sentido inverso, calculando os gradientes da perda em relação a todas as entradas com base na regra da cadeia do cálculo
- É implementado com a classe Value: cada Value encapsula um único escalar (
.data) e rastreia como ele foi calculado- Ao realizar operações como soma e multiplicação, o novo Value guarda as entradas (
_children) e as derivadas locais (_local_grads) da operação correspondente - Ex.:
__mul__registra ∂(a·b)/∂a=b e ∂(a·b)/∂b=a
- Ao realizar operações como soma e multiplicação, o novo Value guarda as entradas (
- Blocos de operação suportados: soma, multiplicação, potência, log, exp, ReLU
- O método
backward()percorre o grafo em ordem topológica inversa, aplicando a regra da cadeia em cada etapa- Começa no nó da perda com
self.grad = 1(∂L/∂L=1) - Multiplica os gradientes locais ao longo do caminho e os propaga até os parâmetros
- Começa no nó da perda com
- Acumula com += (não é atribuição): quando o grafo se ramifica, gradientes fluem independentemente por cada ramo e devem ser somados (resultado da regra da cadeia multivariável)
- É algoritmicamente idêntico ao
.backward()do PyTorch, mas opera em escalares em vez de tensores; por isso é muito mais simples, porém menos eficiente
Inicialização de parâmetros
- Os parâmetros são o conhecimento do modelo: um grande conjunto de números de ponto flutuante que começa aleatoriamente e é otimizado repetidamente durante o treinamento
- São inicializados com pequenos valores aleatórios de uma distribuição gaussiana
- São compostos por matrizes nomeadas em um
state_dict: tabela de embeddings, pesos de atenção, pesos da MLP e projeção final de saída - Configuração de hiperparâmetros:
n_embd = 16: dimensão do embeddingn_head = 4: número de cabeças de atençãon_layer = 1: número de camadasblock_size = 16: comprimento máximo da sequência
- O modelo pequeno tem 4.192 parâmetros (o GPT-2 tem 1,6 bilhão, e LLMs modernos têm centenas de bilhões)
Arquitetura
- A arquitetura do modelo é uma função sem estado: recebe tokens, posições, parâmetros e chaves/valores em cache de posições anteriores, e retorna os logits (pontuações) do próximo token
- Segue o GPT-2, mas com algumas simplificações: RMSNorm (em vez de LayerNorm), sem bias, ReLU (em vez de GeLU)
-
Funções auxiliares
- linear: multiplicação matriz-vetor que calcula um produto interno para cada linha da matriz de pesos; é a transformação linear aprendida, bloco básico da rede neural
- softmax: converte pontuações brutas (logits) em uma distribuição de probabilidade, fazendo com que todos os valores fiquem no intervalo [0,1] e somem 1; para estabilidade numérica, subtrai primeiro o valor máximo
- rmsnorm: reajusta o vetor para ter raiz da média quadrática unitária, evitando que as ativações cresçam ou diminuam ao passar pela rede e estabilizando o treinamento
-
Estrutura do modelo
- Embeddings: o ID do token e o ID da posição consultam linhas nas tabelas de embedding (
wte,wpe) e os dois vetores são somados, codificando ao mesmo tempo o que é o token e onde ele está na sequência- LLMs modernos costumam pular embeddings posicionais e usar técnicas de posicionamento relativo como RoPE
- Bloco de atenção: projeta o token atual em três vetores, Q (query), K (key) e V (value)
- Query: “o que estou procurando?”, key: “o que eu contenho?”, value: “o que eu forneço se for selecionado?”
- Exemplo: ao prever o próximo caractere em "emma", o segundo "m" pode aprender uma query como “qual foi a vogal mais recente?”; o "e" anterior combina bem com essa query e recebe um peso de atenção alto
- Keys e values são adicionados ao cache KV, permitindo consultar posições anteriores
- Cada cabeça de atenção calcula o produto interno entre a query e todas as keys em cache (escalado por √d_head), obtém pesos de atenção com softmax e calcula a soma ponderada dos values em cache
- As saídas de todas as cabeças são concatenadas e projetadas por
attn_wo - O bloco de atenção é o único lugar onde o token na posição t pode “ver” os tokens passados 0..t-1; a atenção é o mecanismo de comunicação entre tokens
- Bloco MLP: uma rede feedforward de 2 camadas: expande para 4x a dimensão do embedding → aplica ReLU → reduz de volta
- É onde ocorre a maior parte do “raciocínio” por posição
- Diferente da atenção, é um cálculo totalmente local no tempo t
- O Transformer intercala comunicação (atenção) e computação (MLP)
- Conexões residuais: tanto o bloco de atenção quanto o MLP somam suas saídas de volta à entrada
- Isso permite que os gradientes atravessem a rede diretamente, tornando possível treinar modelos profundos
- Saída: o estado oculto final é projetado por
lm_headpara o tamanho do vocabulário, gerando um logit por token (aqui, 27 valores); logit alto = maior chance de aquele token vir em seguida - Particularidade do cache KV: usar cache KV durante o treinamento é incomum, mas como o microgpt processa apenas um token por vez, ele é construído explicitamente; as keys e values em cache são nós Value vivos no grafo computacional e participam da retropropagação
- Embeddings: o ID do token e o ID da posição consultam linhas nas tabelas de embedding (
Loop de treinamento
- O loop de treinamento repete: (1) escolher um documento → (2) executar a passagem direta do modelo sobre os tokens → (3) calcular a perda → (4) obter gradientes via retropropagação → (5) atualizar os parâmetros
-
Tokenização
- Em cada passo de treinamento, um documento é escolhido e envolvido com BOS nos dois lados: "emma" →
[BOS, e, m, m, a, BOS] - O objetivo do modelo é prever cada próximo token dados os tokens anteriores
- Em cada passo de treinamento, um documento é escolhido e envolvido com BOS nos dois lados: "emma" →
-
Passagem direta e perda
- Os tokens são alimentados ao modelo um por vez, construindo o cache KV
- Em cada posição, o modelo produz 27 logits, convertidos em probabilidades com softmax
- A perda em cada posição é a probabilidade logarítmica negativa do próximo token correto: −log p(target), chamada de perda de entropia cruzada
- A perda mede o quanto o modelo ficou surpreso com o que realmente veio: se atribui probabilidade 1,0, a perda é 0; se atribui probabilidade perto de 0, a perda tende a +∞
- Faz-se a média das perdas de todas as posições do documento para obter uma única perda escalar
-
Passagem para trás
- Uma única chamada a
loss.backward()executa a retropropagação em todo o grafo computacional - Depois disso, o
.gradde cada parâmetro informa como ele deve mudar para reduzir a perda
- Uma única chamada a
-
Otimizador Adam
- Em vez de descida de gradiente simples (
p.data -= lr * p.grad), usa-se Adam - Mantém duas médias móveis por parâmetro:
m: média dos gradientes recentes (momentum)v: média dos quadrados dos gradientes recentes (adaptação da taxa de aprendizado por parâmetro)
m_hatev_hatsão as versões com correção de viés de m e v, inicializados em 0- A taxa de aprendizado decresce linearmente durante o treinamento
- Após a atualização,
.grad = 0para zerar
- Em vez de descida de gradiente simples (
-
Resultados do treinamento
- Ao longo de 1.000 passos, a perda cai de cerca de 3,3 (chute aleatório entre 27 tokens: −log(1/27)≈3,3) para cerca de 2,37
- Quanto menor, melhor, e o mínimo é 0 (previsão perfeita), então ainda há espaço para melhorar, mas já fica claro que o modelo está aprendendo padrões estatísticos de nomes
Inferência
- Após o treinamento, é possível amostrar novos nomes do modelo; com os parâmetros fixos, executa-se a passagem direta em loop e cada token gerado é realimentado como próxima entrada
-
Processo de amostragem
- Cada amostra começa com o token BOS (“início de um novo nome”)
- O modelo gera 27 logits → converte em probabilidades → amostra aleatoriamente um token de acordo com essas probabilidades
- Esse token é realimentado como próxima entrada, repetindo até o modelo gerar BOS novamente (“fim”) ou atingir o comprimento máximo da sequência
-
Temperatura (Temperature)
- Antes do softmax, os logits são divididos pela temperatura
- Temperatura 1,0: amostragem direta da distribuição aprendida pelo modelo
- Temperatura baixa (ex.: 0,5): deixa a distribuição mais aguda, tornando o modelo mais conservador e mais propenso a escolher opções do topo
- Temperatura próxima de 0: sempre escolhe o único token mais provável (decodificação gulosa)
- Temperatura alta: achata a distribuição, gerando saídas mais diversas, porém menos consistentes
Como executar
- Só precisa de Python (sem
pip install, sem dependências):python train.py - Leva cerca de 1 minuto em um MacBook
- Exibe a perda a cada passo: de ~3,3 (aleatório) para ~2,37
- Após o treinamento, gera novos nomes alucinados: "kamon", "ann", "karai" etc.
- Também pode ser executado em um notebook do Google Colab, e você pode fazer perguntas ao Gemini
- É possível testar outros datasets, treinar por mais tempo aumentando
num_stepse obter resultados melhores aumentando o tamanho do modelo
Etapas de evolução do código
| arquivo | conteúdo adicionado |
|---|---|
train0.py |
tabela de contagem de bigramas — sem rede neural, sem gradientes |
train1.py |
MLP + gradientes manuais (numéricos e analíticos) + SGD |
train2.py |
Autograd (classe Value) — substitui gradientes manuais |
train3.py |
embedding posicional + atenção de uma cabeça + rmsnorm + residual |
train4.py |
atenção multi-head + loop de camadas — arquitetura GPT completa |
train5.py |
otimizador Adam — este é o train.py |
- Nas Revisions do Gist build_microgpt.py, é possível ver todas as versões e o diff entre cada etapa
Diferenças em relação a LLMs de produção
- O microgpt inclui a essência algorítmica completa do treinamento e da execução de um GPT; a diferença para LLMs de produção como o ChatGPT não muda o algoritmo central, mas sim os elementos que fazem isso funcionar em escala
-
Dados
- Em vez de 32 mil nomes curtos, ele é treinado com trilhões de tokens de texto da internet (páginas web, livros, código etc.)
- Remoção de duplicatas dos dados, filtragem de qualidade e mistura cuidadosa entre domínios
-
Tokenizador
- Em vez de caracteres isolados, usa-se um tokenizador de subpalavras como BPE (Byte Pair Encoding)
- Sequências de caracteres que aparecem com frequência juntas são fundidas em um único token; palavras comuns como "the" viram um único token, enquanto palavras raras são divididas em partes
- Vocabulário de ~100 mil tokens, vendo mais conteúdo por posição, então é muito mais eficiente
-
Autograd
- Em vez do objeto escalar
Valueem Python puro, usam-se tensores (grandes arrays multidimensionais de números), executados em GPUs/TPUs que realizam bilhões de operações de ponto flutuante por segundo - O PyTorch cuida do autograd sobre tensores, e kernels CUDA como o FlashAttention fundem várias operações
- A matemática é a mesma; muitos escalares são processados em paralelo
- Em vez do objeto escalar
-
Arquitetura
- microgpt: 4.192 parâmetros; um modelo nível GPT-4: centenas de bilhões
- No geral, a rede neural Transformer é muito parecida, mas muito mais larga (dimensão de embedding 10.000+) e muito mais profunda (100+ camadas)
- Tipos extras de blocos LEGO e mudanças na ordem:
- RoPE (embeddings posicionais rotativos) — em vez de embeddings posicionais aprendidos
- GQA (grouped-query attention) — reduz o tamanho do cache KV
- Ativações lineares com gate — em vez de ReLU
- Camadas MoE (mixture of experts)
- A estrutura central de atenção (comunicação) e MLP (cálculo) alternando sobre o fluxo residual é bem preservada
-
Treinamento
- Em vez de um documento por etapa, usam-se grandes lotes (milhões de tokens por etapa), acumulação de gradiente, precisão mista (
float16/bfloat16) e ajuste cuidadoso de hiperparâmetros - O treinamento de modelos de fronteira roda em milhares de GPUs por vários meses
- Em vez de um documento por etapa, usam-se grandes lotes (milhões de tokens por etapa), acumulação de gradiente, precisão mista (
-
Otimização
- microgpt: Adam + redução linear simples da taxa de aprendizado
- Em larga escala, otimização é uma área própria: precisão reduzida (
bfloat16,fp8), treinamento em grandes clusters de GPU - As configurações do otimizador (taxa de aprendizado, weight decay, parâmetros beta, cronogramas de warmup/decay) precisam de ajuste fino, e os valores corretos variam conforme o tamanho do modelo, o tamanho do lote e a composição do dataset
- Leis de escala (por exemplo, Chinchilla) orientam como distribuir um orçamento fixo de computação entre o tamanho do modelo e a quantidade de tokens de treinamento
- Errar esses detalhes em larga escala pode desperdiçar milhões de dólares em computação, por isso as equipes fazem muitos experimentos pequenos antes de um treinamento completo
-
Pós-treinamento (Post-training)
- O modelo base que sai do treinamento (modelo pré-treinado) é um completador de documentos, não um chatbot
- O processo para transformá-lo em ChatGPT tem duas etapas:
- SFT (ajuste fino supervisionado): substitui documentos por conversas curadas e continua o treinamento, sem mudança algorítmica
- RL (aprendizado por reforço): o modelo gera respostas → recebe pontuação (de humanos, modelos "juízes" ou algoritmos) → aprende com esse feedback
- No fundo, ele ainda está aprendendo sobre documentos, mas agora os documentos são compostos por tokens gerados pelo próprio modelo
-
Inferência
- Para servir o modelo a milhões de usuários, é necessário um stack de engenharia próprio: batching de requisições, gerenciamento e paginação de cache KV (como no vLLM), decodificação especulativa para velocidade, quantização para reduzir memória (execução em
int8/int4) e distribuição do modelo entre várias GPUs - Fundamentalmente, ele ainda prevê o próximo token da sequência, mas há muito esforço de engenharia para torná-lo mais rápido
- Para servir o modelo a milhões de usuários, é necessário um stack de engenharia próprio: batching de requisições, gerenciamento e paginação de cache KV (como no vLLM), decodificação especulativa para velocidade, quantização para reduzir memória (execução em
FAQ
-
O modelo "entende" alguma coisa?
- É uma questão filosófica, mas mecanicamente: nenhuma mágica acontece
- O modelo é uma grande função matemática que mapeia tokens de entrada para uma distribuição de probabilidade sobre o próximo token
- Durante o treinamento, os parâmetros são ajustados para tornar o próximo token correto mais provável
- Se isso constitui "entendimento" depende de cada pessoa, mas o mecanismo está totalmente contido nessas 200 linhas
-
Por que isso funciona?
- O modelo tem milhares de parâmetros ajustáveis, e o otimizador os move um pouco a cada etapa para reduzir a perda
- Ao longo de muitas etapas, os parâmetros se estabilizam em valores que capturam regularidades estatísticas dos dados
- No caso dos nomes: muitos começam com consoante, "qu" tende a aparecer junto, três consoantes seguidas são raras etc.
- O modelo aprende uma distribuição de probabilidade que reflete isso, não regras explícitas
-
Qual é a relação com o ChatGPT?
- O ChatGPT pega esse mesmo loop central (prever o próximo token, amostrar, repetir), amplia isso enormemente e acrescenta pós-treinamento para torná-lo interativo
- Ao conversar, prompt de sistema, mensagem do usuário e resposta são todos apenas tokens em uma sequência
- O modelo completa um documento um token por vez, exatamente como o microgpt completa nomes
-
O que é "alucinação"?
- O modelo gera tokens amostrando de uma distribuição de probabilidade
- Ele não tem conceito de verdade; conhece apenas sequências estatisticamente plausíveis à luz dos dados de treinamento
- Quando o microgpt "alucina" um nome como "karia", é o mesmo fenômeno de o ChatGPT afirmar um fato falso com confiança
- Em ambos os casos, são completações plausíveis, não necessariamente reais
-
Por que é tão lento?
- O microgpt processa um escalar por vez em Python puro, e uma única etapa de treinamento leva vários segundos
- Em GPUs, a mesma matemática roda milhões de escalares em paralelo, ficando várias ordens de grandeza mais rápida
-
Dá para fazer gerar nomes melhores?
- Sim: treinando por mais tempo (
num_stepsmaior), aumentando o tamanho do modelo (n_embd,n_layer,n_head) ou usando um dataset maior - São as mesmas alavancas importantes em larga escala
- Sim: treinando por mais tempo (
-
E se eu trocar o dataset?
- O modelo aprende qualquer padrão presente nos dados
- Se você trocar por nomes de cidades, nomes de Pokémon, palavras em inglês ou arquivos curtos de poesia, ele aprenderá a gerar isso
- O restante do código não precisa ser alterado
1 comentários
Obrigado pelo ótimo artigo.