Kvantizace modelu: INT8, INT4, GPTQ, AWQ a dále
Plně přesný model GPT-4 zabírá stovky gigabajtů. Llama-3 70B v FP16 vyžaduje 140 GB VRAM. Tato čísla znemožňují provoz velkých modelů na spotřebním hardwaru – ledaže aplikovat kvantování. S kvantizací INT4 klesne stejná Llama-3 70B na 35 GB, pasující do dvou RTX 4090 nebo systému s 64GB RAM. Vše se ztrátou přesnosti často méně než 1 %.
Kvantování modelu se stalo jednou z nejdůležitějších technik v moderním ekosystému LLM. Toto již není trik na úsporu paměti: je to klíč ke zpřístupnění modelů, nasaditelná na okrajová zařízení, spustitelná na spotřebitelském hardwaru a konkurenceschopná z hlediska latence. Algoritmy jako GPTQ, AWQ, SmoothQuant a formát GGUF z llama.cpp demokratizovali přístup k LLM.
V této příručce prozkoumáme kvantování od nuly: od základní matematiky až po výběr metody vhodné pro váš případ použití s příklady pracovního kódu pro každou techniku.
Co se naučíte
- proč je kvantování základem moderní umělé inteligence
- Rozdíl mezi PTQ (Potréninková kvantizace) a QAT (Quantization-Aware Training)
- Kvantování INT8 s bitsandbajty a SmoothQuant
- Kvantování INT4 pomocí NF4, FP4 a QLoRA
- Algoritmus GPTQ: jak to funguje a kdy jej použít
- AWQ (Activation-Aware Weight Quantization): Výhody heterogenního hardwaru
- Formát GGUF a kvantizace pomocí lama.cpp
- Srovnávací přesnost versus rychlost versus paměť
- Trénink zaměřený na kvantizaci s PyTorchem
- Osvědčené postupy a skutečné případy použití
Proč kvantovat? Problém VRAM
V parametru FP32 zabírá 4 bajty. V parametru FP16/BF16 zabírá 2 bajty. Při 8bitové kvantizaci zabírá každý parametr 1 bajt; s INT4, pouze 0,5 bajtu. Následující tabulka ukazuje spotřebu paměti pro Llama-3 70B s různou přesností:
| Přesnost | Bajtů na param | Paměť (70B) | Vyžaduje GPU |
|---|---|---|---|
| FP32 | 4 bajty | 280 GB | Nemožný spotřebitel |
| BF16 / FP16 | 2 bajty | 140 GB | 2x A100 80GB |
| INT8 | 1 bajt | 70 GB | 1x A100 80GB |
| INT4 / NF4 | 0,5 bajtu | 35 GB | 2x RTX 4090 (24GB) |
| INT3 / Q3_K_M | 0,375 bajtů | ~26 GB | RTX 3090 + vyložení RAM |
Kromě VRAM přináší kvantizace výhody z hlediska propustnost (tokeny/s), latence závěr e náklady na cloud. Na hardwaru, jako jsou procesory Apple M-series nebo Raspberry Pis, kvantizace je jediný způsob, jak spustit modely nad 1B parametry.
Údaje o trhu 2026
Gartner předpovídá, že do roku 2027 i Malé jazykové modely (SLM) kvantovaný překoná cloudové LLM ve frekvenci využití 3x. Umělá inteligence na zařízení se snižuje provozní náklady 70 % ve srovnání s cloudem, odstranění latence sítě e Náklady na API. Trh kvantizace se stal kritickým pro edge AI, mobilní nasazení a vestavěné systémy.
Matematické základy kvantifikace
Kvantizace mapuje spojitou hodnotu s plovoucí desetinnou čárkou na diskrétní celé číslo. Proces se dělí na dvě operace: kvantování e dekvantizace.
Daná hmotnostní matice W v FP16 probíhá kvantování INT8 následovně:
# Quantizzazione uniforme (schema base)
# W_quantized = round(W / scale) + zero_point
# W_dequantized = (W_quantized - zero_point) * scale
import torch
import numpy as np
def quantize_tensor_int8(tensor: torch.Tensor, symmetric: bool = True):
"""
Quantizzazione INT8 uniforme.
symmetric=True: zero_point=0, range [-127, 127]
symmetric=False: range asimmetrico con zero_point
"""
if symmetric:
# Scale basato sul valore assoluto massimo
max_val = tensor.abs().max().item()
scale = max_val / 127.0
zero_point = 0
else:
# Scale basato su min/max
min_val = tensor.min().item()
max_val = tensor.max().item()
scale = (max_val - min_val) / 255.0
zero_point = round(-min_val / scale)
zero_point = max(0, min(255, zero_point))
# Quantizzazione: FP16 -> INT8
quantized = torch.clamp(
torch.round(tensor / scale) + zero_point,
-128, 127
).to(torch.int8)
# De-quantizzazione: INT8 -> FP16 (per verifica errore)
dequantized = (quantized.float() - zero_point) * scale
# Errore di quantizzazione
error = (tensor - dequantized).abs().mean().item()
return quantized, scale, zero_point, error
# Esempio pratico
W = torch.randn(1024, 1024, dtype=torch.float16)
q, scale, zp, err = quantize_tensor_int8(W, symmetric=True)
print(f"Originale: {W.dtype}, {W.element_size() * W.numel() / 1024:.1f} KB")
print(f"Quantizzato: {q.dtype}, {q.element_size() * q.numel() / 1024:.1f} KB")
print(f"Riduzione memoria: {(1 - q.element_size()/W.element_size()) * 100:.0f}%")
print(f"Errore medio assoluto: {err:.6f}")
# Output tipico:
# Originale: torch.float16, 2048.0 KB
# Quantizzato: torch.int8, 1024.0 KB
# Riduzione memoria: 50%
# Errore medio assoluto: 0.000394
PTQ vs QAT: Výběr správného přístupu
Pro kvantování modelu existují dvě základní paradigmata:
- PTQ (Potréninková kvantizace): aplikuje se po tréninku na modelu již vycvičený. Vyžaduje pouze malou sadu kalibračních dat. Je to rychlé a praktické, ale může snížit přesnost u modelů s malou nebo velmi nízkou přesností (INT2, INT3).
- QAT (trénink zaměřený na kvantizaci): simulovat kvantování během tréninku, umožňuje modelu přizpůsobit své hmotnosti ztrátě přesnosti. Přináší výsledky lepší, ale vyžaduje výpočetní zdroje srovnatelné s úplným doladěním.
PTQ vs QAT: Praktické srovnání
| čekám | PTQ | QAT |
|---|---|---|
| Čas | Minuty-hodiny | Hodiny-dny |
| Kalibrační datový soubor | Malé (512–2048 vzorků) | Kompletní datová sada |
| VRAM pro kvantování | Nízká (pouze dopředný průjezd) | Vysoká (úplný zpětný průchod) |
| Přesnost INT8 | Vynikající (<0,5% ztráta) | Vynikající |
| Přesnost INT4 | Dobré (ztráta 1–3 %) | Velmi dobrý (<1% ztráta) |
| Případ použití | Velké modely, rychlá výroba | Malé modely, maximální kvalita |
Pro moderní LLM (nad 7B parametry) je PTQ obecně dostačující: redundance parametry znamená, že kvantizace INT4 téměř zcela zachovává možnosti modelu. U modelů s parametry 3B se doporučuje QAT, když je přesnost kritická.
INT8 s bitsandbajty: Nejjednodušší metoda
bitsandbajtů a nejpoužívanější knihovna pro praktickou LLM kvantizaci. Původně vyvinutý Timem Dettmersem, podporuje INT8 i INT4 (NF4, FP4) a je integrovaný nativně v Hugging Face Transformers. Velká výhoda: žádná sada kalibračních dat, on-the-fly kvantování po načtení modelu.
# Installazione
# pip install bitsandbytes transformers accelerate
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
# === CONFIGURAZIONE INT8 ===
config_int8 = BitsAndBytesConfig(
load_in_8bit=True,
llm_int8_threshold=6.0, # Outlier threshold (default ottimale)
llm_int8_has_fp16_weight=False
)
# === CONFIGURAZIONE INT4 NF4 (QLoRA style) ===
config_int4 = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4: ottimale per pesi normalmente distribuiti
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in BF16 (non INT4!)
bnb_4bit_use_double_quant=True, # Double quantization: -0.4 bit/param extra
bnb_4bit_quant_storage=torch.uint8 # Storage format
)
model_name = "meta-llama/Llama-3.1-8B-Instruct"
# Caricamento con INT4 NF4
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=config_int4,
device_map="auto", # Distribuisce automaticamente su GPU/CPU
torch_dtype=torch.bfloat16,
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Verifica memoria usata
mem_gb = model.get_memory_footprint() / 1024**3
print(f"Memoria modello (INT4): {mem_gb:.2f} GB")
# Llama-3.1-8B: ~4.5 GB vs 16 GB in BF16
# Inferenza standard (identica al modello full-precision)
inputs = tokenizer("Spiegami la quantizzazione dei modelli:", return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=200,
temperature=0.7,
do_sample=True
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Omezení bitsandbajtů
- Kvantování probíhá při načítání: kvantovaný model se neuloží přímo (k tomu použijte GPTQ nebo AWQ)
- INT8 bitsandbytes používá smíšenou techniku: 8bitové kvantované váhy, ale aktivace odlehlých hodnot jsou zpracovány v FP16 (LLM.int8())
- Výpočet vždy probíhá v BF16/FP16, ne v INT4: kvantizace snižuje paměť, ale nezrychluje výpočet tolik jako GPTQ/AWQ
- Na CPU a systémech bez CUDA GPU může být výkon slabý
Algoritmus GPTQ: Kvantování vrstva po vrstvě
GPTQ (Generative Pre-Trained Transformer Quantization, Frantar et al. 2022) a Pokročilý algoritmus PTQ, který kvantuje každou vrstvu samostatně, čímž se minimalizuje chyba rekonstrukce. Použijte Hessenská matice (přibližně pomocí kalibračních dat) k určení které váhy jsou nejcitlivější na kvantování a jak kompenzovat zbytkovou chybu.
Proces GPTQ kvantizuje sloupec po sloupci každé matice váhy a aktualizuje sloupce zbývající na kompenzaci zavedené chyby. Díky tomu je GPTQ mnohem přesnější než obyčejný jednotné kvantování, zejména na INT4 a INT3.
# Installazione AutoGPTQ
# pip install auto-gptq optimum
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
model_name = "meta-llama/Llama-3.1-8B-Instruct"
output_dir = "./llama-3.1-8b-gptq-int4"
# === DATASET DI CALIBRAZIONE ===
# GPTQ richiede un piccolo dataset (128-512 sample) per calibrare
# la quantizzazione. Più rappresentativo = migliore accuratezza.
from datasets import load_dataset
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
# Dataset Wikitext per calibrazione (standard per LLM)
calibration_data = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_texts = [
text for text in calibration_data["text"]
if len(text.strip()) > 100
][:128] # 128 sample di calibrazione
# Tokenizza i testi di calibrazione
calibration_tokens = [
tokenizer(text, return_tensors="pt", truncation=True, max_length=2048)
for text in calibration_texts
]
# === CONFIGURAZIONE QUANTIZZAZIONE ===
quantize_config = BaseQuantizeConfig(
bits=4, # INT4 quantization
group_size=128, # Dimensione gruppo (più piccolo = più accurato, più lento)
damp_percent=0.01, # Damping factor per stabilità numerica
desc_act=True, # Activation ordering (migliora qualità, opzionale)
sym=True, # Quantizzazione simmetrica
)
# === CARICAMENTO E QUANTIZZAZIONE ===
print("Caricamento modello FP16...")
model = AutoGPTQForCausalLM.from_pretrained(
model_name,
quantize_config=quantize_config,
torch_dtype="auto"
)
print("Avvio quantizzazione GPTQ (richiede ~30-60 min su A100)...")
model.quantize(
calibration_tokens,
use_triton=False, # True per inferenza ottimizzata con triton
batch_size=1,
cache_examples_on_gpu=True
)
# === SALVATAGGIO ===
model.save_quantized(output_dir, use_safetensors=True)
tokenizer.save_pretrained(output_dir)
print(f"Modello GPTQ salvato in: {output_dir}")
# === CARICAMENTO MODELLO GPTQ GIA QUANTIZZATO ===
print("Caricamento modello GPTQ pre-quantizzato...")
model_gptq = AutoGPTQForCausalLM.from_quantized(
output_dir,
use_triton=False,
device_map="auto",
inject_fused_attention=True, # Ottimizzazione memoria attention
inject_fused_mlp=True # Ottimizzazione memoria MLP
)
# Inferenza
inputs = tokenizer("La quantizzazione GPTQ funziona:", return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model_gptq.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
GPTQ vytváří uložitelné a nasaditelné kvantizované modely. Mnoho modelů na Hugging Face Hub
s příponou -GPTQ o -4bit byly kvantovány tímto algoritmem.
Kvantování zabere čas (obvykle 30–90 minut u modelu 13B na A100), ale stává se
pouze jednou: kvantovaný model lze znovu použít bez opětovného kvantování.
AWQ: Kvantizace hmotnosti při aktivaci
AWQ (Lin et al. 2023) je alternativou k GPTQ, která vychází z pozorování různé: ne všechny váhy jsou stejně důležité. Malé procento závaží (cca 1 %) odpovídá velkým aktivacím a přispívá neúměrně na předpovědi modelu. Pokud jsou tyto „výrazné závaží“ zachovány s větší přesností, celková chyba kvantizace je drasticky snížena.
AWQ měří důležité váhy před kvantizací, čímž snižuje chyby na kritických kanálech. Výsledkem je kvalita srovnatelná nebo lepší než GPTQ, s procesem kvantizace často rychlejší a lepší výkon na heterogenním hardwaru (CPU, Mac M-series, mobilní).
# pip install autoawq
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_name = "meta-llama/Llama-3.1-8B-Instruct"
output_dir = "./llama-3.1-8b-awq-int4"
# Caricamento tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
# Caricamento modello da quantizzare
print("Caricamento modello per quantizzazione AWQ...")
model = AutoAWQForCausalLM.from_pretrained(
model_name,
safetensors=True,
device_map="cuda",
trust_remote_code=True
)
# Configurazione AWQ
quant_config = {
"zero_point": True, # Quantizzazione asimmetrica (migliore per LLM)
"q_group_size": 128, # Dimensione gruppo
"w_bit": 4, # 4-bit quantization
"version": "GEMM" # GEMM: bilanciamento velocità/qualità
# GEMV: ottimizzato per batch size 1 (chatbot)
}
# Dataset di calibrazione personalizzato
calib_data = [
"La quantizzazione AWQ permette di eseguire modelli grandi su hardware limitato.",
"I Transformer hanno rivoluzionato il natural language processing con il meccanismo di attention.",
"Il fine-tuning con LoRA riduce significativamente il numero di parametri addestrabili.",
# ... aggiungere 128-256 esempi rappresentativi del task target
]
print("Avvio quantizzazione AWQ...")
model.quantize(
tokenizer,
quant_config=quant_config,
calib_data=calib_data
)
# Salvataggio
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"Modello AWQ salvato: {output_dir}")
# === CARICAMENTO E INFERENZA MODELLO AWQ ===
model_awq = AutoAWQForCausalLM.from_quantized(
output_dir,
fuse_layers=True, # Ottimizzazione kernel fused
trust_remote_code=True,
safetensors=True
)
from transformers import pipeline
pipe = pipeline(
"text-generation",
model=model_awq,
tokenizer=tokenizer,
device_map="auto"
)
result = pipe("Spiegami AWQ in un paragrafo:", max_new_tokens=150)
print(result[0]["generated_text"])
GPTQ vs AWQ: Co byste si měli vybrat?
- GPTQ: nejlepší pro GPU NVIDIA s CUDA. Rychlá inference s jádry Triton. De facto standard pro nasazení GPU. Nejlepší pro dávkové zpracování.
- AWQ: nejlepší pro heterogenní hardware (CPU, Mac, mobil). Rychlejší kvantování. Preferováno pro aplikace chatbotů (dávka=1). Efektivní s jádrem GEMV s jedním tokenem.
- Základní pravidlo: GPTQ pro dedikované servery GPU, AWQ pro multiplatformní a okrajová nasazení.
GGUF a lama.cpp: Kvantování pro CPU a Edge
Formát GGUF (GGML Unified Format) byl vytvořen projektem llama.cpp umožnit vyvození LLM na CPU, s volitelnou podporou GPU přes Metal (Apple), CUDA nebo OpenCL. GGUF je nástupcem GGML a řeší problémy s dopřednou kompatibilitou mezi verzemi.
Nomenklatura GGUF se řídí přesným vzorem: Q[bits]_[variant] kde varianta
označuje typ kvantizace. Nejběžnější jsou:
| Formát | Průměrné bity | kvalitní | Doporučené použití |
|---|---|---|---|
| Q8_0 | 8,0 bit | Téměř bezeztrátový | Maximální kvalita na výkonném CPU |
| Q6_K | 6,6 bitů | Vynikající | Rovnováha kvalita/velikost |
| Q5_K_M | 5,7 bit | Velmi dobré | Stolní počítač s 16+ GB RAM |
| Q4_K_M | 4,8 bit | Dobré (95 %) | Doporučená výchozí hodnota, notebook s kapacitou 8+ GB |
| Q3_K_M | 3,9 bitů | Přijatelný | Velmi omezený hardware |
| Q2_K | 2,6 bitů | Degradovaný | Pouze pro extrémní testování |
Přípona _K_M označuje "střední" velikost "K-kvantizace": pokročilá technika který používá blokovou kvantizaci s vyššími přesnými měřítky pro nejkritičtější vrstvy, výsledkem je lepší kvalita než jednotná kvantizace.
# Conversione e quantizzazione con llama.cpp
# Prima: installare llama.cpp
# git clone https://github.com/ggerganov/llama.cpp
# cd llama.cpp && make -j4
# 1. Convertire modello HuggingFace -> GGUF FP16
python convert_hf_to_gguf.py \
meta-llama/Llama-3.1-8B-Instruct \
--outfile llama-3.1-8b-f16.gguf \
--outtype f16
# 2. Quantizzare in vari formati
./llama-quantize llama-3.1-8b-f16.gguf llama-3.1-8b-q4_k_m.gguf Q4_K_M
./llama-quantize llama-3.1-8b-f16.gguf llama-3.1-8b-q8_0.gguf Q8_0
./llama-quantize llama-3.1-8b-f16.gguf llama-3.1-8b-q5_k_m.gguf Q5_K_M
# 3. Benchmark performance con llama.cpp
./llama-bench \
-m llama-3.1-8b-q4_k_m.gguf \
-p 512 \ # Token prompt
-n 128 \ # Token da generare
-t 8 # Thread CPU
# Output tipico su M2 Pro (16 GB):
# Q4_K_M: prompt 45.2 t/s, generate 28.1 t/s
# Q8_0: prompt 24.1 t/s, generate 15.8 t/s
# === UTILIZZO CON PYTHON via llama-cpp-python ===
# pip install llama-cpp-python
from llama_cpp import Llama
llm = Llama(
model_path="./llama-3.1-8b-q4_k_m.gguf",
n_ctx=4096, # Context window
n_threads=8, # Thread CPU
n_gpu_layers=35, # Offload 35 layer su GPU (0 = solo CPU)
verbose=False
)
response = llm(
"Q: Come funziona la quantizzazione dei modelli? A:",
max_tokens=256,
stop=["Q:", "\n\n"],
echo=True
)
print(response["choices"][0]["text"])
# === GGUF CON OLLAMA (più semplice) ===
# Crea Modelfile per Ollama
modelfile_content = """
FROM ./llama-3.1-8b-q4_k_m.gguf
PARAMETER temperature 0.7
PARAMETER num_ctx 4096
SYSTEM "Sei un assistente tecnico esperto di deep learning."
"""
with open("Modelfile", "w") as f:
f.write(modelfile_content)
# ollama create mio-llama -f Modelfile
# ollama run mio-llama
Benchmarky: Přesnost, rychlost a paměť
Nejpoužívanější metriky pro hodnocení kvality kvantovaného modelu jsou: zmatek na standardních datových sadách (Wikitext-2), měřítko uvažování (HellaSwag, MMLU) a úkoly specifické pro aplikační doménu.
# Benchmark automatico con lm-evaluation-harness
# pip install lm-eval
# Valutazione modello BF16 (baseline)
lm_eval --model hf \
--model_args "pretrained=meta-llama/Llama-3.1-8B-Instruct" \
--tasks hellaswag,mmlu \
--batch_size 4 \
--output_path results_bf16/
# Valutazione modello quantizzato GPTQ
lm_eval --model hf \
--model_args "pretrained=./llama-3.1-8b-gptq-int4,use_auto_gptq=True" \
--tasks hellaswag,mmlu \
--batch_size 4 \
--output_path results_gptq_int4/
# === SCRIPT BENCHMARK MEMORIA E VELOCITA ===
import torch
import time
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
def benchmark_model(model_name_or_path, quant_config=None, n_tokens=100, n_runs=5):
"""Benchmark completo: memoria, latenza, throughput."""
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
model = AutoModelForCausalLM.from_pretrained(
model_name_or_path,
quantization_config=quant_config,
device_map="cuda",
torch_dtype=torch.bfloat16 if quant_config is None else None
)
# Memoria usata
mem_gb = model.get_memory_footprint() / 1024**3
# Prompt di test
prompt = "Explain the transformer architecture in detail:" * 3
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
# Warm-up
with torch.no_grad():
model.generate(**inputs, max_new_tokens=10)
# Benchmark
torch.cuda.synchronize()
latencies = []
for _ in range(n_runs):
start = time.perf_counter()
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=n_tokens)
torch.cuda.synchronize()
elapsed = time.perf_counter() - start
latencies.append(elapsed)
avg_latency = sum(latencies) / len(latencies)
throughput = n_tokens / avg_latency
return {
"memoria_gb": round(mem_gb, 2),
"latenza_sec": round(avg_latency, 3),
"throughput_tps": round(throughput, 1)
}
# Confronto BF16 vs INT8 vs INT4
results = {}
results["BF16"] = benchmark_model("meta-llama/Llama-3.1-8B-Instruct")
config_8bit = BitsAndBytesConfig(load_in_8bit=True)
results["INT8"] = benchmark_model("meta-llama/Llama-3.1-8B-Instruct", config_8bit)
config_4bit = BitsAndBytesConfig(
load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True
)
results["INT4-NF4"] = benchmark_model("meta-llama/Llama-3.1-8B-Instruct", config_4bit)
for name, r in results.items():
print(f"{name:10} | Mem: {r['memoria_gb']:5.2f} GB | "
f"Latenza: {r['latenza_sec']:.3f}s | "
f"Throughput: {r['throughput_tps']:.1f} t/s")
# Risultati tipici Llama-3.1-8B su RTX 3090:
# BF16 | Mem: 16.02 GB | Latenza: 3.821s | Throughput: 26.2 t/s
# INT8 | Mem: 8.51 GB | Latenza: 4.103s | Throughput: 24.4 t/s
# INT4-NF4 | Mem: 4.89 GB | Latenza: 3.412s | Throughput: 29.3 t/s
Orientační výsledky benchmarku (Llama-3.1-8B na RTX 4090)
| Metoda | Paměť | Propustnost | HellaSwag | Zmatek |
|---|---|---|---|---|
| BF16 (základní hodnota) | 16,0 GB | 38 t/s | 82,1 % | 6.14 |
| INT8 (bit-sandbajty) | 8,5 GB | 35 t/s | 81,8 % | 6.21 |
| INT4 NF4 (bnb) | 4,9 GB | 42 t/s | 81,2 % | 6.47 |
| GPTQ INT4 | 4,8 GB | 55 t/s | 81,5 % | 6.39 |
| AWQ INT4 | 4,7 GB | 52 t/s | 81,6 % | 6.35 |
| Q4_K_M (GGUF, CPU) | 4,9 GB | 18 t/s | 81,3 % | 6.42 |
Poznámka: orientační hodnoty se liší podle hardwaru, konkrétního modelu a velikosti šarže.
Trénink zaměřený na kvantizaci s PyTorchem
Pro scénáře, kde PTQ nestačí – typicky malé modely s parametry 3B nebo 2-3 bitová kvantizace — la QAT umožňuje výrazné zotavení přesnost. PyTorch obsahuje nativní modul pro QAT počínaje verzí 2.0 s podpora pro statické a dynamické INT8.
import torch
import torch.nn as nn
from torch.quantization import (
prepare_qat, convert, get_default_qat_qconfig,
QConfigMapping
)
from torch.ao.quantization import get_default_qat_qconfig_mapping
# === DEFINIZIONE MODELLO SEMPLICE ===
class SimpleTransformerBlock(nn.Module):
def __init__(self, d_model=256, nhead=4, ff_dim=1024):
super().__init__()
self.attn = nn.MultiheadAttention(d_model, nhead, batch_first=True)
self.norm1 = nn.LayerNorm(d_model)
self.ff = nn.Sequential(
nn.Linear(d_model, ff_dim),
nn.ReLU(),
nn.Linear(ff_dim, d_model)
)
self.norm2 = nn.LayerNorm(d_model)
def forward(self, x):
attn_out, _ = self.attn(x, x, x)
x = self.norm1(x + attn_out)
ff_out = self.ff(x)
return self.norm2(x + ff_out)
class SimpleModel(nn.Module):
def __init__(self, vocab_size=1000, d_model=256, n_layers=4):
super().__init__()
self.embed = nn.Embedding(vocab_size, d_model)
self.blocks = nn.Sequential(*[
SimpleTransformerBlock(d_model) for _ in range(n_layers)
])
self.head = nn.Linear(d_model, vocab_size)
def forward(self, x):
x = self.embed(x)
x = self.blocks(x)
return self.head(x)
# === TRAINING BASELINE (FP32) ===
model = SimpleModel()
model.train()
# === CONFIGURAZIONE QAT ===
# QConfig specifica come quantizzare activations e weights
qconfig_mapping = get_default_qat_qconfig_mapping("x86")
# Prepara il modello per QAT: inserisce FakeQuantize nodes
# che simulano la quantizzazione durante il forward pass
from torch.ao.quantization import prepare_qat_fx
# Traccia il modello con esempio di input
example_input = torch.randint(0, 1000, (2, 32))
model_prepared = prepare_qat_fx(model, qconfig_mapping, example_input)
# === QAT TRAINING LOOP ===
optimizer = torch.optim.Adam(model_prepared.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
def train_qat(model, n_epochs=10, freeze_quantizer_epoch=8):
for epoch in range(n_epochs):
# Dati sintetici (sostituire con dataset reale)
x = torch.randint(0, 1000, (32, 64))
y = torch.randint(0, 1000, (32, 64))
optimizer.zero_grad()
out = model(x)
loss = criterion(out.view(-1, 1000), y.view(-1))
loss.backward()
optimizer.step()
# Freeze quantizer dopo N epoche: stabilizza le scale
if epoch == freeze_quantizer_epoch:
model.apply(torch.quantization.disable_observer)
print(f"Epoch {epoch}: FakeQuantize observers disabilitati")
if epoch % 2 == 0:
print(f"Epoch {epoch}/{n_epochs}, Loss: {loss.item():.4f}")
train_qat(model_prepared)
# === CONVERSIONE FINALE INT8 ===
model_prepared.eval()
model_int8 = convert(model_prepared)
# Salvataggio
torch.save(model_int8.state_dict(), "model_qat_int8.pt")
# Confronto dimensioni
fp32_size = sum(p.numel() * 4 for p in model.parameters()) / 1024**2
int8_size = sum(p.numel() * 1 for p in model_int8.parameters()) / 1024**2
print(f"Modello FP32: {fp32_size:.1f} MB")
print(f"Modello QAT INT8: {int8_size:.1f} MB")
print(f"Riduzione: {(1 - int8_size/fp32_size)*100:.0f}%")
SmoothQuant: Lepší INT8 pro LLM
SmoothQuant (Xiao et al. 2022) řeší specifický problém kvantování INT8 LLM: le odlehlé aktivace. Některé aktivační kanály v Transformers mají enormně větší hodnoty než ostatní, což ztěžuje jednotné kvantování (rozsah je plýtván na pokrytí odlehlých hodnot, což snižuje přesnost normálních hodnot).
SmoothQuant přenáší obtížnost kvantování z aktivací na váhy: vydělí aktivace faktorem měřítka na kanál a vynásobí odpovídající váhy pro stejný faktor. Výsledek je matematicky ekvivalentní, ale nyní jak aktivace, tak i váhy se snáze kvantifikují.
# Concetto di SmoothQuant (implementazione semplificata)
import torch
import torch.nn as nn
def compute_smooth_scale(activations: torch.Tensor, weights: torch.Tensor,
alpha: float = 0.5) -> torch.Tensor:
"""
Calcola il fattore di smoothing per SmoothQuant.
alpha: bilanciamento tra difficolta attivazioni e pesi (0.5 = equo)
"""
# Max assoluto per canale nelle attivazioni
act_max = activations.abs().max(dim=0).values # [hidden_dim]
# Max assoluto per canale nei pesi
w_max = weights.abs().max(dim=0).values # [hidden_dim]
# Fattore di scaling: riduce attivazioni, aumenta pesi proporzionalmente
scale = (act_max.pow(alpha) / w_max.pow(1 - alpha)).clamp(min=1e-5)
return scale
def smooth_layer(layer: nn.Linear, calibration_acts: torch.Tensor,
alpha: float = 0.5):
"""
Applica SmoothQuant a un layer Linear.
Divide le attivazioni in input per scale, moltiplica i pesi per scale.
"""
scale = compute_smooth_scale(calibration_acts, layer.weight.T, alpha)
# Modifica in-place (equivalente matematico ma più facile da quantizzare)
layer.weight.data = layer.weight.data * scale.unsqueeze(0)
# Ritorna la scala inversa da applicare alle attivazioni
return scale
# Uso pratico: SmoothQuant e tipicamente applicato con librerie come
# quanto (Hugging Face) o mediante ottimum:
# pip install optimum[quanto]
from transformers import AutoModelForCausalLM, AutoTokenizer
from optimum.quanto import quantize, freeze, qint8
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Quantizzazione INT8 con Quanto (include SmoothQuant internamente)
quantize(model, weights=qint8, activations=qint8)
# Calibrazione con alcuni esempi
with torch.no_grad():
for text in ["Calibration text 1", "Calibration text 2"]:
inputs = tokenizer(text, return_tensors="pt")
model(**inputs)
# Freeze: consolida i parametri quantizzati
freeze(model)
print("Modello quantizzato INT8 con Quanto/SmoothQuant pronto!")
Osvědčené postupy a anti-vzorce
Nejlepší postupy pro kvantizaci
- Vyberte přesnost na základě případu použití: pro nasazení na amerických GPU serverech GPTQ INT4; pro CPU/edge použijte GGUF Q4_K_M; pro jemné doladění použijte NF4 s bitsandbajty.
- Reprezentativní soubor kalibračních dat: pro GPTQ a AWQ použijte podobná data do cílové domény, nikoli generika. 128-512 vzorků je dostačujících, ale musí být významné.
- Vždy vyhodnocujte pomocí metrik specifických pro doménu: zmatek na Wikitextu a proxy, ale nezachycuje problémy u konkrétních úkolů (kódování, matematika, neanglické jazyky).
- Optimální velikost skupiny: group_size=128 a výchozí safe; 64 zlepšuje kvalitu za cenu větší paměti; 256 snižuje paměť, ale zhoršuje kvalitu.
- Dvojité kvantování (bnb): vždy povolit bnb_4bit_use_double_quant=True; ušetří ~0,4 bitů/param s minimálním dopadem na kvalitu.
- Použijte BF16 jako výpočetní dtype: bnb_4bit_compute_dtype=torch.bfloat16 a stabilnější než FP16 a podporován Ampere+ (RTX 3000+, A100).
Anti-vzory, kterým je třeba se vyhnout
- Nekvantizujte kritické vrstvy: první a poslední vrstva (vložení, LM hlava) jsou často citlivější na kvantování. GPTQ a AWQ je automaticky vylučují.
- Nepoužívejte FP4 pro odvození: NF4 a vyšší než FP4 pro váhy LLM, které jsou normálně distribuovány. FP4 je užitečný pouze pro velmi specifické scénáře.
- Neporovnávejte různé kvantizace bez benchmarkingu: modelka "INT4" s metodou GPTQ se liší od "INT4" s bitsandbajty. Skutečná přesnost Záleží na provedení.
- Neignorujte latenci dekvantizace: INT4 bitsandbajty musí dekvantizovat během dopředného průchodu. Na malých GPU s omezenou šířkou pásma paměti, toto může být pomalejší než INT8.
- Nepoužívejte Q2_K ve výrobě: kvalita je na to příliš zhoršená většina případů použití. Q3_K_M je minimum přijatelné pro jednoduché úkoly.
Scénáře nasazení: Průvodce výběrem
Výběr metody kvantizace závisí na kontextu nasazení. Zde je praktický návod:
| Scénář | Železářské zboží | Doporučená metoda | Formát |
|---|---|---|---|
| Produkční server GPU | A100/H100 80 GB | GPTQ INT4 nebo AWQ INT4 | Bezpečné senzory GPTQ |
| Spotřebitelské pracovní stanice | RTX 4090 24GB | GPTQ INT4 (modely do 70B) | Bezpečné senzory GPTQ |
| Notebooky se systémem Windows/Linux | GPU 8-16GB VRAM | bitsandbytes NF4 nebo AWQ | HuggingFace Hub |
| Notebook Apple řady M | Sjednocená paměť 16-96GB | GGUF Q4_K_M nebo Q5_K_M | GGUF + lama.cpp/Ollama |
| Raspberry Pi 5 | 8GB RAM | GGUF Q3_K_M nebo Q4_K_M (modely 1-3B) | GGUF + lama.cpp |
| NVIDIA Jetson Orin | 16GB unifikovaná paměť | GPTQ INT4 nebo GGUF Q4_K_M | GPTQ nebo GGUF |
| Omezené jemné doladění GPU | RTX 3090 24GB | QLoRA (NF4 + LoRA) | bitsandbytes NF4 |
Závěry
Kvantování vzoru se stalo specializovanou technikou na nástroj nezbytné pro každého, kdo pracuje s LLM. Panorama v roce 2026 je bohaté: bitsandbajtů pro rychlé prototypování a jemné ladění QLoRA, GPTQ pro optimalizované nasazení GPU, AWQ pro heterogenní a multiplatformní hardware, GGUF na CPU a okrajová zařízení.
Klíčem je výběr správné metody pro konkrétní kontext. INT4 s GPTQ na RTX 4090 a často rychlejší než BF16 díky optimalizovaným jádrům. GGUF Q4_K_M na MacBooku Pro s M3 umožňuje provozovat Llama-3.1-8B rychlostí 28 tokenů/s bez vyhrazeného GPU. To nejsou ústupky kvalitativní: umožňují dříve nemožné scénáře.
Dalším přirozeným krokem je zkombinovat kvantování s destilace modelů, kterému se budeme věnovat v příštím článku: jak přenést znalosti z velkého modelu kvantován do menšího modelu a získat to nejlepší z obou kompresních technik.
Další kroky
- Další článek: Destilační modely: Přenos znalostí
- Související článek: Jemné doladění pomocí LoRA a QLoRA
- Viz také: Ollama: Místní LLM na notebooku a Raspberry
- Řada MLOps: Obsluha a nasazení modelu







