# 量化概念

量化后的数值只能取一个很小的值集合中的一个，例如 int8 有 256 个值，int4 有 16 个值，因此每种量化方法都会引入舍入误差。不同方法的区别在于如何防止该误差损害模型的输出。本页将解释这些差异背后的选择：数值如何映射到该小值集合、哪些张量会被量化、使用哪些数据类型，以及量化发生在何时。

如果您是 Transformers 量化的新手，请从[概述](./overview)开始。

## 将值映射到更少的位

量化将高精度值的范围映射到低位类型所能表示的少量整数集合上。int8 可以表示 256 个整数，即 `[-128, 127]`，而 int4 可以表示 16 个。

*仿射量化*是最常见的映射方式。它取张量的最小值和最大值，并将整数范围拉伸到该区间上。在下方的示例中，一个取值范围为 -1.8 到 2.4 的张量被映射到 int8，其中 0.0 对应 -19。

```text
bf16  -1.8             0.0                    2.4
       ├────────────────┼──────────────────────┤
       ▼                ▼                      ▼
int8  -128             -19                    127
```

两个参数定义了该映射。

- *scale* `S` 是原始单位中一个整数步长的大小。

$$ S = \frac{val_{max} - val_{min}}{q_{max} - q_{min}} $$

- *零点* `Z` 是表示 `0.0` 的整数。

$$ Z = q_{min} - round\left(\frac{val_{min}}{S}\right) $$

值 `x` 通过除以缩放因子并平移零点，量化为整数 `q`。

$$ q = round\left(\frac{x}{S} + Z\right) $$

反量化会逆转映射并返回 `x` 的近似值。`x` 与其近似值之间的差异即为舍入误差。

$$ x \approx S \cdot (q - Z) $$

在上面的示例中，`S` 约为 0.0165，`Z` 为 -19。数值 0.5 量化后为 11，而 11 反量化后约为 0.494，舍入误差约为 0.006。

```text
┌─────┐  quantize   ┌────┐ dequantize   ┌───────┐
│ 0.5 │ ──────────▶ │ 11 │ ───────────▶ │ 0.494 │
└─────┘             └────┘              └───────┘
```

仿射量化有两种形式。

- *非对称*量化使用上述公式。它映射精确的范围，因此当权重不以零为中心时，不会浪费可表示的值。
- *对称*量化假设数值范围为 `[-a, a]`，并将 `Z` 固定为 0，因此只需存储缩放因子。其计算开销更低，但当权重偏向零的一侧时，部分可表示的数值将处于未使用状态。

## 选择粒度

对整个张量使用单一缩放因子的存储开销很小，但一个较大的离群值会拉宽数值范围，导致其他所有权重可表示的数值分布更加稀疏。更小的分组可以让每个缩放因子覆盖更窄的范围。

- Per-tensor 对整个张量使用一个缩放系数和一个零点。
- Per-channel 为每个输出通道使用一个缩放因子，即权重矩阵的每一行。
- 按组或按块的方式为每组连续值（例如 128 个权重）使用一个缩放因子。每组都需要额外的存储空间来保存其缩放因子。

下图展示了一个三行权重矩阵上的三种选项。每个 `s` 代表一个缩放因子。

```text
per-tensor        per-channel           per-group
┌────────────┐    ┌────────────┐        ┌──────┬──────┐
│            │    │            │ s0     │  s0  │  s1  │
│            │    ├────────────┤        ├──────┼──────┤
│     s      │    │            │ s1     │  s2  │  s3  │
│            │    ├────────────┤        ├──────┼──────┤
│            │    │            │ s2     │  s4  │  s5  │
└────────────┘    └────────────┘        └──────┴──────┘
```

大多数低比特方法按组进行量化。[GPTQ](./gptq) 默认使用 128 个权重为一组，[Fine-grained FP8](./finegrained_fp8) 默认使用 128x128 的块。[Data types](#data-types) 中的 FP4 格式将块大小直接内置于格式本身。

## 量化权重或激活值

不同的方法会对不同的张量进行量化，这决定了矩阵乘法在何处执行。

- *仅权重*方法以低精度存储权重，而激活值保持为 bf16 或 fp16。在每次矩阵乘法之前，内核会将权重反量化到更高精度的计算数据类型。bitsandbytes 4-bit、AWQ、GPTQ 和 HQQ 均采用这种方式。在 bitsandbytes 中，`bnb_4bit_compute_dtype` 用于设置该计算数据类型。
- *权重与激活*方法会对激活值也进行量化，矩阵乘法直接在低精度下执行。这需要硬件支持低精度矩阵乘法，例如近期 GPU 上的 FP8。该方案通常写作 W8A8，表示 8 位权重和激活。

仅权重量化方法可以节省内存，但并不总是能提升运行速度，因为反量化会为每次矩阵乘法增加额外开销。当模型处于*内存瓶颈*时，这类方法能加速生成过程。在逐 token 生成的过程中，GPU 在每一步的大部分时间都用于从内存读取权重，而非执行计算。读取 4 位权重所传输的数据量仅为 bf16 权重的四分之一，这一节省往往能够抵消反量化带来的开销。

权重与激活方法在模型处于*计算受限*状态时（例如在预填充阶段或处理大批量数据时）尤为有效，因为低精度矩阵乘法本身速度更快。部分方法会在两种模式之间进行切换。NVFP4 在预填充阶段执行 4 位权重、4 位激活的矩阵乘法，而在解码阶段执行 4 位权重、16 位激活的矩阵乘法。

## 数据类型

数据类型决定了量化数值可以表示的取值范围。

### int8 和 int4

int8 和 int4 表示等间距的整数，并使用上述的仿射映射。int4 只能表示 16 个值，因此其精度损失比 int8 更大。使用 int4 的方法通常按组进行量化，或使用校准来保持精度。

大多数硬件没有 4 位存储类型，因此两个 int4 值会被*打包*到一个字节中。例如，第一个值可以占用低 4 位，第二个值占用高 4 位。内核在计算之前会先解包这些值。

```text
┌──────┐  ┌──────┐        ┌──────┬──────┐
│ val1 │  │ val2 │        │ val2 │ val1 │
│ 0101 │  │ 0011 │   →    │ 0011 │ 0101 │  1 byte
└──────┘  └──────┘        └──────┴──────┘
  int4      int4            high   low
```

某些 4 位类型以不均匀的方式分布其可表示值，以匹配权重的分布情况。bitsandbytes 的 NF4 在零附近放置了更多的值，因为大多数权重都集中在该区域。

### FP8

FP8 在 8 位中保留了由符号、指数和尾数组成的浮点布局。其可表示的数值在零附近更密集，在绝对值较大时更稀疏，这与权重和激活值中数值的分布范围相契合。

FP8 有两种变体。

- E4M3 拥有 4 个指数位和 3 个尾数位。它的精度更高，但覆盖的数值范围更小。在推理阶段，它通常是权重和激活值的常用选择。
- E5M2 拥有 5 个指数位和 2 个尾数位。它以较低的精度换取了更宽的数值范围。

```text
E4M3   ┌─┬───────┬─────┐
       │S│E E E E│M M M│
       └─┴───────┴─────┘
E5M2   ┌─┬─────────┬───┐
       │S│E E E E E│M M│
       └─┴─────────┴───┘
  S sign   E exponent   M mantissa
```

FP8 矩阵乘法需要硬件支持，例如计算能力为 8.9 或更高的 NVIDIA GPU（Ada、Hopper、Blackwell）、AMD Instinct MI300 以及 Intel XPU。Transformers 通过 [Fine-grained FP8](./finegrained_fp8)、[FBGEMM FP8](./fbgemm_fp8) 和 [compressed-tensors](./compressed_tensors) 支持 FP8。

### 带块缩放的 FP4

FP4 包含 1 个符号位、2 个指数位和 1 个尾数位（E2M1）。它仅能表示 0、0.5、1、1.5、2、3、4 和 6 的正负值。由于可表示的数值极少，FP4 格式无法使用每个张量一个缩放因子的方式。它会将缩放因子附加到每个小的数值块上，并将这些缩放因子作为格式的一部分进行存储。

| 格式 | 块大小 | 按块缩放 | 额外缩放 |
|---|---|---|---|
| [MXFP4](./mxfp4) | 32 个值 | 8 位 2 的幂（E8M0） | 无 |
| [NVFP4](./nvfp4) | 16 个值 | FP8 | 每个张量一个 FP32 缩放 |

MXFP4 是一种微缩放（microscaling）格式。由于 2 的幂次缩放仅会移动指数，因此应用它的开销很低，但它只能落在 2 的幂次上。NVFP4 使用更小的块，并采用 FP8 缩放因子，其取值可以位于 2 的幂次之间。这保留了更高的精度，代价是需要一个逐张量（per-tensor）缩放因子，以确保 FP8 块缩放因子保持在有效范围内。[Four Over Six](./fouroversix) 和 [FP-Quant](./fp_quant) 同样可以生成 NVFP4 或 MXFP4 权重。

同样的分块思想也体现在 8 位上。MXFP8 使用 2 的幂次分块缩放因子来存储 FP8 值，而 [Fine-grained FP8](./finegrained_fp8) 则负责加载它。

### 码本

在 4 bit 以下，某些方法不再将每个权重四舍五入到最接近的可表示值，而是将权重分组，以*向量*的形式进行联合量化。码本（codebook）保存一组学习得到的向量，每组权重则以其最接近向量的索引形式存储。[AQLM](./aqlm) 默认使用 8 个权重为一组，并采用 16 位索引，相当于每个权重 2 bit。[VPTQ](./vptq) 同样使用码本。寻找合适的码本需要在校准数据上进行长时间的优化，因此这些模型会预先完成量化，并以检查点（checkpoint）的形式加载。

## 量化发生的时间

模型可以在训练后或训练过程中进行量化。Transformers 中的大多数方法都是在训练后进行量化。

### 训练后量化

*训练后量化（PTQ）*对已经训练好的模型进行量化，同时不改变其学到的内容。PTQ 方法的区别在于是否需要数据，这决定了该方法在文档目录中归属于哪一类。

- 在没有校准数据的情况下，缩放系数仅由权重决定，量化速度足够快，可以在模型加载过程中完成。bitsandbytes、HQQ 和 torchao 均采用这种方式。这些方法列在 *加载时量化* 下。
- 使用校准数据时，该方法会将一个小数据集输入模型，并利用激活值来决定如何量化每一层。在 4 位及以下精度时，校准通常能带来更高的精度，但耗时从几分钟到数小时不等。

校准方法以不同的方式利用激活值。GPTQ 逐列量化一个层，并调整其余列以补偿每一列的舍入误差。AWQ 会找到激活值最大的权重通道，并在舍入前对其进行放大，以保护这些通道。

校准运行的位置决定了你在 Transformers 中如何使用该方法。

- [GPTQ](./gptq) 在将 `dataset` 传递给 [GPTQConfig](/docs/transformers/main/zh/main_classes/quantization#transformers.GPTQConfig) 时，会在 `from_pretrained` 内部进行校准，因此它被列在 *加载时量化* 下。
- 其他校准方法，例如 AWQ、AutoRound、AQLM 和 VPTQ，在各自的库中进行校准。Transformers 加载其结果，因此它们列在 *加载预量化检查点* 下。

### 量化感知训练

*量化感知训练（QAT）*在训练过程中模拟量化。前向传播按量化的方式对数值进行舍入，而梯度则更新全精度权重，从而使模型学会容忍舍入误差。在极低比特宽度下，QAT 通常能提供最佳精度，但它需要对模型进行训练。

[BitNet](./bitnet) 模型以这种方式进行训练，其 1.58 位权重参与训练循环，而 Transformers 负责加载训练好的检查点。

## 资源

- [使用 Hugging Face 了解量化基础](https://www.deeplearning.ai/short-courses/quantization-fundamentals-with-hugging-face/)
- [深入量化](https://www.deeplearning.ai/short-courses/quantization-in-depth)
- [使用 🤗 和 💗🧑‍🍳 烹制的量化入门](https://huggingface.co/blog/merve/quantization)
- [EfficientML.ai 第 5 讲 - 量化（第一部分）](https://www.youtube.com/watch?v=RP23-dRVDWM)
- [从第一性原理出发，让深度学习飞速运转](https://horace.io/brrr_intro.html)
- [使用 PyTorch 加速生成式 AI 第二部分：LLM 优化](https://pytorch.org/blog/accelerating-generative-ai-2/)

