量化:FP16、INT8、INT4 的取捨

從 FP16 到 INT4,拆解量化的原理、不同精度的取捨、常見量化方法,以及如何在實際部署中做出選擇。

量化:FP16、INT8、INT4 的取捨

上一篇我們談了 KV Cache,理解了它是推理的核心機制,也是記憶體瓶頸的根源。

但 KV Cache 只是問題的一半。另一半是模型權重本身——一個 70B 參數的模型,用 FP16 儲存需要 140 GB 記憶體,這還不包括 KV Cache 和運行時開銷。

當你只有一張 80 GB 的 A100 時,怎麼辦?

這就是**量化(Quantization)**要解決的問題:用更少的位元來儲存模型權重,換取更低的記憶體需求、更快的推理速度、更低的成本。

這一篇,我們要深入量化的原理、不同精度的取捨、常見的量化方法,以及如何在實際部署中做出選擇。

FP32 → FP16 → INT8 → INT4 = Less Memory, More Speed

一、先理解問題:模型為什麼這麼大?

參數數量與記憶體

一個模型的記憶體需求,主要取決於參數數量和精度。

模型大小 = 參數數量 × 每個參數的位元數 / 8

以 LLaMA-2 70B 為例:

精度每個參數位元數模型大小
FP3232 bits280 GB
FP1616 bits140 GB
INT88 bits70 GB
INT44 bits35 GB

可以看到,精度減半,記憶體也減半

為什麼 FP16 是預設?

訓練時通常用 FP32,但推理時可以用 FP16,因為:

  1. 精度足夠:FP16 的數值範圍和精度對推理來說已經夠用。
  2. 硬體支援:現代 GPU(如 A100、H100)對 FP16 有專門的優化。
  3. 記憶體減半:相比 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 模型大小品質速度硬體支援
FP3232280 GB最高通用
FP1616140 GB原生
BF1616140 GB原生
INT8870 GB接近 FP16很快需校準
INT4435 GB可接受最快需專門方法

FP16 vs INT8 vs INT4

四、量化的時機

訓練後量化(PTQ, Post-Training Quantization)

做法:模型訓練完成後,直接對權重進行量化。

優點

  • 不需要重新訓練
  • 快速、簡單
  • 適合大多數場景

缺點

  • 精度可能下降
  • 需要校準數據

流程

訓練好的 FP16 模型

收集校準數據(幾百到幾千個樣本)

計算量化參數(scale、zero_point)

量化權重

量化後的模型

量化感知訓練(QAT, Quantization-Aware Training)

做法:在訓練過程中模擬量化,讓模型適應量化後的精度損失。

優點

  • 精度損失最小
  • 可以達到接近 FP16 的品質

缺點

  • 需要重新訓練
  • 成本高、時間長

流程

訓練時加入「假量化」節點

前向傳播:量化 → 反量化

反向傳播:正常更新權重

訓練完成後,匯出量化模型

什麼時候用哪個?

場景推薦原因
快速部署PTQ不需要重新訓練
追求最高品質QAT精度損失最小
資源有限PTQ成本低
有訓練資源QAT值得投入
大模型(70B+)PTQQAT 成本太高

對大多數團隊來說,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。
  • 主要用於實驗和輕量部署。

方法對比

方法精度記憶體節省速度易用性適用場景
GPTQ75%GPU 部署
AWQ很好75%很快GPU 部署
GGUF50-75%CPU/Mac
bitsandbytes50-75%很高實驗/輕量

六、量化對延遲與成本的影響

對記憶體的影響

以 LLaMA-2 70B 為例:

精度模型大小KV Cache(4096, batch 32)總記憶體需要的 GPU
FP16140 GB320 GB460 GB6× A100 80GB
INT870 GB160 GB230 GB3× A100 80GB
INT435 GB80 GB115 GB2× A100 80GB

INT4 量化讓 70B 模型從需要 6 張 A100 降到 2 張,成本節省 3 倍。

對速度的影響

量化不僅減少記憶體,還能加速推理,因為:

  1. 記憶體頻寬是瓶頸:更小的模型意味著更少的記憶體讀取。
  2. 整數運算更快:現代 GPU 對 INT8 有專門的加速。

以 LLaMA-2 7B 為例:

精度每秒生成 token 數相對速度
FP16301.0x
INT8501.7x
INT4802.7x

注意:量化對預填充階段的加速效果較小,主要加速解碼階段。

對成本的影響

以一個每天處理 10,000 個請求的服務為例:

精度GPU 數量每日成本每月成本
FP164× A100$288$8,640
INT82× A100$144$4,320
INT41× A100$72$2,160

INT4 量化可以讓成本降低 4 倍

Quantization → Less Memory + More Speed + Lower Cost

七、實作:量化一個模型

讓我們用一個完整的範例,展示如何量化一個模型。

使用 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 服務。