Kwantyzacja modelu: INT8, INT4, GPTQ, AWQ i więcej
W pełni precyzyjny model GPT-4 zajmuje setki gigabajtów. Lama-3 70B w FP16 wymaga 140 GB pamięci VRAM. Liczby te uniemożliwiają uruchamianie dużych modeli na sprzęcie konsumenckim – chyba że zastosować kwantyzacja. Przy kwantyzacji INT4 ta sama Llama-3 70B spada do 35 GB, pasujący do dwóch kart RTX 4090 lub systemu z 64 GB pamięci RAM. Wszystko z utratą dokładności często mniej niż 1%.
Kwantyzacja modelu stała się jedną z najważniejszych technik we współczesnym ekosystemie LLM. To już nie jest sztuczka oszczędzająca pamięć: to klucz do udostępnienia modeli, możliwe do wdrożenia na urządzeniach brzegowych, wykonywalne na sprzęcie konsumenckim i konkurencyjne pod względem opóźnień. Algorytmy jak GPTQ, AWQ, Gładka ilość i format GGUF z llama.cpp zdemokratyzowali dostęp do LLM.
W tym przewodniku badamy kwantyzację od podstaw: od podstaw matematyki po wybór metody odpowiednie dla Twojego przypadku użycia, z przykładami działającego kodu dla każdej techniki.
Czego się nauczysz
- dlaczego kwantyzacja ma fundamentalne znaczenie dla współczesnej sztucznej inteligencji
- Różnica między PTQ (kwantyzacja potreningowa) a QAT (szkolenie uwzględniające kwantyzację)
- Kwantyzacja INT8 za pomocą bitsandbytes i SmoothQuant
- Kwantyzacja INT4 za pomocą NF4, FP4 i QLoRA
- Algorytm GPTQ: jak działa i kiedy go używać
- AWQ (kwantyzacja wagowa uwzględniająca aktywację): korzyści w przypadku sprzętu heterogenicznego
- Format GGUF i kwantyzacja za pomocą llama.cpp
- Dokładność testu porównawczego, szybkość i pamięć
- Szkolenie uwzględniające kwantyzację z PyTorch
- Najlepsze praktyki i rzeczywiste przypadki użycia
Dlaczego kwantyzacja? Problem z VRAMem
Parametr wejściowy FP32 zajmuje 4 bajty. Parametr wejściowy FP16/BF16 zajmuje 2 bajty. W przypadku kwantyzacji 8-bitowej każdy parametr zajmuje 1 bajt; z INT4, tylko 0,5 bajta. Poniższa tabela ilustruje zużycie pamięci dla Lamy-3 70B z różnymi dokładnościami:
| Precyzja | Bajty na parametr | Pamięć (70B) | Wymagany procesor graficzny |
|---|---|---|---|
| FP32 | 4 bajty | 280 GB | Niemożliwy konsument |
| BF16/FP16 | 2 bajty | 140 GB | 2xA100 80GB |
| INT8 | 1 bajt | 70 GB | 1x A100 80 GB |
| INT4/NF4 | 0,5 bajta | 35 GB | 2x RTX 4090 (24 GB) |
| INT3 / Q3_K_M | 0,375 bajtów | ~26 GB | RTX 3090 + odciążenie RAM |
Oprócz pamięci VRAM kwantyzacja przynosi korzyści w zakresie przepustowość (tokeny/s), utajenie wnioskowania, tj koszt chmury. Na sprzęcie takim jak procesory Apple z serii M lub Raspberry Pi, kwantyzacja to jedyny sposób na uruchomienie modeli o parametrach przekraczających 1B.
Dane rynkowe 2026
Gartner przewiduje, że do 2027 r., tj Modele małego języka (SLM) skwantowany przewyższy chmurowe LLM pod względem częstotliwości użytkowania 3-krotnie. AI na urządzeniu zmniejsza się koszty operacyjne 70% w porównaniu do chmury, eliminując opóźnienia sieci, np Koszty API. Rynek kwantyzacji stał się krytyczny dla brzegowej sztucznej inteligencji, urządzeń mobilnych systemy wdrażania i systemy wbudowane.
Matematyczne podstawy kwantyzacji
Kwantyzacja odwzorowuje ciągłą wartość zmiennoprzecinkową na dyskretną liczbę całkowitą. Proces dzieli się na dwie operacje: kwantyzacja e dekwantyzacja.
Biorąc pod uwagę macierz wag W w FP16 kwantyzacja INT8 przebiega następująco:
# 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: wybór właściwego podejścia
Istnieją dwa podstawowe paradygmaty kwantyzacji modelu:
- PTQ (kwantyzacja potreningowa): nakłada się go po treningu, na model już przeszkolony. Wymaga jedynie niewielkiego zbioru danych kalibracyjnych. Jest szybki i praktyczny, ale może obniżyć dokładność modeli o małej lub bardzo niskiej precyzji (INT2, INT3).
- QAT (szkolenie uwzględniające kwantyzację): symulować kwantyzację podczas treningu, pozwalając modelowi dostosować swoje ciężary do utraty precyzji. Daje rezultaty lepsze, ale wymaga zasobów obliczeniowych porównywalnych z pełnym dostrojeniem.
PTQ vs QAT: porównanie praktyczne
| Czekam | PQ | QAT |
|---|---|---|
| Czas | Minuty-godziny | Godziny-dni |
| Zestaw danych kalibracyjnych | Mały (512-2048 próbek) | Kompletny zbiór danych |
| VRAM do kwantyzacji | Niski (tylko podanie w przód) | Wysokie (pełne podanie do tyłu) |
| Dokładność INT8 | Znakomity (<0,5% straty) | Doskonały |
| Dokładność INT4 | Dobry (strata 1-3%) | Bardzo dobry (<1% straty) |
| Użyj przypadku | Duże modele, szybka produkcja | Małe modele, maksymalna jakość |
W przypadku nowoczesnych LLM (ponad 7B parametrów) PTQ jest na ogół wystarczające: redundancja parametrów oznacza, że kwantyzacja INT4 niemal całkowicie zachowuje możliwości modelu. W przypadku modeli o parametrach poniżej 3B zaleca się QAT, gdy dokładność ma kluczowe znaczenie.
INT8 z bitsandbytes: najprostsza metoda
bity i bajty i najczęściej używana biblioteka do praktycznej kwantyzacji LLM. Pierwotnie opracowany przez Tima Dettmersa, obsługuje zarówno INT8, jak i INT4 (NF4, FP4) i jest zintegrowany natywnie w Hugging Face Transformers. Duża zaleta: brak zestawu danych kalibracyjnych, kwantyzacja w locie po załadowaniu 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))
Ograniczenia bitsandbajtów
- Kwantyzacja odbywa się w czasie ładowania: skwantowany model nie jest zapisywany bezpośrednio (użyj do tego GPTQ lub AWQ)
- INT8 bitsandbytes wykorzystuje technikę mieszaną: 8-bitowe skwantowane wagi, ale aktywacje wartości odstających są obsługiwane w FP16 (LLM.int8())
- Obliczenia zawsze odbywają się w BF16/FP16, a nie w INT4: kwantyzacja zmniejsza pamięć, ale nie przyspiesza obliczeń tak bardzo jak GPTQ/AWQ
- Na procesorach i systemach bez procesorów graficznych CUDA wydajność może być niska
Algorytm GPTQ: kwantyzacja warstwa po warstwie
GPTQ (Generative Pre-Trained Transformer Quantization, Frantar et al. 2022) oraz Zaawansowany algorytm PTQ, który kwantyzuje każdą warstwę osobno, minimalizując błąd rekonstrukcji. Skorzystaj z Macierz Hessego (w przybliżeniu na podstawie danych kalibracyjnych) w celu ustalenia które wagi są najbardziej wrażliwe na kwantyzację i jak kompensować błąd resztkowy.
Proces GPTQ kwantyzuje kolumnę po kolumnie każdej macierzy wag, aktualizując kolumny pozostałe, aby zrekompensować wprowadzony błąd. To sprawia, że GPTQ jest znacznie dokładniejsze niż zwykłe jednolita kwantyzacja, zwłaszcza na INT4 i 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 tworzy możliwe do zapisania i wdrożenia modele skwantowane. Wiele modeli w Hugging Face Hub
z przyrostkiem -GPTQ o -4bit zostały skwantowane za pomocą tego algorytmu.
Kwantyzacja wymaga czasu (zwykle 30-90 minut dla modelu 13B na A100), ale zdarza się
tylko raz: skwantowany model można wykorzystać ponownie bez ponownej kwantyzacji.
AWQ: Kwantyzacja wagowa uwzględniająca aktywację
AWQ (Lin et al. 2023) jest alternatywą dla GPTQ, która rozpoczyna się od obserwacji różne: nie wszystkie wagi są równie ważne. Niewielki procent wag (ok 1%) odpowiada aktywacjom o dużej skali i ma nieproporcjonalny wkład do przewidywań modelu. Jeśli te „istotne wagi” zostaną zachowane z większą precyzją, ogólny błąd kwantyzacji jest drastycznie zmniejszony.
AWQ skaluje ważne wagi przed kwantyzacją, redukując błąd w krytycznych kanałach. Rezultatem jest jakość porównywalna lub lepsza od GPTQ, przy procesie kwantyzacji często szybsza i lepsza wydajność na sprzęcie heterogenicznym (procesor, komputer Mac z serii M, urządzenia mobilne).
# 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: który wybrać?
- GPTQ: najlepsze dla procesorów graficznych NVIDIA z CUDA. Szybkie wnioskowanie z jądrami Tritona. De facto standard przy wdrażaniu GPU. Najlepsze do przetwarzania wsadowego.
- AWQ: najlepsze dla sprzętu heterogenicznego (CPU, Mac, urządzenia mobilne). Szybsza kwantyzacja. Preferowane w przypadku aplikacji typu chatbot (partia = 1). Działa z pojedynczym tokenem jądra GEMV.
- Praktyczna zasada: GPTQ dla dedykowanych serwerów GPU, AWQ dla wdrożeń wieloplatformowych i brzegowych.
GGUF i llama.cpp: Kwantyzacja dla procesora i Edge
Format GGUF (GGML Unified Format) został stworzony w ramach projektu llama.cpp aby umożliwić wnioskowanie LLM na procesorze, z opcjonalną obsługą GPU przez Metal (Apple), CUDA lub OpenCL. GGUF zastępuje GGML i rozwiązuje problemy ze zgodnością między wersjami.
Nazewnictwo GGUF opiera się na precyzyjnym schemacie: Q[bits]_[variant] gdzie wariant
wskazuje rodzaj kwantyzacji. Najczęstsze to:
| Format | Średnie bity | jakość | Zalecane użycie |
|---|---|---|---|
| Q8_0 | 8,0 bitów | Prawie bezstratny | Maksymalna jakość na wydajnym procesorze |
| Q6_K | 6,6 bitów | Doskonały | Równowaga jakość/rozmiar |
| Q5_K_M | 5,7 bita | Bardzo dobry | Komputer stacjonarny z 16+ GB RAM |
| Q4_K_M | 4,8 bita | Dobry (95%) | Zalecany domyślny, laptop 8+ GB |
| Q3_K_M | 3,9 bitów | Do przyjęcia | Bardzo ograniczony sprzęt |
| Q2_K | 2,6 bita | Zdegradowany | Tylko do ekstremalnych testów |
Sufiks _K_M wskazuje „średni” rozmiar „Kwantyzacja”: zaawansowana technika który wykorzystuje kwantyzację blokową z większą precyzją współczynników skali dla najbardziej krytycznych warstw, co skutkuje lepszą jakością niż jednolita kwantyzacja.
# 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
Benchmarki: dokładność, szybkość i pamięć
Najczęściej używanymi metrykami do oceny jakości skwantowanego modelu są: zakłopotanie na standardowych zbiorach danych (Wikitekst-2), punkt odniesienia w rozumowaniu (HellaSwag, MMLU) i zadania specyficzne dla domeny aplikacji.
# 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
Orientacyjne wyniki testów porównawczych (Llama-3.1-8B na RTX 4090)
| Metoda | Pamięć | Przepustowość | HellaSwag | Zakłopotanie |
|---|---|---|---|---|
| BF16 (wartość bazowa) | 16,0 GB | 38 t/s | 82,1% | 6.14 |
| INT8 (bity i bajty) | 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, procesor) | 4,9 GB | 18 t/s | 81,3% | 6.42 |
Uwaga: wartości orientacyjne, różnią się w zależności od sprzętu, konkretnego modelu i wielkości partii.
Szkolenie uwzględniające kwantyzację z PyTorch
W przypadku scenariuszy, w których PTQ nie jest wystarczające — zazwyczaj małe modele o parametrach 3B lub 2-3-bitowa kwantyzacja — la QAT pozwala na znaczną regenerację dokładność. PyTorch zawiera natywny moduł dla QAT począwszy od wersji 2.0, z obsługa statycznego i dynamicznego 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: Lepszy INT8 dla LLM
Gładka ilość (Xiao et al. 2022) porusza specyficzny problem kwantyzacji INT8 LLM: le aktywacje odstające. Niektóre kanały aktywacji w Transformersach mają one ogromnie większe wartości niż inne, co utrudnia jednolitą kwantyzację (zakres jest marnowany na pokrycie wartości odstających, zmniejszając precyzję wartości normalnych).
Gładka ilość przenosi trudność kwantyzacji z aktywacji na wagi: dzieli aktywacje przez współczynnik skalujący na kanał i mnoży odpowiednie wagi dla tego samego czynnika. Wynik jest matematycznie równoważny, ale teraz zarówno aktywacje, jak i wagi są łatwiejsze do skwantowania.
# 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!")
Najlepsze praktyki i anty-wzorce
Najlepsze praktyki dotyczące kwantyzacji
- Wybierz precyzję w oparciu o przypadek użycia: do wdrożenia na amerykańskich serwerach GPU GPTQ INT4; dla procesora/krawędzi użyj GGUF Q4_K_M; do dostrojenia użyj NF4 z bitsandbytes.
- Reprezentatywny zbiór danych kalibracyjnych: dla GPTQ i AWQ użyj podobnych danych do domeny docelowej, a nie leków generycznych. 128–512 próbek wystarczy, ale muszą być znaczące.
- Zawsze oceniaj na podstawie wskaźników specyficznych dla domeny: zamieszanie na Wikitekście i proxy, ale nie wychwytuje problemów związanych z konkretnymi zadaniami (kodowanie, matematyka, języki inne niż angielski).
- Optymalna wielkość grupy: group_size=128 i domyślny sejf; 64 poprawia jakość kosztem większej ilości pamięci; 256 zmniejsza pamięć, ale pogarsza jakość.
- Podwójna kwantyzacja (bnb): zawsze włączaj bnb_4bit_use_double_quant=True; oszczędza ~0,4 bitów/parametr przy minimalnym wpływie na jakość.
- Użyj BF16 jako typu obliczeniowego: bnb_4bit_compute_dtype=torch.bfloat16 i bardziej stabilny niż FP16 i obsługiwany przez Ampere+ (RTX 3000+, A100).
Anty-wzorce, których należy unikać
- Nie kwantyzuj warstw krytycznych: pierwsza i ostatnia warstwa (zatapianie, głowica LM) często są bardziej wrażliwe na kwantyzację. GPTQ i AWQ automatycznie je wykluczają.
- Nie używaj FP4 do wnioskowania: NF4 i wyższe niż FP4 dla wag LLM, które są normalnie dystrybuowane. 4PR jest przydatny jedynie w bardzo specyficznych scenariuszach.
- Nie porównuj różnych kwantyzacji bez testów porównawczych: modelka „INT4” z metodą GPTQ różni się od „INT4” z bitami i bajtami. Rzeczywista precyzja To zależy od wdrożenia.
- Nie ignoruj opóźnienia dekwantyzacji: INT4 bity i bajty muszą dekwantyzację podczas podania do przodu. Na małych procesorach graficznych z ograniczoną przepustowością pamięci jest to możliwe może być wolniejszy niż INT8.
- Nie używaj Q2_K w produkcji: jakość jest zbyt obniżona dla większość przypadków użycia. Q3_K_M to minimum akceptowalne dla prostych zadań.
Scenariusze wdrożeń: przewodnik po wyborze
Wybór metody kwantyzacji zależy od kontekstu wdrożenia. Oto praktyczny przewodnik:
| Scenariusz | Sprzęt komputerowy | Zalecana metoda | Format |
|---|---|---|---|
| Serwer produkcyjny GPU | A100/H100 80 GB | GPTQ INT4 lub AWQ INT4 | Zabezpieczenia GPTQ |
| Konsumenckie stacje robocze | RTX4090 24 GB | GPTQ INT4 (modele do 70B) | Zabezpieczenia GPTQ |
| Laptopy z systemem Windows/Linux | Karta graficzna 8-16 GB VRAM | bitsandbytes NF4 lub AWQ | Hub HuggingFace |
| Laptop Apple z serii M | Zunifikowana pamięć 16–96 GB | GGUF Q4_K_M lub Q5_K_M | GGUF + llama.cpp/Ollama |
| RaspberryPi5 | 8 GB RAM-u | GGUF Q3_K_M lub Q4_K_M (modele 1-3B) | GGUF + lama.cpp |
| NVIDIA Jetson Orin | 16 GB zunifikowanej pamięci | GPTQ INT4 lub GGUF Q4_K_M | GPTQ lub GGUF |
| Ograniczone dostrajanie procesora graficznego | RTX3090 24 GB | QLoRA (NF4 + LoRA) | bitsandbytes NF4 |
Wnioski
Kwantyzacja wzorców przestała być techniką niszową i stała się narzędziem niezbędne dla każdego, kto pracuje z LLM. Panorama roku 2026 jest bogata: bity i bajty do szybkiego prototypowania i dostrajania QLoRA, GPTQ dla zoptymalizowanego wdrożenia GPU, AWQ dla sprzętu heterogenicznego i wieloplatformowego, GGUF na procesor i urządzenia brzegowe.
Kluczem jest wybór właściwej metody dla konkretnego kontekstu. INT4 z GPTQ na RTX 4090 i często szybszy niż BF16 dzięki zoptymalizowanym jądrom. GGUF Q4_K_M na MacBooku Pro z M3 pozwala na uruchomienie Llama-3.1-8B z szybkością 28 tokenów/s bez dedykowanego procesora graficznego. To nie są ustępstwa jakościowe: umożliwiają realizację wcześniej niemożliwych scenariuszy.
Następnym naturalnym krokiem jest połączenie kwantyzacji z destylacja modeli, o czym napiszemy w kolejnym artykule: jak przenieść wiedzę z dużego modelu skwantowany do mniejszego modelu, uzyskując to, co najlepsze z obu technik kompresji.
Następne kroki
- Następny artykuł: Modele destylacji: transfer wiedzy
- Powiązany artykuł: Dostrajanie za pomocą LoRA i QLoRA
- Zobacz także: Ollama: Lokalny LLM na laptopie i malinach
- Seria MLOps: Udostępnianie i wdrażanie modeli







