llama.cpp internals: ggml, KV cache, FlashAttention
- ⬜📐 Quantização: GGUF, AWQ, GPTQ, INT8/INT4 explicados(Local LLMs & Edge AI)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
llama.cpp é a infraestrutura silenciosa do ecossistema de LLMs locais. Por trás de Ollama, LM Studio, Jan, Faraday e dezenas de wrappers, está o mesmo binário em C++ puro de Georgi Gerganov — sem PyTorch, sem Python, com mmap, kernels SIMD/Metal/CUDA escritos à mão, FlashAttention-2 e continuous batching. Entender o que acontece dentro de llama-cli é o que separa quem usa o ecossistema de quem o domina.
ggml: a biblioteca de tensores que ninguém vê
ggml é o coração do projeto. Uma library em C99 que implementa: tipos de tensor (F32, F16, BF16, Q2_K...Q8_0, IQ-quants), graph builder estático em runtime, kernel dispatcher por backend (CPU SIMD, CUDA, Metal, Vulkan, ROCm, SYCL, CANN para Ascend), e memory allocator com pools pré-alocados. Sem dependências Python. Sem libtorch. Binário final ~5MB.
// ggml — exemplo minimal de graph (do source de llama.cpp)
struct ggml_context * ctx = ggml_init({ .mem_size = 16*1024*1024 });
// Tensores: pesos quantizados e input
struct ggml_tensor * w = ggml_new_tensor_2d(ctx, GGML_TYPE_Q4_K, n_in, n_out);
struct ggml_tensor * x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, n_in);
// Forward: y = w @ x — kernel dispatch automático para backend
struct ggml_tensor * y = ggml_mul_mat(ctx, w, x);
// Build graph e compute
struct ggml_cgraph * gf = ggml_new_graph(ctx);
ggml_build_forward_expand(gf, y);
ggml_graph_compute_with_ctx(ctx, gf, /*n_threads=*/8);A separação graph builder + backend dispatcher permite a mesma graph rodar em CPU AVX-512, GPU Metal de M3 Ultra e GPU CUDA de H100 sem mudar uma linha de código de modelo. A troca de backend é decidida na compilação.
KV cache: o monstro da memória
Em cada step de decoding, o modelo precisa do K e V de TODOS os tokens anteriores para calcular attention. Recomputar a cada token seria O(n²) — em vez disso, cacheamos. KV cache é tipicamente o segundo maior consumidor de memória depois dos pesos, e em contextos longos vira o primeiro.
| Modelo | Contexto | KV cache FP16 | KV cache Q8_0 | KV cache Q4_0 |
|---|---|---|---|---|
| Llama 3.1 8B (GQA-8) | 8k | 1.0 GB | 512 MB | 256 MB |
| Llama 3.1 8B (GQA-8) | 128k | 16 GB | 8 GB | 4 GB |
| Llama 3.1 70B (GQA-8) | 8k | 2.5 GB | 1.25 GB | 640 MB |
| Llama 3.1 70B (GQA-8) | 128k | 40 GB | 20 GB | 10 GB |
| Mistral 7B (MHA-32) | 32k | 16 GB | 8 GB | 4 GB |
# llama.cpp — controle de KV cache
./llama-cli \
-m llama-3.1-70b-Q4_K_M.gguf \
--ctx-size 32768 \
--cache-type-k q8_0 \ # quantiza K cache para INT8
--cache-type-v q8_0 \ # quantiza V cache para INT8
--flash-attn \ # ativa FlashAttention-2 (CUDA/Metal)
-ngl 80 \ # offload TODAS as 80 layers para GPU
--temp 0.7 -p "Explique MVCC em PostgreSQL."Q4_0 cache pode causar drift em contextos longos (8k+) — erros de quantização acumulam ao longo de cada step. Default: Q8_0 K + Q8_0 V (invisível em qualidade). Se memória apertar, comece quantizando V (mais resiliente) antes de K.
FlashAttention-2: attention IO-aware
Tri Dao, Stanford (NeurIPS 2022 / arXiv:2307.08691 para v2) notou que attention não é compute-bound — é memory-bound. Em GPUs modernas, FLOPs sobram; o gargalo é mover tensores entre HBM (~2 TB/s) e SRAM on-chip (~20 TB/s). Attention naive materializa a matriz N×N (scores) em HBM, fazendo O(N²) reads/writes. FlashAttention nunca materializa essa matriz: tila Q, K, V em blocos que cabem em SRAM e computa softmax incrementalmente.
FlashAttention é matematicamente exato — não é aproximação. Mesmos resultados de attention naive até erros de ponto flutuante. O speedup é puro ganho de bandwidth. Em llama.cpp, ativar dá 1.5-3× em decoding longo (e habilita KV cache quantizado).
Batched decoding e continuous batching
Single-request decoding subutiliza a GPU: cada step gera 1 token usando todo o tensor de pesos. Batched decoding processa N requests em paralelo (pesos lidos 1 vez, N tokens emitidos). Mas batching estático sofre de head-of-line blocking: prompts curtos esperam os longos.
# Servidor llama.cpp com continuous batching
./llama-server \
-m llama-3.1-8b-Q4_K_M.gguf \
--host 0.0.0.0 --port 8080 \
-ngl 32 \
--parallel 8 \ # até 8 requests concorrentes
--cont-batching \ # preempção entre steps
--ctx-size 16384 \ # contexto TOTAL dividido entre slots
--batch-size 512 \ # prefill chunk size
--flash-attn \
--cache-type-k q8_0 --cache-type-v q8_0
# A API REST é compatível com OpenAI:
# curl http://localhost:8080/v1/chat/completions \
# -d '{"model":"local","messages":[{"role":"user","content":"oi"}]}'Sampling: como escolher o próximo token
O modelo gera logits sobre todo o vocab (~128k entradas). Sampling transforma logits em token. llama.cpp tem uma pipeline configurável de samplers aplicados em sequência: cada um filtra ou modifica a distribuição. Default 2026: min_p tornou-se preferido sobre top_p para chat factual.
| Sampler | Como funciona | Quando usar |
|---|---|---|
| greedy | argmax sobre logits | Tasks determinísticas (código, classificação) |
| temperature | logits /= T antes de softmax | Controla criatividade; T=0.7 default chat |
| top-k | mantém só k tokens com maior probabilidade | Limita o "long tail"; k=40 razoável |
| top-p (nucleus) | mantém menor conjunto cuja soma ≥ p | Adaptativo; p=0.9 chat criativo |
| min-p | mantém tokens com prob ≥ min_p × prob_max | Robust a diferentes ranges de logits; preferido 2026 |
| typical-p | baseado em entropia local | Reduz repetição em texto narrativo |
| repetition penalty | penaliza tokens recentes | Combate loops; cuidado com falsos positivos |
| DRY (don't repeat yourself) | detecta n-gram repetidos e penaliza | Default em chat 2026; mata loops sem afetar parágrafos legítimos |
| logit_bias | soma viés a tokens específicos | Forçar formatos (JSON), banir palavras |
# Sampling chain moderna (default em chat de qualidade)
./llama-cli -m model.gguf \
--temp 0.7 \
--min-p 0.05 \ # filtra long tail adaptativamente
--repeat-penalty 1.0 \ # neutro — DRY já cuida de repetição
--dry-multiplier 0.8 \ # ativa DRY
--dry-base 1.75 \
--dry-allowed-length 2 \
--top-k 0 --top-p 1.0 \ # desativados, min_p assumePor que projetos de inferência em C++ ganharam tanta adoção para uso local?
Hardware backends e onde llama.cpp brilha
| Backend | Hardware | Status 2026 | Performance relativa |
|---|---|---|---|
| CUDA | NVIDIA GPUs (Ampere+, Hopper, Blackwell) | Mainstream, mais maduro | 100% (referência) |
| Metal | Apple Silicon M1-M4 | Excelente — unified memory shines | 60-90% de CUDA equivalente |
| Vulkan | AMD/Intel/qualquer GPU moderna | Estável, fallback universal | 50-70% de CUDA |
| ROCm/HIP | AMD MI300, RX 7900 | Bom mas exige toolchain AMD | 70-85% de CUDA |
| SYCL | Intel Arc, Data Center GPU Max | OK; depende de oneAPI | 50-70% |
| CANN | Huawei Ascend (China) | Estável; mercado regional | N/D |
| CPU (AVX2/AVX-512) | Qualquer x86 moderno | Surpreendentemente bom para Q4_K_M | 5-15% de GPU |
| CPU (ARM NEON/SVE) | Raspberry Pi, servidores Ampere | Estável; SVE2 em chips novos é forte | 3-10% de GPU |
Em Mac Studio M3 Ultra (192GB unified), llama.cpp roda Llama 3.1 405B em Q4_K_M (~200GB) — algo impossível em GPU consumer. Metal backend explora unified memory: pesos não copiam entre CPU/GPU. Tokens/s baixo (~3-5 tok/s) mas qualidade FP16-near em hardware de escritório.
Speculative decoding em llama.cpp
llama.cpp suporta speculative decoding (módulo dedicado em sequência): um modelo "draft" pequeno e rápido gera N tokens candidatos, o modelo "target" grande valida em paralelo. Quando aceitos, ganha-se N×; rejeitados, paga-se o custo do draft. Funciona porque a maioria dos tokens é "fácil" (continuações óbvias).
# Speculative com draft Llama 3.2 1B e target Llama 3.1 70B
./llama-speculative \
-m llama-3.1-70b-Q4_K_M.gguf \
-md llama-3.2-1b-Q8_0.gguf \ # draft, mesmo tokenizer
--draft 8 \ # propõe 8 tokens por step
--gpu-layers 80 --gpu-layers-draft 16 \
-p "Explique consenso Raft." \
--temp 0.6
# Ganho típico: 1.8-2.5× tokens/s em texto técnico (alta previsibilidade)
# Ganho menor (1.2-1.5×) em código com baixa entropia localPerguntas frequentes
❓ llama.cpp suporta multi-GPU?
❓ Como debugar performance ruim em llama.cpp?
❓ Posso treinar/fine-tunear em llama.cpp?
❓ Como atualizar de um GGUF velho para a versão atual?
Referências
Fixando
O que determina a velocidade de geração em execução local na CPU?
Qual formato de arquivo se tornou padrão para distribuição de modelos quantizados, e por quê?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…