作者:多伦多打折优惠信息_205 | 来源:互联网 | 2023-07-31 09:54
将卷积展开后要进行的运算实质上是大规模矩阵运算,因此卷积模块的实现时最容易的,什么都不需要考虑,数据按顺序来了就计算,而这个顺序是数据读取部分需要考虑的,计算完了输出去这部分是下一
将卷积展开后要进行的运算实质上是大规模矩阵运算,因此卷积模块的实现时最容易的,什么都不需要考虑,数据按顺序来了就计算,而这个顺序是数据读取部分需要考虑的,计算完了输出去这部分是下一层的数据数据存储部分需要考虑的。因此整体而言,整个网络模型中最容易实现的却是这里面最核心的计算部分。
言归正传。首先要对卷积的循环进行分析,这也是很多基于FPGA的CNN加速器里面所重点研究的。这里推荐一篇FPGA2017的论文,对循环的优化做了比较详细的分析。我们的demo就用最简单的方式进行了。
卷积循环分为四层,这里引用上面说的论文中的伪代码图。
分别解释一下:
- Loop1:是指卷积核的xy方向计算
- Loop2:是指滤波器不同输入通道的卷积核
- Loop3:是指滤波器在特征图上面的滑动
- Loop4:是指多个滤波器
循环真正影响到的应该是数据的读取时序。循环1要做的是读取一个卷积核的参数和特征图中对应的部分进行乘加运算;循环2要做的是循环读取不同的输入通道的卷积核和特征图数据;循环3要做的是在特征图上滑动,也就是说循环的在特征图上读取以不同像素为中心的数据块;循环4要做的是循环读取不同的滤波器。而卷积模块中就是要针对于这些数据读取顺序进行相应的计算。
这部分优化的方法有很多,举个例子,对于大型的网络模型而言,大多数参数和特征图是要存在片外的,而片外访存的代价是很大的,因此尽可能的进行数据复用是非常有效的优化方式,因此有些研究对循环进行的顺序交替,来提升数据的复用效率,在此不再赘述,有兴趣的同学可以去看看17/18年的论文,对这部分的讨论已经很清晰了。
我们仅仅使用最简单的方式,也就是最原始的4层循环来进行。下面要做的就是在模块中设计一个乘法阵列,也就是很多研究中所说的PE阵列、脉动阵列之类的,都是相似的思想。
这里我们调用DSP单元用作定点数乘法器设计。注意,这里使用的是定点数乘法器,也就是说,我们前面提到的定点数量化在这里派上了用场,根据前面的量化位宽来对DSP乘法器进行设计。
这里有一个trick可以使用一个DSP进行多个定点乘法操作,详见这里。
得到的结果还需要进行位宽截断处理。举个例子。如果我们使用8bit进行量化,那么我们得到的乘法输出是16bit数据,再经过加法运算,会进一步增大位宽,假设
3
×
3
3 \times 3
3×3的卷积核,那么加法结果会变成20bit,而下一层的输入仍需要8bit位宽。这时候就需要在软件层面进行量化的时候就设计好,在这一层需要保留多少位小数位。假设需要保留4位小数位,而之前的8bit输入是3位小数位,那么结果应该是有6位小数位,也就是说我们需要去掉最后的2位(好像还是很大,这说明什么?要不然高位全是0,要不然就不可能存在3位4位小数的样子。总之这里要在软件层面量化的时候就要考虑好)。
要注意的是,我们除了卷积的乘加运算之外,还有bias 的加法运算,这里的量化也需要对小数位进行对齐。
最后,我们还可以在这一层加入**函数,如果是ReLU就很舒服了,直接判断一下输出的符号位,是1就输出0,是0就输出原始数据,其他的也一样,只需要在流水线的最后加上一级判断就可以得到**值。一般来说,如果使用了一些其他如sigmoid的**函数,是要使用分段线性函数进行拟合的,这样虽然会产生一定的误差,但是如果在软件模型就使用了分段线性函数,那么模型是可以学习到这种的误差的,而使用分段线性函数,那么这里的计算就又变成了和ReLU类似的选择和乘法运算。值得注意的是,一般而言为了减少乘法数量,我们会选择使用2的幂作为分段斜率,从而将乘法转化为移位运算。
理论说完了,我们来看一下程序中是如何做的。
首先是一个基础计算单元,这个单元中可以设置卷积核的尺寸,从而得知累加需要的时钟个数。也就是说PE单元中将会完成Loop1的内容。详细见这里。
在外层模块我们使用了generate来进行循环布线,这是verilog语法中一种比较便利的可以进行并行化布线的方式,即循环内的所有内容都会并行的进行布线。
第一组循环使用嵌套的方式完成了Loop2和Loop4的乘法计算内容。这里值得注意的是,存在着输入通道为1的特殊情况,如果不为1呢?可以参考这里。
第二组循环中对乘法的结果进行的顺序整合,以便于后面池化层进行池化操作。另外,这里还进行了截位处理和一次选择运算,也就是前面提到的位宽对齐和ReLU**函数。
第三组循环将输出进行整合,便于模块间通信。
第四组循环用于将输入数据进行拆分,分散给不同的基础计算单元。
第五组循环用于将输入的权重进行拆分,分散给不同的基础计算单元。
整个模块可以提供了大量的parameter,可以很方便的进行配置。
细心的朋友可能会发现,这里缺少了Loop3的计算部分。这是因为我们将Loop3和Loop4进行了交换,并将Loop3归入了上一节数据读写模块当中。
这就是卷积模块的全部内容了。这部分程序设计还是蛮容易的,因为涉及到的时序问题很少,都是一些并行问题。而这部分优化主要是结合数据读写部分的优化,如循环展开、交换等,还有DSP的复用技术,定点数量化bit位宽越低,DSP可以计算的定点数乘法个数越多。
另外一种优化策略涉及到整个模型的改变,也就是使用快速算法,如FFT、Winograd算法,很多文章对此进行了研究,比如FPGA2018的这篇就使用了Winograd算法。这方面我没有过多的研究,感兴趣的朋友可以自己研究一下。