作者 | Oldpan 编辑 | 江大白
点击下方卡片,关注“自动驾驶之心”公众号
ADAS巨卷干货,即可获取
点击进入→自动驾驶之心【模型部署】技术交流群
导读
INT8量化是一种深度学习推理加速技术,可以将32位浮点数格式的神经网络权重和激活值转换为8位整数格式,从而大幅降低神经网络的计算量和存储空间需求。本文分享了神经网络的INT8量化教程,值得收藏学习。
开篇
刚开始接触神经网络量化是2年前那会,用NCNN和TVM在树莓派上部署一个简单的SSD网络。那个时候使用的量化脚本是参考于TensorRT和NCNN的PTQ量化(训练后量化)模式,使用交叉熵的方式对模型进行量化,最终在树莓派3B+
上部署一个简单的分类模型(识别剪刀石头布静态手势)。
转眼间过了这么久啦,神经网络量化应用已经完全实现大面积落地了、相比之前成熟多了!
我工作的时候虽然也简单接触过量化,但感觉还远远不够,趁着最近项目需要,重新再学习一下,也打算把重新学习的路线写成一篇系列文,分享给大家。
本篇系列文的主要内容计划从头开始梳理一遍量化的基础知识以及代码实践。因为对TensorRT比较熟悉,会主要以TensorRT的量化方式进行描述以及讲解。不过TensorRT由于是闭源工具,内部的实现看不到,咱们也不能两眼一抹黑。所以也打算参考Pytorch、NCNN、TVM、TFLITE的量化op的现象方式学习和实践一下。
当然这只是学习计划,之后可能也会变动。对于量化我也是学习者,既然要用到这个技术,必须要先理解其内部原理。而且接触了挺长时间量化,感觉这里面学问还是不少。好记性不如烂笔头,写点东西记录下,也希望这系列文章在能够帮助大家的同时,抛砖引玉,一起讨论、共同进步。
参考了以下关于量化的一些优秀文章,不完全统计列了一些,推荐感兴趣的同学阅读:
神经网络量化入门--基本原理(https://zhuanlan.zhihu.com/p/149659607)
从TensorRT与ncnn看卷积网络int8量化(https://zhuanlan.zhihu.com/p/387072703)
模型压缩:模型量化打怪升级之路 - 1 工具篇(https://zhuanlan.zhihu.com/p/355598250)
NCNN Conv量化详解(一)(https://zhuanlan.zhihu.com/p/71881443)
当然在学习途中,也认识了很多在量化领域经验丰富的大佬(田子宸、JermmyXu等等),嗯,这样前进路上也就不孤单了。
OK,废话不多说开始吧。
Why量化
我们都知道,训练好的模型的权重一般来说都是FP32也就是单精度浮点型,在深度学习训练和推理的过程中,最常用的精度就是FP32。当然也会有FP64、FP16、BF16、TF32等更多的精度:
![33a6d5178c215ab72e11cfce278b8b86.jpeg](https://i-blog.csdnimg.cn/blog_migrate/3322e1d4eb3413eb7e8563932c986832.jpeg)
FP32 是单精度浮点数,用8bit 表示指数,23bit 表示小数;FP16半精度浮点数,用5bit 表示指数,10bit 表示小数;BF16是对FP32单精度浮点数截断数据,即用8bit 表示指数,7bit 表示小数。TF32 是一种截短的 Float32 数据格式,将 FP32 中 23 个尾数位截短为 10 bits,而指数位仍为 8 bits,总长度为 19 (=1 + 8 + 10) bits。
对于浮点数来说,指数位表示该精度可达的动态范围,而尾数位表示精度。之前的一篇文章中提到,FP16的普遍精度是~5.96e−8 (6.10e−5) … 65504
,而我们模型中的FP32权重有部分数值是1e-10
级别。这样从FP32->FP16会导致部分精度丢失,从而模型的精度也会下降一些。
![d51ce21711defb93938a3632d8b01640.jpeg](https://i-blog.csdnimg.cn/blog_migrate/9c23b07ac9fbd341bfe2185508c0c624.jpeg)
其实从FP32->FP16也是一种量化,只不过因为FP32->FP16几乎是无损的(CUDA中使用__float2half
直接进行转换),不需要calibrator
去校正、更不需要retrain
。
而且FP16的精度下降对于大部分任务影响不是很大,甚至有些任务会提升。NVIDIA对于FP16有专门的Tensor Cores可以进行矩阵运算,相比FP32来说吞吐量提升一倍。
![3885b7814b153e16d000672d4d8f2f9f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/7f1896c8437c5ea0b90028568ac9e0ae.jpeg)
实际点来说,量化就是将我们训练好的模型,不论是权重、还是计算op,都转换为低精度去计算。因为FP16的量化很简单,所以实际中我们谈论的量化更多的是INT8的量化,当然也有3-bit、4-bit的量化,不过目前来说比较常见比较实用的,也就是INT8量化了,之后的重点也是INT8量化。
那么经过INT8量化后的模型:
模型容量变小了,这个很好理解,FP32的权重变成INT8,大小直接缩了4倍
模型运行速度可以提升,实际卷积计算的op是INT8类型,在特定硬件下可以利用INT8的指令集去实现高吞吐,不论是GPU还是INTEL、ARM等平台都有INT8的指令集优化
对于某些设备,使用INT8的模型耗电量更少,对于嵌入式侧端设备来说提升是巨大的
所以说,随着我们模型越来越大,需求越来越高,模型的量化自然是少不了的一项技术。
如果你担心INT8量化对于精度的影响,我们可以看下NVIDIA量化研究的一些结论:
![f1fff59d4717fd38d20e239351e12c22.jpeg](https://i-blog.csdnimg.cn/blog_migrate/9b4ca9533202aa93d9488f125e52e391.jpeg)
出自《INTEGER QUANTIZATION FOR DEEP LEARNING INFERENCE: PRINCIPLES AND EMPIRICAL EVALUATION》。
量化现状
量化技术已经广泛应用于实际生产环境了,也有很多大厂开源了其量化方法。不过比较遗憾的是目前这些方法比较琐碎,没有一套比较成熟比较完善的量化方案,使用起来稍微有点难度。不过我们仍可以从这些框架中学习到很多。
谷歌是比较早进行量化尝试的大厂了,感兴趣的可以看下Google的白皮书Quantizing deep convolutional networks for efficient inference: A whitepaper
以及Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference
。
TensorFlow很早就支持了量化训练,而TFLite也很早就支持了后训练量化,感兴趣的可以看下TFLite的量化规范 (https://www.tensorflow.org/lite/performance/quantization_spec) ,目前TensorRT支持TensorFlow训练后量化的导出的模型。
TensorRT
TensorRT在2017年公布了自己的后训练量化方法,不过没有开源,NCNN按照这个思想实现了一个,也特别好用。不过目前TensorRT8也支持直接导入通过ONNX导出的QTA好的模型,使用上方便了不少,之后会重点讲下。
![fd3283bf89b132fe73813c54e4af6fc1.jpeg](https://i-blog.csdnimg.cn/blog_migrate/b55f5f9995289f368d2a38b58e2cb6da.jpeg)
NVIDIA自家也推出了针对Pytorch的量化工具(为什么没有TensorFlow,因为TF已经有挺好用的官方工具了),支持PTQ以及QTA,称为Pytorch Quantization,之后也会提到。
TVM
TVM有自己的INT8量化操作,可以跑量化,我们也可以添加自己的算子。不过TVM目前只支持PTQ,可以通过交叉熵或者percentile的方式进行校准。不过如果动手能力强的话,应该可以拿自己计算出来的scale值传入TVM去跑,应该也有人这样做过了。
比较有参考意义的一篇:
ViT-int8 on TVM:提速4.6倍,比TRT快1.5倍(https://zhuanlan.zhihu.com/p/365686106)
...
当然还有很多优秀的量化框架,想看详细的可以看这篇(https://zhuanlan.zhihu.com/p/355598250),后续如果涉及到具体知识点也会再提到。
量化基本知识
进入主题前需要提两个概念,也就是量化的两个重要过程,一个是量化(Quantize),另一个是反量化(Dequantize):
量化就是将浮点型实数量化为整型数(FP32->INT8)
反量化就是将整型数转换为浮点型实数(INT8->FP32)
![255e8b2a721d4e74b938a43e02ba3e04.jpeg](https://i-blog.csdnimg.cn/blog_migrate/65fd6be7ac2b29c5fbe1e86d0c20cc61.jpeg)
量化和反量化操作在最终的模型推理中都会用到,接下来我们就具体说下。
之后实数就代表我们的FP32浮点数,而整数就代表INT8整型数。
量化操作
比如有一个FP32的浮点型数字 , 然后我们需要把这个数变为整型, 也就是要量化它, 怎么搞。我们可以把这个数字乘上一个量化系数 , 比如 , 那么量化后的值 , 然后我们对这个数字进行四舍五入(也就是round操作)最终为
这样就行了吗, 523有点大啊, 我们的整型INT8的范围是 , 无符号INT8的范围也才 [0255], 这个量化后的值有点放不下呀。
怎么办, 当然是要截断了, 假设我们的INT8范围是 , 因为我们使用的是INT8, 所以这里的 , 那么上述的式子又可以变为:
这样就结束了么?
当然没有, 刚才的这个数字 , 被映射到了 127 , 那么如果是 呢? 貌似直接带入算出来也是0, 但是这样做对么?
基于线性量化的对称量化和非对称量化
对不对的关键在于我们是否是采用对称量化,什么是对称量化呢?这里的对称指的是以0为中心进行量化(还有另一种说法,这里先略过),然后0两边的动态范围都是一样的。
![ff237e1426de06aeaac3ffc876a7ea21.jpeg](https://i-blog.csdnimg.cn/blog_migrate/ee4221b9dceabca66bd3e35f61a942dd.jpeg)
可以看上图,左边是非对称量化,右边是对称量化(也称为Affine quantization和Scale quantization)。可以观察到:
对称量化的实数0也对应着整数的0, 而非对称量化的实数 0 不一定对应着整数 0 , 而是 。
对称量化实数的范围是对称的 , 而非对称量化的则不对称
对称量化整数的范围是对称的 , 而非对称量化的则不对称
所以上述的非对称量化过程可以简述为 , 其中 是 zero-point, 这个数字就代表实数0映射到整数是多少, 而对称量化则是 。
这样就明白了刚才的问题:如果是 呢?貌似直接带入算出来也是0, 如果我们采用的是对称量化,那就没问题!
需要说明一点,不论是非对称还是对称量化,是基于线性量化(也可以称作均匀量化)的一种。线性量化将FP32映射到INT8数据类型,每个间隔是相等的,而不相等的就称为非线性量化。非线性量化因为对部署并不是很友好,虽然能够更好地捕捉到权重分布的密集点,但感觉用的并不多,这里也就先不多说了。
关于详细的非对称量化,对称量化对比
可以参考这篇文章:
Affine Quantization vs Scale Quantization(https://liq.opengenus.org/affine-quantization-vs-scale-quantization/)
对称量化
接下来的重点是对称量化,也就是TensorRT中使用的量化方式,这里的范围也就是[-127,127],因为只比[-128,127]少了一个范围,所以实际量化中并没有太大的影响。
话说回来, 上文量化操作中, 量化系数随便说了个 , 这个当然是不对的, 这个 需要根据 我们的实际数据分布来计算。
![e6345d6f7b5ce3e440d5d05c034b2ea7.jpeg](https://i-blog.csdnimg.cn/blog_migrate/efcb059d1c6f732393aea2723d5a2de2.jpeg)
如上式, 代表当前输入数据分布中的实数最大值, 因为是对称, 因此实际范围是 。而 代表INT8量化,那么上述的量化公式就是之前提到的对称量化公式。
可以对比下非对称和对称的量化公式, 对称量化因为 , 所以公式简化了很多。
![26c3e76ec8fbe8ce6e87deb200583e38.jpeg](https://i-blog.csdnimg.cn/blog_migrate/96b47d675907b157c45d3646a9754960.jpeg)
对于对称量化, 假设当前根据权重分布, 选取的 为 4 , 那么 。
如下式子, 在反量化的时候我们需要将反向操作一番, 将量化后的结果乘以 重新变为浮点型。这里其实也就相当于乘以 , 因为有 。
![01ae62c83ef6f20db0dd258d659d87d3.jpeg](https://i-blog.csdnimg.cn/blog_migrate/0ed2eaca8bbff3cc1afb03e4936ba459.jpeg)
那么实际操作过程中,scale系数是怎么用呢?或者说这个量化系数是怎么作用于所有的输入、所有的权重呢?
一般量化过程中,有pre-tensor
和pre-channel
两种方式,pre-tensor
显而易见,就是对于同一块输入(比如某个卷积前的输入tensor)我们采用一个scale,该层所有的输入数据共享一个scale值;而pre-channel
呢一般是作用于权重,比如一个卷积的权重维度是[64,3,3,3](输入通道为3输出通道为64,卷积核为3x3),pre-channel
就是会产生64个scale值,分别作用于该卷积权重参数的64个通道。
为什么权重不能是pre-tensor
呢?这个对精度的影响太大了,所以一般不用。输入就可以pre-tensor
?当然可以,也经过测试了,对精度的影响不是很大,完全可以用。
那为什么权重必须是pre-channel
呢?不能是每个权重值都有自己的scale么?呃,这个问题嘛,首先可以想到,这个计算量,应该挺大,其次嘛,让我们分析一下。
卷积操作量化
铺垫了这么多,那么接下来说下量化最核心的操作吧,量化过程中最核心的操作当然是卷积量化。
我们都知道卷积操作可以拆分为im2col+sgemm,而大部分的计算都在矩阵运算也就是sgemm中,我们量化的重点也就是这个操作。以前是FP32计算,而现在变成INT8去计算,这是怎么转换的呢?
接下来重点分析一下量化公式!注意!这个很重要!
首先,矩阵相乘可以表示为,X为输入W为权重,Y为输出。偏置bias一般可以去掉,对精度影响也不大,所以就先不考虑了。
![c325fb01c85f8f1215fd19ba6a730891.jpeg](https://i-blog.csdnimg.cn/blog_migrate/2c15e6f62913e32369a55fb49f1f6f78.jpeg)
注意看上图输入X的维度为[m,p]而W的维度为[p,n],因此i的范围为[0,m),k的范围为[0,p)。W和Y同理。这里的输入和权重都是FP32精度,也就是实数。
而对应的INT8精度的输入和权重为,q下标就代表quantize也就是量化:
![d5338ca97e454d28a271a5c6f8d7614d.jpeg](https://i-blog.csdnimg.cn/blog_migrate/c4b5587e143eaaed21e4c49e4337a3e8.jpeg)
接下来,我们把矩阵公式细粒度拆成一个一个计算,也就是行和列每个元素相乘然后求和:
![58e4bdb7a625cb739f2bd9790bf152cb.png](https://i-blog.csdnimg.cn/blog_migrate/5c382165e7bb24b7ebe90d66a8d8b8ad.png)
首先是最左边, 和 分别代表浮点型的输入和权重, 代表第 行, 代表第 列, 因此 代表第 行, 第 列的元素, 同理。两者相乘求和就可以得到 , 可以看到这里求和的范围是 从1到 变化。
进一步,两个浮点型的运算可以被近似为INT8反量化后的运算,进一步等于量化后的运算:
![265c9bdead28c49edff21fe598fa4830.jpeg](https://i-blog.csdnimg.cn/blog_migrate/40e419a2f8f8fefb30a860537513416f.jpeg)
可以看到上式每个元素都有自己的scale值,也就是,而我们也必须把x和w的scale值提取到前面才能让x和w实现INT8类型的矩阵运算:
![d8f45ac9d1136daf0f8633e3aa1682cc.jpeg](https://i-blog.csdnimg.cn/blog_migrate/551ee98603d87ff6f042bac30b67ab6d.jpeg)
这里可以发现,如果想要把这两个scale元素,也就是 和 提出来, 那么这个 必须干掉,这里可以暂停一下想下为什么?
![19a369f32f37f90d40545bf7ebbefb2f.jpeg](https://i-blog.csdnimg.cn/blog_migrate/83f958a70613b76cb8c4e26a14f1af73.jpeg)
当把k去除将s取出来之后,我们发现 和 分别代表输入的第 行的scale和权重的第jjj列的scale值,这样输入的每一行必须共享scale,而权重的每一列也必须共享scale!
![0e57bd9406760ad40fdf92256998ec5d.jpeg](https://i-blog.csdnimg.cn/blog_migrate/ee6931e8a0c4e3c1f8a87a3d2b1d5d4e.jpeg)
那么pre-channel
又是怎么来的呢?
还记得之前说过的 im2col+sgemm 操作吗(如果不记得强烈建议去看看),其中的sgemm
是这样的,需要注意,下图左边的kernel矩阵,每一行代表一个输出通道的kernel集合(这里因为输入图像是三通道的,因此kernel有三个,不同颜色代表一个kernel):
![8d584848caeba0fb3b1b70ec3b404742.jpeg](https://i-blog.csdnimg.cn/blog_migrate/4a066e6c8a337cecbf4cb566829629fa.jpeg)
这就是pre-channel
或者详细点就是per-output-channel
也就是卷积输出通道,我们对每一个卷积权重的输出通道那一维进行量化,然后共享一个scale,这也就呼应了上述的公式!
国内首个自动驾驶学习社区
近1000人的交流社区,和20+自动驾驶技术栈学习路线,想要了解更多自动驾驶感知(分类、检测、分割、关键点、车道线、3D目标检测、多传感器融合、目标跟踪、光流估计、轨迹预测)、自动驾驶定位建图(SLAM、高精地图)、自动驾驶规划控制、领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球,这是一个真正有干货的地方,与领域大佬交流入门、学习、工作、跳槽上的各类难题,日常分享论文+代码+视频,期待交流!
【自动驾驶之心】全栈技术交流群
自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、BEV感知、多传感器融合、SLAM、光流估计、深度估计、轨迹预测、高精地图、NeRF、规划控制、模型部署落地、自动驾驶仿真测试、产品经理、硬件配置、AI求职交流等方向;
添加汽车人助理微信邀请入群
备注:学校/公司+方向+昵称