Skip to content
BEE
Backend Engineering Essentials

[BEE-30061] LLM 推論量化

INFO

量化將模型權重精度從 16 位浮點數降低到 4–8 位,將記憶體佔用減少 2–4 倍,在 GPU 上的推論延遲最高降低 4.5 倍——大多數 Llama 規模模型的困惑度(perplexity)退化不超過一個單位。選擇正確的量化方法取決於硬件、激活值是否與權重一起量化,以及部署目標是 GPU 服務器還是消費級 CPU。

背景

BF16 格式的 Llama-3.1-70B 模型佔用 140 GB 的 GPU VRAM,需要兩張 H100 80 GB 才能提供服務。標準自回歸解碼受記憶體頻寬瓶頸限制(見 BEE-30059):GPU 每次前向傳遞的大部分時間花在從 HBM 加載權重,而非計算。降低權重精度直接減少每個 token 加載的字節數,從而減少每個 token 的時間。

2022–2024 年間,隨著模型超過單 GPU 容量,量化研究加速發展。三個不同的方法族應運而生,各自解決不同的約束:

純權重量化(W4A16): 權重以 INT4 存儲;激活值保持 FP16。反量化在融合 CUDA 核心中實時進行。加速完全來自降低的記憶體頻寬。GPTQ(Frantar 等人,arXiv:2210.17323,ICLR 2023)將最優腦量化(OBQ)框架的二階 Hessian 信息應用於壓縮,可在單個 A100 上四個 GPU 小時內將 175B 模型壓縮到 INT4,推論速度提升 3.25 倍。AWQ(Lin 等人,arXiv:2306.00978,MLSys 2024 最佳論文)觀察到只有約 1% 的權重是「顯著的」——由輸入激活量級決定,而非權重量級——並開發了一種激活感知的按通道縮放方法,無需梯度計算即可保護這些權重。AWQ 在指令微調和多模態模型上的泛化性優於 GPTQ。

權重和激活值量化(W8A8): 權重和激活值都量化為 INT8,可使用比 FP16 單元更快的硬件 INT8 GEMM 單元。挑戰在於 LLM 激活值在特定通道中包含大量異常值,超出 INT8 範圍。SmoothQuant(Xiao 等人,arXiv:2211.10438,ICML 2023)通過數學等價的離線變換解決了這一問題:將每個激活通道除以平滑因子 s,將對應的權重通道乘以 s。異常值量級從激活值(難以量化)遷移到權重(易於量化)。SmoothQuant 在 Llama-2-7B 上實現 W8A8,困惑度差值僅 +0.04,同時提供最高 1.56 倍的加速和 2 倍的記憶體降低。

FP8: NVIDIA H100 Tensor Core 原生支持 FP8 矩陣乘法(Micikevicius 等人,arXiv:2209.05433,2022)。E4M3 格式(4 位指數、3 位尾數,最大 ±448)用於前向傳遞中的權重和激活值;E5M2(更寬的動態範圍)用於訓練梯度。FP8 在 H100 上提供比 FP16/BF16 理論上多 2 倍的 FLOPS。與 INT8 不同,FP8 保留了浮點動態範圍,使每張量校準更簡單。

CPU 和邊緣部署(GGUF): llama.cpp(Gerganov 等人,github.com/ggml-org/llama.cpp)引入了 k-quants——一種超級塊結構,其中每組權重都有自己的高精度縮放和最小值,以 6 位精度存儲。Q4_K_M 格式以 GGUF 容器存儲帶有 6 位縮放的 4 位權重,Llama-3.1-8B 的大小減少 3.26 倍(4.58 GiB 對比 FP16 的 14.96 GiB),困惑度接近原始水平。llama.cpp CPU 推論可在消費級硬件上部署,無需 CUDA GPU。

比較

方法位數(W/A)對比 FP16 記憶體典型加速倍數困惑度差值(Llama-2-7B,WikiText-2)最適合
FP16(基準)16/161x1x0(基準:5.474)默認
SmoothQuant W8A88/8減少 2 倍1.56x+0.04GPU 服務器,實際 INT8 GEMM
FP8 E4M38/8減少 2 倍最高 2x可忽略H100 生產推論
GPTQ INT44/16減少 4 倍3.25x(A100)小幅增加GPU,延遲敏感
AWQ INT44/16減少 4 倍3x 以上略優於 GPTQGPU,指令微調模型
GGUF Q4_K_M4.89/FP32 計算減少 3.26 倍CPU 優化小幅增加CPU/邊緣
GGUF Q8_08.5/FP32 計算減少 1.9 倍中等極小CPU,高質量要求

GPTQ 和 AWQ 是純權重量化:激活值保持 FP16,權重實時反量化。加速來自減少的記憶體頻寬。SmoothQuant 和 FP8 是真正的 W8A8:兩個操作數都被量化,可使用更快的硬件單元。GGUF 格式使用 FP32 計算和量化存儲,針對 CPU 吞吐量優化,而非 GPU FLOPS。

最佳實踐

根據部署目標選擇量化方法

應該(SHOULD) 在選擇量化格式前遵循以下決策樹:

  1. H100 GPU 服務器(生產): 使用 FP8。比 FP16 多 2 倍 FLOPS,質量損失可忽略,原生硬件支持。vLLM 的 --quantization fp8 配合 --kv-cache-dtype fp8 在 H100 上使用。
  2. A100/A10G GPU 服務器(舊硬件): 對記憶體瓶頸的單用戶延遲使用 AWQ INT4 或 GPTQ INT4。在 INT8 GEMM 吞吐量比單個請求速度更重要的大批次場景下使用 SmoothQuant W8A8。
  3. 消費級 GPU(RTX 系列)或移動 GPU: 使用 AWQ INT4。AWQ 在 RTX 4090 上比 HF FP16 快 3 倍以上,並支持邊緣部署(NVIDIA Jetson Orin),是首選。
  4. CPU 或混合 CPU+GPU 卸載: 使用 GGUF Q4_K_M(推薦默認)。當質量是優先考慮時使用 Q5_K_M 或 Q6_K。VRAM 允許時使用 Q8_0 進行接近無損的 CPU 推論。

不得(MUST NOT) 在非 H100 硬件上應用 FP8 量化並期望加速——FP8 在 pre-Hopper GPU 上需要軟件模擬,比 INT8 更慢。

使用 vLLM 進行量化模型的 GPU 服務

應該(SHOULD) 為所有帶量化模型的 GPU 服務使用 vLLM。vLLM 原生支持 AWQ、GPTQ、FP8 和 bitsandbytes:

python
from vllm import LLM, SamplingParams

# AWQ INT4——純權重量化,適合 A100/A10G 和消費級 GPU
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct-AWQ",  # 或本地 AWQ 檢查點
    quantization="awq",
    dtype="auto",
)

# 通過 GPTQModel 後端的 GPTQ INT4
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct-GPTQ-4bit",
    quantization="gptq",
    dtype="auto",
)

# H100 上的 FP8——權重和 KV 快取都在 FP8 中
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    quantization="fp8",
    kv_cache_dtype="fp8",          # FP8 KV 快取也將 KV 記憶體減半
    dtype="bfloat16",              # 非量化操作保持 BF16
)

params = SamplingParams(temperature=0.0, max_tokens=256)
outputs = llm.generate(["用一段話解釋量化。"], params)

使用 AutoAWQ 或 GPTQModel 在本地量化模型

應該(SHOULD) 在使用尚未提供預量化版本的模型時,使用 GPTQModel(AutoGPTQ 的繼任者)進行 GPTQ 量化,使用 AutoAWQ 進行 AWQ 量化:

python
# 使用 AutoAWQ 進行 AWQ 量化
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "meta-llama/Llama-3.1-8B-Instruct"
quant_path = "./Llama-3.1-8B-Instruct-AWQ"

tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoAWQForCausalLM.from_pretrained(model_path, device_map="auto")

quant_config = {
    "zero_point": True,       # 非對稱量化——質量更好
    "q_group_size": 128,      # 每個量化組的權重數;越小質量越好,但開銷越大
    "w_bit": 4,               # 權重位數
    "version": "GEMM",        # GEMM 核心(用於批次推論)vs GEMV(單 token 解碼)
}

# 校準使用 Pile 中的 128 個隨機樣本——無需標籤
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
python
# 使用 GPTQModel 進行 GPTQ 量化
from gptqmodel import GPTQModel, QuantizeConfig

model_path = "meta-llama/Llama-3.1-8B-Instruct"
quant_path = "./Llama-3.1-8B-Instruct-GPTQ-4bit"

quantize_config = QuantizeConfig(
    bits=4,
    group_size=128,     # 組大小越小質量越好;70B+ 模型建議 32 或 64
    desc_act=False,     # 激活順序;True 提升質量但減慢量化速度
)

model = GPTQModel.load(model_path, quantize_config=quantize_config)
# 校準數據集:WikiText-2、C4 或您的領域數據(128–512 個樣本)
model.quantize(calibration_dataset)
model.save(quant_path)

組大小指南: group_size=128 是標準默認值。對於記憶體緊張的 70B+ 模型,group_size=64group_size=32 可以在增加約 5–10% 權重存儲的代價下顯著提升質量。對於 13B 以下的模型,group_size=128 通常足夠。

為 CPU 和邊緣推論部署 GGUF 模型

應該(SHOULD) 對 CPU 和混合設備部署使用 llama.cpp 或 Ollama。Q4_K_M 是推薦的默認格式;只有在困惑度測試顯示任務有明顯退化時才提升到 Q5_K_M 或 Q6_K:

bash
# 從 HuggingFace 下載 GGUF 並使用 llama.cpp 運行
./llama-cli \
  -m ./Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  -n 256 \
  -ngl 99 \        # 如果有 GPU,卸載 99 層到 GPU
  --temp 0.0 \
  -p "用一段話解釋量化。"

# 自行將本地 FP16 GGUF 量化為 Q4_K_M
./llama-quantize ./Llama-3.1-8B-Instruct-F16.gguf \
                 ./Llama-3.1-8B-Instruct-Q4_K_M.gguf \
                 Q4_K_M

應該(SHOULD) 在為新任務領域選擇 GGUF 量化級別前評估困惑度:

bash
# llama.cpp 內置困惑度評估(WikiText-2)
./llama-perplexity \
  -m ./Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  -f wikitext-2-raw/wiki.test.raw \
  --ctx-size 512 \
  --chunks 50

# 與 Q5_K_M 和 Q6_K 比較,決定質量提升是否值得增加的大小

部署前在您的任務上驗證量化模型質量

必須(MUST) 在代表性的生產查詢樣本上評估量化模型,而不僅僅是 WikiText-2 困惑度。困惑度衡量的是通用語言建模能力;指令遵循、代碼生成和領域特定任務可能出現不成比例的退化。

python
def evaluate_quantization_quality(
    full_model_outputs: list[str],
    quantized_model_outputs: list[str],
    prompts: list[str],
) -> dict:
    """
    在任務特定提示上比較全精度與量化輸出。
    根據任務類型使用 LLM 作為評判者或精確匹配。
    """
    exact_matches = sum(
        f.strip() == q.strip()
        for f, q in zip(full_model_outputs, quantized_model_outputs)
    )
    return {
        "exact_match_rate": exact_matches / len(prompts),
        "n_prompts": len(prompts),
    }

# 經驗法則:如果精確匹配率相對全精度降低 15% 以上,
# 在部署前考慮更高的位寬或更大的組大小。

圖解

常見錯誤

假設所有 INT4 方法等同。 GPTQ 基於 Hessian,校準每層重建誤差;在有足夠校準數據的情況下,對基礎模型可能更精確。AWQ 基於激活,對指令微調模型的泛化性更好,因為它不會過擬合校準集的 token 分布。兩者在所有設置下都沒有絕對優劣;應在您的具體模型和任務上分別測試。

在 A100 或更舊 GPU 上應用 FP8 量化。 A100 沒有原生 FP8 GEMM 硬件。vLLM 在 pre-Hopper GPU 上會回退到 FP16 計算配合 FP8 存儲,提供記憶體節省但沒有速度提升。改用 INT4(GPTQ 或 AWQ)。

在新工作中使用 AutoGPTQ。 AutoGPTQ 已於 2025 年 4 月歸檔。其繼任者 GPTQModel(github.com/ModelCloud/GPTQModel)是截至 2025 年與 HuggingFace Transformers 兼容的活躍維護工具。類似地,AutoAWQ 已於 2025 年 5 月棄用,維護工作轉移到 vLLM 項目。

完全跳過校準並使用隨機數據。 GPTQ 和 AWQ 都需要少量校準集(128–512 個樣本)來確定量化參數。使用隨機噪聲作為校準數據會產生最小化隨機分布誤差的量化,而非您實際輸入分布的誤差。使用代表生產查詢的樣本,或至少使用 WikiText-2 / C4。

對所有模型大小設置 group_size=128 組大小是質量-存儲的權衡:更大的組(更少的縮放因子)節省記憶體但積累更多量化誤差。對於已經很大的 70B+ 模型,group_size=32group_size=64 可以以略微增加存儲(幾個百分點)的代價顯著改善困惑度。對於 7–13B 模型,group_size=128 通常足夠。

在未測試的情況下將量化與投機解碼結合。 基於 EAGLE 的投機解碼(BEE-30059)使用在基礎模型隱藏狀態上訓練的草稿頭。如果目標模型被量化(特別是 AWQ 或 GPTQ),隱藏狀態分布會發生偏移。啟用量化後始終測試接受率(BEE-30059 的 SpecDecMetrics);如果接受率降至 0.6 以下,可能需要在量化模型上重新訓練草稿頭。

相關 BEE

  • BEE-30021 -- LLM 推論優化與自托管:量化所屬的更廣泛優化領域
  • BEE-30059 -- LLM 推論的投機解碼:與量化隱藏狀態交互;量化後需驗證接受率
  • BEE-30060 -- Multi-LoRA 服務與適配器管理:FP16 訓練的 LoRA 適配器必須反量化以匹配適配器原始精度;vLLM 自動處理此問題

參考資料