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
称为零点,它是在float32
领域中对应于值0
的int8
值。这对于能够精确表示值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
是如何计算的,您可以阅读 《为高效的纯整数算术推理而进行的神经网络量化与训练》 论文,或 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,提供了更大的灵活性,但需要您做更多的工作。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
- 博客文章 Basics of Quantization in Machine Learning (ML) for Beginners
- 博客文章 How to accelerate and compress neural networks with quantization
- 维基百科关于整数表示的页面在此和在此
- 维基百科页面关于