派大星 · 2020年07月13日

走向ARM CPU 1-bit推理的极致道路

本文转自:NeuralTalk
知乎作者:十七树
原文链接:https://zhuanlan.zhihu.com/p/...
本文已获原作者转载授权

深度学习的端侧学习对轻量化有着极致的追求,从2016到现在,我们看到业界在推理时使用的数据类型位数一降再降:FP32、FP16、INT8……。显而易见,低比特能节省模型占用的存储空间功耗,在某些精心设计的算法上面也能获得性能优势。在这一点上,1-bit毫无疑问是经典计算机上可以做到的极致。

举一个实际的例子,Birealnet18(二值化的Resnet18)用FP32类型存储大概需要45MB,而通过Bolt转换后得到的1-bit+FP16存储只需要占用2.7MB,exciting!

低位数与性能提升的可能

那么,权重位数走向极致,是不是就能带来计算速度的提升呢?答案取决于你使用的硬件。

现阶段深度学习的核心计算还是矩阵乘法,这里面涉及到的就是乘法和加法。大多数硬件的ALU可以处理的最小数据类型就是INT8,要是按照严格的矩阵乘法定义,低于8位的数据类型都得先展开到8位来操作(INT4和INT2哭晕在厕所)。但是1-bit可以来个“降维打击”:权重只有0和1,干脆省掉乘法,直接全都做加减法,多么HAPPY。看到这里,你已经领会到Binary Weight Net的精神了,在经典CPU上已经可以靠着加法相比于乘法的优势行走江湖了。

还没有换台的观众们,估计眉头一锁,觉得此事没有那么简单。是的,深度学习早已经皈依了黄老板的皮衣神教,移动端CPU的绝对霸主ARM也推出了SIMD单元。这些个SIMD单元,乘法和加法用的就是同一个单元,甚至可以用一个指令完成乘加操作。

在现有的SIMD上,1-bit要想占有一席之地,必须再次发动降维。当我们把 feature map 也二值化了后,乘法的两端只会出现0和1,而最终的乘积也只会是0和1,直接位操作就行了,总有一款适合你,而且还不用改动现有的硬件——这就是BNN的精髓。

试想一下,中低端SoC是不支持FP16和INT8的专门优化的,能找到的估计就是跑FP32的CPU。部署FP32的模型,不仅存储感人,推理时间也会感人。但你要是训出一个BNN模型,存储一下子就下去了,而且还能轻松在原有芯片上打败FP32。有些读者可能在担心精度问题,但BNN已经取得长足的进步了,不搞识别,搞个实时检测还是很有价值的。

如何用Bolt基于ARM NEON进行BNN推理

接下来,本文将会介绍Bolt是怎样基于ARM NEON进行BNN推理的。

怎样基于ARM NEON进行BNN推理的。 在这个事情上,Bolt没有抢到先发,我们被大缺弦老师的BNN框架(daBNN)截胡了。但我们也要感谢大老师选择了开源,特别是把他训练的birealnet18模型也放到了repo上,做BNN框架才有了真正意义上的benchmark。

在我们调试该网络的过程中,他也知无不言,衷心感谢(https://github.com/JDAI-CV/dabnn)。

在一款搭载A55小核心的开发板上,daBNN实测birealnet18(带stem优化版本)单张图片是243ms,而Bolt的单张推理时间是78ms——对于这个结果我们当时也是很震惊的。

关于Bolt相比daBNN的性能提升原因,大家容易想到的第一个因素是在模型的浮点部分,Bolt用的是FP16啊,性能优势是这样来的吗?但是,我们最初开始开发的时候,直接把整个网络按FP16来跑,也需要160ms。其实,在A55小核心上,由于关键指令无法双发射,daBNN是在客场作战的——Bolt几乎所有汇编kernel都有针对A55进行指令发射的优化(注重能耗的开发者记得pick我们)。那么,性能对比在A76上又如何呢?请容许我卖个关子。

接下来先进入技术解密,看完你就更能理解Bolt BNN推理为什么有这么大的优势了。

技术解密:ARM CPU做BNN推理的基本思路

正如大老师在他的论文中讲的,ARM上面做BNN推理最重要的是以下两个指令:

  • AND:逻辑门位操作,要是使用XNORnet那就换一条逻辑门指令;
  • CNT:在向量寄存器上每8个比特统计1的数目,得到0到8之间的数字,按照UINT8存回到那8个比特上

这两条指令应该怎么理解呢?在64位的芯片上,向量寄存器的宽度是128bit。一条AND指令就可以完成128个乘法,而一条CNT指令就完成了16组8个数字的加法。换言之,这两条指令就相当于16组8x8的向量内积。

ARM CPU做BNN推理:daBNN的做法及Bolt的改进

那么,接下来就要说到Bolt和daBNN的重要区别了。大老师的128bit装的都是对应同一个输出通道的操作数,所以16组8x8内积得到的16个UINT8数字,还得通过ADDV指令加到一起,然后才加到对应的输出位置上,并行度突然就砍掉了。大老师也发现了ADDV指令是很吃亏的,于是他尽可能先把很多组16个UINT8数字通过向量ADD加起来——毕竟UINT8最大能存255嘛,只用8很浪费——然后再用ADDV。

但是,大家有没有想过可以完全省掉ADDV呢?AND+CNT输出的16个数字,要是对应的是16个输出通道,那我们就只需要直接ADD起来了。这其实是经典的gemm tiling思想,就不在此赘述了。

可能有课代表发现了,直接ADD起来,顶天也就只能加到255啊,不可能一路ADD下去。是的,BNN卷积的结果大多数情况至少也要用INT16才能存储,我们的确也是需要累加到INT16向量的。跟上面一样的道理,为了节省INT16累加的指令,我们需要在ADD的时候尽量用满UINT8的空间。理论上,8x8内积最多产生数字8,而UINT8最大能存255,也就是最多约可以累加32次,追求极致的我们需要想这个32怎么来。

卷积的时候的每一组加法次数,等于ic*fh*fw(input\_channel*filter\_h*filter\_w)。在Bolt中,我们要求BNN卷积层的输入通道数必须是32的倍数,每次8x8内积会用掉ic里的8,那么ic/8我们只能保证是4的倍数,距离32还差一个8。这个8要怎么来呢?欢迎大家想一想。

daBNN和Bolt的性能对比

640.png

图 1-bit推理性能时间(ms)对比

感谢大老师放出来的模型,然后测试平台是麒麟990。由于stem版模型的onnx比较晚才放出来,做这张图的时候就没加上去 。Bolt+stem在A55上是70.9ms,而在A76上是17.0ms



作者文章推荐:

欢迎关注公众号,关注模型压缩、低比特量化、移动端推理加速优化、部署。
嵌入式AI.jpg
获取更多嵌入式AI文章内容,请关注嵌入式AI专栏
推荐阅读
关注数
16797
内容数
1234
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息