Danh sách bài viết

Bài 55: Model Quantization — INT8 / INT4 để giảm RAM và tăng tốc

Quantization chuyển weight model từ FP32/FP16 xuống INT8 hoặc INT4, giảm RAM 2-8x và tăng inference speed trên cả GPU lẫn CPU. Bài này trình bày toán symmetric/asymmetric quantization, ba chiến lược PTQ/QAT/weight-only, GPTQ và AWQ cho LLM, bitsandbytes on-the-fly, GGUF cho CPU inference với llama.cpp, PyTorch quantize_dynamic cho CNN/model nhỏ, bảng so sánh size/speed/accuracy thực tế cho LLM 7B, cách đo accuracy sau quantize, và danh sách pitfalls thường gặp trong production.

27/05/2026
1 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu quantization là gì, tại sao nó giảm RAM và tăng inference speed.
  • Nắm toán cơ bản symmetric/asymmetric quantization — scale, zero_point, round error.
  • Phân biệt PTQ, QAT, và weight-only quantization — ưu/nhược từng loại.
  • Dùng được quantize_dynamic của PyTorch cho CNN/model nhỏ.
  • Chạy được GPTQ và AWQ cho LLM với auto-gptqautoawq.
  • Load model quantized on-the-fly với bitsandbytes (BitsAndBytesConfig).
  • Biết cách convert và quantize GGUF để chạy trên CPU với llama.cpp.
  • Biết cách đo accuracy sau quantize và quyết định khi nào nên/không nên quantize.

Quan hệ với bài QLoRA (Series 4, bài 50): bài QLoRA tập trung vào quantization trong quá trình fine-tuning (training side) — dùng NF4 + LoRA để giảm VRAM khi train. Bài này tập trung vào quantization cho inference/serving — GPTQ, AWQ, GGUF, PyTorch dynamic quant — mục tiêu là giảm RAM và tăng throughput khi serve user thật.

2

Quantization là gì

Model được train với weight kiểu dấu phẩy động:

  • float32 (FP32): 4 byte/weight — precision cao nhất, dùng khi train.
  • float16 (FP16) / bfloat16 (BF16): 2 byte/weight — tiêu chuẩn cho inference trên GPU hiện đại.

Quantization là quá trình chuyển weight (và đôi khi cả activation) sang kiểu số nguyên có độ rộng nhỏ hơn:

  • INT8: 1 byte/weight — giảm 2x so với FP16, 4x so với FP32.
  • INT4: 0.5 byte/weight — giảm 4x so với FP16, 8x so với FP32.

Hai hệ quả trực tiếp:

  • Giảm RAM/VRAM: model nhỏ hơn → fit được GPU nhỏ hơn, hoặc chạy nhiều instance hơn trên cùng GPU.
  • Tăng inference speed: phép nhân integer nhanh hơn float trên nhiều phần cứng (đặc biệt CPU, và NVIDIA Turing+ với Tensor Core INT8).

Trade-off: weight không còn được biểu diễn chính xác → accuracy giảm một phần. Mức giảm phụ thuộc vào phương pháp quantize, thường 0.5–3% trên các benchmark tiêu chuẩn với INT4 tốt.

3

Tại sao quantization quan trọng cho inference

Vấn đề cụ thể khi serve LLM lớn:

Model FP16 VRAM INT4 VRAM Kịch bản
LLM 7B ~14 GB ~4 GB FP16 không fit RTX 3060 (12 GB), INT4 fit thoải mái
LLM 13B ~26 GB ~7 GB FP16 cần A100 40 GB, INT4 fit RTX 3090 (24 GB)
LLM 70B ~140 GB ~35 GB FP16 cần 2×A100 80 GB, INT4 fit 1×A100 40 GB

Ngoài RAM, inference latency cũng giảm rõ với INT4 tốt (GPTQ/AWQ): token/s tăng 2-3x so với FP16 trên cùng GPU, vì memory bandwidth là bottleneck của autoregressive decode — weight nhỏ hơn → load nhanh hơn mỗi token.

Với CNN và model nhỏ hơn (BERT, ResNet): INT8 dynamic quantization giảm latency 1.3–2x trên CPU mà accuracy thường giảm <1%.

4

Toán cơ bản — symmetric và asymmetric quantization

Symmetric quantization

Giả sử weight w nằm trong khoảng [-max_val, max_val]. Với INT8 signed (range [-128, 127]):

import numpy as np

w = np.array([0.12, -0.34, 0.89, -1.23, 0.05])  # FP32 weights

max_val = np.max(np.abs(w))           # = 1.23
scale   = max_val / 127               # = 0.00969

# Quantize: FP32 → INT8
q = np.round(w / scale).astype(np.int8)
# clip [-128, 127] tự động khi cast sang int8

# Dequantize: INT8 → FP32 (approximate)
w_approx = q.astype(np.float32) * scale

# Quantization error
error = np.abs(w - w_approx)
print(f"Max error: {error.max():.5f}")  # ~ 0.00485 (half of scale)

Lưu ý: round + clip gây ra sai số. Sai số tối đa là scale/2 cho mỗi weight. Với INT4 (range [-8, 7]): scale = max_val / 7 — sai số lớn hơn INT8 ~18 lần với cùng khoảng weight.

Asymmetric quantization

Khi weight distribution lệch (không đối xứng quanh 0), dùng thêm zero_point:

w_min, w_max = w.min(), w.max()   # không yêu cầu đối xứng

# Scale và zero_point cho uint8 (0–255)
scale      = (w_max - w_min) / 255
zero_point = np.round(-w_min / scale).astype(np.uint8)

# Quantize
q = np.clip(np.round(w / scale + zero_point), 0, 255).astype(np.uint8)

# Dequantize
w_approx = scale * (q.astype(np.float32) - zero_point)

Asymmetric dùng nhiều hơn cho activation (thường có phân phối lệch). Symmetric đơn giản hơn và đủ tốt cho weight của hầu hết model.

Quantization theo channel (per-channel)

Thay vì dùng 1 scale cho toàn bộ layer, dùng 1 scale riêng cho mỗi output channel (hoặc mỗi group of weights với group_size). Độ chính xác cao hơn per-tensor, nhưng cần lưu thêm một vector scale nhỏ. GPTQ và AWQ đều dùng per-group scale với group_size=128 mặc định.

5

Ba chiến lược chính: PTQ, QAT, weight-only

a) Post-Training Quantization (PTQ)

Quantize model đã train xong, không cần train lại.

  • Dynamic PTQ: weight quantized trước, activation quantized on-the-fly mỗi inference. Dễ cài đặt nhất, không cần calibration data.
  • Static PTQ: cả weight lẫn activation quantized trước; cần một tập calibration nhỏ (vài trăm sample) để đo distribution của activation. Nhanh hơn dynamic khi inference.
  • Accuracy drop: 0.5–3% tùy model và task. Với LLM lớn (>7B) PTQ thường drop <1.5% MMLU nếu dùng GPTQ/AWQ.

b) Quantization-Aware Training (QAT)

Train model với "fake quantization" — simulate INT trong forward pass, backward pass vẫn dùng FP. Model học cách chịu đựng quantization noise.

  • Accuracy gần như FP16 gốc, đôi khi bằng hoàn toàn với INT8.
  • Tốn thêm 30–50% compute và thời gian so với train FP thuần.
  • Cần dataset train đầy đủ — không phù hợp khi chỉ có model pre-trained.
  • Ứng dụng phổ biến: mobile/edge model (MobileNet với TFLite, EfficientNet).

c) Weight-only Quantization

Chỉ quantize weight, activation giữ nguyên FP16/FP32 khi tính toán. Weight được dequantize on-the-fly trong matmul.

  • GPTQ (Frantar et al., arXiv:2210.17323, 2022): dùng Hessian thứ hai để minimize lỗi quantize từng layer.
  • AWQ (Lin et al., arXiv:2306.00978, 2023): phát hiện 1% weight quan trọng nhất (activation-aware), scale chúng lên trước khi quantize để giảm error.
  • Accuracy INT4 với weight-only gần INT8 full quantization, vì activation vẫn FP → không tích lũy lỗi qua nhiều layer.
Chiến lược Cần data lại? Accuracy Speed inference Phù hợp
PTQ dynamic Không -0.5 ~ -2% Trung bình CNN, BERT trên CPU
PTQ static Calibration nhỏ -0.3 ~ -1% Tốt CNN, model nhỏ
QAT Full training ~0% drop Tốt nhất Mobile/edge, accuracy-critical
Weight-only (GPTQ/AWQ) Calibration nhỏ -0.5 ~ -1.5% Rất tốt trên GPU LLM lớn (7B–70B)
6

PyTorch quantize_dynamic — cho CNN và model nhỏ

PyTorch có built-in support cho quantization CPU. Dynamic quantization là cách đơn giản nhất — không cần calibration data, chỉ cần model đã train.

Dynamic quantization (PTQ)

import torch
from torch.quantization import quantize_dynamic

# Model FP32 đã train xong, chuyển sang eval mode
model_fp32 = MyModel()
model_fp32.load_state_dict(torch.load("model.pth", map_location="cpu"))
model_fp32.eval()

# Quantize Linear và LSTM layers sang qint8
# Conv2d cũng có thể thêm, nhưng thường ít lợi hơn trên CPU
model_int8 = quantize_dynamic(
    model_fp32,
    qconfig_spec={torch.nn.Linear, torch.nn.LSTM},
    dtype=torch.qint8,
)

# So sánh size
import os

torch.save(model_fp32.state_dict(), "/tmp/model_fp32.pth")
torch.save(model_int8.state_dict(), "/tmp/model_int8.pth")

size_fp32 = os.path.getsize("/tmp/model_fp32.pth") / 1024 / 1024
size_int8 = os.path.getsize("/tmp/model_int8.pth") / 1024 / 1024
print(f"FP32: {size_fp32:.1f} MB  |  INT8: {size_int8:.1f} MB")
# FP32: 418.2 MB  |  INT8: 107.3 MB  (BERT-base)

Static quantization (PTQ) với calibration

import torch
from torch.quantization import (
    get_default_qconfig,
    prepare,
    convert,
)

model = MyModel().eval()

# Gán qconfig cho các module cần quantize
model.qconfig = get_default_qconfig("x86")  # hoặc "fbgemm", "qnnpack"

# Bước 1: prepare — chèn observer để collect activation statistics
model_prepared = prepare(model)

# Bước 2: calibration — chạy vài trăm sample qua model
with torch.no_grad():
    for batch in calibration_dataloader:
        model_prepared(batch)

# Bước 3: convert — freeze quantization params, chuyển sang INT8
model_int8 = convert(model_prepared)
torch.save(model_int8.state_dict(), "model_static_int8.pth")

Lưu ý: PyTorch quantization native hoạt động tốt nhất trên CPU. Với GPU, dùng TensorRT (bài 56) hoặc GPTQ/AWQ cho LLM.

7

GPTQ — weight-only INT4 cho LLM

GPTQ (Frantar et al., "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers", arXiv:2210.17323, 2022) dùng thông tin Hessian bậc hai để minimize lỗi quantize từng layer. Quantize theo từng cột weight, cập nhật các cột còn lại để bù cho sai số.

Tự quantize model với auto-gptq

pip install auto-gptq transformers accelerate
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer

model_name = "meta-llama/Llama-2-7b-hf"

quantize_config = BaseQuantizeConfig(
    bits=4,           # 4-bit weight
    group_size=128,   # per-group scale, default 128
    desc_act=False,   # True cho accuracy cao hơn nhưng chậm hơn
)

# Load model FP16 để quantize
model = AutoGPTQForCausalLM.from_pretrained(
    model_name,
    quantize_config,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Calibration data: 128 sample ngắn là đủ
calibration_texts = [
    "The quick brown fox jumps over the lazy dog.",
    "Artificial intelligence is transforming software development.",
    # ... thêm đến ~128 sample đa dạng từ domain của bạn
]
examples = [
    tokenizer(text, return_tensors="pt", max_length=512, truncation=True)
    for text in calibration_texts
]

# Quantize — mất khoảng 10-30 phút cho 7B trên A100
model.quantize(examples)

# Lưu model đã quantize
save_dir = "./llama-2-7b-gptq-4bit"
model.save_quantized(save_dir, use_safetensors=True)
tokenizer.save_pretrained(save_dir)

Load và inference model GPTQ

from auto_gptq import AutoGPTQForCausalLM
from transformers import AutoTokenizer, pipeline

# Load model đã quantize (local hoặc từ Hub)
model = AutoGPTQForCausalLM.from_quantized(
    "./llama-2-7b-gptq-4bit",  # hoặc "TheBloke/Llama-2-7B-GPTQ"
    device_map="auto",
    use_triton=False,  # True nếu đã cài triton (nhanh hơn)
)
tokenizer = AutoTokenizer.from_pretrained("./llama-2-7b-gptq-4bit")

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
result = pipe("Explain gradient descent in one paragraph:", max_new_tokens=200)
print(result[0]["generated_text"])

Hub có sẵn hàng ngàn model GPTQ pre-quantized, ví dụ: TheBloke/Llama-2-7B-GPTQ, TheBloke/Mistral-7B-v0.1-GPTQ — tải về và dùng ngay không cần tự quantize.

group_size: giá trị nhỏ hơn (ví dụ 32) → accuracy cao hơn nhưng overhead metadata lớn hơn. Mặc định 128 là trade-off hợp lý. Không dùng group_size=16 — overhead quá lớn mà accuracy cải thiện không đáng.

8

AWQ — Activation-aware Weight Quantization

AWQ (Lin et al., "AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration", arXiv:2306.00978, 2023) quan sát rằng khoảng 1% weight tương ứng với các activation lớn (salient weights) ảnh hưởng đến accuracy nhiều hơn 99% còn lại. AWQ scale những weight đó lên trước khi quantize — thay vì giữ chúng FP32 — để giảm lỗi mà vẫn quantize toàn bộ model INT4.

Tự quantize với autoawq

pip install autoawq transformers
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_name = "meta-llama/Llama-2-7b-hf"

model = AutoAWQForCausalLM.from_pretrained(
    model_name,
    device_map="auto",
    safetensors=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

quant_config = {
    "zero_point": True,    # asymmetric quantization
    "q_group_size": 128,   # per-group scale
    "w_bit": 4,            # 4-bit weight
    "version": "GEMM",     # GEMM kernel (default) hoặc GEMV cho batch_size=1
}

# Calibration: AWQ dùng dataset nhỏ để tìm salient weights
model.quantize(
    tokenizer,
    quant_config=quant_config,
    calib_data="pileval",  # dataset tự động load, hoặc truyền list string
)

model.save_quantized("./llama-2-7b-awq-4bit", safetensors=True)
tokenizer.save_pretrained("./llama-2-7b-awq-4bit")

Load model AWQ đã quantize

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

# Có thể load từ Hub: "TheBloke/Llama-2-7B-AWQ"
model = AutoAWQForCausalLM.from_quantized(
    "./llama-2-7b-awq-4bit",
    device_map="auto",
    fuse_layers=True,    # fuse attention layers để tăng speed ~20%
)
tokenizer = AutoTokenizer.from_pretrained("./llama-2-7b-awq-4bit")

AWQ thường cho accuracy tốt hơn GPTQ trên các benchmark MMLU, HellaSwag, và tốc độ decode nhanh hơn (~10–20%) nhờ kernel được optimize riêng. vLLM và TGI đều hỗ trợ AWQ natively.

9

bitsandbytes — quantize on-the-fly khi load

bitsandbytes (Dettmers et al.) tích hợp trực tiếp vào Hugging Face Transformers: model FP16 được quantize on-the-fly khi from_pretrained, không cần pre-quantize riêng. Đây là lựa chọn nhanh nhất để thử nghiệm khi chưa có model GPTQ/AWQ sẵn.

pip install bitsandbytes>=0.43.0 transformers>=4.40.0 accelerate

8-bit (LLM.int8)

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

bnb_8bit_config = BitsAndBytesConfig(load_in_8bit=True)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_8bit_config,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")

4-bit NF4 (QLoRA-style, dùng cho inference)

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

bnb_4bit_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",             # NF4 tốt hơn INT4 đều cho weight Gaussian
    bnb_4bit_compute_dtype=torch.float16,   # compute dtype khi dequantize trong matmul
    bnb_4bit_use_double_quant=True,         # quantize cả quantization constants (tiết kiệm ~0.4 bit/param)
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_4bit_config,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")

# Inference như bình thường
inputs = tokenizer("Hello, what is machine learning?", return_tensors="pt").to("cuda")
with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

bitsandbytes không pre-quantize model ra file riêng — mỗi lần load lại phải quantize lại (mất vài giây). Nếu serve production, nên dùng GPTQ hoặc AWQ đã quantize sẵn để load nhanh hơn. bitsandbytes phù hợp cho: prototype nhanh, QLoRA fine-tuning (đã nói ở bài 50), và inference không yêu cầu tốc độ tối đa.

Hardware requirement: bitsandbytes CUDA kernel yêu cầu NVIDIA GPU với compute capability ≥ 7.5 (Turing, RTX 20xx/30xx/40xx, T4, A100, H100). Không chạy trên CPU hoặc AMD GPU.

10

GGUF — CPU inference với llama.cpp

GGUF (GGML Unified Format) là định dạng model file dùng cho llama.cpp — framework C++ inference chạy tốt trên CPU (và GPU tùy chọn). Ollama dùng llama.cpp làm backend. Phù hợp khi không có GPU NVIDIA, hoặc cần chạy trên laptop, edge server.

Các mức quantize GGUF phổ biến

Format Bits/weight (trung bình) 7B Size Quality Khuyến nghị
Q8_0 8-bit ~7.7 GB Rất gần FP16 Khi RAM đủ, muốn accuracy tốt nhất
Q5_K_M 5-bit mixed ~5.1 GB Rất tốt Balance tốt cho 8 GB RAM
Q4_K_M 4-bit mixed ~4.4 GB Tốt Default cho hầu hết use case CPU
Q4_0 4-bit uniform ~3.8 GB Chấp nhận được RAM rất hạn chế
Q2_K 2-bit mixed ~2.9 GB Giảm rõ Chỉ khi không có lựa chọn khác

Suffix _K_M = "K-quant medium" — dùng quantization khác nhau cho từng layer quan trọng (attention vs FFN) để balance accuracy/size tốt hơn so với quantize đều.

Convert model HuggingFace sang GGUF và quantize

# Build llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make -j$(nproc)

# Cài Python deps
pip install -r requirements.txt

# Bước 1: Convert HF model sang GGUF FP16
python convert.py /path/to/llama-2-7b-hf --outfile /tmp/llama-7b-f16.gguf --outtype f16

# Bước 2: Quantize GGUF sang Q4_K_M
./quantize /tmp/llama-7b-f16.gguf /tmp/llama-7b-q4_k_m.gguf q4_K_M

# Chạy inference
./main -m /tmp/llama-7b-q4_k_m.gguf -p "What is machine learning?" -n 200

Dùng GGUF với Python qua llama-cpp-python

pip install llama-cpp-python
from llama_cpp import Llama

# n_gpu_layers=0 → CPU only, -1 → tất cả layer lên GPU nếu có
llm = Llama(
    model_path="/tmp/llama-7b-q4_k_m.gguf",
    n_ctx=4096,         # context window
    n_gpu_layers=0,     # CPU inference
    verbose=False,
)

response = llm("What is machine learning?", max_tokens=200, echo=False)
print(response["choices"][0]["text"])

GGUF chạy trên GPU qua llama.cpp CUDA backend, nhưng thường chậm hơn GPTQ/AWQ vì kernel GPU của llama.cpp chưa được optimize bằng. Nếu có GPU NVIDIA, ưu tiên GPTQ hoặc AWQ. Dùng GGUF khi cần chạy CPU-only hoặc dùng Ollama.

11

Bảng so sánh: size, speed, accuracy (LLM 7B)

Số liệu tham khảo cho LLaMA/Mistral 7B. Speed đo trên Apple M2 (unified memory) và RTX 3090 (nếu có ghi chú). MMLU drop = giảm so với FP16 baseline. Số liệu có thể dao động ±5% tùy prompt và implementation version.

Format Size Token/s (M2 CPU) Token/s (RTX 3090) MMLU drop
FP16 14 GB N/A (không fit RAM thông thường) ~95 Baseline
INT8 (bitsandbytes) 7 GB N/A ~75 ~-0.3%
INT4 NF4 (bitsandbytes) 4 GB N/A ~90 ~-1.5%
INT4 GPTQ (group=128) 4 GB N/A ~150 ~-1%
INT4 AWQ (group=128) 4 GB N/A ~165 ~-0.5%
Q4_K_M GGUF 4.4 GB ~15–20 ~80 (với CUDA offload) ~-1%
Q8_0 GGUF 7.7 GB ~10–12 N/A (hết VRAM 24 GB) ~-0.1%

Nhận xét từ bảng:

  • AWQ có accuracy tốt nhất trong nhóm INT4 GPU, đồng thời nhanh nhất.
  • GPTQ nhanh hơn bitsandbytes NF4 rõ rệt (~65%) vì kernel GPU được optimize tốt hơn.
  • bitsandbytes INT8 gần FP16 về accuracy nhưng chậm hơn — khi VRAM đủ, FP16 vẫn là lựa chọn tốt hơn về tốc độ.
  • GGUF Q4_K_M tốt cho CPU (15–20 t/s trên M2 là đủ cho chatbot), nhưng chậm hơn GPU 5–10x.
12

Đo accuracy sau quantize

Không nên deploy quantized model mà không kiểm tra accuracy. Hai phương pháp thực tế:

1. Perplexity (PPL) trên test set

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import math

def compute_perplexity(model, tokenizer, texts, device="cuda", max_length=512):
    model.eval()
    total_loss = 0.0
    total_tokens = 0

    with torch.no_grad():
        for text in texts:
            inputs = tokenizer(
                text,
                return_tensors="pt",
                max_length=max_length,
                truncation=True,
            ).to(device)

            labels = inputs["input_ids"].clone()
            outputs = model(**inputs, labels=labels)
            loss = outputs.loss  # cross-entropy loss

            n_tokens = inputs["input_ids"].numel()
            total_loss += loss.item() * n_tokens
            total_tokens += n_tokens

    ppl = math.exp(total_loss / total_tokens)
    return ppl

# Load FP16 baseline và quantized model, so sánh PPL
# Chấp nhận nếu ppl_quantized <= ppl_baseline * 1.05 (trong ngưỡng 5%)

2. Domain-specific accuracy

from transformers import pipeline

pipe_fp16 = pipeline("text-generation", model="meta-llama/Llama-2-7b-hf", device=0)
pipe_int4 = pipeline("text-generation", model="TheBloke/Llama-2-7B-GPTQ", device=0)

# Tập test từ domain của bạn (Q&A, classification, summarization...)
test_cases = [
    {"prompt": "What is gradient descent?", "expected_keywords": ["optimization", "loss", "gradient"]},
    # ... thêm 100-1000 case
]

def check_accuracy(pipe, test_cases):
    correct = 0
    for case in test_cases:
        output = pipe(case["prompt"], max_new_tokens=100)[0]["generated_text"]
        # Kiểm tra đơn giản qua keyword
        if any(kw.lower() in output.lower() for kw in case["expected_keywords"]):
            correct += 1
    return correct / len(test_cases)

acc_fp16 = check_accuracy(pipe_fp16, test_cases)
acc_int4 = check_accuracy(pipe_int4, test_cases)
print(f"FP16: {acc_fp16:.3f}  |  INT4: {acc_int4:.3f}  |  Drop: {acc_fp16 - acc_int4:.3f}")

3. Benchmark chuẩn (lm-evaluation-harness)

pip install lm-eval

# Đánh giá MMLU, ARC, HellaSwag
lm_eval --model hf \
    --model_args pretrained=TheBloke/Llama-2-7B-GPTQ,autogptq=True \
    --tasks mmlu,arc_easy,hellaswag \
    --batch_size 8 \
    --output_path ./results_gptq

Quy tắc thực tế: nếu accuracy drop > 2% trên domain test, cân nhắc dùng INT8 thay INT4, hoặc tăng group_size (ví dụ từ 128 lên 64), hoặc giữ FP16.

13

Hỗ trợ framework

Framework Formats hỗ trợ Phần cứng Ghi chú
PyTorch native INT8 dynamic, INT8 static CPU Built-in, không cần cài thêm
TensorFlow Lite INT8, FP16 Mobile, edge, CPU ARM QAT + PTQ, deploy Android/iOS
ONNX Runtime INT8 static/dynamic CPU, GPU Export từ PyTorch/TF, cross-platform
TensorRT FP8, INT8 với calibration NVIDIA GPU Latency thấp nhất cho NVIDIA, xem bài 56
vLLM AWQ, GPTQ, FP8 (H100) NVIDIA GPU High-throughput serving cho LLM
TGI (Hugging Face) AWQ, GPTQ, EETQ, bitsandbytes NVIDIA GPU Production LLM serving
llama.cpp GGUF (Q2_K đến Q8_0) CPU, NVIDIA, Apple Metal Chạy được trên consumer hardware mọi OS
14

Pattern serve quantized model trong production

Kết hợp với bài 5 (lifespan event) và bài 51 (batching), pattern serve quantized model:

from contextlib import asynccontextmanager
from fastapi import FastAPI
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

# Global model state
model_state = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load 1 lần khi startup
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.float16,
    )

    model_state["model"] = AutoModelForCausalLM.from_pretrained(
        "TheBloke/Mistral-7B-Instruct-v0.2-GPTQ",  # GPTQ nhanh hơn bitsandbytes
        device_map="auto",
        # Với GPTQ, không cần BitsAndBytesConfig — model đã quantize sẵn
    )
    model_state["tokenizer"] = AutoTokenizer.from_pretrained(
        "TheBloke/Mistral-7B-Instruct-v0.2-GPTQ"
    )

    # Log VRAM usage sau khi load
    if torch.cuda.is_available():
        vram_mb = torch.cuda.memory_allocated() / 1024 / 1024
        print(f"VRAM sau load: {vram_mb:.0f} MB")

    yield

    # Cleanup khi shutdown
    model_state.clear()
    torch.cuda.empty_cache()

app = FastAPI(lifespan=lifespan)

@app.post("/generate")
async def generate(prompt: str, max_new_tokens: int = 200):
    model = model_state["model"]
    tokenizer = model_state["tokenizer"]

    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(**inputs, max_new_tokens=max_new_tokens)

    generated = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
    return {"text": generated}

Nếu dùng vLLM, code đơn giản hơn và throughput cao hơn nhờ continuous batching (đã có ở bài 51):

# Khởi động vLLM server với AWQ model
python -m vllm.entrypoints.openai.api_server \
    --model TheBloke/Mistral-7B-Instruct-v0.2-AWQ \
    --quantization awq \
    --max-model-len 4096 \
    --port 8000
15

Khi nào quantize, khi nào không

Nên quantize khi:

  • GPU consumer (RTX 3060 12 GB, RTX 3090 24 GB) — model FP16 không fit VRAM.
  • Cần serve nhiều user đồng thời trên cùng 1 GPU — quantize giải phóng VRAM cho nhiều request hơn.
  • CPU-only inference hoặc edge device — GGUF là lựa chọn duy nhất thực tế.
  • Cost-sensitive: 1 GPU thay 2 GPU cho cùng model.
  • Model lớn (>7B param) — lợi ích RAM rõ rệt, accuracy drop thường nhỏ.

Không nên quantize khi:

  • Có đủ A100/H100 — không cần tiết kiệm RAM, FP16 cho accuracy và speed tốt nhất.
  • Accuracy-critical domain (y tế, pháp lý, tài chính) — cần đo kỹ và chỉ quantize nếu drop < ngưỡng domain yêu cầu.
  • Model nhỏ (<500M param, ví dụ BERT-base 110M): RAM không phải bottleneck, latency benefit nhỏ, risk accuracy drop không đáng.
  • Trong quá trình training — cần FP16+ để gradient stable. Quantization chỉ áp dụng cho inference (ngoại trừ QAT và QLoRA có xử lý riêng).
  • Model đã quantize rồi fine-tune thêm — accuracy giảm nhiều. Thứ tự đúng: train FP16 → fine-tune FP16 → quantize post-training.
16

Common pitfalls

  • Quantize model nhỏ (<500M) mà accuracy giảm rõ: model nhỏ ít tham số, mỗi weight mang nhiều thông tin hơn → quantize error ảnh hưởng lớn hơn. Dùng QAT thay PTQ, hoặc giữ FP16.
  • group_size INT4 quá nhỏ (16 hoặc 32): metadata scale tăng, net size tiết kiệm kém. Mặc định 128 là hợp lý. group_size=64 chỉ nên dùng nếu model <3B param.
  • Activation outlier với LLM lớn: một số model (đặc biệt OPT, BLOOM) có activation outlier lớn ở vài dimension, gây lỗi quantize nặng. Dùng SmoothQuant hoặc AWQ (detect và xử lý outlier tự động).
  • PTQ static không có calibration data: nếu bỏ qua calibration, activation range ước tính sai → accuracy drop mạnh. Cần ít nhất 100–500 sample representative từ distribution đầu vào thật.
  • Quantize sau fine-tune, không phải trước: nếu quantize model rồi mới fine-tune (ví dụ GPTQ → SFT), kết quả thường kém hơn fine-tune FP16 → quantize. Thứ tự đúng: base FP16 → fine-tune FP16 → quantize → verify accuracy.
  • GGUF trên GPU chậm hơn GPTQ/AWQ: llama.cpp CUDA backend tuy chạy được trên GPU nhưng kernel không optimize bằng. Nếu có GPU, dùng GPTQ hoặc AWQ; GGUF chỉ dùng cho CPU hoặc Ollama.
  • bitsandbytes chỉ chạy trên NVIDIA: không chạy trên CPU, AMD GPU, hay Apple Silicon. Nếu deploy trên non-NVIDIA, cần GGUF (llama.cpp), ONNX Runtime, hoặc llama.cpp với Metal backend cho Apple.
  • Không monitor VRAM sau khi load: quantized model nhỏ hơn, nhưng KV cache và activation vẫn FP16 — tổng VRAM khi serving có thể vẫn cao nếu context window dài. Luôn đo tổng VRAM với context dài nhất dự kiến.