A métrica de ouro do marketing e a conta que ninguém faz
"Esse modelo suporta 128 mil tokens de contexto!" Virou a frase de efeito do anúncio de todo modelo novo, e quase ninguém fala sobre o preço em silício que essa conta cobra quando o servidor vai para a vida real.
O erro de dimensionamento mais comum é olhar apenas para o tamanho dos pesos. Você calcula que um modelo quantizado precisa de 10 GB de VRAM, coloca numa placa de 12 GB e acha que a folga é confortável. Dois usuários colam PDFs de quarenta páginas e o servidor morre com o temido OOM, Out of Memory. O que faltou na conta tem nome: KV cache.
Por que o KV cache existe
Na atenção, cada token novo precisa olhar para todos os tokens anteriores. Sem cache, gerar o token número mil exigiria recalcular as projeções de chave e valor dos 999 anteriores, e de novo para o token 1.001, e assim por diante. A geração inteira viraria um processo quadrático de recomputação pura.
A solução é guardar os tensores K e V de cada token, em cada camada, assim que forem calculados. Isso troca computação por memória. E essa memória é VRAM dedicada, travada na GPU enquanto a sequência estiver viva.
A fórmula que todo mundo deveria ter no bolso
\[ \text{KV}_{\text{bytes}} = 2 \cdot L \cdot n_{kv} \cdot d_{head} \cdot s \cdot B \cdot b \]
Em que 2 são os tensores K e V; \(L\) é o número de camadas; \(n_{kv}\) são as cabeças de chave e valor (com GQA, bem menos que as cabeças de query); \(d_{head}\) é a dimensão por cabeça; \(s\) são os tokens no contexto; \(B\) são as requisições simultâneas; e \(b\) são os bytes por valor.
Repare na estrutura: o consumo cresce linearmente com três fatores multiplicados entre si, contexto, concorrência e profundidade da rede. Dobrar o contexto e dobrar os usuários ao mesmo tempo quadruplica a memória.
A conta com números reais
Pegue um modelo típico da classe de 7 a 8 bilhões de parâmetros com GQA: 32 camadas, 8 cabeças de KV, \(d_{head} = 128\), em FP16.
\[ 2 \cdot 32 \cdot 8 \cdot 128 \cdot 2 = 131\,072 \text{ bytes} = 128 \text{ KiB por token} \]
Agora multiplique:
| Configuração | Por token | 8k tokens | 32k tokens | 128k tokens |
|---|---|---|---|---|
| MHA clássico (32 cabeças KV), FP16 | 512 KiB | 4 GiB | 16 GiB | 64 GiB |
| GQA (8 cabeças KV), FP16 | 128 KiB | 1 GiB | 4 GiB | 16 GiB |
| GQA (8 cabeças KV), KV em FP8 | 64 KiB | 0,5 GiB | 2 GiB | 8 GiB |
Leia a linha do meio com atenção: uma única sequência de 128 mil tokens consome 16 GiB de KV cache. Mais do que o modelo quantizado inteiro ocupa estaticamente, e mais do que a VRAM total da maioria das placas de consumo.
E a linha de cima explica por que as arquiteturas modernas abandonaram o MHA clássico: sem GQA, o mesmo contexto custaria 64 GiB. Agrupar cabeças de chave e valor não foi um detalhe de pesquisa. Foi o que tornou contexto longo economicamente viável.
Um PDF de quarenta páginas dá uns 20 mil tokens. Com GQA em FP16, são cerca de 2,5 GiB por usuário. Dois usuários simultâneos, 5 GiB. Se a placa tem 12 GB e o modelo ocupa 9 GB, a conta já estourou antes do terceiro usuário chegar. Foi exatamente assim que o nosso servidor caiu da primeira vez.
Como isso aparece na telemetria
Em um servidor vLLM, a VRAM é reservada de uma vez: o motor toma uma fração configurada da placa, tipicamente entre 0,85 e 0,90, descarrega os pesos e transforma todo o resto em um pool paginável de blocos de KV cache.
Por isso o painel de um servidor saudável mostra coisas como 88% de VRAM ocupada em cada GPU e um "contexto máximo" de 262.144 tokens. Esse número não é o limite de uma conversa. É o total agregado que o pool comporta, somando todas as sequências vivas. Um único usuário com 128 mil tokens consome metade do servidor inteiro.
E existe uma métrica que denuncia o estouro antes do crash: preempções. Quando o pool acaba, o escalonador precisa expulsar sequências, ou mandando o KV para a RAM, ou descartando e recomputando depois. Preempção maior que zero sob carga normal significa que o dimensionamento está no limite, e a latência percebida vai oscilar de um jeito que o usuário não consegue explicar.
Como contornar o limite físico
- PagedAttention. O problema clássico era alocar um bloco contíguo do tamanho máximo de contexto para cada requisição, mesmo quando ela usava 300 tokens. O desperdício chegava a 60 a 80%. PagedAttention faz com KV cache o que o sistema operacional faz com memória virtual: blocos pequenos, não contíguos, alocados sob demanda e compartilhados entre sequências com prefixo comum.
- Prefix caching. Prompts de sistema idênticos entre requisições deixam de ser copiados: as sequências apontam para os mesmos blocos. Economiza memória e derruba o TTFT no mesmo movimento.
- Quantização do próprio KV cache. Guardar K e V em FP8 ou INT8 em vez de FP16 corta metade da VRAM de contexto, com impacto pequeno em coerência na maioria dos casos. É algo para medir na sua tarefa, não para assumir.
- GQA, MQA e MLA. O ganho estrutural está na arquitetura do modelo, como a tabela mostra. Se contexto longo é requisito do produto, isso pesa mais na escolha do modelo do que o número de parâmetros.
- Limitar o comprimento máximo declarado. A defesa mais simples e a mais ignorada: se ninguém na sua aplicação usa 128 mil, não anuncie 128 mil. Cada token de janela declarada é VRAM que o motor precisa estar pronto para entregar.
Uma correção técnica que vale fazer
FlashAttention entra em quase toda lista de "como economizar memória", e a atribuição costuma estar errada. Ela não reduz o KV cache. O que ela faz é evitar materializar a matriz de atenção n × n na memória de alta largura de banda, processando a atenção em blocos dentro da SRAM do chip. O ganho é enorme em tráfego de memória, em velocidade e na memória temporária da atenção, principalmente durante o prefill. Mas os tensores K e V dos tokens anteriores continuam lá, ocupando exatamente os mesmos bytes. Quem confunde as duas coisas dimensiona o servidor contando com uma economia que nunca vai acontecer.
A regra de dimensionamento
\[ \text{tokens simultâneos} = \frac{\text{VRAM}_{\text{total}} \cdot u - \text{pesos} - \text{overhead}}{\text{bytes por token}} \]
Comece pela VRAM da placa, multiplique pela fração \(u\) que o motor pode usar, subtraia os pesos e um overhead real (ativações, buffers, CUDA graphs, fragmentação; não é desprezível), e divida pelo custo por token da arquitetura. O resultado é quantos tokens somados entre todos os usuários o servidor aguenta ao mesmo tempo. Divida pela concorrência esperada e você tem o contexto médio viável por pessoa.
Janela de contexto não é um número no anúncio do modelo. É memória de GPU dedicada e reservada, e é a concorrência que decide quanto dela cabe para cada um.
Resumo da conversa
- O KV cache troca recomputação quadrática por VRAM, e essa VRAM cresce com contexto vezes concorrência vezes profundidade.
- Num modelo 7–8B com GQA em FP16, 128 KiB por token. Uma sequência de 128k, 16 GiB. Sem GQA, 64 GiB.
- O "contexto máximo" do painel é agregado entre todas as sequências. Preempção acima de zero é o alarme antes do OOM.
- PagedAttention, prefix caching, KV em FP8 e escolha de arquitetura são as alavancas. Limitar a janela declarada é a mais barata.
- FlashAttention não reduz KV cache. Dimensione sem contar com isso.
Igor Nascimento