1. 前言
随着人工智能的发展,深度模型越来越大,主流模型通常包含大量参数,需要消耗大量算力。
神经网络通常用两个指标来衡量模型规模和计算复杂度:
参数量(Parameters):模型需要存储多少权重,影响模型大小和显存占用。
浮点运算量(FLOPs, Floating Point Operations):一次前向推理需要多少浮点计算,影响推理速度。
两者并不是线性对应关系。例如,一个参数可以被重复使用很多次,因此参数量相近的模型,FLOPs可能相差数倍。
下面列出一些常见神经网络的大致规模(以常见输入尺寸为准): | 模型 | 参数量 | FLOPs(单次前向)| | ----------------------------- | -----: | -----------: | | LeNet-5 | 6 万 | 0.002 GFLOPs | | AlexNet | 6100 万 | 0.72 GFLOPs | | VGG16 | 1.38 亿 | 15.5 GFLOPs | | VGG19 | 1.44 亿 | 19.6 GFLOPs | | ResNet18 | 1170 万 | 1.8 GFLOPs | | ResNet34 | 2180 万 | 3.6 GFLOPs | | ResNet50 | 2560 万 | 4.1 GFLOPs | | ResNet101 | 4450 万 | 7.8 GFLOPs | | ResNet152 | 6020 万 | 11.5 GFLOPs | | MobileNetV2 | 350 万 | 0.30 GFLOPs | | MobileNetV3-Large | 540 万 | 0.22 GFLOPs | | EfficientNet-B0 | 530 万 | 0.39 GFLOPs | | EfficientNet-B7 | 6600 万 | 37 GFLOPs | | Vision Transformer (ViT-B/16) | 8600 万 | 17.6 GFLOPs | | ViT-L/16 | 3.07 亿 | 61 GFLOPs | | Swin-T | 2800 万 | 4.5 GFLOPs | | Swin-B | 8800 万 | 15.4 GFLOPs |
实时地运行这些模型需要消耗大量内存和算力,这使得它们难以部署至Android和其他低功耗边缘设备。为了进一步推动深度神经网络模型在移动端/嵌入式设备中的快速部署,深度学习领域提出了一系列的模型压缩与加速方法:
剪枝(Parameter pruning) 低秩分解(Low-rank factorization) 知识蒸馏(Knowledge distillation) 量化(quantization)
量化(quantization)是模型压缩的一种常用方法,通常情况下可以使用不同的量化策略,将深度学习模型参数与运算的精度从浮点数(FP32)降低至较低的精度,如INT8,一方面可以提升模型在cpu/gpu等硬件的推理计算效率,减少计算成本,另一方面能够减小模型的size,在边缘设备具有存储优势。现在无论是服务器端或者是移动端,支持INT8量化都是一个发展趋势,比如阿里含光, 华为altlas, 华为npu, 高通npu,nvidia的tensorcore等计算设备均支持INT8计算。在Nvidia T4 GPU上(启用 Tensor Core)INT8算力是单精度算力的16倍。FP32 Mult运算能耗是INT8 Mult运算的18.5倍,芯片占用面积是INT8的27.3倍。
为什么T4 INT8算力比FP32高16倍?原因就是:Tensor Core本身就是按照低精度优化设计的。
| 类型 | FP32 | INT8 | | ------------ | ----- | ---- | | 每个数字 | 32bit | 8bit | | 内存 | ×4 | ×1 | | 带宽 | 大 | 小 | | Cache利用率 | 低 | 高 | | TensorCore吞吐 | 较低 | 极高 |
速度提升不仅来自计算,更重要的是:内存访问减少。现代GPU很多时候真正瓶颈不是ALU,而是Memory Bandwidth。INT8一下把数据量缩小4倍,很多模型自然就快了。
量化是一个信息有损压缩的过程,如果训练过程中使用FP32,在模型推理时使用Post-training Quantization(PTQ)直接量化为INT8模型,模型精度会存在一定损失。而量化感知训练(Quantization-aware-training, QAT)在模型训练过程中就引入了伪量化(Fake-quantization)来模拟量化过程中带来的误差,通过这种方式能够进一步减少量化后模型的精度损失。
2.Quantization-aware-training原理
Google在《Quantization and training of neural networks for efficient integer-arithmetic-only inference》论文中提出了QAT,在训练过程加入了模拟量化。相对于PTQ而言,QAT能够在量化模型后减少精度损失。
2.1 线性量化计算方法简介(Linear Quantization)
在介绍QAT之前,首先简单提一下量化的计算方法,这里以FP32到INT8的量化为例。量化的核心思想就是将浮点数区间的参数映射到INT8的离散区间中,论文中的公式为
r = S(q - Z) 其中: r:FP32值 q:INT8值 S:Scale Z:Zero Point 量化公式为: q = round(r/S) + Z
S、Z 分别为缩放因子(Scale-factor)和零点(Zero-Point),我们需要做的就是要确定 S、Z 两个参数。
其中:
S = (rmax - rmin) / (qmax - qmin) 其中 rmax、rmin 分别为统计得到的FP32权重浮点数的实际最大值与最小值,qmax、qmin 分别为量化格式理论最大值与最小值。 INT8量化中,qmax、qmin为-128、127,原始值将被映射至[-128,127]整数区间。 Z表示FP32浮点数 r=0 对应的量化值,因为实际的权重值不一定关于0对称。 将两套格式小于0的区间对齐:(Z - qmin) * S = -rmin,整理可得: Z = -rmin / S + qmin
Zero Point(Z)为什么必须是整数?
这是因为 INT8 的取值只能是整数,Zero Point 表示的是:
表示浮点数中的0.0在量化后对应的整数值
量化公式是:
q = round(r/S) + Z
这里 round(…) 的结果已经是整数。Z 如果还是浮点数,得到的就不是 INT8 整数,无法存储。所以 Z 必须是整数。
Zero Point 的计算过程中可以是浮点数,但最终保存和推理使用时一定是整数,真正保存时,会进行四舍五入:
Z=round(Z)
随后还要进行裁剪(Clip):
Z=clip(Z,qmin,qmax)
| 参数 | 数据类型 | 是否参与推理 | | ---------- | -------------------- | ------ | | Scale | 浮点数(FP32 或 FP16) | 是 | | Zero Point | 整数(INT8/UINT8/INT32)| 是 |
反量化公式: r=S(q−Z)
但要注意一个例外:对于很多深度学习框架(包括 TensorRT、PyTorch 的权重量化),并不会使用上面的完整公式,而是采用对称量化(Symmetric Quantization):
Z=0 不使用 rmin Scale 改为 S = max(rmin, rmax) / 127 例如: rmin = -5 rmax = 7 则 S = 7/127 ≈ 0.0551
这种方式实现更简单,推理效率也更高,因此权重量化几乎都采用对称量化;而激活值由于通常不是以 0 为中心,很多框架仍会采用非对称量化(Asymmetric Quantization)
2.2 为什么存在量化误差
模型量化精度损失的主要原因为量化-反量化(quantization-dequantization)过程中取整引起的误差。误差来源不是INT8本身,而是取整(Round)。
训练时所有浮点数都会经历:
FP32
↓
INT8
↓
FP32
为什么训练仍然需要FP32梯度
1.因为梯度本身是连续的小数,而INT8是离散整数,INT8分辨率太粗了,无法有效进行梯度下降。
例如:Scale=0.1, Z=0
那么:INT8 = 5
真正对应的是:FP32 = 0.5
训练时梯度往往只有 0.0001 甚至 0.000001
现在梯度告诉它:应该减去0.0001
结果变成0.4999
重新量化:Round(0.4999 / 0.1)=5,完全没有变化。
2.Round 不可导,大部分地方梯度都是 0,在整数边界处梯度不存在,如果直接反向传播梯度0,所有参数都不更新,训练直接失败。
那 Round 的梯度怎么办?
这里用了一个非常巧妙的方法:STE(Straight-Through Estimator,直通估计器)。
训练时前向:Round(x)
反向:直接假装Round(x)≈x
于是:梯度变成dRound(x)/dx≈1,而不是0
这就是为什么 Fake Quant 可以训练。
2.3 PTQ(Post Training Quantization)
FP32训练完成
│
▼
统计激活值范围(Calibration)
│
▼
计算 Scale、Zero Point
│
▼
直接变成 INT8
│
▼
部署
特点:
训练过程中模型完全不知道以后要量化。 最后一次性把 FP32 改成 INT8。 因此模型会突然遇到: Round() Clip() INT8 造成精度下降。
2.4 QAT(Quantization Aware Training)
FP32训练完成
│
▼
插入 FakeQuant 伪量化节点
│
▼
继续 Finetune
│
▼
保存 Scale、Zero Point
│
▼
导出 INT8
这里最大的区别就是:
模型在训练阶段,就已经经历了量化误差。
因此最后真正部署成 INT8 时,模型不会”措手不及”。
2.4.2 伪量化节点(Fake-quantize)
伪量化实际上是quantization+dequantization的结合,实际上就是模拟量化round引起的误差。
也就是:
FP32 ↓ 量化 ↓ INT8 ↓ 立即反量化 ↓ FP32 注意: 最终输出仍然是 FP32,但是已经包含了量化误差。 所以训练框架根本不需要支持 INT8 梯度。
Fake Quant 数学公式可以拆成三步理解
第一步:限制数据范围(Clamp) r′=clamp(r;a,b) 其中: a:FP32浮点数值最小值 b:FP32浮点数值最大值 这一步就是:截断(Clipping),如果超出量化范围[a,b],那么截断到[a,b] 第二步:计算 Scale s=(b-a)/(n-1) 其中: n:量化等级数 例如 INT8:n=256 所以: s=(b−a)/255 和上一节介绍的 Scale 完全一致。 第三步:真正执行 Fake Quant 可以理解为: Clamp ↓ 除Scale,减去a ↓ Round ↓ 乘Scale,加上a ↓ 输出
Fake Quant 不是为了模拟 INT8。真正模拟的是:Round() 带来的误差。
Fake Quant 插在哪里?
论文中的典型做法是在两个地方插入 Fake Quant:
Weight │ FakeQuant │ Conv │ Activation │ FakeQuant │ 下一层 即: Weight Quantization:卷积权重进入卷积之前。 Activation Quantization:激活输出之后。 原因是最终部署时,通常权重和激活都会量化为 INT8,因此训练时需要同时模拟这两类误差。
2.4.3 QAT finetune过程量化参数处理
伪量化节点会保存finetune过程中的量化参数,伪量化节点的计算公式中 [a,b] 即为FP32浮点数值的范围,这个值将会在finetune过程中进行估计与更新,上面介绍了伪量化节点分别weight quantization以及activation quantizaion:
对于weight quantization的量化节点,每次前向传播直接将 [a,b] 设置为weights的最大值与最小值即可,即 a=min(w), b=max(w) 对于activation quantizaion,处理方式类似于batch norm,使用了指数移动平均,在finetune的每个batch动态地更新 [a,b]
为什么叫指数移动平均(Exponential Moving Average,EMA)
普通移动平均(Moving Average)通常是最近 N 个值的简单平均,例如: (x1 + x2 + x3 + x4 + x5) / 5 而 EMA 不需要保存历史所有数据,只保存上一次 EMA 的结果即可。 vt = β⋅vt−1 + (1−β)⋅xt 其中: vt:当前时刻的移动平均值 xt:当前时刻的观测值(比如这个 batch 的激活值最大值) β:动量系数,通常取 0.9 ~ 0.999,越接近1,历史信息权重越大,平滑效果越强 vt−1:上一时刻的移动平均值 展开这个递推式: vt=(1−β)*xt + (1−β)β*xt−1 +(1−β)β^2*xt−2 +… 可以看到越久远的历史数据,权重按 β^k 指数衰减,所以叫指数移动平均。
weight quantization为什么不使用EMA?
最根本的原因:权重是参数,激活是数据 | Weight | Activation | | -------- | -------------- | | 网络参数 | 网络输出 | | 由优化器更新 | 由输入数据决定 | | 始终保存在模型中 | 每个 Batch 都重新计算 | | 当前时刻只有一份 | 每个 Batch 都不同 | 也就是说: Weight 是"固定资产",Activation 是"流数据"。
为什么 Activation 需要 EMA?
Activation 的统计值主要受输入数据影响。输入不同的图片,激活范围可能相差非常大。
如果每个 Batch 都直接更新 Scale,Fake Quant 每一步都会改变,训练就会非常不稳定。
为什么 Weight 不需要 EMA?
Weight 本身就是通过梯度下降连续变化的。并不会因为输入图片的不同突然发生巨大变化。
也就是说:权重已经天然是连续更新的。
更重要的原因:Weight 可以直接遍历
一句话总结
Activation 的分布主要由输入数据决定,每个 Batch 波动很大,因此需要 EMA 来获得稳定的量化参数;Weight 是模型参数,本身通过梯度下降平滑更新,并且可以随时完整遍历,因此直接根据当前权重计算 Scale 即可,使用 EMA 反而可能因为统计滞后而增大量化误差。
伪量化操作看起来输入输出没变,但是实际上模拟了量化 round 操作,将这种误差当做训练噪声,在 QAT finetune 时模型会主动适应这种噪声,从而减少最终 INT8 部署时的精度损失。