LLM 后训练实践
第5课:压缩部署与扩展

5.1 模型量化

深入理解模型量化的原理、主流 PTQ 方法(INT8/INT4/GPTQ/AWQ)与 QAT,掌握精度-速度-显存的权衡

为什么需要量化?

在部署大语言模型时,显存(GPU memory)是最核心的瓶颈。让我们用一个简单的例子来理解这个问题:

显存估算经验公式:一个参数量为 NN 的模型,以 FP16(16 bit = 2 bytes)存储权重,需要的显存约为 2N2N bytes。例如,Qwen3-8B 的 FP16 权重需要约 2×8×109=16GB2 \times 8 \times 10^9 = 16\text{GB}

但实际推理时,显存需求远不止权重本身:

推理显存=Wmodel模型权重+KcacheKV 缓存+Aact激活值+Ooverhead框架开销\text{推理显存} = \underbrace{W_{\text{model}}}_{\text{模型权重}} + \underbrace{K_{\text{cache}}}_{\text{KV 缓存}} + \underbrace{A_{\text{act}}}_{\text{激活值}} + \underbrace{O_{\text{overhead}}}_{\text{框架开销}}

以 Qwen3-8B(FP16)为例:

组成部分估算大小说明
模型权重(FP16)~16 GB2×8B2 \times 8\text{B} 参数
KV 缓存(2048 tokens,batch=1)~1-2 GB随序列长度线性增长
激活值与中间结果~1-2 GB取决于 batch size
框架开销~1-2 GBCUDA context 等
合计~20-22 GB单张 A100-40G 可容纳,但余量不大

如果是 7B 模型(如 Qwen3-8B 的前身 Qwen2.5-7B),FP16 权重约 14 GB——在 16GB 的 T4 GPU 上几乎无法运行推理。

量化的核心思想:将模型权重从高精度(FP16/BF16,16 bit)压缩到低精度(INT8 为 8 bit,INT4 为 4 bit),从而将显存占用减少到原来的 1/2 甚至 1/4,同时尽可能保持模型质量。

量化压缩比=原始精度位宽目标精度位宽=168=2×164=4×\text{量化压缩比} = \frac{\text{原始精度位宽}}{\text{目标精度位宽}} = \frac{16}{8} = 2\times \quad \text{或} \quad \frac{16}{4} = 4\times

数值精度基础

在深入量化方法之前,先回顾常见的数值精度格式:

精度格式位宽指数位尾数位动态范围典型用途
FP3232 bit8231038\sim 10^{38}传统训练精度
BF1616 bit871038\sim 10^{38}现代训练首选(与 FP32 同范围)
FP1616 bit5106.5×104\sim 6.5 \times 10^4推理常用
INT88 bit[128,127][-128, 127]量化推理
INT44 bit[8,7][-8, 7]极致压缩
NF44 bit非均匀分布QLoRA 专用

BF16 vs FP16:BF16 保留了与 FP32 相同的指数位(8位),因此动态范围与 FP32 一致,不容易溢出。FP16 的动态范围小得多,在训练中容易出现梯度溢出(gradient overflow)。现代 LLM 训练几乎都使用 BF16。

量化的数学原理

最基本的均匀量化(Uniform Quantization)公式:

xq=round(xs)+zx_q = \text{round}\left(\frac{x}{s}\right) + z

其中 ss 是缩放因子(scale),zz 是零点(zero-point)。反量化时:

x^=s(xqz)\hat{x} = s \cdot (x_q - z)

对称量化(Symmetric Quantization)令 z=0z = 0

s=max(x)2b11s = \frac{\max(|x|)}{2^{b-1} - 1}

其中 bb 是目标位宽。例如 INT8 对称量化中,s=max(x)/127s = \max(|x|) / 127

训练后量化(PTQ)方法详解

训练后量化(Post-Training Quantization, PTQ)是在模型训练完成后,不需要重新训练即可进行的量化方法。以下介绍四种主流方案。

方法一:INT8 动态量化(bitsandbytes LLM.int8())

核心思想:Dettmers 等(2022)发现,Transformer 模型的激活中存在少量异常值特征(outlier features)——某些隐藏维度的激活值远大于其他维度。如果直接用 INT8 量化,这些异常值会严重损害精度。

LLM.int8() 的解决方案:混合精度分解(Mixed-precision Decomposition)。

检测异常值:在激活矩阵中,找出绝对值超过阈值(默认 6.0)的特征维度

分离计算:将矩阵乘法分为两部分——异常值维度保持 FP16 计算,其余维度用 INT8 计算

合并结果:将 FP16 和 INT8 的计算结果合并,得到最终输出

Y=XoutlierWoutlierT+dequant(Q(Xnormal)Q(Wnormal)T)Y = X_{\text{outlier}} W_{\text{outlier}}^T + \text{dequant}(Q(X_{\text{normal}}) \cdot Q(W_{\text{normal}})^T)

效果:几乎无精度损失(<0.1% 性能下降),显存减少约 50%。

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

# INT8 量化配置
quantization_config = BitsAndBytesConfig(
    load_in_8bit=True,  # 启用 INT8 量化
)

model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B",
    quantization_config=quantization_config,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")

# 查看显存占用
print(f"模型显存: {model.get_memory_footprint() / 1024**3:.2f} GB")
# 预期:约 8-9 GB(相比 FP16 的 ~16 GB 减少约一半)

方法二:INT4 NormalFloat(bitsandbytes NF4)

核心思想:Dettmers 等(2023)在 QLoRA 论文中提出了 NormalFloat(NF4) 量化格式。其关键洞察是:预训练语言模型的权重近似服从零均值正态分布

NF4 的量化步骤:

归一化:将每个量化块(通常 64 个权重)除以该块的绝对值最大值,映射到 [1,1][-1, 1]

最优分位数映射:将 [1,1][-1, 1] 区间按标准正态分布 N(0,1)\mathcal{N}(0, 1) 的分位数划分为 24=162^4 = 16 个区间,每个区间对应一个量化值

查表量化:将归一化后的权重映射到最近的量化值(4-bit 索引)

NF4 量化值={qi:qi=Φ1(2i+12×24),i=0,1,,15}\text{NF4 量化值} = \left\{ q_i : q_i = \Phi^{-1}\left(\frac{2i + 1}{2 \times 2^4}\right), \quad i = 0, 1, \ldots, 15 \right\}

其中 Φ1\Phi^{-1} 是标准正态分布的逆累积分布函数。

双重量化(Double Quantization):QLoRA 还对量化的缩放因子本身再做一次量化(FP32 → FP8),进一步节省约 0.5 bit/参数。

# INT4 NF4 量化配置(QLoRA 所用)
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,              # 启用 4-bit 量化
    bnb_4bit_quant_type="nf4",      # 使用 NormalFloat 格式
    bnb_4bit_use_double_quant=True,  # 启用双重量化
    bnb_4bit_compute_dtype=torch.bfloat16,  # 计算时用 BF16
)

model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B",
    quantization_config=quantization_config,
    device_map="auto",
)

print(f"模型显存: {model.get_memory_footprint() / 1024**3:.2f} GB")
# 预期:约 5-6 GB(相比 FP16 的 ~16 GB 减少约 3/4)

NF4 是 QLoRA 的基石:QLoRA = NF4 量化 + LoRA 微调。在 INT4 量化的模型上附加 LoRA 适配器,使得 7B 模型可以在单张 16GB GPU 上进行微调——这是推动 LLM 微调民主化的关键技术。

方法三:GPTQ — 基于校准的最优量化

核心思想:Frantar 等(2023)将量化视为一个逐层优化问题。对于每一层的权重矩阵 WW,找到量化权重 W^\hat{W},使得量化误差最小:

W^=argminW^WXW^X22\hat{W} = \arg\min_{\hat{W}} \| WX - \hat{W}X \|_2^2

其中 XX 是该层在校准数据上的输入激活。

GPTQ 算法步骤

准备校准数据:收集 128-256 条代表性文本,送入模型获取每层的输入激活

计算 Hessian 矩阵H=2XXTH = 2 X X^T,衡量每个权重对输出的影响程度

逐列量化:按 Hessian 对角元素排序,依次量化每个权重,并用 Hessian 信息将量化误差最优地分摊到尚未量化的权重上

分组量化:将权重分成若干组(group_size,通常 128),每组使用独立的缩放因子

# GPTQ 量化通常使用 auto-gptq 或 optimum 库
# 以下为使用预量化 GPTQ 模型的示例
from transformers import AutoModelForCausalLM, AutoTokenizer

# 加载社区提供的 GPTQ 量化模型
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B-GPTQ-Int4",  # 预量化的 GPTQ INT4 模型
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B-GPTQ-Int4")

# 自行量化的流程(需要 auto-gptq 库)
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

quantize_config = BaseQuantizeConfig(
    bits=4,              # 量化到 4 bit
    group_size=128,      # 每 128 个权重共享一组量化参数
    desc_act=True,       # 按激活值大小排序量化顺序
)

# 量化需要校准数据集
model = AutoGPTQForCausalLM.from_pretrained(
    "Qwen/Qwen3-8B",
    quantize_config=quantize_config,
)
model.quantize(calibration_dataset)  # 校准 + 量化
model.save_quantized("Qwen3-8B-GPTQ-Int4")

GPTQ 量化需要离线校准:与 bitsandbytes 的即时量化不同,GPTQ 需要提前准备校准数据并运行量化过程(约需 1-2 小时),但生成的量化模型在推理时更快、质量更高。

方法四:AWQ — 激活感知权重量化

核心思想:Lin 等(2024)观察到,在权重量化中,并非所有权重同等重要——少数与大激活值对应的权重通道(channels)远比其他权重重要

AWQ 的关键洞察:

输出误差=(WQ(W))X2j(wjq(wj))2sj2\text{输出误差} = \| (W - Q(W)) \cdot X \|^2 \approx \sum_j (w_j - q(w_j))^2 \cdot s_j^2

其中 sj=E[Xj]s_j = \mathbb{E}[|X_j|] 是第 jj 个输入通道的平均激活幅度。激活幅度大的通道,量化误差会被放大。

AWQ 的解决方案:对重要通道的权重先乘以一个缩放因子 α>1\alpha > 1(使其更易量化),再对缩放后的权重做量化:

Q(wα)xαwxQ(w \cdot \alpha) \cdot \frac{x}{\alpha} \approx w \cdot x

缩放因子 α\alpha 通过在校准数据上搜索最优值获得。

AWQ vs GPTQ 对比

特性GPTQAWQ
量化粒度逐列量化 + 误差补偿通道级缩放 + 均匀量化
校准数据量128-256 条128 条
量化速度较慢(逐列优化)较快(仅需搜索缩放因子)
推理速度更快(权重格式更规整)
质量优秀优秀,稍优于 GPTQ
vLLM 支持支持支持(推荐)
# AWQ 量化通常使用 autoawq 库
from awq import AutoAWQForCausalLM

model = AutoAWQForCausalLM.from_pretrained("Qwen/Qwen3-8B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")

quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM",
}

# 量化(需要校准数据)
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized("Qwen3-8B-AWQ")

量化感知训练(QAT)

与 PTQ 不同,量化感知训练(Quantization-Aware Training, QAT)在训练过程中模拟量化效果:

前向传播:在计算中插入伪量化节点(fake quantization),模拟量化/反量化的舍入误差

反向传播:使用直通估计器(Straight-Through Estimator, STE),将量化函数的梯度近似为 1

收敛后导出:训练完成后,将伪量化替换为真实量化操作

STE:LxLx^(绕过不可导的 round 操作)\text{STE}: \frac{\partial L}{\partial x} \approx \frac{\partial L}{\partial \hat{x}} \quad (\text{绕过不可导的 round 操作})

QAT 的优缺点

优点缺点
量化质量最高需要重新训练
适合极低精度(INT2/INT3)训练成本高
模型自适应量化误差需要训练数据

在实际应用中,PTQ 是主流选择——因为 GPTQ 和 AWQ 在 4-bit 量化下已经能达到接近 FP16 的质量。QAT 通常只在需要 2-3 bit 极致压缩时才值得投入。

精度-速度-显存权衡总表

以 Qwen3-8B(约 80 亿参数)在单张 A100-40G 上为例:

精度方案显存占用相对 FP16tokens/s首 token 延迟GSM8K 准确率推荐场景
FP16~16 GB1.0x基准基准基准 (~80%)质量优先、显存充足
INT8 (LLM.int8())~9 GB0.56x~0.9x~1.1x~79.5%显存受限但需高质量
INT4 (NF4)~5.5 GB0.34x~1.1x~1.0x~77-78%QLoRA 微调
GPTQ-Int4~5 GB0.31x~1.3x~0.8x~78%生产部署
AWQ-Int4~5 GB0.31x~1.4x~0.7x~78.5%生产部署(推荐)

量化对推理能力的影响最大:实验表明,量化对简单任务(如文本分类、情感分析)影响很小,但对需要精确计算的任务(如数学推理、复杂逻辑)影响较大。INT4 量化可能导致 GSM8K 准确率下降 2-3 个百分点。

完整代码示例:多精度模型加载对比

import torch
import time
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_name = "Qwen/Qwen3-8B"

def load_model_with_precision(model_name, precision="fp16"):
    """以不同精度加载模型并测量显存"""
    torch.cuda.reset_peak_memory_stats()
    start_time = time.time()

    if precision == "fp16":
        model = AutoModelForCausalLM.from_pretrained(
            model_name,
            torch_dtype=torch.float16,
            device_map="auto",
        )
    elif precision == "int8":
        quantization_config = BitsAndBytesConfig(load_in_8bit=True)
        model = AutoModelForCausalLM.from_pretrained(
            model_name,
            quantization_config=quantization_config,
            device_map="auto",
        )
    elif precision == "int4":
        quantization_config = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_quant_type="nf4",
            bnb_4bit_use_double_quant=True,
            bnb_4bit_compute_dtype=torch.bfloat16,
        )
        model = AutoModelForCausalLM.from_pretrained(
            model_name,
            quantization_config=quantization_config,
            device_map="auto",
        )
    else:
        raise ValueError(f"不支持的精度: {precision}")

    load_time = time.time() - start_time
    memory_gb = torch.cuda.max_memory_allocated() / 1024**3

    print(f"[{precision.upper()}] 加载时间: {load_time:.1f}s | 显存: {memory_gb:.2f} GB")
    return model, memory_gb, load_time

# 依次加载不同精度的模型
tokenizer = AutoTokenizer.from_pretrained(model_name)

for precision in ["fp16", "int8", "int4"]:
    model, mem, t = load_model_with_precision(model_name, precision)
    # 简单推理测试
    inputs = tokenizer("请解释量子计算的基本原理", return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(**inputs, max_new_tokens=100)
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(f"  回复预览: {response[:100]}...")
    del model
    torch.cuda.empty_cache()

量化方法选择指南

根据你的使用场景,选择合适的量化方法:

开发调试

使用 bitsandbytes INT8/INT4,零配置即时量化,适合快速原型开发

微调训练(QLoRA)

使用 bitsandbytes NF4 + 双重量化,显存节省最大化

生产部署

使用 AWQ 或 GPTQ 预量化模型,配合 vLLM 部署,推理速度最优

边缘设备

使用 GGUF 格式 + llama.cpp,支持 CPU/混合推理,适合无 GPU 环境

本节小结

  1. 量化的本质是用更少的 bit 表示权重,以显存换质量的权衡
  2. PTQ 四大方法各有适用场景:LLM.int8()(通用)、NF4(QLoRA)、GPTQ(高质量部署)、AWQ(最佳性能部署)
  3. 量化对推理和数学任务影响最大,对通用指令跟随影响较小
  4. 生产环境推荐 AWQ + vLLM 组合,开发环境推荐 bitsandbytes 即时量化
  5. QAT 在极低精度(2-3 bit)下有优势,但在 4-bit 及以上时 PTQ 已足够好