量化:FP16、INT8、INT4 的取捨
從 FP16 到 INT4,拆解量化的原理、不同精度的取捨、常見量化方法,以及如何在實際部署中做出選擇。
量化:FP16、INT8、INT4 的取捨
上一篇我們談了 KV Cache,理解了它是推理的核心機制,也是記憶體瓶頸的根源。
但 KV Cache 只是問題的一半。另一半是模型權重本身——一個 70B 參數的模型,用 FP16 儲存需要 140 GB 記憶體,這還不包括 KV Cache 和運行時開銷。
當你只有一張 80 GB 的 A100 時,怎麼辦?
這就是**量化(Quantization)**要解決的問題:用更少的位元來儲存模型權重,換取更低的記憶體需求、更快的推理速度、更低的成本。
這一篇,我們要深入量化的原理、不同精度的取捨、常見的量化方法,以及如何在實際部署中做出選擇。
一、先理解問題:模型為什麼這麼大?
參數數量與記憶體
一個模型的記憶體需求,主要取決於參數數量和精度。
模型大小 = 參數數量 × 每個參數的位元數 / 8
以 LLaMA-2 70B 為例:
| 精度 | 每個參數位元數 | 模型大小 |
|---|---|---|
| FP32 | 32 bits | 280 GB |
| FP16 | 16 bits | 140 GB |
| INT8 | 8 bits | 70 GB |
| INT4 | 4 bits | 35 GB |
可以看到,精度減半,記憶體也減半。
為什麼 FP16 是預設?
訓練時通常用 FP32,但推理時可以用 FP16,因為:
- 精度足夠:FP16 的數值範圍和精度對推理來說已經夠用。
- 硬體支援:現代 GPU(如 A100、H100)對 FP16 有專門的優化。
- 記憶體減半:相比 FP32,FP16 直接省一半記憶體。
但即使是 FP16,70B 模型還是需要 140 GB,超過單張 A100 的 80 GB。
這就是為什麼我們需要更激進的量化:INT8 和 INT4。
二、量化的基本原理
什麼是量化?
**量化(Quantization)**是把高精度的數值(如 FP16)映射到低精度的數值(如 INT8、INT4)的過程。
用一個比喻:
- FP16 像用一個精確到小數點後很多位的尺規。
- INT8 像用一個只有 256 個刻度的尺規。
- INT4 像用一個只有 16 個刻度的尺規。
刻度變少了,但如果你只需要大概的長度,這就夠了。
量化的數學
量化的核心是一個線性映射:
量化:q = round(x / scale + zero_point)
反量化:x ≈ (q - zero_point) × scale
其中:
x:原始浮點數q:量化後的整數scale:縮放因子zero_point:零點偏移
以 INT8 為例:
原始值範圍:[-1.0, 1.0]
量化範圍:[-128, 127]
scale = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 ≈ 0.00784
zero_point = 0
x = 0.5 → q = round(0.5 / 0.00784) = 64
x = -0.3 → q = round(-0.3 / 0.00784) = -38
反量化:
q = 64 → x ≈ 64 × 0.00784 = 0.502
q = -38 → x ≈ -38 × 0.00784 = -0.298
可以看到,量化會帶來一些精度損失,但通常在可接受範圍內。
對稱量化 vs 非對稱量化
對稱量化:
q = round(x / scale)
x ≈ q × scale
零點固定在 0。
適合權重分布對稱的情況。
非對稱量化:
q = round(x / scale + zero_point)
x ≈ (q - zero_point) × scale
零點可以偏移。
適合權重分布不對稱的情況(如 ReLU 後的激活值)。
量化粒度
| 粒度 | 說明 | 精度 | 開銷 |
|---|---|---|---|
| Per-tensor | 整個張量共用一組 scale | 低 | 小 |
| Per-channel | 每個通道有自己的 scale | 中 | 中 |
| Per-group | 每組權重有自己的 scale | 高 | 大 |
實務上,Per-group 是最常用的,因為它在精度和開銷之間取得了平衡。
三、不同精度的取捨
FP32(32-bit 浮點數)
- 用途:訓練
- 記憶體:4 bytes per parameter
- 精度:最高
- 推理:很少用,太慢太佔記憶體
FP16(16-bit 浮點數)
- 用途:推理的預設精度
- 記憶體:2 bytes per parameter
- 精度:足夠
- 推理:現代 GPU 的原生支援
優點:精度好、硬體支援好。
缺點:70B 模型仍需 140 GB。
BF16(16-bit 腦浮點數)
- 用途:訓練和推理
- 記憶體:2 bytes per parameter
- 精度:範圍比 FP16 大,但精度略低
- 推理:與 FP16 類似
優點:數值範圍大,不易溢出。
缺點:精度略低於 FP16。
INT8(8-bit 整數)
- 用途:推理
- 記憶體:1 byte per parameter
- 精度:接近 FP16
- 推理:需要量化校準
優點:記憶體減半,速度提升。
缺點:需要校準,精度略有損失。
INT4(4-bit 整數)
- 用途:推理
- 記憶體:0.5 bytes per parameter
- 精度:有明顯損失,但可接受
- 推理:需要專門的量化方法
優點:記憶體大幅減少,可以在單張 GPU 上跑大模型。
缺點:精度損失較明顯,需要仔細選擇量化方法。
精度對比表
| 精度 | 每參數位元 | 70B 模型大小 | 品質 | 速度 | 硬體支援 |
|---|---|---|---|---|---|
| FP32 | 32 | 280 GB | 最高 | 慢 | 通用 |
| FP16 | 16 | 140 GB | 高 | 快 | 原生 |
| BF16 | 16 | 140 GB | 高 | 快 | 原生 |
| INT8 | 8 | 70 GB | 接近 FP16 | 很快 | 需校準 |
| INT4 | 4 | 35 GB | 可接受 | 最快 | 需專門方法 |
四、量化的時機
訓練後量化(PTQ, Post-Training Quantization)
做法:模型訓練完成後,直接對權重進行量化。
優點:
- 不需要重新訓練
- 快速、簡單
- 適合大多數場景
缺點:
- 精度可能下降
- 需要校準數據
流程:
訓練好的 FP16 模型
↓
收集校準數據(幾百到幾千個樣本)
↓
計算量化參數(scale、zero_point)
↓
量化權重
↓
量化後的模型
量化感知訓練(QAT, Quantization-Aware Training)
做法:在訓練過程中模擬量化,讓模型適應量化後的精度損失。
優點:
- 精度損失最小
- 可以達到接近 FP16 的品質
缺點:
- 需要重新訓練
- 成本高、時間長
流程:
訓練時加入「假量化」節點
↓
前向傳播:量化 → 反量化
↓
反向傳播:正常更新權重
↓
訓練完成後,匯出量化模型
什麼時候用哪個?
| 場景 | 推薦 | 原因 |
|---|---|---|
| 快速部署 | PTQ | 不需要重新訓練 |
| 追求最高品質 | QAT | 精度損失最小 |
| 資源有限 | PTQ | 成本低 |
| 有訓練資源 | QAT | 值得投入 |
| 大模型(70B+) | PTQ | QAT 成本太高 |
對大多數團隊來說,PTQ 是首選,因為它快速、便宜、效果已經很好。
五、常見的量化方法
1. GPTQ(GPT Quantization)
核心思想:用二階資訊(Hessian 矩陣)來決定如何量化權重,最小化量化誤差。
特點:
- 支援 INT4、INT3、甚至 INT2。
- 量化速度快。
- 廣泛應用於開源社群。
使用方式:
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM
model_name = "TheBloke/Llama-2-7B-GPTQ"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoGPTQForCausalLM.from_quantized(
model_name,
use_safetensors=True,
device="cuda:0",
)
優點:
- 記憶體節省明顯(4-bit 可省 75%)。
- 推理速度快。
- 生態系成熟。
缺點:
- 量化過程需要一些時間。
- 對某些模型可能精度損失較大。
2. AWQ(Activation-aware Weight Quantization)
核心思想:根據激活值的重要性來決定權重的量化精度。重要的權重用更高的精度。
特點:
- 比 GPTQ 更好的精度。
- 特別適合 INT4。
- 推理速度快。
使用方式:
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_name = "TheBloke/Llama-2-7B-AWQ"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoAWQForCausalLM.from_quantized(
model_name,
fuse_layers=True,
)
優點:
- 精度比 GPTQ 好。
- 推理速度更快。
- 特別適合大模型。
缺點:
- 量化過程比 GPTQ 慢。
- 生態系相對較新。
3. GGUF(GPT-Generated Unified Format)
核心思想:為 CPU 和 Apple Silicon 優化的量化格式,支援多種量化級別。
特點:
- 專為 llama.cpp 設計。
- 支援 CPU、GPU、Apple Silicon。
- 多種量化級別(Q2、Q3、Q4、Q5、Q6、Q8)。
使用方式:
# 用 llama.cpp 轉換模型
./quantize model.gguf model-q4.gguf Q4_K_M
# 用 llama.cpp 推理
./main -m model-q4.gguf -p "今天天氣真好"
優點:
- 跨平台支援好。
- 多種量化級別可選。
- 適合本地部署。
缺點:
- 主要為 CPU 優化,GPU 支援較弱。
- 推理速度不如 GPTQ/AWQ。
4. bitsandbytes
核心思想:HuggingFace 生態系的量化方案,支援 8-bit 和 4-bit。
特點:
- 與 Transformers 無縫整合。
- 支援 8-bit 和 4-bit。
- 不需要額外的量化步驟。
使用方式:
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=quantization_config,
device_map="auto",
)
優點:
- 使用簡單。
- 與 Transformers 整合好。
- 支援 4-bit 和 8-bit。
缺點:
- 推理速度不如 GPTQ/AWQ。
- 主要用於實驗和輕量部署。
方法對比
| 方法 | 精度 | 記憶體節省 | 速度 | 易用性 | 適用場景 |
|---|---|---|---|---|---|
| GPTQ | 好 | 75% | 快 | 中 | GPU 部署 |
| AWQ | 很好 | 75% | 很快 | 中 | GPU 部署 |
| GGUF | 好 | 50-75% | 中 | 高 | CPU/Mac |
| bitsandbytes | 好 | 50-75% | 中 | 很高 | 實驗/輕量 |
六、量化對延遲與成本的影響
對記憶體的影響
以 LLaMA-2 70B 為例:
| 精度 | 模型大小 | KV Cache(4096, batch 32) | 總記憶體 | 需要的 GPU |
|---|---|---|---|---|
| FP16 | 140 GB | 320 GB | 460 GB | 6× A100 80GB |
| INT8 | 70 GB | 160 GB | 230 GB | 3× A100 80GB |
| INT4 | 35 GB | 80 GB | 115 GB | 2× A100 80GB |
INT4 量化讓 70B 模型從需要 6 張 A100 降到 2 張,成本節省 3 倍。
對速度的影響
量化不僅減少記憶體,還能加速推理,因為:
- 記憶體頻寬是瓶頸:更小的模型意味著更少的記憶體讀取。
- 整數運算更快:現代 GPU 對 INT8 有專門的加速。
以 LLaMA-2 7B 為例:
| 精度 | 每秒生成 token 數 | 相對速度 |
|---|---|---|
| FP16 | 30 | 1.0x |
| INT8 | 50 | 1.7x |
| INT4 | 80 | 2.7x |
注意:量化對預填充階段的加速效果較小,主要加速解碼階段。
對成本的影響
以一個每天處理 10,000 個請求的服務為例:
| 精度 | GPU 數量 | 每日成本 | 每月成本 |
|---|---|---|---|
| FP16 | 4× A100 | $288 | $8,640 |
| INT8 | 2× A100 | $144 | $4,320 |
| INT4 | 1× A100 | $72 | $2,160 |
INT4 量化可以讓成本降低 4 倍。
七、實作:量化一個模型
讓我們用一個完整的範例,展示如何量化一個模型。
使用 GPTQ 量化
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from datasets import load_dataset
def quantize_with_gptq(
model_name: str = "meta-llama/Llama-2-7b-hf",
output_dir: str = "llama-2-7b-gptq",
bits: int = 4,
group_size: int = 128,
):
"""用 GPTQ 量化模型"""
# 1. 載入模型和分詞器
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 2. 準備校準數據
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_texts = [
text for text in dataset["text"]
if len(text) > 100
][:128] # 取 128 個樣本
calibration_data = [
tokenizer(text, return_tensors="pt")
for text in calibration_texts
]
# 3. 設定量化參數
quantize_config = BaseQuantizeConfig(
bits=bits, # 4-bit
group_size=group_size, # 每 128 個權重共用一個 scale
desc_act=False, # 是否啟用 activation reordering
)
# 4. 載入模型並量化
model = AutoGPTQForCausalLM.from_pretrained(
model_name,
quantize_config=quantize_config,
)
model.quantize(calibration_data)
# 5. 儲存量化模型
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"量化完成,模型已儲存至 {output_dir}")
def load_quantized_model(model_dir: str):
"""載入量化模型"""
tokenizer = AutoTokenizer.from_pretrained(model_dir)
model = AutoGPTQForCausalLM.from_quantized(
model_dir,
device="cuda:0",
use_safetensors=True,
)
return model, tokenizer
使用 AWQ 量化
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
def quantize_with_awq(
model_name: str = "meta-llama/Llama-2-7b-hf",
output_dir: str = "llama-2-7b-awq",
):
"""用 AWQ 量化模型"""
# 1. 載入模型和分詞器
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoAWQForCausalLM.from_pretrained(model_name)
# 2. 設定量化參數
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM",
}
# 3. 量化
model.quantize(tokenizer, quant_config=quant_config)
# 4. 儲存
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"量化完成,模型已儲存至 {output_dir}")
使用 bitsandbytes 動態量化
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
def load_with_bnb(
model_name: str = "meta-llama/Llama-2-7b-hf",
load_in_4bit: bool = True,
):
"""用 bitsandbytes 動態量化"""
# 設定量化配置
if load_in_4bit:
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
)
else:
quantization_config = BitsAndBytesConfig(
load_in_8bit=True,
)
# 載入模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
device_map="auto",
)
return model, tokenizer
比較不同量化方法
def compare_quantization_methods():
"""比較不同量化方法"""
import time
results = {}
# 1. FP16 基準
model_fp16, tokenizer = load_model("meta-llama/Llama-2-7b-hf")
results["FP16"] = benchmark(model_fp16, tokenizer)
# 2. INT8
model_int8, _ = load_with_bnb("meta-llama/Llama-2-7b-hf", load_in_4bit=False)
results["INT8"] = benchmark(model_int8, tokenizer)
# 3. INT4 (GPTQ)
model_int4, _ = load_quantized_model("llama-2-7b-gptq")
results["INT4_GPTQ"] = benchmark(model_int4, tokenizer)
# 4. INT4 (AWQ)
model_awq, _ = load_quantized_model("llama-2-7b-awq")
results["INT4_AWQ"] = benchmark(model_awq, tokenizer)
return results
def benchmark(model, tokenizer, prompt: str = "今天天氣真好,適合", n_runs: int = 10):
"""基準測試"""
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# 預熱
with torch.no_grad():
model.generate(**inputs, max_new_tokens=10)
# 測量
start = time.time()
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=50)
duration = time.time() - start
# 計算記憶體
memory = torch.cuda.max_memory_allocated() / 1024**3
return {
"duration": duration,
"tokens_per_second": 50 / duration,
"memory_gb": memory,
"output": tokenizer.decode(outputs[0], skip_special_tokens=True),
}
八、量化的最佳實踐
1. 從 INT8 開始
如果你不確定該用哪種量化,先從 INT8 開始。
# INT8 是安全的起點
# 記憶體減半,精度損失小
2. 需要更省記憶體時用 INT4
# INT4 可以省 75% 記憶體
# 但需要選擇好的量化方法(AWQ 或 GPTQ)
3. 選擇正確的量化方法
| 場景 | 推薦方法 |
|---|---|
| GPU 部署,追求速度 | AWQ |
| GPU 部署,追求穩定 | GPTQ |
| CPU/Mac 部署 | GGUF |
| 快速實驗 | bitsandbytes |
| 需要最高精度 | QAT |
4. 校準數據要代表性
# 差:用不相關的數據校準
calibration_data = load_dataset("imdb") # 電影評論
# 好:用與任務相關的數據
calibration_data = load_dataset("your_domain_data")
5. 測試量化後的品質
def test_quantized_quality(model, tokenizer, test_cases):
"""測試量化後的品質"""
results = []
for case in test_cases:
inputs = tokenizer(case["input"], return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
output_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 與 FP16 版本比較
similarity = compute_similarity(output_text, case["expected"])
results.append(similarity)
return {
"avg_similarity": sum(results) / len(results),
"min_similarity": min(results),
}
6. 監控延遲與記憶體
def monitor_quantized_model(model):
"""監控量化模型的表現"""
return {
"memory_allocated_gb": torch.cuda.memory_allocated() / 1024**3,
"memory_reserved_gb": torch.cuda.memory_reserved() / 1024**3,
"model_size_gb": sum(
p.numel() * p.element_size()
for p in model.parameters()
) / 1024**3,
}
7. 考慮混合精度
不是所有層都需要同樣的精度。關鍵層用高精度,其他層用低精度。
8. 使用 vLLM 等優化框架
# vLLM 原生支援 GPTQ 和 AWQ
from vllm import LLM
llm = LLM(
model="TheBloke/Llama-2-7B-AWQ",
quantization="awq",
)
九、常見的陷阱
1. 量化過度
# 不好的例子:直接用 INT2,品質崩壞
# 好的例子:從 INT8 開始,根據需求調整
2. 忽略校準數據
# 不好的例子:用隨機數據校準
# 好的例子:用與任務相關的數據
3. 沒有測試品質
# 不好的例子:量化後直接部署
# 好的例子:量化後跑測試,比較與 FP16 的差異
4. 忽略硬體支援
# 不好的例子:在不支援 INT4 的 GPU 上用 INT4
# 好的例子:確認 GPU 支援的精度
5. 量化所有層
# 不好的例子:所有層都用 INT4
# 好的例子:關鍵層保留 FP16
6. 沒有監控
# 不好的例子:量化後就不管了
# 好的例子:監控延遲、記憶體、品質
7. 忽略 KV Cache
# 不好的例子:只量化權重,KV Cache 還是 FP16
# 好的例子:同時量化 KV Cache
8. 選錯量化方法
# 不好的例子:CPU 部署用 GPTQ
# 好的例子:CPU 部署用 GGUF
十、總結:用更少的位元做更多的事
讓我們回顧這一篇的核心:
- 為什麼需要量化:FP16 的模型太大,70B 需要 140 GB。
- 量化的原理:把高精度數值映射到低精度,用線性變換實現。
- 不同精度的取捨:FP32 → FP16 → INT8 → INT4,記憶體遞減,精度遞減。
- 量化的時機:訓練後量化(PTQ)和量化感知訓練(QAT)。
- 常見的量化方法:GPTQ、AWQ、GGUF、bitsandbytes。
- 量化對延遲與成本的影響:記憶體減少、速度提升、成本降低。
- 實作:用 GPTQ、AWQ、bitsandbytes 量化模型。
- 最佳實踐:從 INT8 開始、需要更省時用 INT4、選對方法、代表性校準、測試品質、監控、混合精度、用優化框架。
- 常見陷阱:量化過度、忽略校準、沒有測試、忽略硬體、量化所有層、沒有監控、忽略 KV Cache、選錯方法。
量化是 LLM 部署中最有效的優化技術之一。
它讓你在同樣的硬體上,跑更大的模型、服務更多的使用者、花更少的錢。
但它不是免費的——精度會損失,需要仔細選擇方法和測試品質。
理解了量化,你就掌握了降低部署成本的關鍵武器。
接下來,我們要談一個更進一步的優化:vLLM 與 PagedAttention。
下一篇預告
《vLLM 與 PagedAttention:高吞吐推理的關鍵》
我們會解釋 vLLM 的架構、PagedAttention 的原理、它如何解決 KV Cache 的碎片化問題、如何實現連續批次,以及如何用 vLLM 部署一個高吞吐的 LLM 服務。