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
- Por quê — o que a recorrência não conseguia fazer.
- A ideia — cada palavra relê as outras e o mecanismo Q/K/V, com uma conta feita à mão.
- Os detalhes que fazem funcionar — o \(\sqrt{d_k}\), as máscaras, a posição, as cabeças.
- 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:
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:
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:
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:
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:
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\).
- 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\).
- Escala. Dividindo por \(\sqrt{2} \approx 1{,}41\): \([0{,}71;\ 2{,}12;\ 2{,}83]\).
- 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.
- Mistura. A saída é a média dos valores com esses pesos:
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.
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
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\):
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:
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\):
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:
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:
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
- 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.
- 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.
- 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 é.
- 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.
- 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.
- 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.
- 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\).
- 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.
- 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.
-
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. ↩
-
Vaswani, A., et al. (2017). Attention Is All You Need — NeurIPS. Atenção escalada por produto escalar, multi-cabeça, posições senoidais. ↩↩↩
-
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). ↩
-
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. ↩
-
Ainslie, J., et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints — EMNLP. O meio-termo que virou padrão. ↩
-
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. ↩
-
Su, J., Lu, Y., Pan, S., Wen, B., & Liu, Y. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding. RoPE. ↩
-
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. ↩
-
Press, O., Smith, N., & Lewis, M. (2022). Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation — ICLR. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩