软件实现卷积的原理
上图是卷积计算的原理图
- 首先说明一下图中的各个参数:
- CHin:输入特征图的通道数(channel depth)
- CHout:输出特征图的通道数
- R:输出特征图的行数(Row)
- C:输出特征图的列数(Column)
- K:卷积核的大小(kennel size)
- S:步进步数(stride)
- 对于输出的特征图,其计算公式如下图所示
注意当卷积核在输入的特征图上进行滑动时,需要乘上步进步数
- 将上图中的计算公式转换成C语言代码
常用时间术语总结
在对卷积核运算进行硬件加速前,需要对HLS中的一些常用的时间术语进行解释
- Area:实现该C代码多用的资源量,该资源包括LUT、registers、Block RAM、DSP48等等。
- Latency:C函数完成所有的一次输出所需要的周期数。
- Initiation interval(II):C函数需要多少时间才可以重新接受新的数据,也就是C函数本次开始到下一次开始所需要的周期数。
- Loop iteration latency:C函数中的for循环每迭代一次需要多少时钟周期。
- Loop initiation interval:本次循环开始到下一次循环开始所需要的周期数。
- Loop latency:完成整个循环需要多少个时钟周期。
- Trip Count:for循环的循环迭代的次数。
通过下方两个图可以更好理解上方的这些术语
HLS实现
代码实现(未优化)
卷积运算的各参数取值如下图所示(其中步进步数S为1)
-
首先定义三个多维数组
-
分别代表输入特征图、输出特征图以及卷积核的权值。其中卷积核中的CHout表示卷积核的个数,每个卷积核都是一个三维数组,并且通道数CHin都和输入特征图一致
-
然后编写卷积运算的循环体代码
-
其中循环体的顺序为:
Output_Channel --> Input_Channel --> Row --> Column --> Kernel_Row --> kernel_Column -
仿真后的性能报告如下图所示
加速器架构
- 本文的加速方案是从Channel层面进行展开的。也就是当卷积核在对输入的特征图进行乘法操作时,对每一层Channel的计算作为一个处理元件( Processing Elements,简称PE),这些PE将进行并行运算。而对每一个单独的PE在进行Pipeline展开。同时对每个待操作的多维数组数组也要在Channel维度上进行展开(Partition),以适应PE的并行计算。
添加约束条件(Directive)
循环展开
- 首先要对循环体中的Output_Channel以及Input_Channel循环进行Unroll展开,具体代码如下:
- 同时,对多维数组也要进行Channel维度上的展开
- 其中数组In和数组Out分别在CHin和CHout维度展开,而数组W(卷积核权重)同时包含两个维度,需要同时在两个维度都展开。
- 最后经过仿真的报告如下:
- 从图中可以看到,经过Unroll的并行优化后,程序的Latency明显下降了。
循环体流水化处理
- 由上一步Unroll后的结果报告,我们可以观察到,循环体并没有进行Pipeline展开,因此还有进一步优化的余地。下面我们对Outout_Channel和Input_Channel循环体进行Pipeline展开。
- 由于Pipeline的特性,Pipeline内部的循环自动进行Unroll展开,所以我们不需要在额外的添加展开的Directive。
- 下面是仿真后的结果报告:
- 从报告中我们可以看出,经过流水化处理之后,程序的Latency得到了进一步的减少。
- 但是,同时我们也发现,Pipeline结果中的Initiation Interval并没有达到理想中的1个时钟周期。
循环体顺序问题
- 上次的仿真报告中II不为1的原因,其实是和循环体的顺序有关。
- 由于最后的输出out [ cho ] [ r ] [ c ]其实只与输出的Channel、输入特征图的行r以及输入特征图的列c有关。
- 如果将r和c放在循环体的最外侧的话,那么程序的运行顺序就如下图所示。
- 可以看到相邻两次循环之间,Iteration 0的输出与Iteration 1的输入存在一定的读写关系。也就是说下一次循环必须等到上一次循环完成,并将结果写入到RAM中,下一次循环才能读取到上一次的结果,开始下一次的循环。正因为存在这一层的读写关系,所以才不能实现Pipeline来对循环体进行有效的加速。
- 想要解决这一问题,其实方法也很简单,只需要将循环体的顺序调整一下。将kr和kc的循环与r和c的循环位置调换一下,使得对r和c的循环在整个循环体的内测,就可以避免两次循环之间的读写关系。
- 上图即为循环体顺序调换过后程序的运行时序图,可以看到相邻的循环之间没有了依赖关系,相应的II也变为了1。