Logo NascimentoIgor Nascimento / Blog
Inferência

Janela de contexto é VRAM: a conta do KV cache que derruba servidores

"Esse modelo suporta 128 mil tokens de contexto" virou a métrica de ouro do marketing, e quase ninguém fala do preço em silício. Você dimensiona pelo tamanho dos pesos, sobra folga na placa, dois usuários colam um PDF de quarenta páginas e o servidor morre. Uma conversa sobre a fórmula que todo mundo deveria ter no bolso, os números que ela produz, e o que o painel do servidor mostra antes do estouro.

InferênciaInfraestrutura13 min de leituraIgor Nascimento

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çãoPor token8k tokens32k tokens128k tokens
MHA clássico (32 cabeças KV), FP16512 KiB4 GiB16 GiB64 GiB
GQA (8 cabeças KV), FP16128 KiB1 GiB4 GiB16 GiB
GQA (8 cabeças KV), KV em FP864 KiB0,5 GiB2 GiB8 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.

O caso cotidiano

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

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.

A frase para levar

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