datawhale 2411组队学习:模型压缩4 模型量化理论(数据类型、int8量化方法、PTQ和QWT)
- 《datawhale2411组队学习之模型压缩技术1:模型剪枝(上)》:介绍模型压缩的几种技术;模型剪枝基本概念、分类方式、剪枝标准、剪枝频次、剪枝后微调等内容
- 《datawhale11月组队学习 模型压缩技术2:PyTorch模型剪枝教程》:介绍PyTorch的prune模块具体用法
- 《datawhale11月组队学习 模型压缩技术3:2:4结构稀疏化BERT模型》:介绍基于模式的剪枝——2:4结构稀疏化及其在BERT模型上的测试效果
语言模型一直在变大。例如PaLM 有 5400 亿参数,OPT、GPT-3 和 BLOOM 有大约 1760 亿参数,而且我们仍在继续朝着更大的模型发展。下图总结了最近的一些语言模型的尺寸。
迄今为止,市面上显存最大的 GPU 芯片是 80GB 显存的 A100,因此这些模型无法在单个设备上运行。举个例子,如果我们使用 BLOOM-176B
模型的 Bfloat16 版本,其大小为
176
×
1
0
9
×
2
b
y
t
e
=
352
G
B
176 \times 10^9 \times 2 byte=352GB
176×109×2byte=352GB。仅推理 BLOOM-176B
模型,就需要 8 个 80GB A100 。而如果要微调的话,则需要更多。
移动端的硬件资源是有限的,比如内存和算力。而量化可以最直观的减少模型的大小,从而减少内存、硬盘和算力的占用。同时,量化可以提高模型的推理速度。下图为不同数据类型的加法和乘法操作的耗时对比。
以最常见的int8模型量化来说,其零花过程可以分为两部分:将模型从 fp32
转换为 int8
;使用 int8
进行推理,整个量化过程都和数据类型的转换息息相关,所以我们先讲解最基础的数据类型。
一、数据类型
1.1 整型
如下图所示,整型数据可以分为无符号整型(Unsigned Integer
)和有符号整型(Signed Integer
)。
- 无符号整型:数据范围为 0 到 2 n − 1 2^{n-1} 2n−1,n 为数据位数。
- 有符号整型:
- 原码表示(
Sign-Magnitude Representation
):其实现的原理是取二进制数的最高位(左起第一位)为符号位,约定符号位为0时表示正数,符号位为1时表示负数,其余二进制位则用于待表示数值的绝对值。数据范围为 − 2 n − 1 − 1 -2^{n-1}-1 −2n−1−1 到 2 n − 1 − 1 2^{n-1}-1 2n−1−1,n 为数据位数。 - 补码表示(
Two's Complement Representation
):为了弥补原码表示,有 +0 和 -0 两种表示的缺点,最高位除了具有符号表示的功能,也具有权重值。数据范围为 − 2 n − 1 -2^{n-1} −2n−1 到 2 n − 1 − 1 2^{n-1}-1 2n−1−1,n 为数据位数。
- 原码表示(
1.2 定点数
定点数,即在表示小数数据时,把小数点的位置已经约定好固定在某个位置。与之对应的是浮点数,其小数点的位置不是固定的。如下图所示,指定蓝色部分为符号位,绿色部分为整数位,橙色部分为小数位。
图中,蓝色格表示
−
2
3
-2^3
−23,绿色格依次表示
2
0
,
2
1
,
2
2
2^0,2^1,2^2
20,21,22,橙色格依次表示
2
−
1
,
2
−
2
,
2
−
3
,
2
−
4
2^{-1},2^{-2},2^{-3},2^{-4}
2−1,2−2,2−3,2−4。将每个格数字(0或1)乘以对应scale就是最终结果。
1.3 浮点数
定点数表示法采用固定的位数来表示整数和小数部分,这样能很容易地进行加减运算,但范围有限。而浮点数的计算方式不同于定点数中的简单相加,它利用指数来扩展数值的动态范围。这样的计算方式允许浮点数在有限的位数下表示极大的数值范围,例如从非常小的数(接近于零)到极大的数(如 1 0 38 10^{38} 1038 )。
1.3.1 正规浮点数(fp32)
在1EEE 754标准中,我们用fraction
表示小数部分,exponent
表示指数部分。二者的的位数分别决定了数据的精度和表示范围大小。对于FP32浮点数来说,其计算方式为:
f
p
32
=
(
−
1
)
s
i
g
n
⋅
(
1
+
f
r
a
c
t
i
o
n
)
⋅
2
e
x
p
o
n
e
n
t
−
127
fp32 = (-1)^{sign}·(1+fraction)·2^{exponent-127}
fp32=(−1)sign⋅(1+fraction)⋅2exponent−127
如上图所示,FP32格式使用8位来表示指数,因此它的无符号整数范围是0到255。如果我们希望指数可以表示负值,就需要一个偏置。通过设定偏置为127,指数范围可以表示为:
指数实际值 = 存储的指数值 − 127 指数实际值 = 存储的指数值 - 127 指数实际值=存储的指数值−127
选择 2 ( 8 − 1 ) − 1 = 127 2^{(8-1)}-1=127 2(8−1)−1=127作为偏置的原因是为了确保实际指数范围对称。正规浮点数中,存储的指数范围是从 1 到 254(即存储值从 00000001 到 11111110,全0和全1是特特殊情况),对应的实际指数范围是:−126到+127,可以均匀表示小于1的数和大于1的数。
对于一个标准化(正规)浮点数,通常表示为:
value = ( − 1 ) sign × 1. fraction × 2 exponent \text{value} = (-1)^{\text{sign}} \times 1.\text{fraction} \times 2^{\text{exponent}} value=(−1)sign×1.fraction×2exponent
1. fraction 1.\text{fraction} 1.fraction 是尾数部分,其中 1 是隐含的,也就是说,虽然在存储尾数时,这个1并没有明确存储在内存中,但它默认存在,而 fraction 是实际存储的尾数部分。
例如对于数值 6.25,它可以表示为:
6.25 = 1.100 1 2 × 2 2 6.25 = 1.1001_2 \times 2^2 6.25=1.10012×22
在内存中,它会存储为:
value = ( − 1 ) 0 × 1.100 1 2 × 2 2 \text{value} = (-1)^{0} \times 1.1001_2 \times 2^2 value=(−1)0×1.10012×22
但在内存存储时,不会显式存储前面的 1,只存储 . 100 1 2 .1001_2 .10012,这样就省去了1位的存储空间。
1.3.2 非正规浮点数(fp32)
非正规浮点数是指数部分的值为最小值(即 0),但尾数部分不为零的浮点数。在这种情况下,尾数部分的表示不再以 1 开头,而是以 0 开头,所以叫非正规浮点数。数学表达式为:
( − 1 ) sign × 0. fraction × 2 exponent − bias (-1)^{\text{sign}} \times 0.\text{fraction} \times 2^{\text{exponent} - \text{bias}} (−1)sign×0.fraction×2exponent−bias
此时,指数部分强制为 1-bias = -126 。此时,非正规浮点数的有效数值范围是非常小的。
- 正常浮点可表示的最小正值为 fraction = 0,exponent = 1,结果为
2
−
126
2^{-126}
2−126 。
( 1 + 0 ) ⋅ 2 1 − 127 = 2 − 126 (1+0)·2^{1-127} = 2^{-126} (1+0)⋅21−127=2−126 - 正常浮点数可表示的最大值为 fraction = 2 − 23 2^{-23} 2−23,结果为 ( 1 + 1 − 2 − 23 ) ⋅ 2 127 (1+1-2^{-23})·2^{127} (1+1−2−23)⋅2127 。
- 非正规浮点可表示的最小正值为 fraction =
2
−
23
2^{-23}
2−23,结果为
2
−
149
2^{-149}
2−149 。
2 − 23 ⋅ 2 1 − 127 = 2 − 149 2^{-23}·2^{1-127} = 2^{-149} 2−23⋅21−127=2−149 - 非正规浮点可表示的最大值为 fraction 部分全为1 ,结果为 2 − 126 − 2 − 149 2^{-126}-2^{-149} 2−126−2−149。
- 正规浮点数的小数部分全为0,指数部分全为1时分别表示正无穷和负无穷(
inf
和-inf
)。
所以,非正规浮点数主要用于表示非常小的数值,确保在接近零时,尽可能保持一定的精度,提高数值稳定性和减少精度丢失问题。
1.3.3 其它数据类型
机器学习中常用的数据类型还有:
- 当expontent位数为5,fraction位数为11时,为fp16。
f p 16 = ( − 1 ) s i g n ⋅ ( 1 + f r a c t i o n ) ⋅ 2 e x p o n e n t − 15 fp16 = (-1)^{sign}·(1+fraction)·2^{exponent-15} fp16=(−1)sign⋅(1+fraction)⋅2exponent−15
指数的值从-14到15,共30个区间。能表示的最大数是 2 ( 30 − 15 ) = 65504 2^{(30-15)}=65504 2(30−15)=65504,最小的非零数是 2 − 14 2^{-14} 2−14。
- 当expontent位数为8,fraction位数为7时,为bf16。
b f 16 = ( − 1 ) s i g n ⋅ ( 1 + f r a c t i o n ) ⋅ 2 e x p o n e n t − 127 bf16 = (-1)^{sign}·(1+fraction)·2^{exponent-127} bf16=(−1)sign⋅(1+fraction)⋅2exponent−127 - 当expontent位数为4,fraction位数为3时,为fp8(E4M3)。
f p 8 = ( − 1 ) s i g n ⋅ ( 1 + f r a c t i o n ) ⋅ 2 e x p o n e n t − 7 fp8 = (-1)^{sign}·(1+fraction)·2^{exponent-7} fp8=(−1)sign⋅(1+fraction)⋅2exponent−7 - 当expontent位数为5,fraction位数为2时,为fp8(E5M2)。
f p 8 = ( − 1 ) s i g n ⋅ ( 1 + f r a c t i o n ) ⋅ 2 e x p o n e n t − 15 fp8 = (-1)^{sign}·(1+fraction)·2^{exponent-15} fp8=(−1)sign⋅(1+fraction)⋅2exponent−15
1.3.4 浮点数误差
浮点数在计算机中的表示会涉及到精度误差,原因在于其有限的表示方式。
在计算机中,所有数字都通过二进制表示。对于一个十进制小数如9.23
,计算机需要将其映射到二进制表示中,并确定其所在的范围和精确度。以fp16
类型来说,正整数部分被划分为多个区间:
[1,2)
[2,4)
[4,8)
[8,16)
...
所以9.23
被划分在
[
8
,
16
)
[8, 16)
[8,16)区间内(即
2
3
2^3
23到
2
4
2^4
24之间)。这种区间划分使得9.23
可以通过以下方式表示:
- 确定区间起点(8,即2的3次方)
- 确定在此区间内的相对位置(9.23比8多1.23)
为了表示9.23
的1.23
部分,该区间被细分为1024
个等份(fp16的尾数部分为10bit),每个等分大小为
s
=
8
/
1024
=
0.0078125
s=8/1024=0.0078125
s=8/1024=0.0078125。
由于每个区间段的大小有限,计算机只能在这些细分点中找到最接近的数值,而1.23落在区间157s到158s之间,所以1.23只能被近似表示为
157
×
s
=
1.2265625
≈
1.2266
157\times s=1.2265625\approx 1.2266
157×s=1.2265625≈1.2266,这意味着浮点数9.23
在fp16中会被表示为8 + 1.2266 = 9.2266,这种近似导致精度损失。总结就是:
- 二进制无法精确表示某些十进制小数(如1.23),因其需被转化为有限的二进制数。
- 细分区间的有限分辨率带来了约数值的逼近误差。
显而易见的是, 随着数值范围的增大,精度的损失越严重,因为尾数是固定划分为1024个区间的。数学上来说,就是 f r a c t i o n ⋅ 2 e x p o n e n t − b i a s fraction·2^{exponent-bias} fraction⋅2exponent−bias扩大了精度损失。
1.3.5 浮点数导致的模型训练问题
FP16在深度学习模型训练中是一种精度较低的表示方式,容易出现浮点数溢出与精度损失问题。
- 上溢出
FP16的最大正数为65504。当试图表示超过这个数的值(如65505)时,浮点数不会发生溢出,但会发生精度损失。当浮点数超过fp16能表示的最大值才会发生上溢出,,通常表示为无穷大inf
。
- 下溢出
在softmax,sigmoid,attention
等涉及指数运算的过程中,容易导致上溢出问题。一个常见的优化方法是在计算前先进行归一化,比如对输入图片(0-255的ndarray表示)归一化到[-1,1]
;或者是训练时使用fp32
类型。
如果数值非常小,则可能发生下溢出,即数值被“舍入”到零,这对于一些非常小的数值表示时会有问题。比如模型后期梯度计算中梯度等于e-7,或者是在标准化(如Batch Normalization、Layer Normalization)操作中需要计算 x ′ = x − μ σ 2 {x}'=\frac{x-\mu }{\sigma^{2} } x′=σ2x−μ,当batch size很小或者数值都很接近时,方差过小,也可能导致数值下溢出。
- nan
指数计算和均值计算中,当数值很大时,都有可能出现nan
m1=torch.tensor(100000,dtype=torch.float16).cuda()
m2=torch.tensor(200000,dtype=torch.float16).cuda()
print(torch.exp(m1)/(torch.exp(m1)+torch.exp(m2)))
tensor(nan,device='cuda:0,dtype=torch.float16)
最简单的避免方式是减去最大值,有严格的数学推导:
print(torch.exp(m1-max(m1,m2))/(torch.exp(m1-max(m1,m2))+torch.exp(m2-max(m1,m2)))
均值计算中也容易出现nan,
s=torch.tensor(500000*[1000],dtype=torch.float16)
print(torch.mean(s))
另外,模型输入的异常值可能导致数值溢出或不稳定,进而导致模型计算产生NaN值;有时,当batch size过大时,可能会导致计算过程中的溢出问题。
- 相加精度损失
当一个较小的数值加到一个较大的数值时,可能会因为精度限制导致“加了等于没加”的情况。
import torch
pi_16=torch.tensor(3.141,dtype=torch.float16)
print("3.141的float16表示",pi_16)
pi_32=torch.tensor(3.141,dtype=torch.float32)
print("3.141的float32表示",pi_32)
print("3.141的float16转float32表示",pi_16.float())
3.141的float16表示 tensor(3.1406, dtype=torch.float16)
3.141的float32表示 tensor(3.1410)
3.141的float16转float32表示 tensor(3.1406)
s=torch.tensor(0.0005,dtype=torch.float16)
print(pi_16+s)
print(pi_32+s)
tensor(3.1406, dtype=torch.float16)
tensor(3.1415)
在训练过程中,尤其是模型微调时,更新的梯度通常会很小,学习率也很小。参数更新时,由
W
(
t
+
1
)
=
W
(
t
)
−
η
⋅
g
r
a
d
i
e
n
t
W^{(t+1)} = W^{(t)} - \eta \cdot gradient
W(t+1)=W(t)−η⋅gradient可知,可能会因为精度不足导致“加了等于没加”的情况,从而无法更新参数,训练过程停滞,即因为数值精度丢失,导致模型无法有效更新。下图为 SSD 网络在训练过程中的梯度统计,有67%的值下溢出变成 0。
为了避免梯度消失(即“下溢出”),可以放大损失函数(Loss function):
- 在损失计算后,使用FP32表示来存储损失函数,防止精度丢失。
- 放大损失函数的数值,使其足够大,从而避免下溢出。
- 然后使用FP16表示的梯度进行更新,并在更新后将结果缩小到原始范围。
二、量化基本方法
2.1 int8量化
INT8量化 是一种常见的低位量化方法,它将浮点数(通常是32位浮点数)映射到8位整数(INT8)表示。量化后的数值范围通常是 [-128, 127],但不包括 -128,这有助于优化推理。以下是 INT8 量化的基本步骤:
- 计算量化步长(scale)和零点(zero-point)。
- 将浮点数值映射到整数,通常使用线性公式: q ( x ) = round ( x − zero_point scale ) q(x) = \text{round}\left(\frac{x - \text{zero\_point}}{\text{scale}}\right) q(x)=round(scalex−zero_point) 这样可以将浮动值转化为低精度整数表示,减少计算资源消耗。
模型量化对象主要包括以下几个方面:
- 权重(Weights):量化权重是最常见和流行的方法,它可以减少模型大小、内存使用和空间占用。
- 激活(Activations):在实践中,激活通常占内存使用的大部分。因此,量化激活不仅可以大大减少内存使用,而且与权重量化结合时,可以充分利用整数计算来实现性能提升。
- KV缓存(KV cache):量化KV缓存对于提高长序列生成的吞吐量至关重要。
- 梯度(Gradients):与上面相比,梯度稍微不常见,因为它们主要用于训练。训练深度学习模型时,梯度通常是浮点数。量化梯度主要用于减少分布式计算中的通信开销,也可以减少后向传递过程中的成本。
2.1.1 k-means 量化
K-means 量化是一种用于压缩和加速神经网络模型的技术,通过对权重进行聚类到不同的簇,将其近似为一组有限的中心值(质心),从而降低存储和计算复杂度。下图展示了整个过程:
-
weights (32-bit float):神经网络中的原始权重矩阵,数据以32位浮点数存储。不同颜色的方块代表分到不同的簇。
-
cluster index (2-bit int):权重经过 k-means 聚类,被分配到不同的簇,得到每个权重的簇索引矩阵。这样权重只需要使用2位整型就可以进行表示,显著减少存储所需的位数。
-
centroids:计算每个簇的质心,这些质心是簇内权重的平均值。此时,每个簇内的权重都可以用一个质心来近似表示。例如:
c l u s t e r 0 = m e a n ( − 0.98 , − 1.08 , − 0.91 , − 1.03 ) = − 1 cluster_0=mean(-0.98, -1.08, -0.91, -1.03)=-1 cluster0=mean(−0.98,−1.08,−0.91,−1.03)=−1 -
gradient:原始的梯度值
-
group by和reduce:根据 cluster index 将梯度矩阵中的梯度值按所属簇分组。每个簇内的梯度被视为一组。对每个簇内的梯度进行汇总求和,得到簇的梯度更新值。
-
fine-tuned centroids:利用 reduce 得到的簇梯度更新值和学习率,更新每个簇的质心。新的质心用于更新权重,使其在量化后仍能接近最佳解。
c i ′ = c i − η ⋅ g i c_i' = c_i - \eta \cdot g_i ci′=ci−η⋅gi
最终,存储占用从 32bit x 16 = 512 bit = 64 B => 2bit x 16 + 32 bit x 4 = 32 bit + 128 bit = 160 bit = 20 B。当weight矩阵更大时,压缩比例将会更大。
- 推理时,我们读取转换表,根据索引值获取对应的值。
- 训练时,我们将gradient按照weights的聚类方式进行聚类相加,反向传播到转换表,更新转换表的值。
以下是将上一节的剪枝和k-means 量化结合起来的压缩流程。
- 循环进行微调和剪枝,得到最优的剪枝模型。
- k-means 量化将剪枝后的参数进行聚类,将聚类的索引值存储在模型中,并构建相应的索引表,并使用哈夫曼编码进一步压缩。
2.1.2 线性量化
线性量化是将原始浮点数据和量化后的定点数据之间建立一个简单的线性变换关系,因为卷积、全连接等网络层本身只是简单的线性计算,因此线性量化中可以直接用量化后的数据进行直接计算。
2.1.2.1 零点量化(Zero-Point Quantization)
我们用 r r r 表示浮点实数, q q q 表示量化后的定点整数。浮点和整型之间的换算公式为:
r = S ( q − Z ) r = S(q - Z) r=S(q−Z)
q
=
r
o
u
n
d
(
r
/
S
+
Z
)
q = round(r / S + Z)
q=round(r/S+Z)
其中,
S
S
S 是量化放缩的尺度,表示实数和整数之间的比例关系,
Z
Z
Z 是偏移量,表示浮点数中的 0 经过量化后对应的数(量化偏移),根据偏移量
Z
Z
Z是否为0,可以将浮点数的线性量化分为对称量化(
Z
Z
Z=0)和非对称量化(
Z
Z
Z≠0)。大多数情况下量化是选用无符号整数,比如INT8的值域为[0,255],这种情况下需要要用非对称量化。
S
S
S和
Z
Z
Z的计算方法为:
S
=
r
m
a
x
−
r
m
i
n
q
m
a
x
−
q
m
i
n
S = \frac{r_{max} - r_{min}}{q_{max} - q_{min}}
S=qmax−qminrmax−rmin
Z
=
r
o
u
n
d
(
q
m
a
x
−
r
m
a
x
S
)
Z = round(q_{max}-\frac{r_{max}}{S})
Z=round(qmax−Srmax)
其中, r m a x r_{max} rmax 和 r m a x r_{max} rmax分别表示浮点数中的最小值和最大值, q m a x q_{max} qmax 和 q m i n q_{min} qmin分别表示定点数中的最小值和最大值。
下面举一个例子来详细说明。如下图所示,给定一个矩阵,可以通过上面的公式计算出Z和S。
可进一步利用上面的公式计算出量化后的矩阵。
2.1.2.2 绝对最大( absmax )量化
absmax 量化通过找到输入数据或权重的绝对值最大值来计算缩放因子,将数据映射到指定的整数范围内。在绝对最大量化(absmax 量化)中,整个计算过程可以用以下公式来描述:
-
确定缩放因子 S S S: 计算输入矩阵 X X X 的绝对最大值:
S = absmax ( X ) Q max S = \frac{\text{absmax}(X)}{Q_{\text{max}}} S=Qmaxabsmax(X)
其中, absmax ( X ) = max ( ∣ X i , j ∣ ) \text{absmax}(X) = \max(|X_{i,j}|) absmax(X)=max(∣Xi,j∣) 表示 X X X 中的绝对值最大元素, Q max Q_{\text{max}} Qmax 是量化整数的最大绝对值。例如,对于 8 位量化, Q max = 127 Q_{\text{max}} = 127 Qmax=127。
-
量化映射公式: 将原始输入矩阵 X X X 转换为量化后的矩阵 X quant X_{\text{quant}} Xquant:
X quant = round ( X S ) X_{\text{quant}} = \text{round}\left(\frac{X}{S}\right) Xquant=round(SX)
这里, round \text{round} round 表示对结果进行四舍五入,以确保 X quant X_{\text{quant}} Xquant 是整数。
-
反量化公式(从量化值恢复原始浮点值):
X dequant = r o u n d ( X quant × S ) X_{\text{dequant}} = round(X_{\text{quant}} \times S) Xdequant=round(Xquant×S)
假设输入矩阵 X X X 为:
X = [ 0.5 − 1.2 2.4 − 0.7 ] X = \begin{bmatrix} 0.5 & -1.2 \\ 2.4 & -0.7 \end{bmatrix} X=[0.52.4−1.2−0.7]
步骤 1:计算 S S S:
absmax ( X ) = 2.4 , S = 2.4 127 ≈ 0.0189 \text{absmax}(X) = 2.4, \quad S = \frac{2.4}{127} \approx 0.0189 absmax(X)=2.4,S=1272.4≈0.0189
步骤 2:计算 X quant X_{\text{quant}} Xquant:
X quant = round ( X S ) = round ( [ 0.5 0.0189 − 1.2 0.0189 2.4 0.0189 − 0.7 0.0189 ] ) = round ( [ 26.5 − 63.5 127 − 37 ] ) X_{\text{quant}} = \text{round}\left(\frac{X}{S}\right) = \text{round}\left(\begin{bmatrix} \frac{0.5}{0.0189} & \frac{-1.2}{0.0189} \\ \frac{2.4}{0.0189} & \frac{-0.7}{0.0189} \end{bmatrix}\right) = \text{round}\left(\begin{bmatrix} 26.5 & -63.5 \\ 127 & -37 \end{bmatrix}\right) Xquant=round(SX)=round([0.01890.50.01892.40.0189−1.20.0189−0.7])=round([26.5127−63.5−37]) X quant = [ 27 − 64 127 − 37 ] X_{\text{quant}} = \begin{bmatrix} 27 & -64 \\ 127 & -37 \end{bmatrix} Xquant=[27127−64−37]
步骤 3:反量化 X dequant X_{\text{dequant}} Xdequant:
X dequant = X quant × S = [ 27 − 64 127 − 37 ] × 0.0189 ≈ [ 0.51 − 1.21 2.4 − 0.7 ] X_{\text{dequant}} = X_{\text{quant}} \times S = \begin{bmatrix} 27 & -64 \\ 127 & -37 \end{bmatrix} \times 0.0189 \approx \begin{bmatrix} 0.51 & -1.21 \\ 2.4 & -0.7 \end{bmatrix} Xdequant=Xquant×S=[27127−64−37]×0.0189≈[0.512.4−1.21−0.7]
区别 | 绝对最大量化(Absmax 量化) | 零点量化(Zero-point 量化) |
---|---|---|
核心思想 | 基于绝对最大值进行量化 | 引入零点,使数据能更好适应非对称分布 |
缩放因子计算 | S = absmax ( X ) Q max S = \frac{\text{absmax}(X)}{Q_{\text{max}}} S=Qmaxabsmax(X) | S = max ( X ) − min ( X ) Q max − Q min S = \frac{\text{max}(X) - \text{min}(X)}{Q_{\text{max}} - Q_{\text{min}}} S=Qmax−Qminmax(X)−min(X) |
零点计算 | 无需计算零点 | Z = Q max − max ( X ) S Z = Q_{\text{max}} - \frac{\text{max}(X)}{S} Z=Qmax−Smax(X) |
量化公式 | X quant = round ( X S ) X_{\text{quant}} = \text{round}\left(\frac{X}{S}\right) Xquant=round(SX) | X quant = round ( X S ) + Z X_{\text{quant}} = \text{round}\left(\frac{X}{S}\right) + Z Xquant=round(SX)+Z |
反量化公式 | X dequant = X quant × S X_{\text{dequant}} = X_{\text{quant}} \times S Xdequant=Xquant×S | X dequant = ( X quant − Z ) × S X_{\text{dequant}} = (X_{\text{quant}} - Z) \times S Xdequant=(Xquant−Z)×S |
实现复杂度 | 简单,计算和存储开销小 | 较复杂,需要额外计算零点 |
适用场景 | 由于这种方法使用对称量化(即数据在正负区间使用相同的范围),它适用于输入数据中零点(即数据中心)接近 0 的情况 | 数据偏移明显,需更高精度的动态推理任务,比如量化神经网络的中间激活值或在输入数据非对称的情况下,提升模型的表达能力。 |
2.2 INT4 和 FP4量化
- INT4量化将权重和激活值从高位宽(如INT8或FP32)压缩到4位整数(INT4,表示范围为 -8 到 7)
- FP4量化是将浮点数(例如FP32)压缩为4位浮点数表示的方法,其表示的范围根据不同的指数位和小数位而有所不同。这是一个相对较新的量化策略,保留了浮点数的结构,包括符号位、指数位和尾数位。
2.3 二值量化 (Binarization)
在二值量化中,模型的权重或激活值被限制为两个离散值,通常是 -1 和 1 。这样可以大幅减少模型的存储需求,因为每个参数只需要一位 bit 来表示。二值神经网络的计算也可以大幅加速,因为二值运算比浮点运算要简单得多。
二值化(Binarization)的具体实现有两种方法:确定性二值化(Deterministic Binarization) 和 随机二值化(Stochastic Binarization)。
-
确定性二值化(Deterministic Binarization):
- 直接根据一个阈值(通常是0)计算位值,结果为符号函数:
q = sign ( r ) = { + 1 , r ≥ 0 − 1 , r < 0 q = \text{sign}(r) = \begin{cases} +1, & r \geq 0 \\ -1, & r < 0 \end{cases} q=sign(r)={+1,−1,r≥0r<0 - 即,如果输入大于等于0,则输出1;否则输出-1。
- 直接根据一个阈值(通常是0)计算位值,结果为符号函数:
-
随机二值化(Stochastic Binarization):
- 使用全局统计或输入数据的值来确定输出为 -1 或 +1 的概率。例如,在 Binary Connect (BC) 方法中,概率由 sigmoid 函数
σ
(
r
)
\sigma(r)
σ(r) 确定:
q = { + 1 , with probability p = σ ( r ) − 1 , with probability 1 − p q = \begin{cases} +1, & \text{with probability } p = \sigma(r) \\ -1, & \text{with probability } 1 - p \end{cases} q={+1,−1,with probability p=σ(r)with probability 1−p
其中, σ ( r ) = min ( max ( r + 1 2 , 0 ) , 1 ) \sigma(r) = \min(\max(\frac{r+1}{2}, 0), 1) σ(r)=min(max(2r+1,0),1)。 - 这种方法的实现较为困难,因为量化时需要硬件生成随机比特。
- 使用全局统计或输入数据的值来确定输出为 -1 或 +1 的概率。例如,在 Binary Connect (BC) 方法中,概率由 sigmoid 函数
σ
(
r
)
\sigma(r)
σ(r) 确定:
上图展示了在二值化中最小化量化误差的方法。为了更好地逼近原始权重,二值权重
W
B
W^\mathbb{B}
WB 乘以一个缩放因子
α
\alpha
α,计算后得到缩放后的二值权重
α
W
B
\alpha W^\mathbb{B}
αWB,其中 n 为矩阵中元素的个数。
α
=
1
n
∥
W
∥
1
\alpha = \frac{1}{n} \| W \|_1
α=n1∥W∥1
通过引入缩放因子,误差从9.28减少到9.24,显示出缩放对减小误差的效果。
2.4 三值量化 (Ternary Quantization)
在三值量化中,模型的权重或激活值被限制为三个离散值,通常是 -1 、 0 和 1 。相比二值量化,三值量化允许模型拥有一个额外的零值,这可以压缩模型参数的同时保留模型的精度。
三值量化的具体规则如下:
q
=
{
r
t
,
r
>
Δ
0
,
∣
r
∣
≤
Δ
−
r
t
,
r
<
−
Δ
q = \begin{cases} r_t, & r > \Delta \\ 0, & |r| \leq \Delta \\ -r_t, & r < -\Delta \end{cases}
q=⎩
⎨
⎧rt,0,−rt,r>Δ∣r∣≤Δr<−Δ
其中,
Δ
=
0.7
×
E
(
∣
r
∣
)
\Delta = 0.7 \times \mathbb{E}(|r|)
Δ=0.7×E(∣r∣),
r
t
=
E
∣
r
∣
>
Δ
(
∣
r
∣
)
r_t = \mathbb{E}_{|r|>\Delta}(|r|)
rt=E∣r∣>Δ(∣r∣)
如上图所示为三值量化的具体示例,展示了一个权重矩阵 $ W $ 如何被量化为三值权重矩阵。
-
量化的阈值 Δ \Delta Δ 被计算为:
Δ = 0.7 × 1 16 ∥ W ∥ 1 = 0.73 \Delta = 0.7 \times \frac{1}{16} \|W\|_1 = 0.73 Δ=0.7×161∥W∥1=0.73
其中, ∥ W ∥ 1 \|W\|_1 ∥W∥1 是原始权重矩阵 W W W 的 L1 范数,即所有元素绝对值的平均值。 -
确定非零权重的值 r t r_t rt:
r t = 1 11 ∥ W W T ≠ 0 ∥ 1 = 1.5 r_t = \frac{1}{11} \|W_{W^T \neq 0}\|_1 = 1.5 rt=111∥WWT=0∥1=1.5
其中, ∥ W W T ≠ 0 ∥ 1 \|W_{W^T \neq 0}\|_1 ∥WWT=0∥1 是非零权重的 L1 范数。
二值量化和三值量化可以显著减小模型的尺寸和加速推理速度,但通常也会导致模型精度的下降。因此,这些方法常用于对精度要求不太高的应用场景或需要在低计算资源环境下运行的场景中。
2.5 混合精度量化
混合精度量化是结合多种量化方法的技术,例如在不同的层或不同的模型部分使用不同精度的量化方法。例如,某些计算密集型的层可能使用 INT8 量化,而其他层使用 INT16 或 FP16 量化,以达到精度和效率之间的平衡。
三、 训练后量化 (Post-Training Quantization)
训练后量化是指在训练完成后,对模型进行量化,因此也叫做离线量化,可分为对称量化和非对称量化(量化零点是否为 0)。根据量化粒度区分,训练后量化又分为逐张量量化和逐通道量化以及组量化。
量化误差来自两方面,一个是clip操作,一个是round操作。如何选取量化时所用参数(如scaling factor,zero point)可以尽可能地减少对准确率的影响呢?这也是我们需要关注的地方。
3.1 量化粒度
量化通常会导致模型精度下降,不同的量化粒度下降程度不一样。
3.1.1 逐张量量化(Per-Tensor Quantization)
逐张量量化(Per-Tensor Quantization)是指对整个张量使用相同的量化参数(缩放因子和零点)来进行量化。但是在张量之间应用相同的参数会导致精度下降,因为张量内参数值的范围可能会有所不同。如下图的红框所示,3个channel共享一个量化参数。但是我们可以看到不同channel的数据范围是不同的。因此当 Layer-wise 量化效果不好时,需要对每个channel进行分别量化。
3.1.2 逐通道量化(Per Channel Quantization)
逐通道量化是将数据按照通道维度进行拆分,分别对每一通道的数据进行量化。由于现阶段模型越来越大,每个通道的参数也原来越多,参数的数值范围也越来越大,因此我们需要更细粒度的量化方式。逐通道量化可以更准确地捕获不同通道中的变化,比起逐张量量化,精度损失更少,但需要更多的存储空间(存储多个r和S)。
下图演示了逐张量量化和逐通道量化过程。最终对比原始矩阵 W 与量化之后重建矩阵之间的误差 ∥ W − W ′ ∥ F = ∑ i , j ( W i , j − W i , j ′ ) 2 \|W - W'\|_F = \sqrt{\sum_{i,j} (W_{i,j} - W'_{i,j})^2} ∥W−W′∥F=∑i,j(Wi,j−Wi,j′)2可以看出,逐通道量化量化损失更小,但需要的存储空间更大。
3.1.3 组量化(Group Quantization)
与逐通道量化或逐张量量化不同,组量化将一个通道内的数据拆分为多个较小的向量组,然后进行两级量化:
-
先对较小粒度的组(如向量)使用较简单的整数缩放因子 S q S_q Sq进行缩放:
r 向量 = S q ⋅ ( q − Z ) r_{\text{向量}} = S_q \cdot (q - Z) r向量=Sq⋅(q−Z) -
再对较大粒度的整体(如通道)使用更复杂的浮点数缩放因子 γ \gamma γ进行缩放:
r = γ ⋅ r 向量 = γ ⋅ S q ⋅ ( q − Z ) r = \gamma \cdot r_{\text{向量}} = \gamma \cdot S_q \cdot (q - Z) r=γ⋅r向量=γ⋅Sq⋅(q−Z)
第一步目的是在较细粒度的范围内进行初步量化,适应不同的向量分布特性,减少量化误差;第二步将先前量化的结果进行全局调整。这种方法通过结合不同粒度的缩放因子,实现了精度和硬件效率的平衡。
存储开销分析:假设我们使用 4-bit 量化,即每个元素都被量化为一个 4-bit 整数。组量化中,为了更高的精度,每 16 个元素(即一个组)共享一个 4-bit 的向量缩放因子
S
q
S_q
Sq。这个缩放因子需要额外存储,但它是每个组共享的,因此只需为每组存储一次。下面计算有效位宽(元素本身的位宽和每个向量缩放因子带来的额外存储):
4
bits
+
4
bits
16
个元素
=
4.25
bits
4 \text{ bits} + \frac{4 \text{ bits}}{16 \text{ 个元素}} = 4.25 \text{ bits}
4 bits+16 个元素4 bits=4.25 bits
通过适当分配粒度和缩放因子,存储开销得到了优化。
Microscaling(MX) 是一种具体实现两级缩放因子的方案,从微软的浮点(MSFP)数据类型演化而来。MX 系列(如 MX4、MX6、MX9)在不同的数据类型、缩放因子设计和组大小上有所区别,以便根据不同应用优化性能。每个向量的浮点比例因子被分成整数和浮点逐通道的分量,进一步提高量化的灵活性和精度。
3.2 动态量化参数的计算 ( Cliping )
上面介绍的都是静态量化,即使用固定的量化参数进行量化。动态量化参数是指在量化过程中,随着输入数据或模型运行情况的变化而实时更新的量化参数(例如,量化的范围或缩放因子)。这种动态更新使得量化更能适应不同的数据分布,减少精度损失,提高模型在推理过程中的性能。
动态量化一般应用于已经训练好的模型,用来优化推理过程中的速度和内存占用。它通过将模型的权重和激活值(在推理时)转换为低精度的整数(如8位),减少了存储需求和计算负担,从而提高推理效率,特别适用于推理设备(如移动设备、嵌入式系统)对存储和计算资源有限制的场景,也适合在生产环境中快速部署模型进行推理。
3.2.1 指数移动平均(EMA)
指数移动平均(Exponential Moving Average, EMA)是一种常用的统计方法,用于计算数据的指数移动平均值。
EMA 收集了训练过程中激活函数的取值范围
r
m
i
n
r_{min}
rmin 和
r
m
a
x
r_{max}
rmax,然后在每个 epoch 对这些取值范围进行平滑处理。EMA的计算公式如下:
r
m
i
n
,
m
a
x
t
+
1
=
α
r
m
i
n
,
m
a
x
t
+
(
1
−
α
)
r
m
i
n
,
m
a
x
t
+
1
r^{t+1}_{min,max} = \alpha r^{t}_{min,max} + (1-\alpha) r^{t+1}_{min,max}
rmin,maxt+1=αrmin,maxt+(1−α)rmin,maxt+1
其中, r m i n , m a x t r^{t}_{min,max} rmin,maxt 表示第 t t t 步的取值范围, α \alpha α 表示平滑系数。
3.2.2 Min-Max
Min-Max 是一种常用的校准方法,通过在训练好的 fp32 模型上跑少量的校准数据。统计校准数据的 r m i n , m a x r_{min,max} rmin,max 并取平均值作为量化参数。
3.2.3 KL 量化
KL 量化是用 KL 散度来衡量数据和量化后的数据之间的相似性;这种方法不是直接将 [ m i n , m a x ] [min, max] [min,max]v映射到 [ − 127 , 128 ] [-127,128] [−127,128],而是去寻找一个阈值 ∣ T ∣ < m a x ( ∣ m a x ∣ , ∣ m i n ∣ ) |T| < max(|max|, |min|) ∣T∣<max(∣max∣,∣min∣) ,将 [ − T , T ] [-T, T] [−T,T] 映射到 [ − 127 , 128 ] [-127, 128] [−127,128] 。并假设只要阈值选取得当,使得两个数据之间的分布相似,就不会对精度损失造成影响。
D K L ( P ∣ ∣ Q ) = ∑ i = 1 n P ( x i ) log P ( x i ) Q ( x i ) D_{KL}(P||Q) = \sum_{i=1}^nP(x_i)\log\frac{P(x_i)}{Q(x_i)} DKL(P∣∣Q)=i=1∑nP(xi)logQ(xi)P(xi)
3.2.4 均方误差(MSE)
均方误差量化是指通过最小化输入数据 X X X 和量化后的数据 Q ( X ) Q(X) Q(X) 之间的均方误差,计算得到最合适的量化参数。
m i n ∣ r ∣ m a x E ∣ ( X − Q ( X ) ) 2 ∣ min_{|r|_{max}}E|(X-Q(X))^2| min∣r∣maxE∣(X−Q(X))2∣
通过动态调整 | r | m a x |r|_{max} |r|max 来最小化均方误差。
- EMA方法能适应变化的数据分布,适用于实时或在线推理任务。
- Min-Max方法通过简单的最大最小值计算,速度快,适合较为稳定的数据分布,常见于硬件加速支持的低精度推理中,如嵌入式设备。
- KL量化侧重于保留数据的原始分布信息,适用于需要对数据分布高度保真、精度要求高的任务,比如文本生成或图像生成等任务中。
- MSE量化则关注量化后的误差最小化,适合科学计算等对误差控制严格的应用。
3.3 Rounding
在量化过程中,通常会将浮点数值(如32位浮点数)转化为整数(如8位整数),然而,舍入过程会引入误差,影响模型的预测准确性。传统的舍入方法(如向最近整数舍入)并不会考虑多个权重之间的舍入影响,可能导致某些权重在量化后与真实值的差异较大,影响最终输出的精度。
比如上图中,对每个权重的最近舍入不一定是对整个张量的最好舍入。
AdaRound
是一种改进的舍入策略,会根据量化误差的积累情况来动态调整舍入策略,从而减少累计的误差。简单来说就是在舍入时引入了一个 偏差项
σ
\sigma
σ ,会根据之前的舍入结果来调整下一次舍入的方向,而不是单纯地对每个权重进行四舍五入,用数学表示就是:
W
^
=
⌊
⌊
W
⌋
+
σ
⌉
\hat{W} = \lfloor \lfloor W \rfloor + \sigma \rceil
W^=⌊⌊W⌋+σ⌉
其中:
- W W W 是原始的浮动权重。
- ⌊ W ⌋ \lfloor W \rfloor ⌊W⌋ 是权重 W W W 向下取整后的值。
- σ ∈ [ 0 , 1 ] \sigma \in [0, 1] σ∈[0,1] 是一个在 [ 0 , 1 ] [0, 1] [0,1] 区间内的偏差项,用来决定是向上舍入还是向下舍入。具体而言, σ \sigma σ 会影响最终舍入的方向,优化量化后误差的大小。
四、 量化感知训练(Quantization-Aware Training)
论文《Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference》
传统的量化模型做法,是先用浮点数训练网络,然后进行量化。这种方法对于大规模模型有效,但对于小型模型常常会导致显著的精度下降。常见的失败模式包括:
- 权重范围差异过大:某些输出通道的权重范围差异超过100倍,导致量化时,范围较小的通道会有较大的相对误差。
- 异常权重值:某些权重值异常,影响量化后的精度。
量化感知训练(QAT)通过在训练的前向传播中模拟量化效果(对模型的权重和激活值进行量化),来让模型更适应低精度计算,避免直接的后期量化带来的问题,从而提高了最终量化后的模型精度。
量化操作(特别是低精度量化)会导致数据精度损失,这会影响模型的性能。如果在训练时不考虑这些量化带来的误差,训练出的模型可能在量化后表现不佳。因此,模拟量化的目的就是让模型在训练过程中感知到量化的影响,从而让模型调整参数,使得它可以更好地适应低精度计算环境。这样,训练得到的模型会在量化后保留较好的精度。
4.1 前向传播
QAT训练时所有计算是在高精度下完成的(FP32),但是在前向传播过程中加入了伪量化节点(quantization+dequantization),用于模拟模型量化时引起的误差。以INT8量化为例,QAT处理流程为:
-
在数据集上以FP32精度进行模型训练,得到训练好的baseline模型;
-
在baseline模型中插入伪量化节点(fake quantization),以便在训练期间模拟推理时的量化效果,其前向过程为:
- 输入量化: L a y e r N − 1 Layer_{N-1} LayerN−1 层的输出 Q ( X ) Q(X) Q(X) 作为下一层( L a y e r N Layer_{N} LayerN)的输入。 Q ( X ) Q(X) Q(X) 表示已经过量化和反量化的输入数据。
- 权重量化:在与输入进行卷积前,先对权重进行量化。当前层的权重 W W W 会通过量化反量化操作得到 Q ( W ) Q(W) Q(W),然后使用 Q ( W ) Q(W) Q(W) 计算得到输出 Y Y Y。
- 输出量化/激活值量化:在激活函数应用后,或者在像ResNet这样的跳跃连接中,量化激活值(输出
Y
Y
Y 经过量化反量化,得到
Q
(
Y
)
Q(Y)
Q(Y)),作为下一层
L
a
y
e
r
N
+
1
Layer_{N+1}
LayerN+1 的输入。
-
在模拟量化模式下训练直到收敛(即微调量化模型)。这一过程会考虑量化误差的影响,从而让模型适应低位宽(例如8位)表示。
-
训练完成后,模型的权重将被真正量化,并且在实际硬件上进行推理。
Tips:训练过程中,偏置不需要进行量化,它的量化参数是根据权重和激活的量化参数推断出来的。
论文中是在weights输入conv之前(weight quantization)以及activation之后(activation quantizaion)插入了伪量化节点,伪量化节点插入位置就是需要进行量化操作的位置。
量化过程通过以下函数实现:
- 将输入值
r
限制在区间[a, b]
内:
clamp ( r ; a , b ) = min ( max ( r , a ) , b ) \text{clamp}(r; a, b) = \min(\max(r, a), b) clamp(r;a,b)=min(max(r,a),b) - 计算量化步长,即在给定的量化范围
[a, b]
和量化级数n
下(8bit量化时,n=256),量化的间隔:
s ( a , b , n ) = b − a n − 1 s(a, b, n) = \frac{b - a}{n - 1} s(a,b,n)=n−1b−a - 将实数值
r
量化到[a, b]
范围内的离散级别。首先使用clamp
限制r
在[a, b]
范围内,然后根据步长s(a, b, n)
将其映射到n
个量化级别中的一个,并进行四舍五入。其公式为:
q ( r ; a , b , n ) = ( ⌊ clamp ( r ; a , b ) − a s ( a , b , n ) ⌉ ) ⋅ s ( a , b , n ) + a q(r; a, b, n) = \left( \left\lfloor \frac{\text{clamp}(r; a, b) - a}{s(a, b, n)} \right\rceil \right) \cdot s(a, b, n) + a q(r;a,b,n)=(⌊s(a,b,n)clamp(r;a,b)−a⌉)⋅s(a,b,n)+a
其中, ⌊ ⋅ ⌉ \left\lfloor \cdot \right\rceil ⌊⋅⌉表示四舍五入到最近的整数。
- 对于权重量化,通常通过取权重的最小值和最大值来确定量化范围,并对这些范围进行微调,确保零点值能够精确表示。
- 对于激活量化,通过在训练过程中使用指数加权平均(EMA)平滑激活范围。另外为了避免训练初期激活量化范围不稳定,通常在训练的前50,000到200万步内禁用激活量化。
量化过程中的学习会调整量化的边界[a, b]
,确保在量化后能精确表示零值。最终,学习到的量化参数会映射到公式中的比例因子S
和零点Z
,公式为:
S = s ( a , b , n ) , Z = z ( a , b , n ) S = s(a, b, n),Z = z(a, b, n) S=s(a,b,n),Z=z(a,b,n)
4.2 反向传播
量化感知训练的损失函数与普通训练的损失函数类似,但是量化后的权重是离散值。如图所示为 W W W 和 Q ( W ) Q(W) Q(W) 的关系。
由于
Q
(
W
)
Q(W)
Q(W)是离散值,故:
∂
Q
(
W
)
∂
W
=
0
\frac{\partial Q(\mathbf{W})}{\partial \mathbf{W}}=0
∂W∂Q(W)=0
梯度计算公式为:
g W = ∂ L ∂ W = ∂ L ∂ Q ( W ) ⋅ ∂ Q ( W ) ∂ W = 0 g_{\mathbf{W}}=\frac{\partial L}{\partial \mathbf{W}}=\frac{\partial L}{\partial Q(\mathbf{W})} \cdot \frac{\partial Q(\mathbf{W})}{\partial \mathbf{W}}=0 gW=∂W∂L=∂Q(W)∂L⋅∂W∂Q(W)=0
如果按照上述式子进行梯度计算,这样的话梯度就永远为 0,无法进行梯度更新。因此人们提出了一个修正的方式,被称为直通估计器(Straight-Through Estimator,STE)。在STE中,在STE中,假设量化和反量化操作不会改变权重,此时 W = Q ( W ) W = Q(W) W=Q(W), ∂ Q ( W ) ∂ W = 1 \frac{\partial{Q(W)}}{\partial{W}}=1 ∂W∂Q(W)=1 ,梯度公式可以转换为如下式子:
g W = ∂ L ∂ W = ∂ L ∂ Q ( W ) g_{\mathbf{W}}=\frac{\partial L}{\partial \mathbf{W}}=\frac{\partial L}{\partial Q(\mathbf{W})} gW=∂W∂L=∂Q(W)∂L
这样,量化感知训练就能够顺利地进行反向传播,进行权重更新。
原文地址:https://blog.csdn.net/qq_56591814/article/details/143836975
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!