Ir para o conteúdo

11. Attention Mechanisms

Uma convolução decide de antemão quais entradas podem influenciar quais saídas: as vizinhas, sempre, com os mesmos pesos. A atenção faz a escolha oposta. Ela calcula as forças de conexão a partir dos dados, em tempo de execução, para cada par de posições.

Esse é o mecanismo inteiro e vale guardá-lo como definição de uma linha: atenção é uma camada densa cujos pesos são uma função da entrada. Todo o resto deste capítulo — o \(\sqrt{d_k}\), as máscaras, as múltiplas cabeças, o cache que domina sua conta de inferência — decorre dessa frase e do custo de levá-la a sério.

Historicamente ela chegou como remendo. A tradução sequência-a-sequência comprimia a frase de origem inteira em um único vetor e frases longas pioravam quanto mais longas fossem; Bahdanau et al. deixaram o decodificador olhar de volta para cada posição da origem1. Sete anos depois o remendo havia devorado a arquitetura: "Attention Is All You Need" removeu a recorrência e manteve apenas o olhar de volta2.

Roteiro do capítulo

  1. Por quê — o que a recorrência não conseguia fazer.
  2. A ideia — cada palavra relê as outras e o mecanismo Q/K/V, com uma conta feita à mão.
  3. Os detalhes que fazem funcionar — o \(\sqrt{d_k}\), as máscaras, a posição, as cabeças.
  4. O custo — o cache de KV e o \(O(n^2)\), que é onde a engenharia de 2026 acontece.

O que veio antes: a recorrência e o alcance do gradiente

Antes da atenção, uma sequência era lida um passo de cada vez. Uma RNN carrega um estado \(h_t\) e o reescreve a cada token:

\[ h_t = \tanh\!\left(W h_{t-1} + U x_t\right) \]

Pense no estado como uma folha de anotações de tamanho fixo que você reescreve a cada palavra lida. Para lembrar a primeira palavra no fim de um parágrafo, a anotação dela precisa sobreviver a todas as reescritas do caminho.

O LSTM14 acrescenta a isso uma célula \(c_t\) e três portas que a própria rede aprende a abrir e fechar — esquecer, entrar e sair:

\[ f_t = \sigma\!\left(W_f [h_{t-1}, x_t]\right) \qquad i_t = \sigma\!\left(W_i [h_{t-1}, x_t]\right) \qquad o_t = \sigma\!\left(W_o [h_{t-1}, x_t]\right) \]
\[ c_t = f_t \odot c_{t-1} + i_t \odot \tanh\!\left(W_c [h_{t-1}, x_t]\right) \qquad h_t = o_t \odot \tanh(c_t) \]

A célula é o truque inteiro. A RNN simples multiplica o estado por uma matriz a cada passo; o LSTM multiplica por \(f_t\), um número entre 0 e 1 escolhido pela rede. Com \(f_t\) perto de 1 a informação atravessa muitos passos sem ser reescrita — a mesma ideia do atalho residual do capítulo 9, dezenove anos antes dele.

Mas perto de 1 não é 1. O gradiente que vai do último token ao primeiro continua sendo um produto de \(T\) fatores e um produto de muitos números menores que 1 encolhe depressa: \(0{,}9^{64} \approx 10^{-3}\). O painel mede os três caminhos na mesma escala:

Com 64 tokens e pesos de escala 1, o gradiente chega ao primeiro token com \(5 \times 10^{-4}\) do que saiu do último na RNN simples e com \(4 \times 10^{-2}\) pelo caminho da célula do LSTM. Suba para 256 tokens e viram \(10^{-14}\) e \(2 \times 10^{-6}\). As duas curvas são retas num gráfico logarítmico, que é a assinatura de um decaimento exponencial: o LSTM não resolveu o problema, comprou uma constante melhor.

A atenção não tem esse produto. A saída lê todas as posições de uma vez, então o caminho até qualquer token tem um passo: o gradiente chega dividido pelo peso de atenção, não pela distância. Some a isso o segundo custo da recorrência, que o gráfico não mostra: os \(T\) passos são sequenciais e uma GPU inteira fica esperando um estado de cada vez. Foi essa combinação — alcance e paralelismo — que aposentou a recorrência, não um ganho de acurácia isolado.

A ideia: cada palavra relê as outras

Compare duas frases:

  • "Sentei no banco da praça."
  • "Fui ao banco sacar dinheiro."

A primeira camada do modelo (a de embedding, logo abaixo) entrega para "banco" o mesmo vetor nas duas frases: ela olha só para o token, não para os vizinhos. Quem desfaz a ambiguidade é a atenção. Ela deixa cada palavra montar um vetor novo misturando os vetores das outras, com mais peso para as que importam: "banco" puxa informação de "sentei" e "praça" na primeira frase e de "sacar" e "dinheiro" na segunda. A saída de uma camada de atenção é isso — um vetor por token, agora com contexto.

Como decidir quanto pesa cada vizinho? A resposta vem de uma estrutura que você já conhece, o dicionário:

estoque = {"maçã": 3, "pera": 7}
estoque["pera"]     # a chave casa exatamente com uma entrada e devolve o valor dela: 7

A atenção é um dicionário suave. Em vez de exigir casamento exato, ela compara a pergunta com todas as chaves, dá a cada uma uma nota de semelhança e devolve uma média ponderada de todos os valores. Como a média é uma função suave das notas, dá para derivar — e a rede aprende, por gradiente, o que perguntar e o que anunciar.

Dos tokens aos vetores: a camada de embedding

Antes de haver o que comparar, o texto precisa virar vetores. Ele chega ao modelo como índices inteiros e a primeira camada é uma tabela \(E \in \mathbb{R}^{V \times d}\) com uma linha por token do vocabulário. A passagem direta é uma busca:

\[ y_t = E[i_t] \qquad\text{que é o mesmo que}\qquad y_t = \text{one-hot}(i_t)^\top E \]

Empilhando os \(n\) vetores \(y_t\) da frase obtemos a matriz \(X \in \mathbb{R}^{n \times d}\) que a atenção recebe. As duas formas dão o mesmo vetor, mas a segunda explica a volta. Multiplicar por um vetor que é zero em toda parte menos numa posição produz um gradiente que também é zero em toda parte menos naquela linha, com as repetições somadas:

\[ \frac{\partial L}{\partial E[i]} \;=\; \sum_{t \,:\, i_t = i} \frac{\partial L}{\partial y_t} \]

Três consequências:

  • A atualização é esparsa. Num passo com poucos milhares de tokens, a maior parte das linhas de \(E\) recebe gradiente exatamente zero. É por isso que os frameworks têm um caminho separado para embeddings (sparse=True, IndexSelect) em vez de tratar \(E\) como uma matriz densa qualquer.
  • Token raro aprende devagar. O painel sorteia tokens por uma lei de Zipf, como a linguagem de verdade: os primeiros aparecem várias vezes por sequência, a cauda longa aparece quase nunca e as linhas dela quase não se movem.
  • A tabela é enorme e costuma ser reaproveitada. Com 128 mil linhas e 4096 colunas são 525 milhões de parâmetros só na entrada. Amarrar essa matriz à camada de saída, que projeta de volta para o vocabulário, economiza metade disso — e funciona porque as duas falam do mesmo espaço.

Query, Key, Value

O dicionário suave precisa de três coisas de cada token e a atenção dá um nome a cada uma:

  • Query (consulta) — o que esta posição está procurando. É a pergunta.
  • Key (chave) — o que cada posição anuncia sobre si mesma. É a etiqueta que será comparada com a pergunta.
  • Value (valor) — o que cada posição de fato entrega, se for escolhida. É o conteúdo.

Pontue cada chave contra a query com um produto escalar, transforme os scores em uma distribuição com o softmax e devolva a mistura correspondente de valores:

\[ \text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V \]

Q, K e V são três projeções lineares diferentes da mesma entrada: \(Q = XW_Q\), \(K = XW_K\), \(V = XW_V\). As matrizes \(W_Q\), \(W_K\) e \(W_V\) são os únicos parâmetros aprendidos da camada. As três projeções são o motivo pelo qual um token pode pedir uma coisa, anunciar outra e entregar uma terceira — um pronome pode consultar por "o substantivo a que me refiro" sem ele próprio ser um substantivo.

flowchart LR
  X["X<br/>(n × d)"] --> Q["Q = X·W_Q"]
  X --> K["K = X·W_K"]
  X --> V["V = X·W_V"]
  Q --> S["S = Q·Kᵀ / √d_k<br/>(n × n)"]
  K --> S
  S --> A["A = softmax(S)<br/>cada linha soma 1"]
  A --> O["O = A·V<br/>(n × d_v)"]
  V --> O

Um exemplo feito à mão

Fique com a primeira frase e três palavras dela: "sentei · banco · praça" (o "no" e o "da" ficam de fora só para a conta caber). Vamos calcular a saída de "banco". Os vetores têm duas dimensões e foram escolhidos à mão; os valores têm um significado legível, \([\text{assento},\ \text{dinheiro}]\):

Token chave \(k\) valor \(v\)
sentei \([1,\ 0]\) \([1,\ 0]\) — aponta para assento
banco \([1,\ 1]\) \([0{,}5,\ 0{,}5]\) — sozinho, é ambíguo
praça \([0,\ 2]\) \([1,\ 0]\) — aponta para assento

A query de "banco" é \(q = [1,\ 2]\) e \(d_k = 2\).

  1. Scores. O produto escalar de \(q\) com cada chave: \(q \cdot k_\text{sentei} = 1\), \(q \cdot k_\text{banco} = 1 + 2 = 3\), \(q \cdot k_\text{praça} = 4\).
  2. Escala. Dividindo por \(\sqrt{2} \approx 1{,}41\): \([0{,}71;\ 2{,}12;\ 2{,}83]\).
  3. Softmax. \(e^{0{,}71} \approx 2{,}0\), \(e^{2{,}12} \approx 8{,}3\), \(e^{2{,}83} \approx 16{,}9\); somam \(27{,}3\). Dividindo cada um pela soma, os pesos são \(a = [0{,}07;\ 0{,}31;\ 0{,}62]\). Eles são positivos e somam 1.
  4. Mistura. A saída é a média dos valores com esses pesos:
\[ o_\text{banco} = 0{,}07\,[1,\ 0] + 0{,}31\,[0{,}5,\ 0{,}5] + 0{,}62\,[1,\ 0] = [0{,}85;\ 0{,}15] \]

O "banco" que entrou em cima do muro, \([0{,}5;\ 0{,}5]\), sai da camada como \([0{,}85;\ 0{,}15]\): assento. Nenhum peso foi fixado por posição. A escolha veio de quanto a query de "banco" se parece com a chave de cada vizinho.

O painel começa exatamente nesses números. Refaça a conta acompanhando os passos ①–④, depois experimente:

  • troque o valor de "praça" para \([0,\ 1]\) — é como se a frase fosse "…banco… dinheiro" — e veja o veredito mudar sem que nenhum peso de atenção se mexa;
  • mova a query para \([1,\ 0]\) e veja o peso sair de "praça" e se dividir entre "sentei" e "banco";
  • zere a query: todos os scores viram 0 e a atenção vira uma média simples.
q — a query de "banco": [q1, q2]
d_k (só muda o divisor √d_k)
chave k — o que ele anuncia
valor v — [assento, dinheiro]
sentei
banco
praça

As formas de cada matriz

Para uma frase de \(n\) tokens, cada um com \(d\) dimensões, é assim que as formas se encadeiam. Metade dos bugs de uma implementação nova aparece como um erro de forma nesta tabela:

Tensor Forma O que é
\(X\) \(n \times d\) um vetor por token, vindo do embedding (ou da camada anterior)
\(W_Q,\ W_K\) \(d \times d_k\) parâmetros aprendidos
\(W_V\) \(d \times d_v\) parâmetros aprendidos
\(Q,\ K\) \(n \times d_k\) uma query e uma chave por token
\(V\) \(n \times d_v\) um valor por token
\(S = QK^\top/\sqrt{d_k}\) \(n \times n\) o score de cada par de tokens — daí o \(O(n^2)\)
\(A = \text{softmax}(S)\) \(n \times n\) a linha \(i\) diz quanto o token \(i\) olha para cada token
\(O = AV\) \(n \times d_v\) um vetor novo, com contexto, por token

O mesmo, em código

A função inteira cabe em três linhas. O script refaz a conta de cima e depois roda a mesma função para as três queries ao mesmo tempo — a linha do meio de \(A\) e de \(O\) é o "banco" que você calculou à mão:

"""O exemplo feito à mão na página, refeito em código — e depois a mesma conta para todas as linhas.

Contexto: "sentei · banco · praça". A query é a de "banco"; os values têm duas dimensões,
[assento, instituição financeira]. O "banco" sozinho é ambíguo ([0.5, 0.5]); o contexto decide.
"""
import numpy as np

def softmax(s):
    e = np.exp(s - s.max(axis=-1, keepdims=True))     # subtrair o máximo não muda o resultado
    return e / e.sum(axis=-1, keepdims=True)

def attention(Q, K, V):
    S = Q @ K.T / np.sqrt(K.shape[-1])                 # (n, n): score de cada par
    A = softmax(S)                                     # (n, n): cada linha soma 1
    return A @ V, A                                    # (n, d_v): mistura de values

K = np.array([[1.0, 0.0],      # sentei
              [1.0, 1.0],      # banco
              [0.0, 2.0]])     # praça
V = np.array([[1.0, 0.0],      # sentei -> assento
              [0.5, 0.5],      # banco  -> ambíguo
              [1.0, 0.0]])     # praça  -> assento
q = np.array([1.0, 2.0])       # a query de "banco"

np.set_printoptions(precision=2, suppress=True)
s = K @ q
print("q·k         =", s)
print("q·k / √2    =", s / np.sqrt(2))
print("a = softmax =", softmax(s / np.sqrt(2)))
print("Σ a·v       =", softmax(s / np.sqrt(2)) @ V)

# A mesma função, para as três queries ao mesmo tempo: uma linha de A por token.
Q = np.array([[2.0, 0.0],      # sentei
              [1.0, 2.0],      # banco (a mesma query de cima)
              [0.0, 1.0]])     # praça
O, A = attention(Q, K, V)
print("\nA =\n", A)
print("O =\n", O)
q·k         = [1. 3. 4.]
q·k / √2    = [0.71 2.12 2.83]
a = softmax = [0.07 0.31 0.62]
Σ a·v       = [0.85 0.15]

A =
 [[0.45 0.45 0.11]
 [0.07 0.31 0.62]
 [0.14 0.28 0.58]]
O =
 [[0.78 0.22]
 [0.85 0.15]
 [0.86 0.14]]
Verifique você mesmo — se todas as chaves forem idênticas, qual é a saída?

Todos os scores \(q \cdot k_j\) ficam iguais, o softmax de números iguais é uniforme e cada peso vale \(1/n\). A saída é a média simples dos valores, a mesma para qualquer query. A atenção só consegue escolher alguém se as chaves forem diferentes entre si — é por isso que \(W_K\) é aprendido.

A matriz inteira e por que \(\sqrt{d_k}\) não é cosmético

Uma query contra três chaves é o mecanismo. O que uma camada de fato computa é a matriz \(n \times n\) completa: cada posição consulta cada posição. O painel abaixo a constrói para uma frase real. Leia por linha: a linha de um token mostra para onde ele olha e cada linha soma 1.

Cada opção em cabeça (head) é uma função de score diferente e as três descrevem padrões que realmente ocorrem em modelos treinados — uma cabeça que casa por conteúdo, uma que apenas olha um token atrás, uma que decai com a distância. Uma camada roda muitas ao mesmo tempo.

Agora desligue dividir por \(\sqrt{d_k}\) e arraste \(d_k\) para cima. A matriz vira binária.

A razão é um argumento de duas linhas. Se \(q\) e \(k\) têm componentes independentes com média 0 e variância 1, então

\[ \text{Var}\!\left(q \cdot k\right) = \text{Var}\!\left(\sum_{i=1}^{d_k} q_i k_i\right) = d_k \]

porque é uma soma de \(d_k\) termos independentes, cada um com variância 1. Os scores têm então desvio padrão \(\sqrt{d_k}\) — com \(d_k = 128\), cerca de 11. Scores rotineiramente diferem por 20. Um softmax sobre scores que diferem por 20 é um argmax duro: \(e^{-20} \approx 2 \times 10^{-9}\), então um peso vale 1, o resto vale \(10^{-9}\) e o gradiente que passa pelo softmax é zero. Dividir por \(\sqrt{d_k}\) restaura variância unitária e mantém a camada no regime em que ela ainda consegue aprender.

Verifique você mesmo — por que dividir por \(\sqrt{d_k}\) e não por \(d_k\)?

Dividir por \(d_k\) deixaria a variância dos scores em \(d_k / d_k^2 = 1/d_k\). Com \(d_k = 128\) os scores ficariam todos perto de zero, o softmax viraria quase uniforme e a atenção viraria uma média que não escolhe ninguém — o problema oposto, mas igualmente ruim. \(\sqrt{d_k}\) é o divisor que deixa a variância em exatamente 1.

Atenção saturada parece bug de treino e não é

Um modelo cuja entropia de atenção colapsa para perto de zero nas primeiras épocas parou de aprender onde olhar; ele vai treinar, devagar, com o que quer que o caminho dos valores consiga fazer sozinho. Registrar a entropia média de atenção por camada não custa nada e pega isso, junto com seus primos: escala ausente, logits estourados por um fluxo residual não normalizado, ou uma temperatura aplicada duas vezes.

A volta por uma cabeça

Esta subseção é a mais técnica do capítulo. Na primeira leitura, basta a conclusão em negrito do terceiro parágrafo.

A frase acima — o gradiente que passa pelo softmax é zero — sai da conta, que custa quatro linhas. Com \(S = QK^\top/\sqrt{d_k}\), \(A = \text{softmax}(S)\) e \(O = AV\):

\[ \frac{\partial L}{\partial V} = A^\top \frac{\partial L}{\partial O} \qquad \frac{\partial L}{\partial A} = \frac{\partial L}{\partial O}\, V^\top \]
\[ \frac{\partial L}{\partial S} = A \odot \left( \frac{\partial L}{\partial A} - \Big( \tfrac{\partial L}{\partial A} \odot A \Big)\mathbf{1}\mathbf{1}^\top \right) \]
\[ \frac{\partial L}{\partial Q} = \frac{1}{\sqrt{d_k}}\frac{\partial L}{\partial S} K \qquad \frac{\partial L}{\partial K} = \frac{1}{\sqrt{d_k}}\left(\frac{\partial L}{\partial S}\right)^{\!\top} Q \]

O termo do meio é a jacobiana do softmax e é onde está a lição. Repare no fator \(A \odot\): se uma linha de \(A\) é quase um one-hot — um peso valendo 1 e o resto valendo \(10^{-9}\) —, então quase todo o parêntese é multiplicado por \(10^{-9}\); e na única posição onde \(A\) não é minúsculo, o parêntese em si é praticamente zero, porque a média ponderada que ele subtrai é o próprio elemento. Uma atenção saturada devolve gradiente numericamente nulo em toda a linha. É disso que o \(\sqrt{d_k}\) protege.

A mesma fórmula dá uma invariante barata para conferir implementação: cada linha de \(\partial L/\partial S\) soma exatamente zero, porque somar uma constante a uma linha de scores não muda o softmax dela.

"""A passagem reversa de uma cabeça de atenção, conferida por diferenças finitas.

O termo que importa é o do softmax: ele não é um fator por elemento, é uma jacobiana que
subtrai a média ponderada da linha — e é ela que zera o gradiente quando a atenção satura.
"""
import numpy as np

def softmax(s):
    e = np.exp(s - s.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)

def forward(Q, K, V):
    d = Q.shape[-1]
    S = Q @ K.T / np.sqrt(d)
    A = softmax(S)
    return A @ V, A

def backward(dO, Q, K, V, A):
    d = Q.shape[-1]
    dV = A.T @ dO
    dA = dO @ V.T
    dS = A * (dA - (dA * A).sum(axis=-1, keepdims=True))     # jacobiana do softmax, linha a linha
    return dS @ K / np.sqrt(d), dS.T @ Q / np.sqrt(d), dV, dS

rng = np.random.default_rng(0)
Q, K, V = (rng.normal(size=(3, 4)) for _ in range(3))
dO = rng.normal(size=(3, 4))

O, A = forward(Q, K, V)
dQ, dK, dV, dS = backward(dO, Q, K, V, A)

def num_grad(M):
    g, h = np.zeros_like(M), 1e-6
    for i in range(M.shape[0]):
        for j in range(M.shape[1]):
            old = M[i, j]
            M[i, j] = old + h; up = (dO * forward(Q, K, V)[0]).sum()
            M[i, j] = old - h; dn = (dO * forward(Q, K, V)[0]).sum()
            M[i, j] = old; g[i, j] = (up - dn) / (2 * h)
    return g

np.set_printoptions(precision=3, suppress=True)
print("pesos de atenção A (cada linha soma 1):\n", A)
print("dQ:\n", dQ)
print("erro máximo contra diferenças finitas — dQ:", f"{np.abs(num_grad(Q) - dQ).max():.1e}",
      " dK:", f"{np.abs(num_grad(K) - dK).max():.1e}",
      " dV:", f"{np.abs(num_grad(V) - dV).max():.1e}")
print("soma de cada linha de dS:", np.round(dS.sum(axis=-1), 12),
      "— sempre zero: somar a mesma constante a uma linha de scores não muda o softmax")
pesos de atenção A (cada linha soma 1):
 [[0.226 0.475 0.298]
 [0.144 0.598 0.259]
 [0.668 0.235 0.097]]
dQ:
 [[0.456 0.354 0.099 0.209]
 [0.407 0.404 0.013 0.129]
 [0.345 0.228 0.109 0.184]]
erro máximo contra diferenças finitas — dQ: 3.2e-10  dK: 6.1e-10  dV: 3.4e-10
soma de cada linha de dS: [ 0. -0. -0.] — sempre zero: somar a mesma constante a uma linha de scores não muda o softmax

Máscaras: causal, de padding e todas as outras

Às vezes um token não pode olhar para outro: o futuro, num modelo que gera texto; o enchimento <pad>, num batch. A máscara é somada aos scores antes do softmax, como \(-\infty\) (na prática, um número negativo grande). Como \(e^{-\infty} = 0\), os pesos proibidos ficam exatamente zero:

\[ M_{ij} = \begin{cases} 0 & j \leq i \\ -\infty & j > i \end{cases} \]

Ligue a máscara causal no painel e o triângulo superior escurece. Essa única linha é toda a diferença entre um encoder e um decoder e a razão pela qual um decoder pode ser treinado em todas as posições em paralelo: com a máscara, a previsão da posição \(i\) não pode ver a própria resposta, então uma passagem direta dá \(n\) exemplos de treino em vez de um. Para a frase "o gato subiu no telhado":

Posição O que ela enxerga O que ela deve prever
1 o gato
2 o gato subiu
3 o gato subiu no
4 o gato subiu no telhado

Quatro exemplos, uma única passagem pela rede. Sem a máscara, a posição 1 veria "gato" logo ao lado e aprenderia a copiar em vez de prever.

Máscara O que proíbe Onde
Causal Olhar o futuro Todo decoder autorregressivo — GPT, Llama, Claude
De padding Atender ao enchimento <pad> de um batch Onde quer que você agrupe sequências de tamanhos diferentes
Janela deslizante Olhar mais de \(w\) tokens para trás Mistral, Gemma e as camadas locais de modelos híbridos
De documento/packing Cruzar a fronteira entre dois documentos empacotados na mesma sequência Todo pré-treino moderno. Omiti-la treina o modelo, silenciosamente, a atender entre documentos sem relação.

De onde vem a posição

Olhe de novo a conta feita à mão: em nenhum momento ela usou a ordem das palavras. A atenção é equivariante a permutações: embaralhe os tokens e as saídas embaralham junto, com valores idênticos. Sozinha, ela não distingue "cachorro morde homem" de "homem morde cachorro". A posição precisa ser injetada e como ela é injetada acabou importando muito para contexto longo.

Esquema Mecanismo Veredito
Senoidal2 Vetores fixos de sen/cos somados aos embeddings de entrada Histórico. Extrapola mal.
Absoluta aprendida Um vetor treinável por posição Histórico (BERT, GPT-2). Não passa do comprimento treinado, de jeito nenhum.
ALiBi9 Uma penalidade linear na distância somada aos scores Extrapola bem; largamente superada
RoPE7 Girar os vetores Q e K por um ângulo proporcional à posição Universal. Llama, Qwen, Mistral, Gemma, DeepSeek
NoPE / híbrida Nenhum sinal posicional em algumas camadas; a causalidade sozinha carrega a ordem Cada vez mais comum em pilhas híbridas e de contexto longo

Vale entender a RoPE em vez de decorá-la. Imagine um relógio: o ponteiro de cada token é girado um tanto a mais a cada posição. O ângulo entre dois ponteiros não depende de que horas são, só de quantas posições os separam. Em símbolos: tome Q e K em pares de dimensões, trate cada par como um número complexo e multiplique por \(e^{i\,m\theta}\), onde \(m\) é a posição. Como rotações se compõem somando ângulos, o produto escalar entre a posição \(m\) e a posição \(n\) depende apenas de \(m - n\):

\[ \langle R_m q,\; R_n k \rangle \;=\; \langle q,\; R_{n-m} k \rangle \]

Rotações absolutas entram, posição relativa sai — sem parâmetros extras e sem tensor extra a somar. Cada par de dimensões gira numa velocidade \(\theta\) diferente, como os ponteiros de horas, minutos e segundos: os rápidos distinguem vizinhos próximos, os lentos distinguem distâncias longas. Essa propriedade é também por que janelas de contexto podem ser estendidas depois do treino: reescale as frequências \(\theta\) e os mesmos pesos endereçam uma sequência mais longa (interpolação de posição, escala NTK, YaRN10). Todo anúncio de "estendemos para 1M de tokens" é, mecanicamente, isso mais fine-tuning.

Multi-head e no que ela se transformou

Uma cabeça produz uma distribuição por posição — uma relação. Mas um token costuma precisar de várias ao mesmo tempo: um verbo quer achar o sujeito e olhar a palavra anterior; um pronome quer o substantivo a que se refere e a pontuação que fecha a frase. Uma única linha de pesos que soma 1 não consegue apontar forte para tudo isso. A atenção multi-cabeça roda \(h\) cabeças em paralelo, cada uma com suas próprias \(W_Q^i, W_K^i, W_V^i\) sobre fatias de dimensão \(d/h\) e concatena as saídas:

\[ \text{MultiHead}(X) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)\, W^O, \qquad \text{head}_i = \text{Attention}(XW_Q^i,\, XW_K^i,\, XW_V^i) \]

Com \(d = 4096\) e \(h = 32\), cada cabeça trabalha com vetores de 128 dimensões; as 32 saídas concatenadas voltam a ter 4096 e \(W^O\) mistura o que cada cabeça achou. As opções de cabeça do painel da matriz são exatamente esse tipo de especialização.

Repare no que isso não custa: com \(d/h\) por cabeça, \(h\) cabeças custam os mesmos FLOPs que uma cabeça de largura total. Cabeças são paralelismo de graça, não capacidade extra — e é por isso que \(h\) só cresceu.

Aí veio a inferência e o desenho mudou. A próxima seção é o porquê.

O cache de KV é o que ninguém ensina e todo mundo paga

Um modelo gera texto um token por vez e cada token novo precisa de atenção sobre todos os anteriores. O que isso exige de cada token antigo? A query dele não: ela só serviu para calcular a própria saída, lá atrás. Mas a chave e o valor dele serão consultados por todo token que vier depois. Recalculá-los a cada passo faria gerar \(n\) tokens custar \(O(n^2)\) passagens completas — então ninguém faz isso. Cada K e V é calculado uma vez e guardado.

Esse cache é hoje o custo dominante de memória para servir um modelo e é a razão pela qual "contexto de 128k" é uma decisão de preço, não de arquitetura:

\[ \text{bytes de cache} \;=\; 2 \times L \times h_{kv} \times d_{\text{head}} \times n \times B \times \text{bytes} \]

O 2 conta K e V; \(L\) é o número de camadas, \(n\) o comprimento do contexto e \(B\) o tamanho do batch. Ponha números de um modelo real, o Llama 3 8B: \(L = 32\), \(h_{kv} = 8\), \(d_\text{head} = 128\), em 16 bits (2 bytes). Cada token custa \(2 \times 32 \times 8 \times 128 \times 2 = 131\,072\) bytes, ou 128 KiB. Um contexto de 128k tokens ocupa então 16 GiB — tanto quanto os próprios pesos do modelo — e isso para um usuário.

Só um termo desse produto é uma variável livre de projeto: \(h_{kv}\), o número de cabeças de chave/valor. As queries podem manter todas as \(h\) cabeças; K e V podem ser compartilhados. O Llama 3 8B tem 32 cabeças de query e só 8 de chave/valor — com uma por query o cache seria quatro vezes maior. Isso produziu a sequência sobre a qual todo modelo atual se posiciona:

Esquema Cabeças de KV Cache Qualidade Situação em 2026
MHA — multi-cabeça2 \(h\) 1× Referência Histórico. Ninguém serve isso em contexto longo.
MQA — multi-query4 1 \(1/h\) Mensuravelmente pior Usado onde memória domina tudo
GQA — grouped-query5 \(g\) (tipicamente 8) \(g/h\) Indistinguível do MHA O padrão. Llama, Mistral, Qwen, Gemma
MLA — latente multi-cabeça6 — ~\(1/10\) Reportada igual ou acima do MHA DeepSeek-V2/V3; comprime KV em um latente de posto baixo e faz cache dele

Isto é um problema de banda de memória, não de FLOPs

Durante a geração você lê o cache de KV inteiro para produzir um token. A intensidade aritmética é péssima, o acelerador fica ocioso e a vazão é ditada pela banda da HBM. Tudo que acelera inferência — GQA, cache de KV quantizado, PagedAttention8, decodificação especulativa, batches maiores — é um ataque a bytes movidos, não a operações realizadas. Reconhecer de qual recurso você está com falta é a maior parte da engenharia prática de LLMs.

Tornando o \(O(n^2)\) pagável

A atenção é \(O(n^2 d)\) em tempo e, escrita ingenuamente, \(O(n^2)\) em memória: a matriz \(S\) da tabela de formas tem uma entrada por par de tokens. Com \(n = 128\text{k}\) ela tem \(1{,}7 \times 10^{10}\) entradas — 34 GB em 16 bits, por cabeça, por camada. Existem quatro famílias de respostas e elas não têm o mesmo sucesso.

  • Mesma matemática, execução melhor


    O FlashAttention3 nunca materializa a matriz \(n \times n\). Ele fatia a computação em blocos que cabem na SRAM e usa o truque do softmax online para combiná-los. A memória cai para \(O(n)\), o tempo de parede cai várias vezes e a saída é comparável bit a bit à versão ingênua.

    Isto não é uma aproximação e não há trade-off de qualidade. FlashAttention-2 e -3 afinam ainda mais para hardware classe Hopper. Se você está escrevendo atenção à mão, é quase certo que está mais lento — use F.scaled_dot_product_attention.

  • Atender a menos


    Janela deslizante: cada token vê apenas os últimos \(w\); empilhar camadas amplia o alcance efetivo exatamente como o campo receptivo de uma CNN. Sumidouros de atenção11: manter os primeiros tokens sempre visíveis — os modelos despejam massa de atenção excedente neles e removê-los destrói a geração.

    Modelos modernos de contexto longo intercalam: algumas camadas de atenção completa entre muitas locais, de modo que a informação global ainda tem rota enquanto o custo fica quase linear.

  • Mudar a matemática


    A atenção linear troca o softmax por um kernel, tornando a operação associativa e o custo \(O(n)\) — e abrindo mão da recuperação afiada que o softmax oferece. Modelos de espaço de estados (Mamba, Mamba-212) alcançam o mesmo \(O(n)\) com um estado recorrente e um cache de tamanho constante.

    O veredito honesto de 2026: modelos lineares puros perdem em recuperação exata de contexto longo. Híbridos vencem — Jamba, Zamba, Nemotron-H e Falcon-H1 misturam camadas SSM com uma minoria de camadas de atenção completa e igualam a qualidade Transformer com uma fração do cache.

  • Aprender o que pular


    Atenção esparsa treinável — o NSA da DeepSeek13 e o MoBA da Kimi — escolhe quais blocos do passado atender, com a seleção treinada ponta a ponta em vez de fixada por um padrão.

    Esta é a mais promissora das quatro no momento: mantém a recuperação exata do softmax onde importa e paga custo quase linear no resto — e, ao contrário de esquemas esparsos anteriores, é alinhada ao hardware o bastante para de fato ser mais rápida.

O que usar, na prática

Atenção completa com FlashAttention e GQA, RoPE para posição, janela deslizante ou camadas híbridas se suas sequências forem genuinamente longas. Essa combinação é o que essencialmente todo modelo lançado nos últimos dois anos faz e sair dela exige uma razão medida.

Pontos principais

  1. Atenção é uma camada densa cujos pesos são calculados a partir da entrada — a escolha de projeto oposta aos pesos fixos, locais e compartilhados de uma convolução. Ela venceu a recorrência por alcance (um passo até qualquer token) e paralelismo.
  2. Ela funciona como um dicionário suave: compara uma pergunta com todas as chaves e devolve uma média ponderada dos valores. A saída é um vetor com contexto por token.
  3. Q, K e V são três projeções da mesma entrada. A separação é o que permite a um token pedir algo que ele não é.
  4. O \(\sqrt{d_k}\) existe porque a variância dos scores cresce com \(d_k\). Sem ele o softmax satura e o gradiente some. Registre a entropia da atenção; é barato e diagnóstico.
  5. A máscara é uma matriz somada e é toda a distinção encoder/decoder. Esquecer a máscara de documento ao empacotar é um bug de pré-treino real e comum.
  6. A atenção não enxerga ordem. A RoPE codifica rotações absolutas que rendem deslocamentos relativos no produto escalar — o que explica por que ela funciona e por que o contexto pode ser estendido reescalando frequências.
  7. Cabeças são paralelismo de graça, não capacidade extra: \(h\) cabeças de largura \(d/h\) custam o que custa uma cabeça de largura \(d\).
  8. O cache de KV domina a memória de inferência (128 KiB por token no Llama 3 8B). \(h_{kv}\) é o único termo que você pode encolher e é por isso que GQA é universal e MLA é para onde a fronteira foi. Inferência é limitada por banda, não por computação: otimize bytes movidos.
  9. FlashAttention é exato e de graça. Esparsidade, janelas deslizantes e SSMs trocam alguma coisa; em 2026 as trocas vencedoras são pilhas híbridas e atenção esparsa treinável.


  1. Bahdanau, D., Cho, K., & Bengio, Y. (2015). Neural Machine Translation by Jointly Learning to Align and Translate — ICLR. Atenção como correção para o gargalo de tamanho fixo. ↩

  2. Vaswani, A., et al. (2017). Attention Is All You Need — NeurIPS. Atenção escalada por produto escalar, multi-cabeça, posições senoidais. ↩↩↩

  3. Dao, T., Fu, D., Ermon, S., Rudra, A., & Ré, C. (2022). FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness — NeurIPS. Veja também FlashAttention-2 (2023) e FlashAttention-3 (2024). ↩

  4. Shazeer, N. (2019). Fast Transformer Decoding: One Write-Head is All You Need. Atenção multi-query e a primeira afirmação clara de que decodificar é limitado por memória. ↩

  5. Ainslie, J., et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints — EMNLP. O meio-termo que virou padrão. ↩

  6. DeepSeek-AI (2024). DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model. Atenção latente multi-cabeça: fazer cache de um latente de posto baixo em vez de K e V. ↩

  7. Su, J., Lu, Y., Pan, S., Wen, B., & Liu, Y. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding. RoPE. ↩

  8. Kwon, W., et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention — SOSP. Memória virtual para o cache de KV; o núcleo do vLLM. ↩

  9. Press, O., Smith, N., & Lewis, M. (2022). Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation — ICLR. ↩

  10. Peng, B., Quesnelle, J., Fan, H., & Shippole, E. (2024). YaRN: Efficient Context Window Extension of Large Language Models — ICLR. Reescala das frequências da RoPE, feita com cuidado. ↩

  11. Xiao, G., Tian, Y., Chen, B., Han, S., & Lewis, M. (2024). Efficient Streaming Language Models with Attention Sinks — ICLR. Por que o primeiro token não pode ser descartado. ↩

  12. Dao, T., & Gu, A. (2024). Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality — ICML. Mamba-2 e a equivalência que torna híbridos projetáveis. ↩

  13. Yuan, J., et al. (2025). Native Sparse Attention: Hardware-Aligned and Natively Trainable Sparse Attention. Esparsidade aprendida durante o pré-treino em vez de imposta depois dele. ↩

  14. Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory — Neural Computation 9(8), 1735–1780. A célula com portas que segurou o gradiente por mais passos do que a recorrência simples conseguia. ↩