Optimum 文档
量化
并获得增强的文档体验
开始使用
量化
量化是一种通过使用低精度数据类型(如 8 位整数 (int8
))而不是常用的 32 位浮点数 (float32
) 来表示权重和激活值,从而降低运行推理的计算和内存成本的技术。
减少位数意味着生成的模型需要更少的内存存储,消耗更少的能量(理论上),并且矩阵乘法等操作可以使用整数算术更快地执行。它还允许在嵌入式设备上运行模型,这些设备有时只支持整数数据类型。
理论
量化背后的基本思想非常简单:将权重和激活值从高精度表示(通常是常规的 32 位浮点数)转换为较低精度的数据类型。最常见的较低精度数据类型是
float16
,累积数据类型float16
bfloat16
,累积数据类型float32
int16
,累积数据类型int32
int8
,累积数据类型int32
累积数据类型指定了数据类型值累积(加法、乘法等)结果的类型。例如,让我们考虑两个 int8
值 A = 127
、B = 127
,并将 C
定义为 A
和 B
的和
C = A + B
这里的结果远大于 int8
中最大的可表示值 127
。因此,需要更高精度的数据类型来避免巨大的精度损失,否则会使整个量化过程毫无用处。
量化
最常见的两种量化情况是 float32 -> float16
和 float32 -> 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
值S
和Z
是量化参数S
是比例,是一个正float32
Z
称为零点,它是int8
值,对应于float32
领域中的值0
。这对于能够精确表示值0
非常重要,因为它在机器学习模型中无处不在。
[a, b]
中 x
的量化值 x_q
可以按如下方式计算
x_q = round(x/S + Z)
并且 [a, b]
范围之外的 float32
值将被裁剪为最接近的可表示值,因此对于任何浮点数 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 个可表示值中丢失了一个值,但它可以提供加速,因为可以跳过许多加法运算。
注意:要了解如何计算量化参数 S
和 Z
,您可以阅读 Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference 论文,或关于此主题的 Lei Mao 的博客文章。
逐张量和逐通道量化
根据您定位的精度/延迟权衡,您可以调整量化参数的粒度
- 量化参数可以在逐张量的基础上计算,这意味着每个张量将使用一对
(S, Z)
。 - 量化参数可以在逐通道的基础上计算,这意味着可以沿张量的维度之一为每个元素存储一对
(S, Z)
。例如,对于形状为[N, C, H, W]
的张量,对于第二个维度具有逐通道量化参数将导致具有C
对(S, Z)
。虽然这可以提供更好的精度,但需要更多内存。
校准
上面的部分描述了从 float32
到 int8
的量化是如何工作的,但仍然存在一个问题:float32
值的 [a, b]
范围是如何确定的?这就是校准发挥作用的地方。
校准是量化期间计算 float32
范围的步骤。对于权重,这非常容易,因为实际范围在量化时是已知的。但对于激活值来说就不那么清楚了,并且存在不同的方法
- 训练后动态量化:每个激活值的范围在运行时动态计算。虽然这在不需要太多工作的情况下给出了很好的结果,但它可能比静态量化慢一点,因为每次计算范围都会引入开销。在某些硬件上,它也不是一个选项。
- 训练后静态量化:每个激活值的范围在量化时预先计算,通常是通过将代表性数据传递到模型并记录激活值来完成的。在实践中,步骤如下
- 观察者被放置在激活值上以记录其值。
- 在校准数据集上完成一定数量的前向传递(大约
200
个示例就足够了)。 - 根据某些校准技术计算每个计算的范围。
- 量化感知训练:每个激活值的范围在训练时计算,遵循与训练后静态量化相同的思路。但是,使用“伪量化”运算符而不是观察者:它们像观察者一样记录值,但它们也模拟量化引起的误差,以使模型适应它。
对于训练后静态量化和量化感知训练,都需要定义校准技术,最常见的是
- 最小值-最大值:计算的范围是
[观察到的最小值,观察到的最大值]
,这对于权重效果很好。 - 移动平均最小值-最大值:计算的范围是
[移动平均观察到的最小值,移动平均观察到的最大值]
,这对于激活值效果很好。 - 直方图:记录值的直方图以及最小值和最大值,然后根据某些标准进行选择
- 熵:范围计算为使全精度数据和量化数据之间的误差最小化的范围。
- 均方误差:范围计算为使全精度数据和量化数据之间的均方误差最小化的范围。
- 百分位数:范围是使用观察值的给定百分位数
p
计算的。其想法是尝试使p%
的观察值在计算的范围内。虽然这在进行仿射量化时是可能的,但在进行对称量化时,并不总是可以完全匹配。您可以查看 ONNX Runtime 中是如何完成的 以了解更多详细信息。
将模型量化为 int8 的实用步骤
要有效地将模型量化为 int8
,需要遵循以下步骤
- 选择要量化的运算符。要量化的好运算符是在计算时间方面占主导地位的运算符,例如线性投影和矩阵乘法。
- 尝试训练后动态量化,如果速度足够快,请在此处停止,否则继续执行步骤 3。
- 尝试训练后静态量化,这可能比动态量化更快,但通常会降低精度。将观察者应用于模型中要量化的位置。
- 选择一种校准技术并执行它。
- 将模型转换为其量化形式:删除观察者,并将
float32
运算符转换为其int8
对应项。 - 评估量化模型:精度是否足够好?如果可以,请在此处停止,否则从步骤 3 重新开始,但这次使用量化感知训练。
🤗 Optimum 中执行量化的支持工具
🤗 Optimum 提供了 API,可以使用不同的工具为不同的目标执行量化
optimum.onnxruntime
包允许使用 ONNX Runtime 工具 量化和运行 ONNX 模型。optimum.intel
包使您能够量化 🤗 Transformers 模型,同时尊重精度和延迟约束。optimum.fx
包围绕 PyTorch 量化函数 提供包装器,以允许在 PyTorch 中对 🤗 Transformers 模型进行图形模式量化。与上面提到的两个 API 相比,这是一个更低级别的 API,提供了更大的灵活性,但需要您付出更多的工作。optimum.gptq
包允许使用 GPTQ 量化和运行 LLM 模型。
深入了解:机器如何表示数字?
本节对于理解其余部分不是根本性的。它简要解释了数字在计算机中的表示方式。由于量化是关于从一种表示形式转换为另一种表示形式,因此了解一些基础知识可能很有用,但绝对不是强制性的。
计算机最基本的表示单元是位。计算机中的一切都表示为位序列,包括数字。但是,表示形式因所讨论的数字是整数还是实数而异。
整数表示
整数通常以以下位长表示:8
、16
、32
、64
。在表示整数时,考虑两种情况
- 无符号(正)整数:它们简单地表示为位序列。每个位对应于 2 的幂(从
0
到n-1
,其中n
是位长),结果数字是这些 2 的幂的总和。
示例: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
- 有符号整数:表示有符号整数不太直接,并且存在多种方法,最常见的是补码。有关更多信息,您可以查看关于该主题的 维基百科页面。
实数表示
实数通常以以下位长表示:16
、32
、64
。表示实数的两种主要方式是
- 定点数:保留固定数量的位数来表示整数部分和小数部分。
- 浮点数:用于表示整数部分和小数部分的位数可以变化。
浮点表示可以表示更大的值范围,这是我们将重点关注的一种,因为它是最常用的。浮点表示中有三个组成部分
- 符号位:这是指定数字符号的位。
- 指数部分
float16
中为 5 位bfloat16
中为 8 位float32
中为 8 位float64
中为 11 位
- 尾数
float16
中为 11 位(显式存储 10 位)bfloat16
中为 8 位(显式存储 7 位)float32
中为 24 位(显式存储 23 位)float64
中为 53 位(显式存储 52 位)
有关每种数据类型的位分配的更多信息,请查看维基百科页面上关于 bfloat16 浮点格式 的精美插图。
对于实数 x
,我们有
x = sign x mantissa x (2^exponent)
参考
- Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference 论文
- 机器学习 (ML) 初学者量化基础知识 博客文章
- 如何使用量化加速和压缩神经网络 博客文章
- 关于整数表示的维基百科页面 此处 和 此处
- 关于以下内容的维基百科页面