Quantization
Lượng tử hóa mô hình
Mô hình Llama 3 70B chiếm 140GB ở FP16 — cần 2 GPU A100 80GB. Có cách nào chạy trên 1 GPU 24GB?
Ẩn dụ: Thước đo nhiệt độ
Nhiệt kế điện tử hiển thị 25.347°C. Nhiệt kế cơ học chỉ25°C. Bạn cần độ chính xác nào để quyết định bật quạt? Cả hai đều được — não bạn chỉ quan tâm "nóng hay lạnh". Con số thập phân sau đâu tạo ra quyết định khác.
FP32
25.34691°C
Nhiệt kế chuyên dụng phòng thí nghiệm
FP16
25.35°C
Nhiệt kế thường dùng trong nhà
INT8
25.3°C
Nhiệt kế y tế
INT4
25°C
Nhiệt kế cơ đơn giản
Trọng số mạng nơ-ron giống nhiệt độ: giá trị chính xác đến 5 chữ số không làm model trả lời tốt hơn so với 1 chữ số. Lượng tử hoá khai thác chính điều này: giảm độ chính xác xuống mức mà kết quả cuối cùng gần như không đổi, trong khi tiết kiệm được 4x-8x bộ nhớ.
Hình minh họa
Lượng tử hoá — trực quan trên phân bố trọng số
Phân bố trọng số (bell curve) + các mức quantization. Chọn mức bit để xem số bin và độ tròn.
Phân bố trọng số + liên tục
FP32/FP16: hầu như liên tục — mọi giá trị phân biệt được. INT8: 256 bin — đủ mịn, mất <0.5% chất lượng. INT4: chỉ 16 bin, mỗi giá trị được làm tròn tới 1 trong 16 mức — vẫn đủ tốt vì trọng số tập trung quanh 0. INT2: chỉ 4 bin — mất nhiều thông tin, cần kỹ thuật QAT đặc biệt để dùng được.
Accuracy drop theo số bit (Llama-7B, c4 perplexity)
Chất lượng gần như không đổi từ FP32 xuống INT8. Mất nhẹ ở INT4 (~2.5%). Vách đá rõ ở INT2 — cần QAT hoặc method đặc biệt (AQLM, BitNet) để dùng được.
Llama 70B ở các mức precision
Cùng mô hình 70B: từ 280GB (FP32, cần cluster) xuống 17.5GB (INT2, chạy laptop). INT4 là sweet spot — 35GB vừa RTX 6000 Ada / 2×RTX 4090 với chất lượng ~97.5%.
PTQ vs QAT — khi nào dùng cái nào?
Post-Training Quantization (PTQ)
Lượng tử hoá sau khi đã train xong. Nhanh, đơn giản, không cần re-train.
Dữ liệu cần
128-1024 mẫu calibration
Chi phí train
Vài phút - vài giờ trên 1 GPU
Chất lượng @4-bit
97%
Inference speed
Không ảnh hưởng inference
+ Ưu điểm
- Không cần dữ liệu huấn luyện gốc
- Nhanh — vài phút cho 7B, vài giờ cho 70B
- Không rủi ro overfitting
− Nhược điểm
- Chất lượng kém hơn QAT ở ≤4 bit
- Nhạy với outlier nếu không xử lý (SmoothQuant giải quyết)
- Không tận dụng được loss signal
Thuật toán quantization phổ biến
GPTQ
4-bit · PTQ
Lượng tử hoá layer-by-layer dùng thông tin Hessian. Chuẩn de-facto cho 4-bit GPU.
HW: GPU (CUDA)
PPL: 5.78 · 3-4x
AWQ
4-bit · PTQ
Bảo vệ kênh outlier salient (1% trọng số quan trọng nhất). Chất lượng nhỉnh hơn GPTQ.
HW: GPU (CUDA)
PPL: 5.75 · 3-4x
NF4 (bnb)
4-bit · PTQ
NormalFloat 4-bit — dùng trong QLoRA. Chậm hơn khi inference, nhanh để fine-tune.
HW: GPU (CUDA)
PPL: 5.73 · 1.5-2x
GGUF (llama.cpp)
4-bit · PTQ
Format K-quants cho llama.cpp, chạy local trên Mac/laptop. Q4_K_M, Q5_K_M phổ biến.
HW: CPU + Metal/CUDA
PPL: 5.8 · 1-3x
SmoothQuant
8-bit · PTQ
Tiền xử lý: smooth outlier input trước khi quantize INT8. Tốc độ cao nhất.
HW: GPU (CUDA)
PPL: 5.71 · 2-3x
Thực tế: AWQ nhỉnh GPTQ chất lượng; GPTQ ecosystem lớn hơn (AutoGPTQ); NF4 cho fine-tune (QLoRA); GGUF cho chạy local trên Mac/laptop; SmoothQuant cho production INT8 tốc độ tối đa.
Bạn có mô hình 13B ở FP16 (26GB). GPU của bạn 16GB VRAM. Lượng tử hoá nào nhỏ nhất mà vẫn dùng được?
Team bạn muốn deploy chatbot INT4 trên GPU server. Giữa GPTQ và AWQ, chọn cái nào và vì sao?
Giải thích
Định nghĩa. Lượng tử hoá (Quantization) là ánh xạ giá trị liên tục (float) sang tập rời rạc ít bit hơn. Trong deep learning, lượng tử hoá áp dụng lên trọng số (weight) và/hoặc kích hoạt (activation) để giảm bộ nhớ và tăng tốc inference. Bên cạnh pruning và distillation, đây là ba trụ cột nén model.
Công thức cơ bản (symmetric/asymmetric):
Trong đó là scale factor (độ phân giải mỗi bin), là zero-point (giá trị 0 ánh xạ tới bin nào). Với symmetric quantization, .
Tính scale cho INT-bit symmetric:
Ví dụ INT8 symmetric: range , 256 bin. INT4: range , chỉ 16 bin.
Granularity — mức độ chia nhỏ scale:
- Per-tensor: 1 scale cho toàn tensor. Đơn giản nhưng kém vì outlier làm bẹt đa số giá trị.
- Per-channel (per-axis): 1 scale cho mỗi output channel của layer. Mặc định cho INT8 LLM.
- Per-group / Block-wise: 1 scale cho mỗi block (ví dụ 32-128 giá trị). Cần cho INT4 — GPTQ, AWQ, NF4 đều dùng.
Hai cách tiếp cận chính — PTQ vs QAT:
- PTQ (Post-Training Quantization): áp dụng sau khi model đã train xong. Chỉ cần vài trăm mẫu calibration để đo range scale. Nhanh nhưng có thể giảm chất lượng ở ≤4-bit.
- QAT (Quantization-Aware Training): mô phỏng lượng tử hoá trong forward pass (fake quant node), loss chảy ngược qua straight-through estimator. Model học cách "bù" lỗi. Tốt hơn PTQ, đặc biệt ở ≤2-bit.
GPTQ — lượng tử hoá layer-by-layer dùng Hessian:
GPTQ xử lý từng cột của ma trận trọng số; sau mỗi cột, cập nhật các cột còn lại để bù lỗi quantization bằng inverse Hessian. Nhờ đó 4-bit chỉ mất ~1.5% PPL trên Llama-7B.
AWQ — Activation-aware Weight Quantization:
AWQ quan sát: chỉ ~1% kênh trọng số (identify qua magnitude activation, không phải magnitude weight!) là quan trọng cho chất lượng. Scale các kênh này lên trước khi quantize để giảm quant error ở đúng chỗ cần thiết. Kết quả: chất lượng nhỉnh GPTQ 0.2-0.5 PPL, kernel tối ưu cho inference.
Ví dụ code 1 — bitsandbytes quantize khi load:
# pip install "transformers>=4.41" "bitsandbytes>=0.43" accelerate
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
MODEL_ID = "meta-llama/Llama-3-70B"
# Config: INT4 quantization khi load
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # "nf4" hoặc "fp4"
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # tiết kiệm thêm ~0.4 bit/param
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
)
# 70B × 16-bit = 140GB → quantized ≈ 36GB VRAM
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
inputs = tokenizer("Quantization in simple terms:", return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=100, do_sample=False)
print(tokenizer.decode(outputs[0]))
# Tốc độ inference ≈ 1.5-2x nhanh hơn FP16 vì giảm memory bandwidth
# Chất lượng: PPL chỉ tăng ~1-2% so với FP16
# Muốn INT8 thay vì INT4? Dùng llm_int8:
bnb_int8 = BitsAndBytesConfig(
load_in_8bit=True,
llm_int8_threshold=6.0, # outlier threshold — giữ FP16 cho kênh >6σ
)
model8 = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_int8,
device_map="auto",
)
# 70B INT8 ≈ 70GB — vừa 1×A100 80GBVí dụ code 2 — GPTQ với AutoGPTQ (production):
# pip install auto-gptq>=0.7 transformers optimum
import torch
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
MODEL_ID = "meta-llama/Llama-3-8B"
OUT_DIR = "./llama3-8b-gptq-4bit"
# Bước 1: Cấu hình GPTQ
quantize_config = BaseQuantizeConfig(
bits=4, # 2, 3, 4, 8 đều hỗ trợ
group_size=128, # block-wise; 128 là chuẩn
desc_act=True, # activation order — tăng chất lượng
damp_percent=0.1, # Hessian dampening
)
# Bước 2: Load model ở FP16 để quantize
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoGPTQForCausalLM.from_pretrained(
MODEL_ID,
quantize_config=quantize_config,
torch_dtype=torch.float16,
)
# Bước 3: Chuẩn bị calibration dataset (128-1024 mẫu đủ)
from datasets import load_dataset
data = load_dataset("c4", "en", split="train", streaming=True)
examples = []
for i, sample in enumerate(data):
if i >= 512:
break
tokens = tokenizer(sample["text"], return_tensors="pt",
truncation=True, max_length=2048)
examples.append({
"input_ids": tokens.input_ids[0],
"attention_mask": tokens.attention_mask[0],
})
# Bước 4: Quantize — chạy layer-by-layer, dùng Hessian
model.quantize(examples) # ~15 phút cho 8B trên A100
# Bước 5: Lưu
model.save_quantized(OUT_DIR)
tokenizer.save_pretrained(OUT_DIR)
# Bước 6: Load lại và dùng (kernel tối ưu)
from auto_gptq import AutoGPTQForCausalLM
q_model = AutoGPTQForCausalLM.from_quantized(
OUT_DIR,
device="cuda:0",
use_safetensors=True,
use_triton=False, # True cho kernel Triton nhanh hơn
)
# Inference: 2-4x nhanh hơn FP16, chất lượng ~98.5% FP16
prompt = "Giải thích quantization:"
tokens = tokenizer(prompt, return_tensors="pt").to("cuda")
output = q_model.generate(**tokens, max_new_tokens=200)
print(tokenizer.decode(output[0]))Ứng dụng thực tế:
- LLM trên consumer GPU: Llama 70B INT4 chạy được RTX 4090, MacBook M2 Max 64GB (qua GGUF).
- Mobile/edge inference: Phi-3, Gemma 2B INT4 chạy được smartphone (Snapdragon 8 Gen 3).
- Production serving throughput: vLLM + AWQ INT4 đạt 3-4x throughput so với FP16 cùng hardware.
- Multi-tenant model serving: INT4 cho phép host nhiều model trên cùng GPU (ví dụ 3 model 13B trên 1×A100 80GB).
- Fine-tuning memory-constrained: QLoRA (NF4) cho phép fine-tune 70B trên 1 GPU — xem QLoRA.
- Embedded device: BERT INT8 chạy được trên Raspberry Pi, Coral TPU, hỗ trợ inference offline.
Pitfall thường gặp:
- Không đo chất lượng sau quantize: luôn benchmark PPL và downstream task (MMLU, HumanEval) trước rollout. Quantize INT4 sai cách có thể làm model "đần" 5-10% mà không ai phát hiện ngay.
- Per-tensor cho INT4: không bao giờ dùng được cho LLM. Luôn group-wise (group 32, 64, 128).
- Calibration sai domain: calibrate bằng C4 English rồi deploy tiếng Việt → PPL tăng bất thường. Match calibration với production.
- Quantize LN và embedding: hai layer này nhạy. Thường giữ FP16/BF16 kể cả khi các layer khác INT4.
- So sánh không công bằng: benchmark tốc độ không phải ở batch size production thực tế. INT4 nhanh ở batch=1 nhưng có thể chậm hơn FP16 ở batch=128 do kernel launch overhead.
- Không kiểm tra numeric stability: INT2/INT3 có thể gây NaN nếu input cực đoan. Test với edge case (input dài, nhiều unicode, multi-turn).
- Bỏ qua memory bandwidth: quantization giúp giảm memory bandwidth (chính là bottleneck LLM inference). Nếu kernel vẫn dequant sang FP16 rồi matmul, bandwidth gain có thể nhỏ hơn kỳ vọng — đo trước khi kết luận.
- Lượng tử hoá giảm số bit/tham số: FP32 (100%) → FP16 (50%) → INT8 (25%) → INT4 (12.5%) → INT2 (6.25%). Chất lượng giảm theo hàm lồi — INT4 là sweet spot.
- PTQ nhanh (cần calibration data), QAT chậm hơn (train lại) nhưng chất lượng tốt hơn ở ≤4-bit. Hầu hết LLM open-source dùng PTQ.
- INT4 giảm 8x kích thước so với FP32 mà chỉ mất ~2.5% chất lượng — chìa khoá để chạy LLM trên consumer GPU. Vách đá ở INT2 cần kỹ thuật đặc biệt (AQLM, BitNet).
- Thuật toán phổ biến: GPTQ (Hessian layer-by-layer), AWQ (bảo vệ 1% kênh salient), NF4 (QLoRA fine-tune), GGUF (llama.cpp CPU/Mac), SmoothQuant (INT8 outlier handling).
- Granularity quan trọng: per-tensor kém cho LLM do outlier; per-channel tốt cho INT8; group-wise (block 32-128) bắt buộc cho INT4.
- Chọn format theo hardware: GPTQ/AWQ cho GPU NVIDIA, GGUF cho CPU/Mac, FP8 cho H100. Luôn chọn mức bit cao nhất mà VRAM cho phép, và luôn benchmark chất lượng sau quantize.
Kiểm tra hiểu biết
Tại sao lượng tử hoá INT4 có thể giảm 8x kích thước mà chỉ mất ~3% chất lượng?