Cache KV
O que é um cache KV?
Em um modelo de linguagem Transformer autorregressivo (um LLM), cada etapa de decodificação prevê o próximo token dado todos os tokens anteriores. As camadas de autoatenção (self-attention) do modelo calculam três projeções para cada token:
- Q (consultas)
- K (chaves)
- V (valores)
Durante a geração, as consultas do token atual “atendem” às chaves/valores de todos os tokens anteriores. Um cache KV armazena as chaves e os valores calculados para tokens anteriores para que etapas futuras de decodificação possam reutilizá-los em vez de recalculá-los repetidamente.
Este é um dos mecanismos de engenharia mais importantes no serving de LLMs porque afeta fortemente:
- Latência (tempo para gerar cada novo token)
- Vazão (tokens/segundo ou requisições/segundo que um servidor consegue produzir)
- Memória (especialmente VRAM de GPU), o que por sua vez limita a concorrência
O cache KV é usado em praticamente todas as pilhas de inferência de LLM em produção (por exemplo, vLLM, TGI, TensorRT-LLM) e normalmente vem habilitado por padrão.
Contexto relacionado: o cache é uma consequência direta do cálculo de atenção descrito em Arquitetura Transformer.
Prefill vs decode: onde o cache KV fica
A inferência de LLM costuma ser dividida em duas fases:
Prefill (também conhecido como processamento do prompt)
Dado um prompt de entrada de comprimento (P), o modelo processa os tokens do prompt em paralelo (com mascaramento causal). Durante essa fase, ele calcula K/V para cada camada e para cada token do prompt e grava isso no cache KV.
- O custo é dominado por processar um prompt longo
- Essa fase é altamente paralela em GPUs, mas ainda assim cara para contextos longos
Decode (também conhecido como geração token a token)
Em seguida, o modelo gera tokens um por vez. No passo (t), ele:
- calcula o estado oculto do novo token,
- calcula K/V apenas para o novo token (por camada),
- anexa isso ao cache,
- executa a atenção usando K/V em cache para todos os tokens anteriores.
- O custo é dominado por atenção repetida contra um cache crescente
- Essa fase é menos paralela e mais sensível à latência
O cache KV acelera principalmente a fase de decodificação.
Por que o cache KV reduz computação (e latência)
Sem um cache KV, cada passo de decodificação teria que recalcular K/V para todos os tokens anteriores, mesmo que esses tokens nunca mudem. Isso é trabalho desperdiçado.
Uma visão simplificada para uma única camada:
- No passo de decodificação (t), há (t) tokens de contexto (prompt + gerados até então).
- Calcular K/V para todos os tokens é trabalho (O(t)).
- Fazer isso em cada passo de 1 até (T) torna o trabalho total (O(T^2)) ao longo da geração.
Com um cache KV:
- No passo (t), você calcula K/V apenas para o novo token: projeção incremental de K/V (O(1)) por passo.
- A atenção ainda precisa ler o K/V em cache para (t) tokens para computar a saída, então a própria atenção cresce com o comprimento do contexto, mas você evita a projeção repetida de K/V sobre todo o prefixo.
Na prática, isso produz uma grande redução na latência por token durante a decodificação, especialmente para contextos mais longos.
Um modelo mental prático
- Sem cache: “reler e recodificar todo o histórico toda vez”
- Cache KV: “lembrar o histórico codificado e codificar apenas o novo token”
Memória do cache KV: o que é armazenado e qual é o tamanho?
O principal trade-off é que o cache KV consome memória significativa — frequentemente o custo dominante de VRAM durante serving em escala.
O que é armazenado?
Para cada camada Transformer, o cache armazena:
- Chaves: forma aproximadamente ([B, H_{kv}, L, D])
- Valores: forma aproximadamente ([B, H_{kv}, L, D])
Onde:
- (B) = número de sequências ativas (tamanho do lote / requisições concorrentes)
- (H_{kv}) = número de cabeças de chave/valor (pode diferir das cabeças de consulta sob MQA/GQA)
- (L) = comprimento atual da sequência (prompt + gerados até então)
- (D) = dimensão da cabeça (por exemplo, 128)
Isso é armazenado para todas as camadas.
Fórmula aproximada de memória
Uma estimativa comum “de guardanapo”:
[ \text{bytes KV} \approx B \cdot L \cdot N_{layers} \cdot 2 \cdot H_{kv} \cdot D \cdot \text{bytes_per_element} ]
- O fator 2 é para K e V.
bytes_per_elementé tipicamente 2 para FP16/BF16, 1 para INT8, etc.
Exemplo resolvido (modelo típico da classe 7B)
Assuma (semelhante a formas no estilo LLaMA-7B):
- (N_{layers} = 32)
- (H_{kv} = 32) (atenção multi-cabeças padrão, não MQA/GQA)
- (D = 128)
- dtype = FP16 (2 bytes)
Por token, por camada:
- elementos K+V = (2 \cdot 32 \cdot 128 = 8192) elementos
- bytes = (8192 \cdot 2 \approx 16{,}384) bytes ≈ 16 KB
Ao longo de 32 camadas:
- (16 \text{ KB} \cdot 32 \approx 512 \text{ KB por token})
Para uma única sequência de comprimento 2048:
- (512 \text{ KB} \cdot 2048 \approx 1{,}048{,}576 \text{ KB} \approx 1 \text{ GB})
Assim, uma única conversa longa com 2k tokens pode consumir na ordem de ~1 GB de VRAM apenas para cache KV (pesos do modelo e ativações são adicionais).
É por isso que serving com contexto longo frequentemente é limitado por memória, e por que técnicas como GQA/MQA, quantização de KV e paginação são tão importantes.
Calculadora rápida (Python)
def kv_cache_bytes(
batch_size: int,
seq_len: int,
n_layers: int,
n_kv_heads: int,
head_dim: int,
bytes_per_element: int = 2, # fp16/bf16
) -> int:
# 2 for K and V
return batch_size * seq_len * n_layers * 2 * n_kv_heads * head_dim * bytes_per_element
def fmt_gb(n_bytes: int) -> str:
return f"{n_bytes / (1024**3):.2f} GiB"
print(fmt_gb(kv_cache_bytes(
batch_size=1, seq_len=2048, n_layers=32, n_kv_heads=32, head_dim=128, bytes_per_element=2
)))
Como o cache KV impacta a vazão (e por que a memória vira o gargalo)
No serving de LLMs, normalmente você quer maximizar a vazão executando múltiplas requisições concorrentemente e/ou usando batching. O cache KV complica isso porque:
- Cada requisição ativa tem seu próprio cache crescente
- Prompts mais longos e gerações mais longas aumentam o tamanho do cache
- Mais requisições concorrentes multiplicam o uso de memória
Como resultado, o sistema frequentemente fica limitado por memória de KV antes de ficar limitado por computação.
Latência vs vazão: o trade-off central
- Mais concorrência (lote maior, mais sequências ativas) melhora a utilização da GPU e a vazão.
- Mas mais concorrência consome mais memória de cache KV, o que pode forçar:
- tamanho máximo de lote menor,
- contexto máximo menor,
- OOMs mais frequentes,
- ou técnicas caras como paginação/offloading.
Isso interage diretamente com políticas de batching discutidas em Batching e Agendamento de Inferência.
Outro multiplicador importante: beam search
Se você usar beam search com (k) beams, você efetivamente replica o estado de decodificação, incluindo o cache KV, para cada beam. A memória pode crescer aproximadamente proporcional ao número de beams (dependente da implementação, mas frequentemente próximo disso).
Por exemplo, beam size 4 pode tornar o cache KV ~4× maior do que a decodificação gulosa.
Padrões de implementação: como o cache KV é representado em código
“past_key_values” em APIs de modelos
Muitas bibliotecas expõem o cache KV explicitamente. No Hugging Face Transformers, o cache frequentemente aparece como past_key_values, e use_cache=True diz ao modelo para retornar caches atualizados.
Conceitualmente:
# Pseudocode illustrating the idea
past = None
for step in range(num_new_tokens):
outputs = model(input_ids=current_token_ids, past_key_values=past, use_cache=True)
logits = outputs.logits
past = outputs.past_key_values # contains K/V for every layer
current_token_ids = sample_next_token(logits)
No serving real, normalmente você não chama o modelo em um loop Python (por desempenho), mas o mecanismo subjacente é semelhante.
Gerenciamento de KV no lado do servidor
Mecanismos de serving em produção tratam o cache KV como um sistema de memória de primeira classe:
- alocar blocos/pedaços para cada requisição,
- anexar tokens conforme eles são gerados,
- liberar blocos quando uma requisição termina,
- às vezes compactar ou paginar blocos para reduzir fragmentação.
É aqui que projetos como “atenção paginada” (block-based KV caching) importam: eles reduzem fragmentação e permitem melhor empacotamento de muitas sequências com comprimentos diferentes.
Técnicas para reduzir a memória do cache KV (e melhorar a vazão)
Como o cache KV frequentemente domina o uso de VRAM, muitas otimizações o visam diretamente.
1) Atenção Multi-Query (MQA) e Atenção Grouped-Query (GQA)
Na atenção multi-cabeças padrão, cada cabeça de consulta tem suas próprias cabeças de K/V. MQA/GQA reduz o número de cabeças de K/V:
- Consultas: muitas cabeças
- Chaves/Valores: menos cabeças compartilhadas entre cabeças de consulta
Isso reduz a memória do cache KV aproximadamente proporcional à redução em (H_{kv}). Por exemplo, passar de 32 cabeças KV para 8 cabeças KV pode cortar o tamanho do cache KV em ~4×.
Trade-off: pode afetar levemente a qualidade do modelo, a menos que o modelo tenha sido treinado com essa variante de atenção.
2) Quantização do cache KV (INT8/FP8 e além)
Armazenar K/V em FP16 é caro. Muitas pilhas suportam quantizar o cache para reduzir memória e largura de banda:
- KV em INT8: ~2× redução vs FP16
- KV em FP8 (onde suportado): objetivos semelhantes, às vezes com melhores trade-offs de acurácia/vazão
Trade-off: a quantização pode degradar levemente a qualidade ou estabilidade da geração dependendo do método e do modelo.
3) Janela deslizante / atenção local (truncar histórico em cache)
Alguns modelos ou modos de serving usam uma janela de atenção fixa, mantendo apenas os (W) tokens mais recentes no cache. A memória vira (O(W)) em vez de (O(L)).
Trade-off: o modelo não consegue mais atender a tokens mais antigos do que a janela, o que pode prejudicar tarefas de contexto longo, a menos que o modelo seja projetado para isso.
4) Paginação e offloading de KV
Quando a memória de GPU acaba, você pode:
- paginar blocos de KV entre memória da GPU e memória da CPU, ou
- manter parte do KV na CPU e buscar conforme necessário.
Isso pode aumentar a concorrência, mas frequentemente aumenta a latência por causa de transferências PCIe/NVLink e de um agendamento mais complexo. Normalmente é usado quando você precisa servir contextos longos ou muitas sessões concorrentes.
5) Prefill em chunks / chunking de prompt
Prompts longos podem causar grandes picos temporários de ativação e alta latência de prefill. Alguns mecanismos “quebram” o prefill em chunks para:
- reduzir memória de pico,
- intercalar trabalho de prefill entre requisições,
- melhorar a latência de cauda.
Isso não reduz o tamanho final de KV (você ainda precisa de K/V para todo o prompt), mas pode tornar prompts longos mais manejáveis operacionalmente.
6) Cache de prefixo (cache compartilhado de prompt)
Em muitas aplicações, uma grande parte do prompt se repete:
- prompts de sistema,
- instruções de ferramentas,
- templates longos,
- contexto recuperado que se repete entre usuários (às vezes).
Se múltiplas requisições compartilham um prefixo idêntico, você pode armazenar o KV para esse prefixo uma vez e reutilizá-lo. Isso pode reduzir dramaticamente tanto a computação de prefill quanto o tempo até o primeiro token.
Trade-offs e ressalvas:
- Requer identidade exata do prefixo (ou gerenciamento cuidadoso)
- Deve garantir isolamento correto (sem vazamento entre usuários)
- Funciona melhor para prompts de sistema e templates estáticos
Detalhes de latência: tempo até o primeiro token vs tempo por token de saída
O cache KV impacta diferentes métricas de latência de maneiras distintas:
TTFT (time to first token) é dominado pelo prefill para prompts longos.
- O cache KV está sendo construído aqui, não reutilizado muito.
- Otimizações: kernels de prefill mais rápidos, chunking, cache de prefixo.
TPOT (time per output token) é dominado pelo decode.
- O cache KV é fortemente reutilizado aqui.
- Otimizações: atenção eficiente sobre K/V em cache, KV quantizado, batching melhor.
Um padrão comum em produção:
- otimizar TTFT para UX de chat interativo,
- otimizar TPOT e vazão para cargas de trabalho de geração em lote.
Detalhes de vazão: batching, “batching contínuo” e crescimento do cache
Diferentemente da inferência clássica de deep learning em que entradas têm tamanho fixo, o serving de LLMs tem sequências de comprimento variável que crescem ao longo do tempo. Isso torna o batching mais difícil:
- requisições chegam em momentos diferentes,
- prompts têm comprimentos diferentes,
- cada requisição gera um número diferente de tokens.
Mecanismos modernos usam batching contínuo (continuous batching, também chamado de “in-flight batching”): eles mesclam muitas sequências ativas em um lote de decodificação a cada passo. O cache KV viabiliza isso, mas também o torna intensivo em memória porque você mantém muitas sequências vivas simultaneamente.
Esta é uma razão pela qual o layout do cache KV importa: empacotamento eficiente de memória e baixa fragmentação podem se traduzir diretamente em tamanhos de lote sustentáveis maiores e maior vazão.
Para mais sobre o lado de agendamento, veja Batching e Agendamento de Inferência.
Interação com decodificação especulativa
A Decodificação Especulativa tenta reduzir a latência fazendo um modelo pequeno “rascunho” propor múltiplos tokens e então verificando-os com o modelo grande.
O cache KV ainda importa:
- O modelo grande precisa manter um cache KV correto para os tokens aceitos.
- Tokens rascunho rejeitados podem exigir rollback ou gerenciamento cuidadoso de atualizações do cache.
- Implementações frequentemente usam estratégias para evitar regravações caras do cache (por exemplo, anexar e então descartar blocos, ou aplicar atualizações em etapas).
A decodificação especulativa pode reduzir computação por token aceito, mas a memória do cache KV continua sendo uma restrição central à concorrência.
Preocupações operacionais em serving de produção
Modo de falha comum: OOM por sequências “descontroladas”
Como o cache KV cresce a cada token gerado, uma requisição que gera muito mais do que o esperado pode explodir a memória.
Mitigações:
- impor
max_new_tokense limites máximos de contexto, - aplicar controle de admissão (não iniciar requisições que você não consegue terminar),
- implementar cancelamento/limpeza robustos para que requisições abortadas liberem KV rapidamente.
Fragmentação e overhead de alocação
Se KV for armazenado como tensores contíguos por requisição, comprimentos variáveis podem causar fragmentação e operações caras de realocação/cópia.
Mitigações:
- alocadores baseados em blocos (KV paginado),
- estratégias de pré-alocação,
- políticas de compactação.
Métricas que valem a pena acompanhar
Painéis práticos de serving frequentemente incluem:
- sequências ativas
- total de tokens em cache (prompt + gerados)
- memória de cache KV usada / livre
- tamanho do lote de decodificação ao longo do tempo
- TTFT e TPOT (p50/p95/p99)
- taxas de despejo/paginação (se aplicável)
Quando você poderia desabilitar o cache KV?
Desabilitar o cache KV é incomum para decodificação autorregressiva, mas há casos em que ele não é necessário ou não é benéfico:
- Pontuação de sequência completa (computar logits para uma entrada fixa sem geração passo a passo): você pode executar um único forward pass e não reutilizar cache.
- Treinamento (teacher forcing): frameworks computam sequências completas e fazem backprop; cache KV para decodificação não é a principal preocupação (embora existam truques de memória relacionados, como activation checkpointing — assunto diferente).
- Gerações muito curtas: o benefício existe, mas pode ser menor; a maioria das pilhas ainda mantém ligado porque o overhead é baixo em relação aos ganhos.
Resumo: o trade-off central de engenharia
O cache KV é central para geração rápida em LLMs:
- Ele reduz a computação na decodificação ao reutilizar chaves/valores de atenção do passado.
- Ele melhora a latência por token e torna a geração em tempo real prática.
- Ele consome memória substancial que escala com:
- comprimento do contexto,
- tokens gerados,
- número de camadas,
- número de cabeças KV,
- número de sequências concorrentes,
- estratégia de decodificação (por exemplo, beam search).
Em produção, o melhor desempenho de serving vem de tratar o cache KV como um recurso restrito a ser gerenciado — via variantes de atenção (GQA/MQA), quantização, paginação, cache de prefixo e batching/agendamento inteligentes — em vez de como um detalhe incidental de implementação.