What Changed Since 2017
Além do exemplo. O bloco de brinquedo das páginas anteriores é o desenho do GPT-2, de 2019: LayerNorm antes de cada subcamada, um MLP com GELU, atenção multi-head completa, posições aprendidas na entrada. Esta página trata de quais partes do bloco de 2017 continuam no que você escreveria hoje, quais foram trocadas e qual família de modelos venceu (encoder, decoder ou os dois). ← Geração · próximo: Escala →
O bloco e as seis coisas que mudaram
O bloco que o script da página do bloco monta é o de 2026. O de 2017 era diferente em seis pontos:
| Peça | 2017 | 2026 | Por quê, em uma linha |
|---|---|---|---|
| Posição da norma | depois da soma (pós-LN) | antes da subcamada (pré-LN) | deixa o caminho residual limpo |
| Normalizador | LayerNorm | RMSNorm | mesma qualidade, menos contas |
| MLP | ReLU, 2 matrizes, \(4d\) | SwiGLU, 3 matrizes, \(\tfrac{8}{3}d\) | um portão multiplicativo: ganho pequeno e consistente |
| Atenção | MHA | GQA | cache de KV 4× menor |
| Posição | senoidal, na entrada | RoPE, em Q e K | posição relativa e contexto extensível |
| Viés (bias) | em todas as lineares | nenhum | não faz falta e custa memória |
Configure-o. O padrão é o que um modelo lançado este ano parece; mude todos os controles para a primeira opção, marque os vieses e você obtém o artigo de 2017, exatamente.
Duas dessas mudanças valem mais do que as outras.
Pré-LN: onde a normalização fica
O bloco de 2017 normaliza depois da soma residual:
o que significa que o caminho residual passa por um normalizador em cada camada. O gradiente deixa de ter a rota identidade limpa que o capítulo 9 identificou como o ponto inteiro de uma conexão residual e a consequência é concreta: Transformers pós-LN não treinam sem warmup na taxa de aprendizado e ficam mais difíceis de treinar quanto mais profundos são2.
Mover a normalização para dentro do ramo resolve:
Agora o fluxo residual fica intocado da perda até a camada 1. Dá para ver isso numa linha: desenrolando a recorrência, a saída de uma pilha pré-LN de \(L\) blocos é a entrada mais a soma de todas as correções,
e esse \(I\) é o caminho expresso: o gradiente chega à primeira camada inteiro, pelo menos por essa parcela, não importa quantos blocos existam no meio. No pós-LN cada bloco termina num \(\text{Norm}\) e a soma nunca aparece sozinha — não há \(I\) limpo. Esse único rearranjo é a razão pela qual pilhas de 100 camadas são banais e é por isso que os cronogramas de warmup dos tutoriais antigos parecem superstição — eles compensavam um projeto que foi abandonado.
O fluxo residual é o objeto com que se deve pensar
O pré-LN sugere um modelo mental melhor do que "uma pilha de camadas". Existe um fluxo residual por posição, atravessando toda a profundidade do modelo e cada bloco lê dele, computa algo e soma o resultado de volta. Nada sobrescreve; tudo se acumula. A atenção move informação entre fluxos; o MLP transforma informação dentro de um. É sobre esse enquadramento que a interpretabilidade mecanicista é construída e ele explica de relance por que se pode apagar ou reordenar alguns blocos de um modelo treinado com surpreendentemente pouco estrago.
Normalização e a camada feed-forward
A RMSNorm3 abandona a subtração da média da LayerNorm e mantém só o reescalonamento:
Um exemplo com \(x = [3,\ 4]\) mostra a diferença. A LayerNorm subtrai a média (3,5) e divide pelo desvio padrão (0,5): sai \([-1;\ 1]\). A RMSNorm só divide pela raiz da média dos quadrados, \(\sqrt{(9+16)/2} \approx 3{,}54\): sai \([0{,}85;\ 1{,}13]\). As duas colocam o vetor numa escala padrão, que é o que importa para a camada seguinte; centrar se mostrou dispensável.
Uma redução em vez de duas, sem viés e sem custo mensurável de qualidade. Universal desde o Llama.
O MLP também mudou. O original são duas matrizes com uma ReLU no meio e largura oculta 4×. O moderno é o SwiGLU4 — três matrizes, em que um ramo abre a passagem do outro:
Leia a fórmula como dois ramos lado a lado: \(xW_3\) propõe um conteúdo e \(\text{Swish}(xW_1)\) decide, dimensão por dimensão, quanto dele passa — perto de zero fecha, valores maiores abrem ou amplificam. É um regulador que a própria entrada controla, o mesmo princípio das portas do LSTM.
A largura oculta cai para \(\tfrac{8}{3}d\) para que a contagem de parâmetros fique igual (três matrizes de \(d \times \tfrac{8}{3}d\) somam \(8d^2\), o mesmo que duas de \(d \times 4d\)) e o portão multiplicativo compra um ganho de qualidade consistente, ainda que modesto. É todo o argumento a favor dele e bastou para todo grande modelo aberto desde o Llama.
Onde os parâmetros de fato estão
No painel acima, entre dois terços (com MHA) e três quartos (com GQA, como na saída do script da página do bloco) de cada bloco é a camada feed-forward, não a atenção. A atenção leva a atenção; o MLP guarda os parâmetros — e é o MLP que a Mixture-of-Experts substitui para crescer um modelo sem crescer seu custo por token (capítulo 15).
Encoder, decoder e qual sobreviveu
O modelo de 2017 era um encoder–decoder para tradução: um encoder que vê a origem inteira de forma bidirecional, um decoder que gera causalmente e lê o encoder por atenção cruzada. Três famílias nasceram dele e seus destinos divergiram muito.
| Família | Atenção | Treinada por | Situação em 2026 |
|---|---|---|---|
| Só encoder — BERT1 | Bidirecional | Previsão de tokens mascarados | Viva, num nicho específico: embeddings, busca e rerankers, onde você codifica uma vez e compara barato. O ModernBERT5 o reconstruiu com RoPE, GLU e contexto de 8k. Ele não gera. |
| Encoder–decoder — T5, BART | Bidirecional + causal + cruzada | Denoising / seq2seq | Largamente deslocada para texto. O padrão sobrevive onde as duas modalidades são genuinamente diferentes — fala (Whisper) e a atenção cruzada que injeta texto num modelo de imagem. |
| Só decoder — GPT, Llama, Claude | Causal | Previsão do próximo token | Venceu. Todo modelo de linguagem de fronteira. |
Por que o só-decoder venceu uma disputa que parece que deveria perder? Afinal, ele vê menos contexto por token. Três razões e nenhuma é sobre elegância:
- Todo token é sinal de treino. Com a máscara causal, uma passagem direta sobre uma sequência de comprimento \(n\) produz \(n\) previsões. O BERT mascara 15% dos tokens e aprende com esses, extraindo cerca de um sétimo do sinal (um token em 6,7) por unidade de compute.
- Um objetivo cobre toda tarefa. "Preveja o próximo token" engloba classificação, extração, tradução e diálogo assim que o modelo fica bom o bastante para ser instruído — sem cabeça específica por tarefa, sem fine-tune separado.
- O cache de KV só funciona causalmente. Um modelo bidirecional precisa recodificar tudo quando a entrada muda; um causal apenas acrescenta. Essa é a diferença entre um chat interativo e um job em lote.
O descompasso pré-treino/inferência que matou o encoder
O BERT é treinado com entradas que contêm [MASK], um token que nunca aparece na inferência. O objetivo inteiro é um andaime que precisa ser descartado. A previsão do próximo token não tem esse buraco: o que o modelo faz no treino é exatamente o que ele faz em produção e isso é boa parte do motivo pelo qual ela escalou melhor.
Verifique você mesmo — por que não usar o BERT para escrever um texto?
Porque ele foi treinado para preencher lacunas olhando para os dois lados. Para gerar, é preciso escrever da esquerda para a direita sem o futuro, que é exatamente a situação que ele nunca viu. E mesmo que funcionasse, cada palavra nova mudaria a representação de todas as anteriores (a atenção é bidirecional), então não haveria cache de KV: cada passo recodificaria a frase inteira.
Um bloco moderno mínimo
import torch, torch.nn as nn, torch.nn.functional as F
class Block(nn.Module):
"""Um bloco de decoder, convenções de 2026: pré-norm, RMSNorm, GQA, SwiGLU, sem vieses."""
def __init__(self, d, n_heads, n_kv_heads):
super().__init__()
self.h, self.kv, self.dh = n_heads, n_kv_heads, d // n_heads
self.n1, self.n2 = nn.RMSNorm(d), nn.RMSNorm(d)
self.wq = nn.Linear(d, d, bias=False)
self.wk = nn.Linear(d, n_kv_heads * self.dh, bias=False) # GQA: menos heads de K/V
self.wv = nn.Linear(d, n_kv_heads * self.dh, bias=False)
self.wo = nn.Linear(d, d, bias=False)
h = round(8 * d / 3 / 256) * 256 # SwiGLU mantém os parâmetros
self.w1, self.w3 = nn.Linear(d, h, bias=False), nn.Linear(d, h, bias=False)
self.w2 = nn.Linear(h, d, bias=False)
def attn(self, x, freqs):
B, T, _ = x.shape
q = self.wq(x).view(B, T, self.h, self.dh).transpose(1, 2)
k = self.wk(x).view(B, T, self.kv, self.dh).transpose(1, 2)
v = self.wv(x).view(B, T, self.kv, self.dh).transpose(1, 2)
q, k = apply_rope(q, freqs), apply_rope(k, freqs) # a posição mora aqui, não na entrada
o = F.scaled_dot_product_attention( # FlashAttention quando disponível
q, k, v, is_causal=True, enable_gqa=True)
return self.wo(o.transpose(1, 2).reshape(B, T, -1))
def forward(self, x, freqs):
x = x + self.attn(self.n1(x), freqs) # pré-norm: o fluxo fica limpo
h = self.n2(x)
return x + self.w2(F.silu(self.w1(h)) * self.w3(h)) # SwiGLU
Compare com o diagrama de 2017 e o diff é exatamente os seis controles do painel acima — confira linha a linha contra a tabela do topo desta página. O mecanismo não mudou; a engenharia em volta dele, sim. A função apply_rope é a rotação do capítulo anterior e fica de fora por brevidade.
-
Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding — NAACL. ↩
-
Xiong, R., et al. (2020). On Layer Normalization in the Transformer Architecture — ICML. Por que o pós-LN precisa de warmup e o pré-LN não. ↩
-
Zhang, B., & Sennrich, R. (2019). Root Mean Square Layer Normalization — NeurIPS. ↩
-
Shazeer, N. (2020). GLU Variants Improve Transformer. Inclui a observação do próprio artigo de que ele não oferece explicação para o funcionamento. ↩
-
Warner, B., et al. (2024). Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder for Fast, Memory Efficient, and Long Context Finetuning and Inference. ModernBERT; o encoder reconstruído com tudo que se aprendeu desde 2018. ↩