모델 양자화: INT8, INT4, GPTQ, AWQ 및 그 이상
완전 정밀도 GPT-4 모델은 수백 기가바이트를 차지합니다. FP16의 Llama-3 70B에는 140GB의 VRAM이 필요합니다. 이러한 수치로 인해 소비자 하드웨어에서 대규모 모델을 실행하는 것은 불가능합니다. 적용하다 양자화. INT4 양자화를 사용하면 동일한 Llama-3 70B가 35GB로 떨어지며, 두 개의 RTX 4090 또는 64GB RAM이 있는 시스템에 적합합니다. 정확성이 떨어지는 모든 것 1% 미만인 경우가 많습니다.
모델 양자화는 현대 LLM 생태계에서 가장 중요한 기술 중 하나가 되었습니다. 이는 더 이상 메모리를 절약하는 트릭이 아닙니다. 모델에 액세스할 수 있게 만드는 핵심입니다. 에지 장치에 배포 가능하고 소비자 하드웨어에서 실행 가능하며 대기 시간 측면에서 경쟁력이 있습니다. 다음과 같은 알고리즘 GPTQ, AWQ, 스무드퀀트 그리고 형식 GGUF llama.cpp는 LLM에 대한 액세스를 민주화했습니다.
이 가이드에서는 기본 수학부터 방법 선택까지 양자화를 처음부터 살펴봅니다. 각 기술에 대한 작동 코드 예제를 통해 귀하의 사용 사례에 적합합니다.
무엇을 배울 것인가
- 양자화가 현대 AI의 기본인 이유
- PTQ(훈련 후 양자화)와 QAT(양자화 인식 훈련)의 차이점
- bitandbytes 및 SmoothQuant를 사용한 INT8 양자화
- NF4, FP4 및 QLoRA를 사용한 INT4 양자화
- GPTQ 알고리즘: 작동 방식 및 사용 시기
- AWQ(Activation-Aware Weight Quantization): 이기종 하드웨어의 이점
- llama.cpp를 사용한 GGUF 형식 및 양자화
- 벤치마크 정확도 vs 속도 vs 메모리
- PyTorch를 사용한 양자화 인식 훈련
- 모범 사례 및 실제 사용 사례
왜 퀀타이즈해야 할까요? VRAM 문제
in 매개변수 FP32 4바이트를 차지합니다. in 매개변수 FP16/BF16 차지하다 2바이트. 8비트 양자화에서는 각 매개변수가 1바이트를 차지합니다. INT4의 경우 0.5바이트에 불과합니다. 다음 표는 다양한 정밀도에 따른 Llama-3 70B의 메모리 소비를 보여줍니다.
| 정도 | 매개변수당 바이트 | 메모리(70B) | GPU 필요 |
|---|---|---|---|
| FP32 | 4바이트 | 280GB | 불가능한 소비자 |
| BF16 / FP16 | 2바이트 | 140GB | 2x A100 80GB |
| INT8 | 1바이트 | 70GB | 1x A100 80GB |
| INT4 / NF4 | 0.5바이트 | 35GB | 2x RTX 4090(24GB) |
| INT3 / Q3_K_M | 0.375바이트 | ~26GB | RTX 3090 + RAM 오프로드 |
VRAM 외에도 양자화는 다음과 같은 이점을 제공합니다. 처리량 (토큰/초), 숨어 있음 추론 e 클라우드 비용. Apple M 시리즈 프로세서와 같은 하드웨어 또는 Raspberry Pis의 경우 양자화는 1B 매개변수를 초과하는 모델을 실행하는 유일한 방법입니다.
시장 데이터 2026
Gartner는 2027년까지 이를 예측합니다. 소규모 언어 모델(SLM) 양자화 사용 빈도에서 클라우드 LLM보다 3배 더 뛰어난 성능을 발휘합니다. 온디바이스 AI는 운영 비용 클라우드 대비 70%, 네트워크 대기 시간 제거 e API 비용. 양자화 시장은 엣지 AI, 모바일에 매우 중요해졌습니다. 배포 및 임베디드 시스템.
양자화의 수학적 기초
양자화는 연속적인 부동 소수점 값을 이산 정수로 매핑합니다. 과정 두 가지 작업으로 나뉩니다. 양자화 e 역양자화.
가중치 행렬이 주어지면 W FP16에서는 INT8 양자화가 다음과 같이 발생합니다.
# 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와 QAT: 올바른 접근 방식 선택
모델 양자화에는 두 가지 기본 패러다임이 있습니다.
- PTQ(훈련 후 양자화): 훈련 후 모델에 적용됩니다. 이미 훈련을 받았습니다. 작은 교정 데이터 세트만 필요합니다. 빠르고 실용적이지만 작거나 매우 낮은 정밀도 모델(INT2, INT3)에서는 정확도가 저하될 수 있습니다.
- QAT(양자화 인식 훈련): 훈련 중 양자화 시뮬레이션 모델이 정밀도 손실에 맞춰 가중치를 조정할 수 있도록 합니다. 결과를 낳는다 더 좋지만 전체 미세 조정에 필적하는 계산 리소스가 필요합니다.
PTQ 대 QAT: 실제 비교
| 나는 기다린다 | PTQ | QAT |
|---|---|---|
| 시간 | 분-시간 | 시간-일 |
| 교정 데이터세트 | 소형(512-2048 샘플) | 완전한 데이터 세트 |
| 양자화를 위한 VRAM | 낮음(정방향 패스만 해당) | 높음(전체 역방향 패스) |
| 정확도 INT8 | 우수함(<0.5% 손실) | 훌륭한 |
| INT4 정확도 | 좋음(1~3% 손실) | 매우 좋음(<1% 손실) |
| 사용 사례 | 대형 모델, 빠른 생산 | 작은 모델, 최고의 품질 |
최신 LLM(7B 매개변수 이상)의 경우 일반적으로 PTQ로 충분합니다. 매개변수는 INT4 양자화가 모델의 기능을 거의 완벽하게 보존한다는 것을 의미합니다. 3B 매개변수 아래 모델의 경우 정확도가 중요한 경우 QAT가 권장됩니다.
비트와 바이트를 사용하는 INT8: 가장 간단한 방법
비트샌드바이트 실용적인 LLM 양자화를 위해 가장 많이 사용되는 라이브러리입니다. 원래 Tim Dettmers가 개발한 이 제품은 INT8과 INT4(NF4, FP4)를 모두 지원하며 통합되어 있습니다. 기본적으로 Hugging Face Transformers에 있습니다. 가장 큰 장점: 교정 데이터세트 없음, 모델 로딩 시 즉시 양자화.
# 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))
비트와 바이트의 한계
- 로드 타임에 양자화가 발생합니다. 양자화된 모델은 저장되지 않습니다 직접(GPTQ 또는 AWQ 사용)
- bitandbytes의 INT8은 혼합 기술(8비트 양자화된 가중치)을 사용하지만 이상값 활성화는 FP16(LLM.int8())에서 처리됩니다.
- 계산은 항상 INT4가 아닌 BF16/FP16에서 발생합니다. 양자화는 메모리를 줄이지만 GPTQ/AWQ만큼 계산 속도를 높이지는 않습니다.
- CUDA GPU가 없는 CPU 및 시스템에서는 성능이 저하될 수 있습니다.
GPTQ 알고리즘: 레이어별 양자화
GPTQ (Generative Pre-Trained Transformer Quantization, Frantar et al. 2022) 및 재구성 오류를 최소화하면서 각 레이어를 개별적으로 양자화하는 고급 PTQ 알고리즘입니다. 사용 헤세 행렬 (교정 데이터를 통해 대략적으로) 결정 양자화에 가장 민감한 가중치와 잔여 오류를 보상하는 방법.
GPTQ 프로세스는 각 가중치 행렬의 열별로 양자화하여 열을 업데이트합니다. 발생한 오류를 보상하기 위해 남아 있습니다. 이는 GPTQ를 일반 것보다 훨씬 더 정확하게 만듭니다. 특히 INT4 및 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는 저장 및 배포 가능한 양자화 모델을 생성합니다. Hugging Face Hub의 다양한 모델
접미사와 함께 -GPTQ o -4bit 이 알고리즘으로 양자화되었습니다.
양자화에는 시간이 걸리지만(A100의 13B 모델의 경우 일반적으로 30~90분) 발생합니다.
한 번만: 양자화된 모델을 다시 양자화하지 않고도 재사용할 수 있습니다.
AWQ: 활성화 인식 중량 양자화
AWQ (Lin et al. 2023)은 관찰에서 시작하는 GPTQ의 대안입니다. 다름: 모든 가중치가 똑같이 중요하지는 않습니다. 작은 비율의 가중치(약 1%)는 대규모 활성화에 해당하며 불균형적으로 기여합니다. 모델의 예측에. 이러한 "뚜렷한 가중치"가 더 정밀하게 보존된다면, 전체 양자화 오류가 대폭 감소됩니다.
AWQ는 양자화 전에 중요한 가중치를 조정하여 중요한 채널의 오류를 줄입니다. 결과는 양자화 프로세스를 통해 GPTQ와 비슷하거나 더 우수한 품질입니다. 이기종 하드웨어(CPU, Mac M 시리즈, 모바일)에서 더 빠르고 더 나은 성능을 제공하는 경우가 많습니다.
# 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와 AWQ: 어느 것을 선택해야 합니까?
- GPTQ: CUDA를 사용하는 NVIDIA GPU에 가장 적합합니다. Triton 커널을 사용한 빠른 추론. GPU 배포를 위한 사실상의 표준입니다. 일괄 처리에 가장 적합합니다.
- AWQ: 이기종 하드웨어(CPU, Mac, 모바일)에 가장 적합합니다. 더 빠르게 양자화할 수 있습니다. 챗봇 애플리케이션에 적합합니다(배치=1). 단일 토큰 GEMV 커널에 효과적입니다.
- 경험 법칙: 전용 GPU 서버를 위한 GPTQ, 멀티 플랫폼 및 엣지 배포를 위한 AWQ.
GGUF 및 llama.cpp: CPU 및 Edge에 대한 양자화
형식 GGUF (GGML 통합 형식)은 llama.cpp 프로젝트에 의해 생성되었습니다. Metal(Apple)을 통한 선택적 GPU 지원을 통해 CPU에서 LLM 추론을 활성화합니다. CUDA 또는 OpenCL. GGUF는 GGML을 계승하고 버전 간의 향후 호환성 문제를 해결합니다.
GGUF 명명법은 정확한 패턴을 따릅니다. Q[bits]_[variant] 변형이 있는 곳
양자화 유형을 나타냅니다. 가장 일반적인 것은 다음과 같습니다.
| 체재 | 평균 비트 | 품질 | 권장 용도 |
|---|---|---|---|
| Q8_0 | 8.0비트 | 거의 무손실 | 강력한 CPU에서 최고의 품질 |
| Q6_K | 6.6비트 | 훌륭한 | 품질/크기 균형 |
| Q5_K_M | 5.7비트 | 매우 좋은 | 16GB 이상의 RAM을 갖춘 데스크탑 |
| Q4_K_M | 4.8비트 | 좋음 (95%) | 권장 기본, 8GB 이상의 노트북 |
| Q3_K_M | 3.9비트 | 허용됨 | 매우 제한된 하드웨어 |
| Q2_K | 2.6비트 | 타락한 | 극단적인 테스트에만 해당 |
접미사 _K_M "중간" 크기를 나타냅니다. "K-양자화": 고급 기술 가장 중요한 레이어에 대해 더 높은 정밀도의 스케일 팩터를 갖춘 블록 양자화를 사용합니다. 균일한 양자화보다 품질이 더 좋습니다.
# 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
벤치마크: 정확성, 속도 및 메모리
양자화된 모델의 품질을 평가하기 위해 가장 많이 사용되는 측정항목은 다음과 같습니다. 당황 표준 데이터 세트(Wikitext-2), 추론 벤치마크 (HellaSwag, MMLU) 및 애플리케이션 도메인별 작업.
# 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
지표 벤치마크 결과(RTX 4090의 Llama-3.1-8B)
| 방법 | 메모리 | 처리량 | HellaSwag | 당황 |
|---|---|---|---|---|
| BF16(기준선) | 16.0GB | 38t/초 | 82.1% | 6.14 |
| INT8(비트와바이트) | 8.5GB | 35t/초 | 81.8% | 6.21 |
| INT4 NF4 (비앤비) | 4.9GB | 42t/초 | 81.2% | 6.47 |
| GPTQ INT4 | 4.8GB | 55t/초 | 81.5% | 6.39 |
| AWQ INT4 | 4.7GB | 52t/초 | 81.6% | 6.35 |
| Q4_K_M(GGUF, CPU) | 4.9GB | 18t/초 | 81.3% | 6.42 |
참고: 표시 값은 하드웨어, 특정 모델 및 배치 크기에 따라 다릅니다.
PyTorch를 사용한 양자화 인식 훈련
PTQ가 충분하지 않은 시나리오의 경우 - 일반적으로 3B 매개변수의 작은 모델 또는 2-3비트 양자화 - la QAT 상당한 회복을 가능하게 함 정확성. PyTorch에는 버전 2.0부터 QAT용 기본 모듈이 포함되어 있습니다. 정적 및 동적 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: LLM을 위한 더 나은 INT8
스무드퀀트 (Xiao et al. 2022)는 양자화의 특정 문제를 다룹니다. LLM의 INT8: le 이상치 활성화. Transformers의 일부 활성화 채널 다른 값보다 엄청나게 큰 값을 가지므로 균일한 양자화가 어렵습니다. (이상값을 처리하기 위해 범위가 낭비되어 정상 값의 정밀도가 감소합니다.)
스무드퀀트 양자화 난이도를 활성화에서 가중치로 이전합니다.: 활성화를 채널당 스케일링 인자로 나누고 해당 가중치를 곱합니다. 같은 요인에 대해. 결과는 수학적으로 동일하지만 이제 활성화와 가중치는 양자화하기가 더 쉽습니다.
# 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!")
모범 사례 및 안티 패턴
양자화 모범 사례
- 사용 사례에 따라 정밀도를 선택하세요. 미국 GPU 서버 배포용 GPTQ INT4; CPU/에지의 경우 GGUF Q4_K_M을 사용합니다. 미세 조정을 위해서는 비트와 바이트와 함께 NF4를 사용하십시오.
- 대표적인 교정 데이터 세트: GPTQ 및 AWQ의 경우 유사한 데이터를 사용합니다. 제네릭이 아닌 대상 도메인으로. 128-512개 샘플이면 충분하지만 상당한 양이어야 합니다.
- 항상 도메인별 측정항목으로 평가하세요. Wikitext의 당혹감 및 프록시이지만 특정 작업(코딩, 수학, 영어 이외의 언어)에 대한 문제를 포착하지 않습니다.
- 최적의 그룹 크기: group_size=128 및 기본 금고; 64는 품질을 향상시킵니다. 더 많은 메모리를 희생해야 합니다. 256은 메모리를 줄이지만 품질이 저하됩니다.
- 이중 양자화(bnb): 항상 bnb_4bit_use_double_Quant=True를 활성화하세요. 품질에 미치는 영향을 최소화하면서 ~0.4비트/매개변수를 절약합니다.
- BF16을 계산 dtype으로 사용합니다. bnb_4bit_compute_dtype=torch.bfloat16 FP16보다 안정적이며 Ampere+(RTX 3000+, A100)에서 지원됩니다.
피해야 할 안티패턴
- 중요한 레이어를 양자화하지 마세요: 첫 번째 및 마지막 레이어(임베딩, LM 헤드) 그들은 종종 양자화에 더 민감합니다. GPTQ 및 AWQ는 자동으로 제외됩니다.
- 추론에 FP4를 사용하지 마세요. LLM 가중치의 경우 NF4 이상, FP4 이상, 이는 일반적으로 분포됩니다. FP4는 매우 구체적인 시나리오에만 유용합니다.
- 벤치마킹 없이 서로 다른 양자화를 비교하지 마세요. 모델 GPTQ 방식의 "INT4"는 비트와 바이트의 "INT4"와 다릅니다. 실제 정밀도 구현에 따라 다릅니다.
- 역양자화 대기 시간을 무시하지 마세요. INT4 비트와 바이트는 다음과 같아야 합니다. 순방향 패스 동안 양자화를 해제합니다. 메모리 대역폭이 제한된 소형 GPU에서는 INT8보다 느릴 수 있습니다.
- 프로덕션에서는 Q2_K를 사용하지 마십시오. 품질이 너무 떨어지네요. 대부분의 사용 사례. Q3_K_M은 간단한 작업에 허용되는 최소값입니다.
배포 시나리오: 선택 가이드
양자화 방법의 선택은 배포 컨텍스트에 따라 다릅니다. 실용적인 가이드는 다음과 같습니다.
| 대본 | 하드웨어 | 권장 방법 | 체재 |
|---|---|---|---|
| GPU 프로덕션 서버 | A100/H100 80GB | GPTQ INT4 또는 AWQ INT4 | 세이프텐서 GPTQ |
| 소비자 워크스테이션 | RTX 4090 24GB | GPTQ INT4(최대 70B 모델) | 세이프텐서 GPTQ |
| Windows/Linux 노트북 | GPU 8~16GB VRAM | 비트샌드바이트 NF4 또는 AWQ | 포옹얼굴 허브 |
| Apple M 시리즈 노트북 | 통합 메모리 16~96GB | GGUF Q4_K_M 또는 Q5_K_M | GGUF + llama.cpp/Ollama |
| 라즈베리 파이 5 | 8GB RAM | GGUF Q3_K_M 또는 Q4_K_M(모델 1-3B) | GGUF + 라마.cpp |
| 엔비디아 젯슨 오린 | 16GB 통합 메모리 | GPTQ INT4 또는 GGUF Q4_K_M | GPTQ 또는 GGUF |
| 제한된 GPU 미세 조정 | RTX 3090 24GB | QLoRA(NF4 + LoRA) | 비트샌드바이트 NF4 |
결론
패턴 양자화는 틈새 기술에서 도구로 전환되었습니다. LLM에서 일하는 모든 사람에게 필수적입니다. 2026년의 파노라마는 풍부합니다. 비트샌드바이트 신속한 프로토타이핑 및 QLoRA 미세 조정을 위해, GPTQ 최적화된 GPU 배포를 위해, AWQ 이기종 및 다중 플랫폼 하드웨어의 경우 GGUF CPU당 및 엣지 장치.
핵심은 특정 상황에 적합한 방법을 선택하는 것입니다. RTX 4090에 GPTQ가 포함된 INT4 최적화된 커널 덕분에 BF16보다 빠른 경우가 많습니다. M3가 탑재된 MacBook Pro의 GGUF Q4_K_M 전용 GPU 없이 초당 28개 토큰으로 Llama-3.1-8B를 실행할 수 있습니다. 이것은 양보가 아닙니다. 질적: 이전에는 불가능했던 시나리오를 가능하게 합니다.
다음 자연스러운 단계는 양자화를 다음과 결합하는 것입니다. 모델의 증류, 다음 기사에서 다룰 내용: 대규모 모델에서 지식을 전달하는 방법 더 작은 모델로 양자화하여 두 압축 기술의 장점을 모두 활용합니다.
다음 단계
- 다음 기사: 증류 모델: 지식 이전
- 관련 기사: LoRA 및 QLoRA를 통한 미세 조정
- 참조: Ollama: 노트북 및 라즈베리의 로컬 LLM
- MLOps 시리즈: 모델 제공 및 배포







