HLS学习笔记——实现卷积层的加速计算

软件实现卷积的原理

在这里插入图片描述
上图是卷积计算的原理图

  • 首先说明一下图中的各个参数:
    • 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有关。
  • 如果将rc放在循环体的最外侧的话,那么程序的运行顺序就如下图所示。

在这里插入图片描述

  • 可以看到相邻两次循环之间,Iteration 0的输出与Iteration 1的输入存在一定的读写关系。也就是说下一次循环必须等到上一次循环完成,并将结果写入到RAM中,下一次循环才能读取到上一次的结果,开始下一次的循环。正因为存在这一层的读写关系,所以才不能实现Pipeline来对循环体进行有效的加速。
  • 想要解决这一问题,其实方法也很简单,只需要将循环体的顺序调整一下。将krkc的循环与rc的循环位置调换一下,使得对rc的循环在整个循环体的内测,就可以避免两次循环之间的读写关系。

在这里插入图片描述
在这里插入图片描述

  • 上图即为循环体顺序调换过后程序的运行时序图,可以看到相邻的循环之间没有了依赖关系,相应的II也变为了1。

版权声明:本文为qq_36334929原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。