如何让深度学习模型更高效地运行

基本概念:

1.FLOPS:每秒可执行的浮点型操作,是一个衡量计算机性能的指标
2.Nas:Neural architecture search,神经网络架构搜索,是一种自动设计人工神经网络的技术,在大数据集上搜索所化的时间非常漫长,NAS与超参数优化密切相关,是自动机器学习(AutoML)的一个子领域。
3.强化学习:强化学习不同于监督学习,不需要明确的输入和标签,还是在与环境的交互过程中获得奖励和惩罚的信号,这适用于一些很难打标签的场景,因为有一些问题是没有标准答案的。
4.Roofline Model:Roofline Model可以计算一个算法在一个计算平台上理论能达到的最快计算速度.
从硬件层面来讲,影响计算速度有两个方面:1.处理器本身的计算能力;2.内存带宽(Bandwidth)
第一点很容易理解,因为GPU/CPU被生产出来后,它的最大计算能力是固定的,不管外部环境再怎么变化,最大计算能力也不会变了。因此我们希望做出来的硬件的计算能力越大越好!
而第二点内存带宽,同样也会成为处理器计算能力的瓶颈。假设某些算法对内存带宽需求很大,需要频繁地将内存里面的数据迁移进处理器,这需要耗费时间。哪怕你的计算能力无敌强,小到计算任何东西的时间接近于0,但是你还需要等数据进来才能计算,所以读取数据的能力成为了瓶颈。
算法对于内存带宽的需求通常用运算强度(operational intensity/arithmetic intensity)来表示,单位是 OPs/byte,指在算法中平均每读入单位数据,能支持多少次运算操作。运算强度越大,则表示单位数据能支持更多次运算,也就是说算法对于内存带宽的要求越低。
典型的 Roofline 曲线模型如下图所示,纵坐标是计算性能、横坐标是运算强度。Roofline 曲线(黑色实线)包含两部分:左边的上升区,以及右边的饱和区。当算法的运算强度较小时,曲线处于上升区,即计算性能实际被内存带宽所限制(memory-bound),有很多计算处理单元是闲置的。随着算法运算强度上升,即在相同数量的数据下算法可以完成更多运算,于是闲置的运算单元越来越少,这时候计算性能就会上升。然后,随着运算强度越来越高,闲置的计算单元越来越少,最后所有计算单元都被用完了,Roofline 曲线就进入了饱和区,此时运算强度再变大也没有更多计算单元可用了,性能被计算能力所限制(compute-bound) 。
在Google的论文中也比较了不同算法在Roofline Model上的位置。由图中可见,经典全联接神经网络(multi-layer perceptrons, MLP)和LSTM 算法的运算强度较低,所以被卡在了 roofline 模型的上升区,很容易就得出瓶颈在于内存带宽。而卷积神经网络模型,尤其是 CNN0,由于卷积神经网络中能实现卷积核复用,因此运算强度非常高,于是可以非常接近 TPU roofline 曲线的屋顶(86 TOPS)。值得注意的是CNN1 模型,虽然运算强度也很高,但是由于由于 CNN1 模型的特征深度较浅无法完全利用 TPU 的计算单元,无法到达Roofline。
如何让深度学习模型更高效地运行_第1张图片

如何让深度学习模型更高效地运行_第2张图片
总结:个人理解,如果计算平台每秒能进行的浮点计算次数越多,每次计算需要的内存交换越少,则计算性能越好,当算法计算强度较低时,计算能力受限于带宽,大于一定值,则受限于FLOP
举例说明:
举两个具体的例子,第一个是矩阵乘矩阵,矩阵C等于A乘B,而A跟B分别是一千乘一千的矩阵。假设存储和计算都是用float 32位来表示,这样一个计算将会做1000乘1000乘1000的浮点乘加,也就是2G FLOPS的运算。我们要读取A和B,然后计算出来C,把它写回去,最少的存储器访问就是三个矩阵的大小,也就是12个MB。
另外一个是矩阵乘向量,也就是矩阵A乘向量B,等于向量C,这时候维度还是1000的情况下,它的计算量就是1000乘1000的浮点乘加,也就是2M。而存储器访问的话最少大约是1000乘于1000个浮点数,也就是4MB。
可以明显地看到上面乘矩阵的操作,它的计算量是2G,访存量是12M,那么它的这个计算量除以访存量,也就是刚刚提到的计算密度,大概是200左右。下面这个矩阵和向量中,它的计算量是2M,访存量是4M,那它的计算量除以访存量大约就只有0.5,显然这两个就是非常不同的程序。
上面矩阵乘矩阵,是一个典型的受计算量约束的程序;而下面矩阵乘向量则是一个典型的受存储器带宽所约束的程序。
另外一个要介绍的背景是,计算机的存储器体系结构、存储器层次结构。
计算机中CPU的速度非常快,存储器通常跟不上它,所以为了让CPU能稳定地工作,存储器被划分为多个层级,最上层是最快,同时也最贵,也是容量最小的寄存器,它紧紧围绕在这个算术逻辑单元附近,可以被这个计算指令直接使用。
往下一层是缓存,它的容量比计算器大很多,但是速度大概慢一个量级,延时大概高一个量级。再往下是主存,也就是通常所说的内存,它一般是在处理器的芯片之外,容量就非常大,延迟可能会比这个CPU的主频要低100倍,甚至更多。我们主要关注上面三个:寄存器、缓存和主存。
列举一些典型的可以用深度学习部署的一些硬件:英伟达的Tesla V100,是目前最强的计算卡,它可以提供120T的FLOPS的计算性能,基于TensorCore。提供HBM2作为主存,带宽是900GB/s,同时还有20MB的片上共享存储和16MB的片上缓存,一共36MB的片上存储。
中间是英伟达的Xavier,是最近推出的SoC,它提供大概30T的计算性能,同时使用LPDDR4作为主存,带宽大概是137GB/s。
右边是一个典型的嵌入式设备:树莓派3,它采用普通的一款ARM芯片,运行在1.2G Hz,峰值计算能力是38.4G FLOPS。同时使用LPDDR2作为它的内存,带宽大约3.6GB/s,同时它还会有512KB的片上缓存。
对比这三个硬件,不管是计算性能,还是内存带宽,片上的缓存,它们都非常的悬殊。
如何让深度学习模型更高效地运行_第3张图片
我们通过计算量和访存量来评判深度学习模型的性能。
计算量是我们非常熟知的一个性能,但通常对访存量关注比较少。事实上根据我们前面提到的,访存量的值会非常严重地影响模型的性能。为了简便计算,我们这里忽略缓存的存在,因为一般需要加速的产品都是嵌入式设备,这种设备上它的片上存储是相对较少的。
假定以Fp32作为计算和存储的单元,我们可以计算出模型的计算量和访存量,以及计算密度。这里计算访存量的规则是:对每一个Operator或者是layer,计算所有输入的Tensor、所有参数的Tensor,还有输出Tensor的总大小作为访存量。
表格里列出来的访存量,实际上是经过了一些优化之后的结果,并不是原始模型中的结果。后面会提到一些基本的优化操作。
先看这个表格中第四列的数值,也就是计算密度。一些大模型,比如VGG 16、ResNet,或者是Inception,他们的计算密度都非常大,都在30或者40或者50这个量级。事实上VGG 16的访存量中绝大部分是被最后巨大的全连接层占据了。如果去掉那个全连接,或者是像ResNet中,先做ROI pooling,然后再接全连接,这种形式的访存量会降到170M到180M,它的计算密度是非常大的。
而通常我们说的小模型,计算量虽然非常小,但它访存量却并没有显著地小于大模型,同时计算密度会显著变小。例如ShuffleNet 0.5x,它使用3个group形式时计算密度只有3.61,如果是8个group形式,也就是计算被拆得更散的情况下,它的计算密度只有2.91。
这就是说,小模型部署在这些硬件上,通常都是被存储带宽所限制住了,而不是被计算量所限制住。

优化方法

1.BottleNeck或者Depthwise或者Winograd
BottleNeck或者Depthwise这两种方法可以大大降低参数量和计算量,Winograd是降低卷积的乘法量(没有任何精度损失),详见我的另外三篇博客;
需要注意的是Winograd/FFT卷积不一定有利于模型加速
2.稀疏化
稀疏化主要指的是核的稀疏化,指的是在模型训练过程中通过正则项对参数加以限制(本意是防止过拟合),使得部分参数为0,这样子可以大大提升卷积im2col的速度,但是如果不是规则化的稀疏化,依赖于特殊的访问和计算库,所以在一般的GPU上实现不了真正的加速,稀疏化后更有利于模型的剪枝操作.
论文[10]提出了Structured Sparsity Learning的学习方式,能够学习一个稀疏的结构来降低计算消耗,所学到的结构性稀疏化能够有效的在硬件上进行加速。由于在GEMM中将weight tensor拉成matrix的结构(即im2col操作),因此可以通过将filter级与shape级的稀疏化进行结合来将2D矩阵的行和列稀疏化,再分别在矩阵的行和列上裁剪掉剔除全为0的值可以来降低矩阵的维度从而提升模型的运算效率。该方法是regular的方法,压缩粒度较粗,可以适用于各种现成的算法库,但是训练的收敛性和优化难度不确定。
它的缺点是工程实现较难,需要fine tune,不易于解耦模型研究和部署加速。另外稀疏矩阵运算的实际效率往往也不高。
那么在工程上如何解决呢?如同上文所说,采用regular的方式,通过结构化稀疏,通过一些方式保证稀疏矩阵中0以一定的规律出现,例如同一列、同一行、某些块等,尽量提高稀疏矩阵运算的效果。
3.低精度运算(量化)
低精度运算具体而言就是使用更低位宽,实现成倍降低的访存量,同时使部分处理器上性能成倍提高。
举个例子,fp16相比fp32,一半的位宽,几乎没有精度损失;Int8相比fp32,1/4的位宽,采用正确的方式进行量化后也几乎没有精度损失。缺点是目前支持的硬件不多,新一代的ARM处理器和部分英伟达的GPU支持。
裁剪的方法是根据预训练得到的全精度神经网络模型中的数据分布,分别对阶码和位数的长度进行适当的减少。实验证明,对于大部分的任务来说,6位比特或者8位比特的数据已经能够保证足够好的测试准确率。
QNN在实际的操作中通常有两种应用方式,一种是直接在软件层面实现整形的量化,通过更少的数据位数来降低神经网络在使用时计算的复杂度;另一种重要的应用是针对于AI专用芯片的开发。
由于芯片开发可以设计各种位宽的乘法器,因此将神经网络中32位的全精度数据可以被处理成6位或8位的浮点数,同时结合硬件指定的乘法规则,就可以在硬件上实现更高的运算效率,达到实时运行深度神经网络的目的。这也是QNN最大的理论意义。但是如果从软件角度而非硬件角度出发,只是将浮点数量化成整形数,就没有办法显著地降低计算复杂度(除非对整形再进行量化),也就无法达到在低配硬件环境上实时运行深度神经网络的目的。因此,在软件设计的层面上,QNN相比BNN并没有特别明显的优势。
4.graph层面上
5.operater粒度上:以上两个没有具体看,具体参照:让深度学习更高效运行的两个视角 | Paper Reading第二季第一期
7.模型剪枝
所谓的模型剪枝,就是把一些不重要的connection或者filter去掉,来减少模型参数和计算量
同样也分为regular和iregular两种方式
Pruning Filters for Efficient Convnets:
把数值低的参数视为不重要的,对于一个filter,其中所有weight的绝对值求和,来作为该filter的评g价指标,将一层中值低的filter裁掉,可以有效的降低模型的复杂度并且不会给模型的性能带来很大的损失,算法流程如下:
1.对于每一个卷积核,计算其参数的和
2.根据参数和排序
3.把参数和最小的m个filter以及它们对应的featureMap去掉,下一层的kernel和featureMap也要去掉
4.为影响到的上下层创建新的kernel Matrix,剩下的参数保存到新模型当中;
作者在裁剪的时候同样会考虑每一层对裁剪的敏感程度,作者会单独裁剪每一层来看裁剪后的准确率。对于裁剪较敏感的层,作者使用更小的裁剪力度,或者跳过这些层不进行裁剪。目前这种方法是实现起来较为简单的,并且也是非常有效的,它的思路非常简单,就是认为参数越小则越不重要。

其他的没怎看了

你可能感兴趣的:(如何让深度学习模型更高效地运行)