跳转至

量化

一句话总结:量化通过降低模型权重与激活的数值精度,换来更小体积、更高吞吐、更低成本,是大模型部署经济可行的关键手段。

🗺️ 本章导览

  • 本章覆盖 AI 推理:量化 → 高效架构 → 服务批处理 → 边缘推理 → 扩展部署
  • 01. 量化 (本节): INT8/INT4/FP8, PTQ 与 QAT
  • 02. 高效架构: MoE,稀疏,蒸馏
  • 03. 服务与批处理: continuous batching、KV cache、PagedAttention
  • 04. 边缘推理:移动端、嵌入式、片上部署
  • 05. 扩展与部署:多机推理、成本优化、SLO

量化降低模型权重与激活的精度,让模型更小、更快、更便宜。本节涵盖数字格式、训练后量化、量化感知训练、仅权重量化方法 (GPTQ、AWQ)、激活量化、混合精度以及 KV 缓存量化。

  • 一个 70B 参数模型用 float16 需要 140 GB 内存,超过任何单卡 GPU 的容量。量化到 INT4 后,只要 35 GB (一张 A100) 甚至 20 GB (消费级 RTX 4090 加上 offload) 就装得下。量化不是可有可无的优化点缀,它是让大模型部署在经济上可行的根本。

  • 核心权衡:更低精度意味着更少内存、更高吞吐、更低功耗,但会引入量化误差 (quantisation error),有可能拖累模型质量。量化的艺术就是把这种质量下降压到最小。

为什么要量化

  • 内存缩减:INT8 比 FP16 小 2 倍,INT4 比 FP16 小 4 倍。对大语言模型 (Large Language Model, LLM) 来说,权重占了绝大部分内存。精度减半,内存需求也减半。

  • 吞吐提升:精度越低,每秒能做的运算就越多。NVIDIA Tensor Core (第 16 章) 在 FP16 相对 FP32 上吞吐翻倍,INT8 相对 FP16 再翻倍。INT4 在 Tensor Core 上没有原生计算支持——权重以 INT4 存储、计算前反量化到 FP16,因此吞吐提升来自内存带宽节省 (内存带宽受限场景下近似线性加速),而非计算吞吐再翻倍 (据 NVIDIA H100 whitepaper 2023)。一张 H100 SXM 在 FP8 Tensor Core 上能跑 1,979 TFLOPS (dense,据 NVIDIA H100 datasheet 2023),而 FP32 (非 Tensor Core) 只有 67 TFLOPS —— 差了近 30 倍。

  • 带宽节省:LLM 推理通常是内存带宽受限 (memory-bandwidth-bound) 的 (第 16 章,Roofline 模型)。瓶颈是从 GPU 显存里加载权重,而不是拿它们做计算。权重越小,需要搬运的字节就越少,每秒生成的 token 数直接上升。这就是为什么量化对 LLM 推理常常能拿到近乎线性的加速。

  • 能耗节省:精度越低,每次运算耗的电就越少。放到数据中心规模 (成千上万张 GPU) 上看,这意味着电费的显著下降。

数字格式

  • IEEE 754 浮点格式我们已经在第 13 章 (计算机体系结构) 讲过。这里是 ML 场景下完整的精度全景:

精度格式的位布局: 从 FP32 到三值, 展示符号位、指数位、尾数位在内存中的排布方式, 以及每个参数占用的内存对比

格式 位数 指数位 尾数位 范围 用途
FP32 32 8 23 ±3.4×10³⁸ 训练 (黄金标准)
TF32 19 8 10 ±3.4×10³⁸ Tensor Core 训练 (A100+)
FP16 16 5 10 ±65504 混合精度训练
BF16 16 8 7 ±3.4×10³⁸ 训练 (与 FP32 相同范围)
FP8 E4M3 8 4 3 ±448 前向计算 (Hopper+)
FP8 E5M2 8 5 2 ±57344 梯度 (范围更宽)
INT8 8 — — -128 到 127 PTQ 推理
INT4 4 — — -8 到 7 仅权重量化
INT2/三值 2 — — {-1, 0, 1} 极限压缩
  • FP8 有两个变种:E4M3 (4 位指数、3 位尾数,范围较窄但精度更高) 用于前向传播;E5M2 (5 位指数、2 位尾数,范围更宽但精度较低) 用于梯度。Transformer Engine (第 16 章) 会按张量自动在两者之间切换。

  • BF16 vs FP16: BF16 拥有和 FP32 一样的指数范围 (不会溢出),但尾数精度较低。FP16 精度更高,但范围窄 (最大 65504),训练时需要 loss scaling。用于推理时两者都可以;用于训练时 BF16 更稳。

  • 整数格式没有指数位 —— 它们表示定点数。要在浮点和整型之间转换,需要一个缩放因子 (scale factor),有时还需要一个零点 (zero point): \(x_{\text{float}} = \text{scale} \times (x_{\text{int}} - \text{zero\_point})\)。

量化方程

  • 所有量化方法都在做同一件事:把浮点值映射到整数,再映射回来:
\[x_q = \text{clamp}\left(\text{round}\left(\frac{x}{\text{scale}}\right) + \text{zero\_point}, \; q_{\min}, \; q_{\max}\right)\]
\[\hat{x} = \text{scale} \times (x_q - \text{zero\_point})\]
  • scale 决定了分辨率:\(\text{scale} = \frac{x_{\max} - x_{\min}}{q_{\max} - q_{\min}}\)。对 INT8 来说:\(q_{\min} = -128\), \(q_{\max} = 127\)。

  • 对称量化 (symmetric quantisation) 把 \(\text{zero\_point}\) 设为 0,于是 \(\text{scale} = \frac{\max(|x|)}{127}\)。更简单也更快 (推理时不用做零点减法)。

  • 非对称量化 (asymmetric quantisation) 使用非零的 \(\text{zero\_point}\),用来处理不对称的分布 (比如 ReLU 输出全是非负数)。把 \([x_{\min}, x_{\max}]\) 映射到 \([0, 255]\),用无符号 INT8 表示。

量化粒度: per-tensor 整个矩阵共用一个缩放因子; per-channel 每列一个; per-group 每小块一个

  • 量化粒度 (quantisation granularity):多少个值共享同一个缩放因子:
    • Per-tensor:整个张量共用一个 scale。最简单但精度最低 (一个离群点就会把整个张量的 scale 拉偏)。
    • Per-channel:每个输出通道 (卷积) 或每一行 (线性层) 一个 scale。精度好很多,开销却几乎没增加。
    • Per-group:每 \(g\) 个元素一组共用一个 scale (比如 \(g = 128\))。精度最好,现代的仅权重量化 (GPTQ、AWQ) 都用它。
    • Per-token:激活按 token 分,每个 token 一个 scale。不同 token 之间激活量级可以差很多,这种粒度处理起来更合适。

训练后量化 (Post-Training Quantisation, PTQ)

  • PTQ 把预训练模型直接量化,不做任何再训练。你只需要拿一个校准集 (calibration set,一小份具代表性的数据,通常 128 到 512 个样本) 过一遍模型,收集激活的统计信息,再算出最优的 scale 因子。

校准方法

  • Min-max:用观测到的最小和最大值算 scale。简单,但对离群点很敏感 (一个极端值会把大部分量化区间浪费在稀疏用到的值上)。

  • 百分位:用 99.99 百分位代替绝对最大值。剪掉极端离群点,主体值的分辨率更好。被剪掉的值会饱和到 \(q_{\min}\) 或 \(q_{\max}\)。

  • MSE 最优:找出让原始张量和量化后张量之间的均方误差 (Mean Squared Error, MSE) 最小的 scale。这是一维优化 (在可选的截断值上搜索),通常给出 PTQ 里最好的精度。

  • 基于熵的方法 (KL 散度):找出让原始值分布与量化后值分布之间 Kullback-Leibler (KL) 散度最小的 scale。TensorRT 的 INT8 校准就用它。

PTQ 实战

# 用 PyTorch 简化实现 PTQ (概念性)
import torch

def quantise_tensor_symmetric(tensor, bits=8):
    qmax = 2 ** (bits - 1) - 1  # INT8 时为 127
    scale = tensor.abs().max() / qmax
    quantised = torch.clamp(torch.round(tensor / scale), -qmax, qmax).to(torch.int8)
    return quantised, scale

def dequantise(quantised, scale):
    return quantised.float() * scale

# 量化一个权重矩阵
weight = torch.randn(512, 512)  # 预训练权重
weight_q, scale = quantise_tensor_symmetric(weight, bits=8)
weight_reconstructed = dequantise(weight_q, scale)

# 量化误差
error = (weight - weight_reconstructed).abs().mean()
print(f"Mean absolute error: {error:.6f}")
print(f"Compression: {weight.numel() * 4 / (weight_q.numel() * 1 + 4):.1f}x")  # scale 多占 4 字节
  • 在大多数模型上,PTQ 做到 INT8 时精度损失不到 1%。到 INT4 时,PTQ 质量下降就明显了 —— 下面的仅权重方法在 INT4 上表现要好得多。

量化感知训练 (Quantisation-Aware Training, QAT)

  • QAT 在训练图里插入伪量化 (fake quantisation) 算子:前向传播时权重和激活会被量化再反量化,但反向传播时梯度当作没量化过一样流过去 (这就是直通估计器,straight-through estimator)。
\[\text{Forward: } \hat{W} = \text{dequant}(\text{quant}(W))$$ $$\text{Backward: } \frac{\partial L}{\partial W} \approx \frac{\partial L}{\partial \hat{W}}\]
  • 模型在训练过程中学会对量化噪声保持稳健。QAT 通常能把 PTQ 丢掉的那点精度找回大部分甚至全部,尤其在极低位宽 (INT4、INT2) 下效果显著。

  • 成本:QAT 需要重训练或微调,对大模型来说很贵。一个 70B 参数模型,QAT 可能要花 $10,000 到 $100,000 的算力。而 PTQ 几乎零成本 (只是做个校准)。

  • 什么时候用 QAT: PTQ 质量不达标时 (通常 INT4 或更低);部署到边缘设备、有严格延迟预算时;或模型会被量化上百万次时 (一次性的 QAT 成本被摊薄了)。

仅权重量化

  • 对 LLM 推理而言,瓶颈在从内存加载权重,而不是拿它们计算 (内存带宽受限区间)。仅权重量化 (weight-only quantisation) 把权重量化到 INT4 或 INT3,但激活保持 FP16。计算发生在 FP16 (权重在计算前实时反量化),而内存占用和带宽却减小了 4 到 8 倍。

GPTQ

  • GPTQ (Frantar 等,2022) 一次量化一列权重,每量化完一列就用调整后续列的方式补偿这一列的量化误差。它借助 Hessian (来自校准集的二阶信息) 决定最优的量化顺序与误差补偿:
\[\hat{W}_{:,j} = \text{quant}(W_{:,j}), \quad W_{:,j+1:} \mathrel{-}= \frac{(\hat{W}_{:,j} - W_{:,j}) \cdot H_{j,j+1:}}{H_{j,j}}\]
  • 核心洞见:量化第 \(j\) 列会引入误差,GPTQ 立刻通过修改后续所有列来补偿,使得整层输出 (\(XW\)) 的变化尽可能小。这就是最优大脑量化 (Optimal Brain Quantisation, OBQ) 在 Transformer 上的应用。

  • GPTQ 配 4 位分组量化 (group size 128) 在大多数 LLM 上困惑度损失不到 1%。在单卡 GPU 上量化一个 70B 模型大约要 1 小时。

AWQ

  • AWQ (激活感知权重量化,Activation-Aware Weight Quantisation, Lin 等,2023) 观察到只有一小部分权重通道 (1% 到 3%) 比其他重要得多 —— 它们对应着幅值特别大的激活通道。保住这些关键通道能大幅降低量化误差。

  • AWQ 在量化前把这些重要通道乘以缩放因子 \(s\) (让它们变大,从而不那么容易被舍入影响),同时把对应的激活乘以 \(1/s\) (保持输出不变)。缩放 \(s\) 按组优化,目标是让整体量化误差最小。

  • AWQ 比 GPTQ 更简单 (不用算 Hessian)、跑得更快,质量却相当。它已经成为很多开源 LLM 量化流水线的默认选择。

GGUF / llama.cpp 量化

  • GGUF (GGML 通用格式,GGML Universal Format) 是 llama.cpp 用于 CPU 推理的格式。它支持很多量化方案:

    • Q4_0: 4 位,32 元素一块,对称。
    • Q4_K_M: 4 位,对重要通道做混合精度处理 (k-quants)。
    • Q5_K_M: 5 位配 k-quants (质量更高)。
    • Q8_0: 8 位,简单快速。
  • "K" 变种 (k-quants) 给重要的权重块分配更多位,思想类似 AWQ,但在格式层就实现了。Q4_K_M 是大多数模型的甜点区:平均 4 位,质量损失极小。

QuIP 与 QuIP

  • QuIP (Chee 等,2023) 引入了不相干处理 (incoherence processing):在量化前用一个随机正交变换把权重矩阵旋转一下。这样信息就均匀铺开到所有权重上,不会让少数离群权重主导量化误差。

  • 直觉:如果一个权重是 100,其他大约是 1,用同一个 scale 量化全部时,大部分 INT4 区间就浪费在这个离群点上。做完正交旋转后 (保留了矩阵的数学性质),所有权重量级相近,均匀量化就好多了。

  • QuIP# 在此基础上加了格码本 (lattice codebooks):不再映射到均匀整数网格,而是映射到最优晶格上的点 (8 维空间里的 E8 晶格)。晶格码在同样的位数下能塞进更多量化点,率失真表现比均匀量化更好。QuIP# 在 2 位精度下还能拿到可用的质量 —— 只用了典型 INT4 方法一半的位数。

SpQR

  • SpQR (Dettmers 等,2023) 观察到极少量权重 (0.1% 到 1%) 是离群点 (outliers),它们对输出质量的贡献远超其他权重。SpQR 不再一视同仁地量化,而是:

    1. 通过敏感性分析 (quantise 掉这个权重会让层输出变多少?) 识别出离群权重。
    2. 用稀疏格式保存离群权重的全精度 (FP16) 值。
    3. 把剩下的权重激进地量化到 INT3 或 INT4。
  • 结果:约 99% 的权重被大幅压缩 (小),而关键的 1% 保留全精度 (准)。稀疏离群点的存储开销很低 (总体积的 <5%)。

HQQ

  • HQQ (半二次量化,Half-Quadratic Quantisation, Badri & Shaji, 2023) 是一种零样本 (zero-shot) 权重量化方法,完全不需要任何校准数据。它把量化建模成一个半二次优化问题,迭代地求解最优量化权重和 scale 因子。

  • 优势:不需要校准集,意味着没有数据依赖、可以立即量化,也不用担心校准数据不匹配。当代表性校准数据难以获取或涉及敏感信息时,HQQ 特别有用。

AQLM

  • AQLM (Egiazarian 等,2024) 把加性量化 (additive quantisation,多码本向量量化) 用到了 LLM 上。AQLM 不再逐权重独立量化,而是把权重组成向量,用多个学习到的码本中的条目求和来表示每个向量:
\[\mathbf{w} \approx \mathbf{c}_1^{(1)} + \mathbf{c}_2^{(2)} + \cdots + \mathbf{c}_M^{(M)}\]
  • 这里 \(\mathbf{c}_i^{(m)}\) 是第 \(m\) 个码本中的一个条目。用 \(M = 2\) 个码本、每个 256 条时,一个 8 元素向量被编码成两个 8 位索引 = 2 字节表示 8 个权重 = 每权重 2 位。在 2 位精度下,AQLM 达到了当前最好的质量,在这个极端压缩水平上超过了 GPTQ 与 AWQ。

BitNet 与 1 位 LLM

  • BitNet (Wang 等,2023) 把量化推向极致:权重取三值 (\(\{-1, 0, +1\}\)),每权重只需约 1.58 位。矩阵乘法退化为加减法 —— 完全不需要浮点乘法。

  • BitNet b1.58 (Ma 等,2024) 把每个权重都限制在 \(\{-1, 0, +1\}\)。所谓 "1.58 位" 来自 \(\log_2(3) \approx 1.58\)。这种精度下,70B 模型只占 ~15 GB,推理不需要乘法,只要加、减、变符号。

  • 矩阵乘法变成:

\[y_j = \sum_i W_{ij} \cdot x_i = \sum_{i: W_{ij}=+1} x_i - \sum_{i: W_{ij}=-1} x_i\]
  • 在任何硬件上,这都比 FP16 matmul 便宜得多,甚至可能让没有浮点单元的设备也跑得动 LLM 推理。就当前模型而言,质量代价还不小,但随规模扩大与训练时的量化感知设计会持续改善。

微尺度 (MX) 格式

  • 微尺度 (Microscaling, MX) 格式是新的行业标准 (AMD、Arm、Intel、Meta、Microsoft、NVIDIA、Qualcomm 都支持),使用块浮点 (block floating point):一组元素共享一个指数,每个元素有自己的尾数。
格式 共享指数 元素位数 每元素总位数 等价于
MXFP8 每块 8 位 8 (E4M3/E5M2) ~8 像 FP8 但范围更好
MXFP6 每块 8 位 6 ~6.5 介于 FP8 与 INT4 之间
MXFP4 每块 8 位 4 ~4.5 像 INT4 但有浮点特性
MXINT8 每块 8 位 8 (整型) ~8.5 INT8 加共享 scaling
  • 共享指数把指数的开销摊到整个块上 (通常 16 到 32 个元素)。每个元素保留的尾数位数比单独指数时多,每位表达的精度更好。MX 格式预计会在未来的硬件里取代单独的 FP8 和 INT8。

FP8 训练

  • 在 NVIDIA Hopper 和 Blackwell 上,用 FP8 做训练 (不只是推理) 已经可行。配方如下:

    • 前向:权重和激活用 E4M3 (精度较高、范围较窄)。Transformer Engine 用延迟缩放 (delayed scaling) 动态算出每张量的 scale 因子 (记录上一个迭代的统计量,用到当前迭代)。

    • 反向:梯度用 E5M2 (范围更宽、精度更低)。梯度的取值范围比权重、激活都宽,多一位指数能防溢出。

    • 主权重 (master weights):用 FP32 维护,供优化器状态使用 (和 FP16 混合精度训练类似,第 6 章)。FP8 只用于 matmul,不用于权重更新。

    • Loss scaling:与 FP16 混合精度依赖动态 loss scaler 不同,FP8 训练由 Transformer Engine 采用 per-tensor delayed scaling——每个张量维护自己的 amax 历史 (记录若干次前向/反向的最大绝对值),据此选择 scale 因子,使张量的动态范围对齐到 E4M3/E5M2 的可表示区间 (据 NVIDIA Transformer Engine 文档 2023)。不再需要传统的全局 loss scaler。

  • 对大多数模型规模,FP8 训练能达到与 BF16 训练相当的质量,吞吐提升约 2 倍。它已经是 H100 集群上新一代大规模训练的默认选择。

激活量化

  • 激活 (在层之间流动的中间张量) 也可以量化,从而让整个计算走 INT8 (权重和激活都是 INT8,累加走 INT32)。

  • 动态量化 (dynamic quantisation):在运行时按实际激活值算 scale。更准确 (每次输入都自适应),但有额外开销 (每层要算 min/max 或百分位)。

  • 静态量化 (static quantisation): scale 因子在校准阶段算好,之后固定不动。推理时更快 (不用现算统计量),但如果校准数据不具代表性,精度会掉。

  • Per-token 量化:序列里每个 token 各算一个 scale。这对 LLM 关键,因为不同 token 的激活量级差异很大 (有些 token 的激活能比其他大 100 倍)。

  • 激活量化比权重量化难,因为激活是数据依赖的 (随输入变化),而权重是固定的。"离群点" 问题在这里尤其严重:少数激活通道有极端值 (是均值的 100 倍),用和普通通道相同的 scale 量化就会浪费大量精度。

  • SmoothQuant (Xiao 等,2022) 通过数学上把量化难度从激活 (因离群点难量化) 转移到权重 (容易量化) 来处理离群点:把激活乘 \(1/s\),权重乘 \(s\),由 \(s\) 平衡两侧的难度。输出 \(XW = (X \cdot \text{diag}(s^{-1})) \cdot (\text{diag}(s) \cdot W)\) 保持不变。

混合精度量化

  • 各层对量化的敏感度并不一样。注意力层通常能容忍 INT4,而嵌入层与最后的分类层需要更高精度。

  • 敏感性分析:逐层单独量化,测量精度受影响的程度。敏感层给更多位;不敏感层给更少位。

  • Transformer Engine (第 16 章,NVIDIA Hopper) 在算子级别做动态混合精度:每个 matmul 根据张量统计自行选择 FP8 还是 FP16,在保证质量的同时把吞吐拉满。

KV 缓存量化

  • 在 LLM 生成过程中,KV 缓存 (KV cache) 保存所有先前 token 的键 (Key) 与值 (Value) 张量。序列一长,它就成了内存大头:
\[\text{KV-cache size} = 2 \times n_{\text{layers}} \times n_{\text{heads}} \times d_{\text{head}} \times \text{seq\_len} \times \text{bytes\_per\_element}\]
  • 一个 70B 模型:80 层、64 个头、每头 128 维,序列长度 128K,用 FP16: \(2 \times 80 \times 64 \times 128 \times 131072 \times 2 = 330\) GB。这已经超出 GPU 内存。

  • KV 缓存量化把缓存的 K、V 从 FP16 改成 INT8 或 INT4 存储。量化误差会沿着序列累积 (每个新 token 都要对所有缓存的 K/V 做注意力),但用 per-channel 或 per-head 粒度时,质量下降是可接受的。

  • KV 缓存量化带来的好处是乘法性的:它同时让序列更长 (更多上下文)、批处理更大 (更多并发用户)、推理更快 (加载缓存的带宽更少)。对 LLM 服务而言,这是收益最高的优化之一。

编程任务 (在 CoLab 或 notebook 里做)

  1. 从零实现对称 INT8 量化。量化一个权重矩阵,再反量化回去,测量重建误差随值分布的变化。

    import jax.numpy as jnp
    import jax
    
    def quantise_int8(tensor):
        scale = jnp.max(jnp.abs(tensor)) / 127.0
        quantised = jnp.clip(jnp.round(tensor / scale), -127, 127).astype(jnp.int8)
        return quantised, scale
    
    def dequantise(quantised, scale):
        return quantised.astype(jnp.float32) * scale
    
    # 正态分布权重 (典型的训练后权重)
    key = jax.random.PRNGKey(0)
    weights = jax.random.normal(key, (1024, 1024)) * 0.02
    
    q, s = quantise_int8(weights)
    recon = dequantise(q, s)
    
    print(f"Original:     {weights.nbytes / 1024:.0f} KB")
    print(f"Quantised:    {q.nbytes / 1024:.0f} KB ({weights.nbytes / q.nbytes:.0f}x smaller)")
    print(f"Mean abs err: {jnp.abs(weights - recon).mean():.6f}")
    print(f"Max abs err:  {jnp.abs(weights - recon).max():.6f}")
    print(f"Relative err: {jnp.abs(weights - recon).mean() / jnp.abs(weights).mean():.4%}")
    

  2. 复现离群点问题。构造少数极端通道的激活,展示 per-tensor 量化如何失败、per-channel 量化如何成功。

    import jax.numpy as jnp
    import jax
    
    key = jax.random.PRNGKey(42)
    
    # 激活: 大多数通道正常, 有 2 个通道是 100 倍的离群点
    activations = jax.random.normal(key, (32, 512)) * 0.1
    activations = activations.at[:, 0].set(activations[:, 0] * 100)   # 离群通道
    activations = activations.at[:, 1].set(activations[:, 1] * 50)    # 离群通道
    
    # Per-tensor 量化 (整个张量一个 scale)
    scale_tensor = jnp.max(jnp.abs(activations)) / 127.0
    q_tensor = jnp.clip(jnp.round(activations / scale_tensor), -127, 127)
    recon_tensor = q_tensor * scale_tensor
    
    # Per-channel 量化 (每个通道一个 scale)
    scales_channel = jnp.max(jnp.abs(activations), axis=0) / 127.0
    q_channel = jnp.clip(jnp.round(activations / scales_channel), -127, 127)
    recon_channel = q_channel * scales_channel
    
    err_tensor = jnp.abs(activations - recon_tensor).mean()
    err_channel = jnp.abs(activations - recon_channel).mean()
    
    print(f"Per-tensor error: {err_tensor:.6f}")
    print(f"Per-channel error: {err_channel:.6f}")
    print(f"Per-channel is {err_tensor / err_channel:.1f}x better")
    print(f"\nOutlier channels waste {(activations.shape[1] - 2) / activations.shape[1]:.0%} "
          f"of the quantisation range for {2 / activations.shape[1]:.1%} of channels")
    

  3. 为不同模型规模与序列长度算 KV 缓存内存。展示为什么长上下文模型必须做 KV 缓存量化。

    def kv_cache_gb(n_layers, n_heads, d_head, seq_len, bytes_per_elem):
        return 2 * n_layers * n_heads * d_head * seq_len * bytes_per_elem / 1e9
    
    models = [
        ("Llama-7B",  32, 32, 128),
        ("Llama-70B", 80, 64, 128),
        ("GPT-4 (est)", 120, 96, 128),
    ]
    
    print(f"{'Model':<15} {'SeqLen':>8} {'FP16 (GB)':>10} {'INT8 (GB)':>10} {'INT4 (GB)':>10}")
    print("-" * 60)
    
    for name, layers, heads, d_head in models:
        for seq_len in [4096, 32768, 131072]:
            fp16 = kv_cache_gb(layers, heads, d_head, seq_len, 2)
            int8 = kv_cache_gb(layers, heads, d_head, seq_len, 1)
            int4 = kv_cache_gb(layers, heads, d_head, seq_len, 0.5)
            print(f"{name:<15} {seq_len:>8} {fp16:>9.1f}  {int8:>9.1f}  {int4:>9.1f}")
        print()
    


作者按: 我第一次真切感受到量化的威力,是在一台单卡 RTX 4090 (24 GB) 上跑 Llama-2 70B。原生 FP16 需要 140 GB,想都别想;换成 GPTQ INT4 加上 CPU offload,竟然就跑起来了,生成速度还有可用的 token/s。那一刻我意识到,量化不是"让模型跑快一点"的锦上添花,而是"让模型能不能跑"的存亡之问。后来在生产环境部署 LLM 时,感受更深:我们花几周做 GPTQ vs AWQ 的对比、调 group size、试各种 KV cache 量化方案,每省下 10 GB 显存都意味着能多塞一个并发用户、或者把上下文从 8K 拉到 32K。真正的量化艺术,是在困惑度、吞吐、延迟三者之间找那个最合适的甜点,而不是盲目追求更低的位宽。我踩过的最大一个坑:早期把激活量化的 outlier 问题想简单了,用 per-tensor 直接上 INT8,结果 MMLU 掉了 5 个点;换成 SmoothQuant + per-token 后只掉 0.3 个点。给读者的忠告——先跑基线,再逐层剖析,别一上来就追求极端压缩。


📌 本节要点

  • 量化的价值:降低数值精度换取更小内存、更高吞吐、更低功耗,是大模型部署经济可行的关键。
  • 数字格式全景:FP32/BF16/FP16/FP8 (E4M3, E5M2)/INT8/INT4/三值,精度、范围与用途各有取舍。
  • 量化方程与粒度:通过 scale 和 zero-point 建立浮点与整数的映射;粒度从 per-tensor 到 per-group、per-token,越细越准。
  • PTQ vs QAT: PTQ 靠校准集直接量化,便宜但精度有限;QAT 通过训练学习抗量化噪声,贵但可救回精度。
  • 仅权重量化:针对内存带宽受限的 LLM 推理,GPTQ/AWQ/GGUF/QuIP#/SpQR/HQQ/AQLM 把权重压到 INT4 甚至 2 位。
  • BitNet 与 MX 格式:三值权重让 matmul 变为加减法;微尺度格式用块浮点在新硬件上取代单独的 FP8/INT8。
  • FP8 训练:E4M3 前向、E5M2 反向、FP32 主权重,在 Hopper/Blackwell 上以 ~2× 吞吐拿到 BF16 相当的质量。
  • 激活离群点:少数通道能到均值 100 倍,per-token 粒度和 SmoothQuant 把难度从激活转移到权重。
  • KV 缓存量化:长上下文场景下 KV 缓存本身就能超 GPU 显存,量化到 INT8/INT4 是最高杠杆的服务侧优化。