Ir para o conteúdo

What Changed Since 2017

O caminho do texto ao próximo token em sete passos: tokens, embeddings, query/key/value, máscara e cabeças, o bloco, a saída e a geração. ATENÇÃO TRANSFORMERS Tokenstexto → ids Embeddingsids → X Q · K · Vuma cabeça Máscaraduas cabeças BlocoLN · MLP Saídalogits → p Geraçãoescolher texto → → próximo token

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:

\[ x \;\leftarrow\; \text{Norm}\big(x + \text{Attn}(x)\big) \qquad \text{(pós-LN, 2017)} \]

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:

\[ x \;\leftarrow\; x + \text{Attn}\big(\text{Norm}(x)\big) \qquad \text{(pré-LN, tudo desde então)} \]

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,

\[ x_L = x_0 + \sum_{\ell=1}^{L} f_\ell(\,\cdot\,) \qquad\Longrightarrow\qquad \frac{\partial x_L}{\partial x_0} = I + \ldots \]

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:

\[ \text{RMSNorm}(x) = \frac{x}{\sqrt{\tfrac{1}{d}\sum_i x_i^2 + \epsilon}} \odot g \]

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:

\[ \text{FFN}(x) = \big(\text{Swish}(xW_1) \odot xW_3\big)W_2 \]

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:

  1. 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.
  2. 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.
  3. 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.


← Geração · próximo: Escala →


  1. Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding — NAACL. ↩

  2. 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. ↩

  3. Zhang, B., & Sennrich, R. (2019). Root Mean Square Layer Normalization — NeurIPS. ↩

  4. 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. ↩

  5. 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. ↩