Quantização: GGUF, AWQ, GPTQ, INT8/INT4 explicados
Ao terminar: Você escolhe entre GGUF, AWQ e GPTQ pelo trade-off real de perplexidade contra VRAM, não pelo nome mais familiar.
Quantização é o que transforma um modelo de 70B parâmetros — 140 GB em FP16, fora do alcance de qualquer hardware consumer — em um arquivo de 35 GB rodando em um Mac Studio. Mas reduzir 16 bits para 4 bits sem destruir a qualidade exige algoritmos não-triviais: GPTQ usa segunda derivada da loss, AWQ analisa ativações para proteger pesos salientes, GGUF mistura precisões por camada com importance matrix, NF4 explora a distribuição gaussiana dos pesos pré-treinados. Este módulo dissecciona cada técnica, os papers originais e quando escolher cada uma.
O problema fundamental: 16 bits por peso é muito
Modelos modernos armazenam pesos em bfloat16 (16 bits cada). Llama 3.1 8B tem 8.03 bilhões de parâmetros — 16 GB de VRAM só para pesos, antes de KV cache, ativações, otimizador. Quantização reduz a representação de cada peso para 8, 4, 3 ou até 2 bits, multiplicando por 2-8× o throughput de memória e diminuindo a pegada de VRAM proporcionalmente.
- → preserva
- Compute
- Conceito de arquitetura
- Segurança e identidade
A ideia central: nem todo peso vale o mesmo. Métodos ingênuos tratam todos igualmente e degradam; os bons identificam onde não economizar — e é essa assimetria que torna quatro bits utilizável.
- 1 · O tamanho em memória é o limite prático. Um modelo que não cabe não roda. Reduzir bits por peso é a alavanca mais direta sobre esse limite.
- 2 · Quantizar também ACELERA, e isso surpreende. Como a geração é limitada por leitura de memória, menos bytes por peso significam mais tokens por segundo — o ganho não é só de espaço.
- 3 · A perda não é uniforme entre os pesos. Uma fração pequena carrega desproporcionalmente a qualidade. Tratá-los como os demais é o que faz a queda ser maior do que precisaria.
- 4 · Por isso os métodos bons não são uniformes. Preservar os pesos sensíveis, ou usar mais bits nas camadas críticas, é o que separa uma quantização utilizável de uma que degrada.
- 5 · Quatro bits é o ponto de equilíbrio prático. Um quarto do tamanho com perda pequena o suficiente para a maioria dos usos. Abaixo disso a degradação deixa de ser sutil.
- 6 · Medir no SEU caso é obrigatório. A perda média num benchmark diz pouco sobre a sua tarefa. Comparar antes e depois no seu conjunto é o único jeito de saber se compensou.
Quantização simétrica mapeia [-max, +max] para [-2^(b-1), 2^(b-1)-1]. A assimétrica adiciona um zero-point e mapeia [min, max] → [0, 2^b-1] — melhor para distribuições skewed (ativações pós-ReLU), pior para pesos quase-gaussianos. LLMs costumam usar simétrica para pesos e assimétrica para ativações.
| Precisão | Bits/peso | Llama 3 8B | Llama 3 70B | Hardware típico |
|---|---|---|---|---|
| FP32 | 32 | 32 GB | 280 GB | A100/H100 datacenter |
| BF16/FP16 | 16 | 16 GB | 140 GB | RTX 4090 24GB (só 8B cabe) |
| INT8 | 8 | 8 GB | 70 GB | 2× RTX 4090 ou A6000 |
| INT4 (AWQ/GPTQ) | ~4.25 | 4.5 GB | 35 GB | RTX 3060 12GB / M-series 36GB+ |
| Q3_K_M (GGUF) | ~3.5 | 3.8 GB | 30 GB | M2 Pro 32GB |
| Q2_K_S (GGUF imatrix) | ~2.6 | 2.8 GB | 22 GB | M1 Max 32GB (com perda) |
GPTQ: quantização guiada por hessiana
Frantar et al., 2022 (paper: arXiv:2210.17323) propuseram GPTQ como evolução do OBQ (Optimal Brain Quantization). A intuição: ao quantizar um peso w_i, podemos compensar o erro ajustando os pesos não- quantizados restantes via inverso da hessiana da loss. Processa-se uma coluna por vez, propagando correções para colunas futuras.
# AutoGPTQ — quantização INT4 com calibração de domínio
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
from datasets import load_dataset
MODEL_ID = "meta-llama/Meta-Llama-3.1-8B-Instruct"
OUT_DIR = "llama-3.1-8b-gptq-int4"
tok = AutoTokenizer.from_pretrained(MODEL_ID)
cfg = BaseQuantizeConfig(
bits=4,
group_size=128, # 128 pesos compartilham mesma escala
desc_act=False, # True = +qualidade, -20% throughput em serving
damp_percent=0.01, # regularização do Cholesky
sym=True, # simétrico para pesos
true_sequential=True, # quantiza camada-a-camada respeitando dependências
)
# Calibração DEVE refletir distribuição de produção
calib = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft[:256]")
examples = [
tok(ex["messages"][0]["content"] + ex["messages"][1]["content"],
return_tensors="pt", max_length=2048, truncation=True)
for ex in calib
]
model = AutoGPTQForCausalLM.from_pretrained(MODEL_ID, cfg, torch_dtype="auto")
model.quantize(examples) # ~3h em RTX 4090 para 8B
model.save_quantized(OUT_DIR, use_safetensors=True)desc_act reordena colunas por norma de ativação antes de quantizar — preserva mais qualidade mas força memory access não-coalesced em kernels GEMM. Em vLLM/TGI, modelos GPTQ com são 20-40% mais lentos. Decida pelo perfil: chat geral → desc_act=False; código/matemática → desc_act=True.
AWQ: proteja os pesos salientes
Lin et al., MIT 2023 (paper: arXiv:2306.00978) observaram que apenas ~1% dos pesos são responsáveis pela maior parte da capacidade do modelo — e esses pesos podem ser identificados pela magnitude das ativações nos canais correspondentes, não pelos próprios pesos. AWQ aplica per-channel scaling para deslocar o range dos canais salientes antes da quantização uniforme.
Matematicamente, Y é invariante sob a transformação. Mas Q(W·s) sofre menos erro porque os canais dominantes foram amplificados antes de quantizar (mais bits efetivos na faixa que importa). A escolha de α é por search: avalia perda de reconstrução em 32 amostras de calibração.
# AWQ — autoawq library
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
MODEL = "Qwen/Qwen2.5-7B-Instruct"
OUT = "qwen2.5-7b-awq-int4"
tok = AutoTokenizer.from_pretrained(MODEL, trust_remote_code=True)
model = AutoAWQForCausalLM.from_pretrained(MODEL, device_map="auto", trust_remote_code=True)
quant_cfg = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM", # GEMM = batch grande; GEMV = decoding token a token
}
# AWQ usa pileval (default) ou seu próprio dataset
model.quantize(tok, quant_config=quant_cfg, calib_data="pileval", n_parallel_calib_samples=32)
model.save_quantized(OUT)
tok.save_pretrained(OUT)
# Loading em vLLM
# vllm serve qwen2.5-7b-awq-int4 --quantization awq_marlin --dtype half📋 Escolha entre AWQ e GPTQ para serving de chat 7B
AWQ preserva 0.3-0.8 PPL melhor que GPTQ act_order=False, e ~paridade com GPTQ desc_act=True — sem o custo de 20-40% de throughput. Marlin kernel (vLLM) é state-of-the-art para INT4 GEMM em Ampere/Hopper.
Alt: GPTQ desc_act=True: prefira em tasks de código/matemática se +1% de accuracy compensa -25% throughput
Alt: GGUF Q4_K_M: prefira para deploy em CPU/Mac ou hardware misto
Alt: bitsandbytes NF4: só para fine-tuning QLoRA, não para serving (lento)
GGUF e k-quants: precisão mista por camada
GGUF é o formato do llama.cpp (Georgi Gerganov). Não é um algoritmo de quantização — é um container que pode armazenar pesos em diversos esquemas (Q4_0 legacy, Q4_K_M, Q5_K_S, Q8_0, etc.). O diferencial dos k-quants é misturar precisões DENTRO do mesmo modelo: layers sensíveis (attention.output, ffn.down) ganham mais bits; outros ganham menos.
| Esquema | Bits efetivos | Estratégia | PPL Llama 7B (wiki) | Uso recomendado |
|---|---|---|---|---|
| Q8_0 | 8.5 | INT8 uniforme + scale per-bloc de 32 | ~5.79 (≈FP16) | CPU rápido, baseline |
| Q6_K | 6.56 | Mix 6-bit majority + 8-bit em layers críticos | 5.81 | Quase indistinguível de FP16 |
| Q5_K_M | 5.69 | Mix 5/6-bit, balanceado | 5.85 | Default para qualidade |
| Q4_K_M | 4.83 | Mix 4/5/6-bit, attn.v + ffn.down em 5/6 | 5.95 | Sweet spot 2026 |
| Q4_K_S | 4.58 | Q4_K_M sem upgrade em layers críticos | 6.02 | Memória apertada |
| Q3_K_M | 3.91 | Mix 3/4-bit + imatrix | 6.30 | Modelos grandes em RAM consumer |
| Q2_K_S | 2.63 | Mix 2/3-bit + imatrix obrigatória | 7.20 | Última fronteira; perda visível |
# llama.cpp — pipeline completo de quantização
# 1. Converter HF safetensors → GGUF FP16
python convert_hf_to_gguf.py meta-llama/Meta-Llama-3.1-8B-Instruct \
--outfile llama-3.1-8b-f16.gguf --outtype f16
# 2. Coletar importance matrix em texto representativo
./llama-imatrix \
-m llama-3.1-8b-f16.gguf \
-f calibration_pt-br.txt \ # mix PT-BR + código + chat real
-o imatrix-llama8b.dat \
--chunks 128
# 3. Quantizar com imatrix → Q4_K_M
./llama-quantize \
--imatrix imatrix-llama8b.dat \
llama-3.1-8b-f16.gguf \
llama-3.1-8b-Q4_K_M.gguf \
Q4_K_M
# 4. Validar perplexity
./llama-perplexity -m llama-3.1-8b-Q4_K_M.gguf -f wikitext-2-raw/wiki.test.rawbitsandbytes e NF4: a magia do QLoRA
Dettmers et al., 2023 (QLoRA paper: arXiv:2305.14314) provaram que dá para fine-tunear Llama 65B em uma única A100 80GB combinando: NF4 (NormalFloat 4-bit), double quantization (quantizar as escalas) e paged optimizers (offload do estado Adam para CPU). NF4 é desenhado especificamente para a distribuição empírica dos pesos pré-treinados.
# QLoRA setup com bitsandbytes — fine-tune 8B em RTX 4090
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
bnb_cfg = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat-4
bnb_4bit_use_double_quant=True, # quantiza escalas → -0.4 bits/peso
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3.1-8B-Instruct",
quantization_config=bnb_cfg,
device_map="auto",
)
model = prepare_model_for_kbit_training(model)
lora = LoraConfig(
r=16, lora_alpha=32,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
lora_dropout=0.05, bias="none", task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora)
# ~0.5% dos params são treináveis; base em NF4 fica congeladaNF4 é excelente para fine-tuning (QLoRA) mas subótimo para serving puro. Para inferência de produção, prefira AWQ ou GPTQ — kernels GEMM otimizados (Marlin, Machete) superam bitsandbytes em 2-4× tokens/s.
O que a quantização faz e o que ela custa?
Como escolher: árvore de decisão
Validação: perplexity ≠ qualidade de tarefa
Perplexity em WikiText mede modelagem geral de linguagem. Não correlaciona perfeitamente com performance em tarefas específicas. Um Q4_K_M com ΔPPL=+0.16 pode perder 3 pontos em GSM8K (matemática) e 5 em HumanEval (código). Sempre valide quantização no seu benchmark de produção, não só em perplexity.
# Bateria mínima de avaliação pós-quantização
# 1. PPL geral
./llama-perplexity -m model-Q4_K_M.gguf -f wiki.test.raw
# 2. lm-evaluation-harness — MMLU, GSM8K, HumanEval, IFEval
lm_eval --model hf --model_args pretrained=./awq-int4-model,trust_remote_code=True \
--tasks mmlu,gsm8k,humaneval,ifeval --batch_size 8
# 3. Seu próprio eval set (golden examples de produção)
python eval_production.py --model-path ./awq-int4-model --testset ./golden.jsonl| Modelo | MMLU FP16 | MMLU AWQ-INT4 | GSM8K FP16 | GSM8K AWQ-INT4 | Δ aceito? |
|---|---|---|---|---|---|
| Llama 3.1 8B | 68.4 | 67.8 | 84.5 | 82.1 | Sim (-0.6 / -2.4) |
| Llama 3.1 70B | 83.6 | 83.2 | 95.1 | 94.6 | Sim (-0.4 / -0.5) |
| Qwen 2.5 7B | 74.2 | 73.5 | 85.4 | 82.0 | Aceitável |
| Phi-4 14B | 84.8 | 83.1 | 91.2 | 85.4 | Marginal — preferir INT8 |
| Llama 3.2 3B | 58.0 | 54.6 | 76.7 | 69.1 | Não — modelo pequeno demais para INT4 |
Perguntas frequentes
❓ Posso quantizar um modelo já fine-tunado, ou tenho que quantizar a base e re-treinar?
❓ Por que minha quantização AWQ deu PPL pior que esperado?
❓ GGUF roda em GPU ou só CPU?
❓ FP8 já substituiu INT4?
Referências canônicas
Fixando
Por que existem vários métodos de quantização em vez de um só?
Qual nível de quantização costuma ser o ponto de equilíbrio na prática?
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…