Optimum 文档

量化

您正在查看 主分支 版本,需要从源代码安装. 如果您需要使用 pip 安装,请查看最新稳定版本 (v1.23.1).
Hugging Face's logo
加入 Hugging Face 社区

并获得增强版文档体验

开始使用

量化

量化是一种通过使用低精度数据类型(如 8 位整数 (int8) 而不是通常的 32 位浮点数 (float32) 来表示权重和激活来减少运行推理的计算和内存成本的技术。

减少位数意味着生成的模型需要更少的内存存储,消耗更少的能量(理论上),并且矩阵乘法等操作可以使用整数算术运算更快地执行。它还允许在嵌入式设备上运行模型,这些设备有时只支持整数数据类型。

理论

量化的基本原理非常简单:从权重和激活的高精度表示(通常是常规的 32 位浮点数)到更低精度的的数据类型。最常见的低精度数据类型是

  • float16,累积数据类型 float16
  • bfloat16,累积数据类型 float32
  • int16,累积数据类型 int32
  • int8,累积数据类型 int32

累积数据类型指定了对所讨论数据类型的值进行累积(加、乘等)的结果的类型。例如,让我们考虑两个 int8A = 127B = 127,并将 C 定义为 AB 的总和

C = A + B

这里,结果远大于 int8 中可表示的最大值 127。因此,需要使用更大精度的数据类型来避免巨大的精度损失,这会导致整个量化过程失效。

量化

两种最常见的量化情况是 float32 -> float16float32 -> int8

量化为 float16

执行量化从 float32 转换为 float16 相当简单,因为两种数据类型都遵循相同的表示方案。在将操作量化为 float16 时,您需要问自己的问题是

  • 我的操作是否有 float16 实现?
  • 我的硬件是否支持 float16?例如,英特尔 CPU 一直支持 float16 作为存储类型,但计算是在转换为 float32 后进行的。完整的支持将在 Cooper Lake 和 Sapphire Rapids 中实现。
  • 我的操作是否对较低精度敏感?例如,LayerNorm 中 epsilon 的值通常非常小(约 1e-12),但 float16 中可表示的最小值约为 6e-5,这会导致 NaN 问题。对大值也是如此。

量化为 int8

执行量化从 float32 转换为 int8 更为棘手。int8 中只能表示 256 个值,而 float32 可以表示非常广泛的值范围。关键在于找到最佳方法将我们 float32 值的范围 [a, b] 投影到 int8 空间。

让我们考虑一个范围 [a, b] 中的浮点数 x,然后我们可以写出以下量化方案,也称为仿射量化方案

x = S * (x_q - Z)

其中

  • x_q 是与 x 关联的量化 int8
  • SZ 是量化参数
    • S 是比例,是一个正的 float32
    • Z 称为零点,它是对应于 float32 域中的值 0int8 值。这对于准确地表示值 0 至关重要,因为 0 在整个机器学习模型中都被使用。

[a, b]x 的量化值 x_q 可以按如下方式计算

x_q = round(x/S + Z)

float32 值超出 [a, b] 范围的值将被剪裁到最接近的可表示值,因此对于任何浮点数 x

x_q = clip(round(x/S + Z), round(a/S + Z), round(b/S + Z))

通常 round(a/S + Z) 对应于所考虑数据类型中最小的可表示值,而 round(b/S + Z) 对应于最大的可表示值。但是,这可能会有所不同,例如,当使用对称量化方案时,您将在下一段中看到这一点。

对称量化方案和仿射量化方案

上面的等式被称为仿射量化方案,因为从 [a, b]int8 的映射是一个仿射映射。

此方案的一个常见特例是对称量化方案,其中我们考虑一个对称的浮点值范围 [-a, a]。在这种情况下,整数空间通常是 [-127, 127],这意味着 -128 被从常规的 [-128, 127] 有符号 int8 范围内排除。原因是拥有一个对称范围允许 Z = 0。虽然 256 个可表示值中的一个值丢失了,但它可以提供加速,因为许多加法运算可以跳过。

注意:要了解量化参数SZ是如何计算的,你可以阅读用于高效整数运算推理的神经网络量化和训练论文,或Lei Mao的博客文章,内容涉及该主题。

张量级和通道级量化

根据你所针对的精度/延迟权衡,你可以使用量化参数的粒度进行调整。

  • 量化参数可以在张量级基础上计算,这意味着每个张量将使用一对(S, Z)
  • 量化参数可以在通道级基础上计算,这意味着可以为张量维度上的一个元素存储一对(S, Z)。例如,对于形状为[N, C, H, W]的张量,对第二维进行通道级量化将导致拥有C(S, Z)。虽然这可以获得更好的精度,但它需要更多的内存。

校准

以上部分描述了如何将float32量化为int8,但还有一个问题:float32值的[a, b]范围是如何确定的?这就是校准发挥作用的地方。

校准是量化过程中计算float32范围的步骤。对于权重来说,这很容易,因为实际范围在量化时是已知的。但对于激活来说就不那么清楚了,而且存在不同的方法。

  1. 训练后动态量化:每个激活的范围在运行时动态计算。虽然这在无需太多工作的情况下就能取得很好的效果,但由于每次计算范围都需要额外的开销,因此它可能比静态量化慢一些。在某些硬件上,它也不是一个选项。
  2. 训练后静态量化:每个激活的范围在量化时提前计算,通常是通过将代表性数据传递给模型并记录激活值来完成。在实践中,步骤如下:
    1. 在激活上放置观察器以记录其值。
    2. 在校准数据集上进行一定数量的前向传递(大约200个示例就足够了)。
    3. 根据某些校准技术计算每个计算的范围。
  3. 量化感知训练:每个激活的范围在训练时计算,遵循与训练后静态量化相同的思路。但使用“伪量化”操作符代替观察器:它们记录值的方式与观察器相同,但它们还模拟量化带来的误差,让模型适应它。

对于训练后静态量化和量化感知训练,都需要定义校准技术,最常见的技术有:

  • 最小值-最大值:计算出的范围是[观察到的最小值,观察到的最大值],这对权重非常有效。
  • 移动平均最小值-最大值:计算出的范围是[观察到的移动平均最小值,观察到的移动平均最大值],这对激活非常有效。
  • 直方图:记录值的直方图以及最小值和最大值,然后根据某些标准进行选择。
    • 熵:范围被计算为最小化全精度数据和量化数据之间误差的范围。
    • 均方误差:范围被计算为最小化全精度数据和量化数据之间均方误差的范围。
    • 百分位数:范围是使用给定的百分位数值p在观察值上计算得出的。这样做是为了尝试将p%的观察值包含在计算出的范围内。虽然在进行仿射量化时可以实现这一点,但在进行对称量化时并不总是能够完全匹配。你可以查看ONNX Runtime中的实现方式,以了解更多细节。

将模型量化为 int8 的实践步骤

要有效地将模型量化为int8,需要遵循以下步骤:

  1. 选择要量化的操作符。值得量化的操作符是那些在计算时间方面占主导地位的操作符,例如线性投影和矩阵乘法。
  2. 尝试训练后动态量化,如果速度足够快,则停止在此处,否则继续执行步骤 3。
  3. 尝试训练后静态量化,它可能比动态量化更快,但通常会降低精度。在要量化的位置对模型应用观察器。
  4. 选择一种校准技术并执行。
  5. 将模型转换为其量化形式:删除观察器并将float32操作符转换为其int8对应项。
  6. 评估量化模型:精度是否足够好?如果是,则停止在此处,否则从步骤 3 重新开始,但这次使用量化感知训练。

🤗 Optimum 中支持的量化工具

🤗 Optimum 提供 API,使用不同的工具针对不同的目标执行量化。

  • optimum.onnxruntime 包允许使用 ONNX Runtime 工具量化和运行 ONNX 模型
  • optimum.intel 包能够在尊重精度和延迟约束的情况下量化 🤗 Transformers 模型。
  • optimum.fx 包提供了围绕PyTorch 量化函数 的包装器,以允许在 PyTorch 中对 🤗 Transformers 模型进行图形模式量化。与上面提到的两个相比,这是一个更低级的 API,它提供了更大的灵活性,但需要你付出更多努力。
  • optimum.gptq 包允许使用 GPTQ量化和运行 LLM 模型

进一步了解:机器如何表示数字?

这部分内容对于理解其他部分并不重要。它简要解释了计算机中数字的表示方式。由于量化是将一种表示形式转换为另一种表示形式,因此了解一些基础知识可能会有所帮助,但这绝对不是必须的。

计算机中最基本的数据表示单位是位。计算机中的所有内容,包括数字,都以位序列的形式表示。但表示形式取决于要表示的数字是整数还是实数。

整数表示

整数通常用以下位长表示:8163264。在表示整数时,要考虑两种情况:

  1. 无符号(正)整数:它们简单地表示为一系列位。每个位对应一个二的幂(从0n-1,其中n是位长),所得数字是这些二的幂的总和。

例如:19表示为无符号 int8 为00010011,因为

19 = 0 x 2^7 + 0 x 2^6 + 0 x 2^5 + 1 x 2^4 + 0 x 2^3 + 0 x 2^2 + 1 x 2^1 + 1 x 2^0
  1. 有符号整数:表示有符号整数并不那么简单,存在多种方法,最常见的是二进制补码。有关更多信息,您可以查看Wikipedia 页面上的主题。

实数表示

实数通常使用以下位长表示:163264。表示实数的两种主要方法是

  1. 定点:为表示整数部分和小数部分保留了固定数量的位。
  2. 浮点:表示整数部分和小数部分的位数可以变化。

浮点表示可以表示更大的值范围,这是我们将重点介绍的表示方法,因为它是最常用的。浮点表示包含三个组成部分

  1. 符号位:它是指定数字符号的位。
  2. 指数部分
  • float16 中有 5 位
  • bfloat16 中有 8 位
  • float32 中有 8 位
  • float64 中有 11 位
  1. 尾数
  • float16 中有 11 位(显式存储 10 位)
  • bfloat16 中有 8 位(显式存储 7 位)
  • float32 中有 24 位(显式存储 23 位)
  • float64 中有 53 位(显式存储 52 位)

有关每种数据类型的位分配的更多信息,请查看 Wikipedia 页面上关于bfloat16 浮点格式的精美插图。

对于实数x,我们有

x = sign x mantissa x (2^exponent)

参考文献

< > 在 GitHub 上更新