Files
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

340 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 量化
*量化降低模型权重和激活值的精度,使模型更小、更快、运行成本更低。本文涵盖数字格式、训练后量化、量化感知训练、仅权重量化方法(GPTQ、AWQ)、激活值量化、混合精度和KV缓存量化*
- 一个70B参数的float16模型需要140 GB内存,超过任何单张GPU。量化为INT4后,它可以装入35 GB(一张A100)甚至20 GB(带卸载的消费级RTX 4090)。量化不是一种可有可无的优化;它是让大模型部署在经济上可行的关键。
- 基本权衡:低精度意味着更少内存、更高吞吐量和更低功耗,但会引入**量化误差**,可能降低模型质量。量化的艺术在于最小化这种降级。
## 为什么要量化
- **内存减少**INT8比FP16小2倍,INT4小4倍。对于LLM,模型权重占主导内存。精度减半意味着内存需求减半。
- **吞吐量提升**:低精度意味着每秒更多操作。NVIDIA Tensor Core(第16章)在FP16 vs FP32上实现2倍吞吐量,INT8 vs FP16再实现2倍,INT4 vs INT8再实现2倍。H100在FP8下达到989 TFLOPS,而FP32下只有67 TFLOPS——相差15倍。
- **带宽节省**:LLM推理通常是**内存带宽受限**的(第16章,屋顶模型)。瓶颈是从GPU内存加载权重,而不是计算。更小的权重意味着更少的传输字节,直接提高每秒token数。这就是量化通常能为LLM推理带来近乎线性加速的原因。
- **节能**:低精度每次操作消耗更少能量。在数据中心规模(数千GPU)下,这转化为显著的电力成本降低。
## 数字格式
- 我们在第13章(计算机体系结构)中介绍了IEEE 754浮点数。以下是ML的完整精度全景:
![精度格式位布局:从FP32到三值,展示符号位、指数和尾数位在内存中的排列方式,以及每参数内存对比](../images/precision_formats_memory.svg)
| 格式 | 位数 | 指数 | 尾数 | 范围 | 用途 |
|--------|------|----------|----------|-------|----------|
| 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),训练时需要损失缩放。对于推理,两者都表现良好;对于训练,BF16更安全。
- **整数格式**没有指数——它们表示定点值。要在浮点和整数之间转换,需要一个**缩放因子**和一个可选的**零点**$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})$$
- **缩放因子**决定分辨率:$\text{scale} = \frac{x_{\max} - x_{\min}}{q_{\max} - q_{\min}}$。对于INT8$q_{\min} = -128$$q_{\max} = 127$。
- **对称量化**设置$\text{zero\_point} = 0$,因此$\text{scale} = \frac{\max(|x|)}{127}$。更简单、更快(推理时无需减去零点)。
- **非对称量化**使用非零$\text{zero\_point}$来处理非对称分布(例如,ReLU输出全为非负)。将$[x_{\min}, x_{\max}]$映射到无符号INT8的$[0, 255]$。
![量化粒度:逐张量为整个矩阵使用一个缩放因子,逐通道每列一个,逐组每小块一个](../images/quantisation_granularity.svg)
- **量化粒度**:多少个值共享同一个缩放因子:
- **逐张量**:整个张量一个缩放因子。最简单但精度最低(一个异常值就会扭曲整个张量的缩放因子)。
- **逐通道**:每个输出通道(卷积)或每行(线性层)一个缩放因子。精度好得多,开销最小。
- **逐组**:每$g$个元素一组(例如$g = 128$)一个缩放因子。精度最佳,用于现代仅权重量化(GPTQ、AWQ)。
- **逐token**:每个token一个缩放因子用于激活值。处理不同token激活值幅度差异很大的情况。
## 训练后量化(PTQ
- **PTQ**量化预训练模型而不需要重新训练。通过**校准集**(一个小的代表性数据集,通常128-512个样本)输入模型收集激活值统计信息,然后计算最优缩放因子。
### 校准方法
- **最小-最大**:基于观察到的最小值和最大值设置缩放因子。简单但容易受异常值影响(一个极端值将大部分量化范围浪费在很少使用的值上)。
- **百分位数**:使用99.99百分位数而不是绝对最大值。裁剪极端异常值,为大多数值提供更好的分辨率。裁剪后的值饱和到$q_{\min}$或$q_{\max}$。
- **MSE最优**:找到最小化原始张量和量化张量之间均方误差的缩放因子。这是一个一维优化(搜索可能的裁剪值),通常给出最好的PTQ精度。
- **基于熵**(KL散度):找到最小化原始和量化值分布之间KL散度的缩放因子。用于TensorRT的INT8校准。
### PTQ实践
```python
# 使用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"平均绝对误差: {error:.6f}")
print(f"压缩比: {weight.numel() * 4 / (weight_q.numel() * 1 + 4):.1f}x") # +4字节用于缩放因子
```
- PTQ在INT8上对大多数模型效果良好,精度下降<1%。对于INT4,PTQ质量显著下降——仅权重量化方法(见下文)处理INT4要好得多。
## 量化感知训练(QAT
- **QAT**在训练图中插入伪量化操作:在前向传播中,权重和激活值被量化和反量化,但梯度像没有量化一样流过(**直通估计器**)。
$$\text{前向: } \hat{W} = \text{反量化}(\text{量化}(W))$$
$$\text{反向: } \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推理,瓶颈是从内存加载权重,而不是计算(内存带宽受限模式)。**仅权重量化**将权重量化为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$)变化尽可能小。这是应用于Transformer的**最优脑量化**OBQ)。
- 使用4位组量化(组大小128)的GPTQ在大多数LLM上达到<1%的困惑度降级。在单GPU上,70B模型的量化大约需要1小时。
### AWQ
- **AWQ**(激活感知权重量化,Lin等人,2023)观察到一小部分权重通道(1-3%)比其他通道重要得多——它们对应于具有大幅度的激活通道。保护这些显著通道可以大幅降低量化误差。
- AWQ在量化前将这些重要通道乘以一个因子$s$(使它们变大,因此受舍入影响更小),并将相应的激活值乘以$1/s$(以保持输出不变)。缩放因子$s$按组优化,以最小化整体量化误差。
- AWQ比GPTQ更简单(无需Hessian计算),运行更快,并达到可比较的质量。它已成为许多开源LLM量化流程的默认选择。
### GGUF / llama.cpp量化
- **GGUF**GGML通用格式)是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)引入了**非相干处理**:在量化之前使用随机正交变换旋转权重矩阵。这会将信息分散到所有权重上,防止少数异常权重主导量化误差。
- 直觉:如果一个权重是100,其余的大约是1,用相同的缩放因子量化所有权重会浪费INT4的大部分范围在异常值上。经过正交旋转(保持矩阵的数学性质)后,所有权重具有相似幅度,均匀量化效果更好。
- **QuIP#** 通过**格点码本**扩展了这一思想:不是映射到均匀整数网格,而是映射到最优格点中的点(8D中的E8格点)。格点编码在相同位数内打包更多量化点,实现了比均匀量化更好的率失真性能。QuIP#在**2位**精度下达到了可用质量——典型INT4方法的一半位数。
### SpQR
- **SpQR**Dettmers等人,2023)观察到极小一部分权重(0.1-1%)是**异常值**,对输出质量的贡献不成比例。SpQR不是将所有内容量化到相同精度,而是:
1. 使用敏感性分析(量化这个权重会改变层输出多少?)识别异常权重。
2. 以**全精度**(FP16)的稀疏格式存储异常值。
3. 将所有剩余权重量化为INT3或INT4。
- 结果:~99%的权重被积极量化(小),而关键的1%保持全精度(准确)。稀疏异常值存储增加的开销最小(占总大小的<5%)。
### HQQ
- **HQQ**(半二次量化,Badri & Shaji2023)是一种**零样本**权重量化方法,完全不需要校准数据。它将量化表述为一个半二次优化问题,迭代求解最优量化权重和缩放因子。
- 优势:无需校准集意味着没有数据依赖,即时量化,也没有校准数据不匹配的风险。HQQ对于无法获得代表性校准数据或数据敏感型的模型特别有用。
### AQLM
- **AQLM**Egiazarian等人,2024)将**加法量化**(多码本向量量化)应用于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位索引 = 8个权重2字节 = 每个权重有效**2位**。AQLM在2位精度下达到了最先进的质量,在这个极限压缩水平上优于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矩阵乘法都要便宜得多,并且可以在没有浮点单元的设备上实现LLM推理。对于当前模型,质量权衡是显著的,但随着规模和训练时量化感知能力的提高而改善。
### 微缩放(MX)格式
- **微缩放**(MX)格式是一种新的行业标准(由AMD、Arm、Intel、Meta、Microsoft、NVIDIA、Qualcomm支持),使用**块浮点**:一组元素共享一个指数,每个元素有自己的尾数。
| 格式 | 共享指数 | 元素位数 | 总计(每元素) | 等价 |
|--------|----------------|-------------|--------------------|----|
| MXFP8 | 每块8位 | 8E4M3/E5M2 | ~8 | 类似FP8,范围更好 |
| MXFP6 | 每块8位 | 6 | ~6.5 | 介于FP8和INT4之间 |
| MXFP4 | 每块8位 | 4 | ~4.5 | 类似INT4,但有浮点行为 |
| MXINT8 | 每块8位 | 8(整数) | ~8.5 | INT8,带共享缩放 |
- 共享指数将指数成本分摊到一个块(通常16-32个元素)。每个元素比单独指数时保留更多尾数位,每位的精度更好。MX格式预计将在未来硬件中替代单独的FP8和INT8格式。
### FP8训练
- 在FP8中训练(不仅仅是推理)现在在NVIDIA Hopper和Blackwell GPU上可行。方案如下:
- **前向传播**:权重和激活值使用E4M3(更高精度,更窄范围)。Transformer Engine使用延迟缩放(跟踪上一次迭代的统计信息,应用于当前迭代)动态计算每张量缩放因子。
- **反向传播**:梯度使用E5M2(更宽范围,更低精度)。梯度的值范围比权重/激活值更广,因此额外的指数位防止溢出。
- **主权重**:以FP32维护,用于优化器状态(就像使用FP16的标准混合精度训练,第6章)。FP8计算仅用于矩阵乘法,不用于权重更新。
- **损失缩放**:FP8仍然需要,就像FP16一样。动态损失缩放器调整缩放因子,使梯度值保持在FP8的可表示范围内。
- FP8训练在大多数模型规模上达到与BF16训练相当的质量,吞吐量提高约2倍。它是在H100集群上进行新的大规模训练运行的默认选择。
## 激活值量化
- 激活值(层之间流动的中间张量)也可以量化,实现完全INT8计算(权重和激活值都是INT8,INT32累加)。
- **动态量化**:在运行时根据实际激活值计算缩放因子。更准确(适应每个输入),但增加开销(每层计算最小值/最大值或百分位数)。
- **静态量化**:在校准期间计算一次缩放因子并固定。推理时更快(无需运行时统计),但如果校准数据不具代表性则精度较低。
- **逐token量化**:为序列中的每个token计算单独的缩放因子。对LLM至关重要,因为不同token的激活值幅度可能差异很大(某些token的激活值比其他token大100倍)。
- 激活值量化比权重量化更难,因为激活值依赖于数据(它们随每个输入变化),而权重是固定的。"异常值"问题尤其严重:少数激活通道具有极值(平均值的100倍),用与正常通道相同的缩放因子量化它们会浪费精度。
- **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)在操作级别实现动态混合精度:每个矩阵乘法根据张量统计信息在FP8和FP16之间选择,最大化吞吐量同时保持质量。
## KV缓存量化
- 在LLM生成过程中,**KV缓存**存储所有先前token的键和值张量。对于长序列,这主导了内存:
$$\text{KV缓存大小} = 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维头,序列长度128KFP16$2 \times 80 \times 64 \times 128 \times 131072 \times 2 = 330$ GB。这超过了GPU内存。
- **KV缓存量化**通过将缓存的键和值以INT8或INT4而不是FP16存储来减少内存。量化误差在序列中累积(每个新token关注所有缓存的K/V),但使用逐通道或逐头量化后,降级是可以接受的。
- **KV缓存量化具有乘法级收益**:它支持更长的序列(更多上下文)、更大的批次大小(更多并发用户)和更快的推理(加载缓存所需的内存带宽更少)。这是LLM服务中影响最大的优化之一。
## 编程任务(使用CoLab或notebook
1. 从头实现对称INT8量化。量化一个权重矩阵,反量化它,并测量作为值分布函数的重建误差。
```python
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"原始: {weights.nbytes / 1024:.0f} KB")
print(f"量化后: {q.nbytes / 1024:.0f} KB ({weights.nbytes / q.nbytes:.0f}x 更小)")
print(f"平均绝对误差: {jnp.abs(weights - recon).mean():.6f}")
print(f"最大绝对误差: {jnp.abs(weights - recon).max():.6f}")
print(f"相对误差: {jnp.abs(weights - recon).mean() / jnp.abs(weights).mean():.4%}")
```
2. 演示异常值问题。创建具有几个极端通道的激活值,展示逐张量量化失败而逐通道量化成功。
```python
import jax.numpy as jnp
import jax
key = jax.random.PRNGKey(42)
# 激活值:大多数通道正常,2个通道有100x异常值
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) # 异常通道
# 逐张量量化(整个张量一个缩放因子)
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
# 逐通道量化(每通道一个缩放因子)
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"逐张量误差: {err_tensor:.6f}")
print(f"逐通道误差: {err_channel:.6f}")
print(f"逐通道好 {err_tensor / err_channel:.1f}x")
print(f"\n异常通道浪费了 {(activations.shape[1] - 2) / activations.shape[1]:.0%} "
f"的量化范围给 {2 / activations.shape[1]:.1%} 的通道")
```
3. 计算不同模型大小和序列长度的KV缓存内存。展示为什么KV缓存量化对长上下文模型至关重要。
```python
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 (估计)", 120, 96, 128),
]
print(f"{'模型':<15} {'序列长度':>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()
```