Por que transbordar até mesmo 100 MB para a RAM do sistema destrói o throughput de LLMs locais em 90%, e como tabelas de blocos virtualizadas, quantização assimétrica INT4/FP8 de KV-cache e decodificação especulativa encaixam uma janela completa de 65.536 tokens dentro de uma RTX 3060 ou 4060 Ti de 8GB.
1. A Realidade dos 8GB de VRAM e o Abismo de Memória PCIe
Se você executa Large Language Models locais em hardware de consumo — uma NVIDIA RTX 3060 8GB, uma RTX 4060 / 4060 Ti 8GB ou uma RTX 4070 8GB de notebook — quase certamente já colidiu com o abismo de memória PCIe (PCIe memory cliff).
Com 4.096 tokens de contexto, um modelo de 8B quantizado em 4-bit roda com folga a 48 a 62 tokens por segundo. Tudo responde de forma instantânea. Então você expande a janela de contexto para 16k ou 32k tokens para analisar um repositório inteiro ou um PDF extenso, e subitamente o terminal congela. O throughput despenca de 55 tok/s para 4,2 tok/s — um colapso de mais de 90% na velocidade — mesmo com a utilização da GPU caindo para 15% e a CPU praticamente ociosa.
O que aconteceu? Você ultrapassou a fronteira de 8.192 MB de VRAM por apenas 100 MB.
Para entender por que um estouro de 1,2% de memória provoca uma queda de 90% no desempenho, precisamos observar a assimetria física entre a memória GDDR6 dedicada na placa e o barramento PCIe:
| Camada de Memória | Hardware Típico | Largura de Banda Efetiva | Latência |
|---|---|---|---|
| GPU L2 / SRAM Compartilhada | RTX 4060 Ti (32 MB L2) | > 2.000 GB/s | ~20–40 ns |
| VRAM Dedicada (GDDR6) | Barramento 128-bit / 192-bit | 288 – 360 GB/s | ~150–250 ns |
| RAM do Sistema via PCIe 4.0 x8/x16 | DDR4/DDR5 + DMA PCIe | 12 – 24 GB/s | ~1.000–3.000 ns |
Durante a fase de decodificação autorregressiva (gerando um token por vez), a inferência de LLMs é estritamente limitada por largura de banda de memória (memory-bandwidth-bound), e não por capacidade de cálculo (FLOPs). Para gerar um único token, a GPU precisa ler todos os pesos do modelo e todo o cache Key-Value (KV) acumulado da VRAM para os Streaming Multiprocessors (SMs), executar uma multiplicação matriz-vetor com intensidade aritmética próxima de 1 FLOP/byte e gravar um novo vetor KV de volta.
Quando todo o conjunto ativo cabe nos 8GB de VRAM, a GPU trafega dados a ~300 GB/s. No instante em que o runtime despeja sequer 100 MB de páginas do KV-cache ou uma única camada do transformer para a RAM do host (via Unified Memory ou CPU offloading), cada token gerado passa a disparar transferências síncronas via PCIe. Como a GPU precisa aguardar esses bytes atravessarem um gargalo de 12–24 GB/s — cerca de 15x a 25x mais lento que a GDDR6 — sua velocidade ponta a ponta cai 90% ou mais.
Runtimes tradicionais tratam isso como um limite físico intransponível: "Compre uma RTX 3090/4090 de 24GB ou limite seu contexto a 8k."
No Benni Inference Engine (BIE), rejeitamos essa premissa. Ao combinar tabelas de blocos PagedAttention, quantização dinâmica de precisão mista no KV-cache (Sink-FP16 + Body-INT4/FP8) e decodificação especulativa consciente de memória, é possível sustentar 64k (65.536 tokens) de contexto em uma GPU de 8GB sem vazar um único byte pelo barramento PCIe.
2. Matemática pelos Primeiros Princípios: Por que o KV-Cache Devora Seus 8GB
Antes de otimizar a memória, precisamos contabilizar cada megabyte dentro de um orçamento de 8.192 MB. Considere um modelo moderno de 8B parâmetros (como Llama-3-8B ou Qwen-2.5-7B/8B) quantizado para pesos em 4-bit (AWQ, GPTQ ou Q4_K_M):
- Pesos do Modelo em 4-bit: ~4,25 GiB
- Contexto CUDA, Workspaces cuBLAS/Triton e Buffers de Ativação: ~0,55 GiB
- Overhead de Vídeo do SO e Gerenciador de Janelas: ~0,35 GiB
Isso deixa aproximadamente 2,85 GiB a 3,05 GiB de VRAM limpa e livre para o KV-Cache.
Agora vamos calcular o consumo exato do KV-Cache em precisão padrão FP16 usando a equação fundamental do KV-Cache:
VRAM_KV = 2 * n_layers * n_kv_heads * d_head * s_seq * batch_size * sizeof(dtype)
Onde:
2contabiliza os dois tensores: Key (K) e Value (V).n_layersé o número de blocos decodificadores do transformer (ex: 32).n_kv_headsé o número de cabeças Key-Value usando Grouped-Query Attention (GQA) (ex: 8, reduzido de 32 cabeças de query).d_headé a dimensão por cabeça de atenção (tipicamente 128).s_seqé o comprimento total da sequência (prompt + tokens gerados).batch_sizeé o número de sequências simultâneas.sizeof(dtype)equivale a 2 bytes para FP16/BF16.
Substituindo os valores para uma única sequência (batch_size = 1) em FP16:
Bytes por Token = 2 * 32 * 8 * 128 * 2 = 131.072 bytes = 128 KiB/token
Em 8.192 tokens, 128 KiB × 8.192 = 1,0 GiB. Gerenciável.
Em 32.768 tokens, 128 KiB × 32.768 = 4,0 GiB. Já ultrapassou nosso orçamento de 3,0 GiB — abismo PCIe acionado!
Em 65.536 tokens (64k), 128 KiB × 65.536 = 8,0 GiB. O KV-cache sozinho equivale a toda a VRAM física da GPU.
E note: isso é antes de contabilizar o desperdício por alocação contígua.
3. PagedAttention: Eliminando o Imposto de 70% da Fragmentação Contígua
Em implementações ingênuas em PyTorch ou engines C++ iniciais, o KV-cache de uma requisição é pré-alocado como um único tensor contíguo dimensionado para max_seq_len:
# Pré-alocação contígua ingênua (desperdício extremo!)
k_cache = torch.empty((batch_size, n_layers, n_kv_heads, max_seq_len, head_dim), dtype=torch.float16, device="cuda")
Se você configurar max_seq_len = 65536, o alocador reserva 8,0 GiB imediatamente mesmo que seu prompt atual tenha apenas 1.200 tokens. Pior ainda, ao executar dois turnos agênticos concorrentes ou buscas em árvore, você sofre com:
- Fragmentação Interna: Slots reservados para tokens futuros que talvez nunca sejam gerados.
- Fragmentação Externa: Memória livre espalhada em lacunas não contíguas que não atendem a um novo
malloccontínuo. - Zero Compartilhamento de Memória: Prompts de sistema idênticos ou schemas de ferramentas são duplicados em cada sequência ativa.
Inspirado na paginação de memória virtual dos sistemas operacionais, o PagedAttention divide o KV-cache em blocos físicos de KV de tamanho fixo (tipicamente B = 16 tokens por bloco).
Como Funcionam as Tabelas de Blocos Lógico-para-Físico
Em vez de um buffer gigante e contínuo, o BIE pré-aloca um Pool de Blocos Físicos plano ocupando os ~3,0 GiB seguros restantes da VRAM. Cada sequência mantém uma Block Table (Tabela de Blocos) leve na GPU:
- Bloco Lógico 0 (tokens 0..15) → Bloco Físico
#412 - Bloco Lógico 1 (tokens 16..31) → Bloco Físico
#089 - Bloco Lógico 2 (tokens 32..47) → Bloco Físico
#903
O kernel customizado em CUDA/Triton lê a Block Table em tempo real, reunindo blocos físicos não contíguos diretamente nos registradores SRAM durante o produto escalar QKT e a agregação ponderada de V.
Compartilhamento de Prefixo via Copy-on-Write (CoW)
Em fluxos com agentes autônomos, de 2.000 a 8.000 tokens do contexto são um prompt de sistema estático, definições de ferramentas MCP ou exemplos few-shot. Com o PagedAttention, múltiplas requisições — ou múltiplos ramos especulativos — apontam seus blocos lógicos iniciais para os mesmos blocos físicos com um contador de referência (ref_count > 1). Somente quando um ramo diverge e grava um novo token em um bloco compartilhado parcialmente preenchido é que a engine executa uma alocação Copy-on-Write de um novo bloco físico.
O PagedAttention reduz o desperdício de memória do KV-cache de 60–80% para menos de 4% (no máximo 15 tokens no bloco final). Contudo, mesmo com fragmentação zero, 64k tokens em FP16 ainda exigem 8,0 GiB. Para caber em 3,0 GiB, precisamos comprimir os bytes por token.
4. Quantização Dinâmica de KV-Cache: FP8 e INT4 com Preservação de Sinks
Podemos quantizar o KV-cache sem destruir o raciocínio do modelo ou sua precisão em buscas "agulha no palheiro" com 64k tokens? Sim — desde que respeitemos a geometria dos outliers de atenção e os Attention Sinks.
Por que o INT4 Global Ingênuo Falha
As chaves (Keys) dos Transformers exibem outliers persistentes por canal: dimensões específicas carregam magnitudes de 10x a 50x maiores que seus vizinhos ao longo de toda a sequência. Paralelamente, os primeiros 4 a 8 tokens de um prompt atuam como Attention Sinks (Sumidouros de Atenção) — eles absorvem massa de probabilidade desproporcional no softmax para estabilizar a entropia da atenção quando não há correspondência semântica forte no meio do texto.
Se você aplicar uma escala de quantização INT4 global única para todas as cabeças e todos os tokens, os outliers esticam o intervalo numérico e esmagam os valores normais, enquanto o ruído de quantização nos tokens iniciais desestabiliza o denominador do softmax ao longo de todos os 64.000 tokens seguintes.
A Solução Híbrida: Sink-FP16 + INT4 Assimétrico por Grupo / FP8
No Benni Inference Engine, cada bloco físico de KV carrega metadados de precisão, habilitando a Quantização Dinâmica de Precisão Mista:
- Modo FP8 (E4M3) (Compressão 2x): Cada valor FP16 de 16 bits é convertido para
float8_e4m3fnde 8 bits com fator de escala dinâmico por cabeça. Isso reduz o KV-cache pela metade, de 128 KiB/token para 64 KiB/token, com perda de perplexidade praticamente nula (ΔPPL < 0,03). Em 64k tokens, o FP8 exige 4,0 GiB — ideal para GPUs de 12GB, ou até ~45k tokens em placas de 8GB. - Modo INT4 por Grupo com Preservação de Sinks (Compressão ~3,8x): Para desbloquear os 64k (65.536 tokens) completos em uma GPU de 8GB:
- Janela Sink (primeiros 8 tokens) e Janela Local Deslizante (últimos 128 tokens): Mantidos em FP16 dentro de blocos dedicados de alta precisão, garantindo zero erro de quantização nos Attention Sinks e no contexto sintático imediato.
- Corpo Intermediário do Contexto: Quantizado por cabeça e por grupo (tamanho de grupo
g = 64elementos ao longo ded_head) usando INT4 assimétrico (q em [0, 15]) com escalase ponto-zerozem FP16:x_dequant = (q - z) * s.
Vamos calcular o consumo exato por token no modo INT4 por grupo (g = 64, d_head = 128):
- Cada grupo de 64 valores consome 32 bytes para os nibbles INT4 empacotados mais 4 bytes para os metadados
(s, z)em FP16 = 36 bytes por 64 elementos (4,5 bits/elemento efetivos). - Ao longo de
2 * 32 * 8 * 128 = 65.536elementos por token:(65.536 / 64) * 36 = 36.864 bytes = 36 KiB/token. - Em 65.536 tokens (64k):
65.536 * 36 KiB ≈ 2,25 GiB.
2,25 GiB! Somados aos 4,25 GiB dos pesos 4-bit do modelo e 0,55 GiB dos workspaces CUDA, o uso total de VRAM estabiliza em ~7,05 GiB — deixando uma margem de segurança superior a 900 MB abaixo do teto de 8.192 MB em uma RTX 3060 / 4060 Ti 8GB. Zero vazamento PCIe. Zero colapso de velocidade.
5. Arquitetura do Sistema: Benni Inference Engine (BIE)
Veja como o pipeline completo opera dentro do BIE para coordenar PagedAttention, quantização dinâmica de KV e decodificação especulativa sem jamais cruzar o abismo PCIe:
(Block Table CoW)"} PrefixCache -->|Cache Hit| ReuseBlocks["Mapeia Blocos Físicos Existentes
(Zero Prefill Redundante)"] PrefixCache -->|Cache Miss| ChunkedPrefill["Agendador de Chunked Prefill
(Limita Pico de VRAM de Ativação)"] ReuseBlocks & ChunkedPrefill --> SinkRouter{"Roteador de Posição de Token"} SinkRouter -->|"Sink (0..7) ou Janela Local (Últimos 128)"| FP16Pool["Pool de Blocos FP16 de Alta Precisão"] SinkRouter -->|"Corpo Intermediário"| QuantKernel["Quantizador Triton Fundido
(INT4 Assimétrico por Grupo / FP8)"] QuantKernel --> INT4Pool["Pool de Blocos INT4/FP8 Comprimido
(~2,25 GiB @ 64k Tokens)"] FP16Pool & INT4Pool --> PagedAttn["Kernel Fundido PagedAttention
(Dequantização em Registradores)"] PagedAttn --> SpecLoop{"Loop Especulativo Draft-Verify
(Rascunho Gamma=5 Tokens)"} SpecLoop -->|Tokens Aceitos| StreamOut["Stream de Tokens (35-50+ tok/s em 8GB)"] SpecLoop -->|Rollback da Cauda| PagedAttn
Observe dois detalhes críticos dessa arquitetura:
- Chunked Prefill: Processar um prompt de 64k em uma única passada forward faria as matrizes temporárias de ativação explodirem além dos 8GB. O BIE fatia o prefill em blocos de 512 tokens, mantendo a VRAM de ativação constante abaixo de 250 MB independentemente do tamanho do prompt.
- Dequantização em Registradores (On-the-Fly): Nunca materializamos uma cópia FP16 do KV-cache na VRAM global. O kernel PagedAttention em CUDA/Triton carrega o
uint8empacotado (dois nibbles INT4) e(scale, zero_point)diretamente nos registradores rápidos do SM, dequantiza para FP16/BF16 em registrador e calcula o produto escalar com Q imediatamente.
6. Decodificação Especulativa: Rompendo o Gargalo de Banda
Mesmo quando tudo cabe nos 8GB de VRAM, a largura de banda da GDDR6 (288–360 GB/s) ainda limita um modelo 8B 4-bit a cerca de 50–60 tok/s, pois cada token exige a leitura de ~4,5 GB de pesos e KV-cache.
A Decodificação Especulativa (Speculative Decoding) transforma um problema limitado por banda de memória em um ganho de paralelismo computacional — sem custo de precisão:
- Um mecanismo rascunhador (draft) minúsculo (um modelo draft de 0.5B–1B em 4-bit ocupando ~350 MB, ou uma cabeça N-gram/EAGLE sem overhead) propõe autorregressivamente γ = 5 tokens candidatos.
- O modelo alvo de 8B executa uma única passada forward paralela sobre todos os γ + 1 tokens simultaneamente. Como a leitura dos 4,25 GB de pesos da VRAM acontece uma única vez para 5 tokens em vez de 5 vezes separadas, a intensidade aritmética salta 5x mantendo o tráfego de VRAM constante.
- A amostragem por rejeição exata garante que a distribuição de saída seja matematicamente idêntica à geração padrão do modelo alvo.
Em uma GPU de 8GB com contexto de 32k–64k, a decodificação especulativa eleva a taxa efetiva de 28–38 tok/s para 48–75 tok/s em código, JSON estruturado e prosa técnica.
7. Exemplos Práticos de CLI e Código
Veja como subir um servidor de contexto 64k em uma GPU de 8GB usando o Benni Inference Engine (BIE), além de um exemplo limpo com o SDK Python e o padrão de kernel Triton para dequantização em registradores.
Inicialização via CLI (Perfil Seguro para 8GB VRAM)
# Clone e instale o Benni Inference Engine
git clone https://github.com/Benni-OS/benni-inference-engine.git
cd benni-inference-engine && pip install -e .
# Sirva um modelo 8B 4-bit com 64k de contexto travado dentro de 8GB VRAM
bie-serve \
--model Benni-OS/Llama-3.1-8B-Instruct-AWQ-4bit \
--max-model-len 65536 \
--kv-cache-dtype int4_asym \
--kv-group-size 64 \
--kv-sink-tokens 8 \
--paged-block-size 16 \
--chunked-prefill-size 512 \
--gpu-memory-utilization 0.90 \
--speculative-draft-tokens 5 \
--port 8080
Uso via SDK Python
from benni_inference import BieEngine, EngineConfig, SamplingParams
config = EngineConfig(
model="Benni-OS/Llama-3.1-8B-Instruct-AWQ-4bit",
max_model_len=65536,
kv_cache_dtype="int4_asym", # Comprime 64k de KV-cache para ~2,25 GiB
kv_group_size=64,
kv_sink_tokens=8, # Preserva Attention Sinks em FP16
paged_block_size=16,
enable_prefix_caching=True, # Compartilhamento CoW para system prompts
chunked_prefill_size=512, # Previne OOM durante prefill de 64k
gpu_memory_utilization=0.90,
speculative_draft_tokens=5,
)
engine = BieEngine(config)
prompt = load_entire_codebase_context(max_tokens=58000)
outputs = engine.generate(
prompt,
SamplingParams(temperature=0.2, max_tokens=2048)
)
print(outputs[0].text)
Dequantização INT4 em Registradores no PagedAttention (Padrão Triton)
import triton
import triton.language as tl
@triton.jit
def load_and_dequant_kv_block(
kv_packed_ptr, # [num_blocks, n_kv_heads, block_size, head_dim // 2] uint8
kv_scale_ptr, # [num_blocks, n_kv_heads, block_size, head_dim // group_size] fp16
kv_zero_ptr, # [num_blocks, n_kv_heads, block_size, head_dim // group_size] fp16
block_id,
head_id,
BLOCK_TOKENS: tl.constexpr,
HEAD_DIM: tl.constexpr,
GROUP_SIZE: tl.constexpr,
):
# Carrega uint8 empacotado (2 valores INT4 por byte) direto nos registradores SM
packed = tl.load(kv_packed_ptr + ...) # Busca indexada por bloco
# Desempacota nibbles de 4 bits (baixo e alto) em registradores
q_low = (packed & 0x0F).to(tl.float16)
q_high = ((packed >> 4) & 0x0F).to(tl.float16)
# Carrega escala e ponto-zero FP16 por grupo e dequantiza em registrador
scale = tl.load(kv_scale_ptr + ...)
zero = tl.load(kv_zero_ptr + ...)
val_low = (q_low - zero) * scale
val_high = (q_high - zero) * scale
return val_low, val_high
8. Benchmarks em Hardware de Consumo 8GB (RTX 4060 Ti 8GB)
Medições realizadas com modelo 8B AWQ 4-bit, batch size 1, em uma RTX 4060 Ti 8GB (GDDR6, 288 GB/s) sobre PCIe 4.0 x8:
| Configuração da Engine | Contexto Máx. Antes do Abismo PCIe | VRAM Pico @ 32k | VRAM Pico @ 64k | Velocidade @ 32k | Velocidade @ 64k | Perplexidade (Δ vs FP16) |
|---|---|---|---|---|---|---|
| KV FP16 Contíguo | ~10.500 tokens | Vaza para RAM | Vaza / OOM | 4,1 tok/s (ABISMO) | Crash / < 1,5 tok/s | 0,00 (Baseline) |
| PagedAttention KV FP16 | ~23.000 tokens | Vaza para RAM | Vaza / OOM | 5,4 tok/s (ABISMO) | Crash / < 2,0 tok/s | 0,00 |
| BIE Paged + KV FP8 | ~46.000 tokens | 6,82 GiB | Vaza ~900 MB | 41,8 tok/s | 6,2 tok/s (ABISMO) | +0,03 |
| BIE Paged + Sink-INT4 + Especulativa | 65.536+ tokens | 6,02 GiB | 7,08 GiB | 61,4 tok/s | 48,9 tok/s | +0,12 |
A conclusão é inequívoca: manter 100% do KV-cache dentro da GDDR6 via PagedAttention + quantização INT4 com preservação de Sinks entrega um ganho superior a 10x sobre FP16 despejado na RAM, com degradação de perplexidade praticamente imperceptível (+0,12 PPL).
9. Construa Infraestrutura de IA Local e Soberana
Você não precisa de um cluster de H100s — nem sequer de uma placa de 24GB — para rodar fluxos agênticos de contexto longo, RAG sobre repositórios inteiros e loops locais de raciocínio. Quando você trata a VRAM como um recurso virtualizado escasso em vez de um array plano, uma GPU de consumo de 8GB torna-se um nó de inferência de 64k altamente capaz.
- Explore a Engine Open-Source: Dê uma estrela, faça fork e teste os kernels no Benni Inference Engine no GitHub.
- Domine Sistemas de IA Soberana: Aprofunde-se em inferência local, engenharia de memória CUDA e arquiteturas de agentes autônomos na Benni OS Academy.