文章来源: 傅里叶的猫微信公众号
转自高亚军老师的HLS视频教程:https://www.bilibili.com/video/BV1bt41187RW?p=18&spm_id_from=pageDriver
FOR循环优化
基本概念
从下面的例子中来解释for循环中的基本概念:
图 4.1 for循环基本概念
由于N等于3,因此每次循环可以分成4个步骤来完成:
c0:读取数据b和c;
c1:获取数据xin 0处地址;
c2:读取对应地址上的数据;
c3:计算yo[0]的值。
后面的计算都是三个时钟周期计算出一个值,因此对一次循环来说,Loop Iteration Latency为3,Loop Iteration Interval也是3,Loop Latency是9,再加上前面读b和c的值的一个周期,整个函数的Latency是10,函数间的Initial Interval是11.
图 4.2 pipeline优化原理
在优化结束后,Loop Iteration Latency为3,Loop Iteration Interval变成1,Loop Latency为5.
如果对函数做pipeline,那么会自动把函数下面的for循环都做unrolling处理;如果对外层的for循环做pipeline,那么会自动对内层的for循环做unrolling处理。
Unrolling
默认情况下for循环是折叠的,就是电路被时分复用。当展开后,资源增加。如下图所示将for循环展开成3倍的情况,资源也扩大了3倍。
图 4.3 展开成3倍
也可以部分展开,循环次数为6,但展开成3倍,程序如下所示:
展开后,程序被分成3部分,资源也复制了3份。
图 4.4 Unroll的设置
Merge
当几个for循环执行的内容很相似时,如下面的程序所示:
两个for循环分别对两个数据做加法和减法,在HLS综合后,会先进行第一个for循环的计算,完成后再进行第二个for循环的计算。这样综合出的Latency为18,Interval为19。
图 4.5 综合后延迟
在HLS中提供了Merge的选项,合并的是for所作用的region,合并后综合后的延迟如下图4.6所示。
图 4.6 Merge后的延迟
上面的例子中两个循环的边界相同,如果两个循环的边界不同,则以最大的作为合并后的边界;如果一个边界是变量,另一个是常量,则不能合并;如果两个循环边界都是变量,依然不能合并。
还可以将for循环封装成一个函数,并在上一层中例化两次,并对函数采用Allocation来使函数并行执行,在allocation中有limit选项,可以指定实例化的次数,该数据与程序中实际的数值应该是一样的。
数据流
在下面的例子中,Task B依赖于Task A,Task C依赖于Task B,如图4.7所示。
图 4.7 程序结构
而且可以分析出,该结构不适合之前所讲的pipeline和merge方式进行处理,在可以使用dataflow的方式。
图 4.8 Dataflow的执行方式
从图中可以看出,在使用DataFlow后,Loop B无需等待A执行完成后才开始执行,而且各个Loop之间也村在间隔。且延迟和资源都明显减少,如图4.9所示。
图 4.9 DataFlow后的延迟和资源对比
DataFlow使用的限制:
1.一个输出在多个Loop模块中使用
2.被Bypass的模块
3.带反馈的模块
4.带条件的模块
5.可变循环边界的模块
6.多个退出条件的模块
下面分别对上面的限制条件进行说明。
1.din在Loop1中输出的temp1同时赋给Loop2和Loop3使用,这时是不能使用dataflow的,如图4.10所示。
图 4.10 DataFlow限制1
通过对代码进行适当的修改,将其结构进行变形,增加一个Loop_copy模块,将其输出一个送个Loop2,另一个输出送给Loop3,但其实这两个输出的结果是相同的。就可以使用DataFlow来完成该函数,如图4.11所示。
图 4.11 结构变换使用DataFlow
且使用了DateFlow后,工程所占用的资源和延迟都相应减少。
2. 被Bypass的模块
如下图4.12所示的例子中,temp1在Loop2中使用,但temp2没有经过Loop2,直接在Loop3中使用,这种情况下也是不能使用DataFlow的。
图 4.12 Bypass模式
同样的,可以对代码进行优化以达到可以使用DataFlow的目的,如下图4.13所示。在Loop2中,增加一个输出端口,使其输出给Loop3,这样就可以使用DataFlow了。
图 4.13 优化Bypass
在DataFlow的循环之间的存储模块,对于scalar、pointer和reference或者函数的返回值,HLS会综合为FIFO;对于数组,结果可能是乒乓RAM或者FIFO:如果HLS可以判断数据是流模式,就会综合为FIFO,且深度为1,若不能判断,就会综合为乒乓RAM。我们也可以指定为FIFO或者乒乓RAM,但在指定为FIFO时,如果指定的深度不合适,综合时就会出现错误。
嵌套for循环
三种嵌套循环:
对于Perfect Loop,对外边的Loop做流水比对内循环做流水更加节省时间。
对于Imperfect Loop,我们总希望可以转换为Perfect Loop或者Semi-Perfect Loop。如下的Imperfect Loop,如果对内层Product做流水,综合结果如右侧的图所示。
图 4.14 内层做流水的结果
如果对第二层即col的Loop做流水,则会提示信息,col下的循环会被展开。
图 4.15 Col层做流水的结果
从图中的warning可以看出,a被综合为一个双端口的RAM,但第14行和第20行对a的操作有一个重叠的区域,意味着吞吐率受限。
如果对最外部的循环做流水,会把下面所有的循环都展开,延迟会减少,但资源会增加。
如果对整个函数做流水,那么函数下面的所有循环都会展开,能获得最好的Latency,但资源也是最多的。
我们可以对代码就行优化,具体代码具体优化。
Rewind
我们在使用了pipeline后,循环之间仍然会有间隔,但使用rewind功能,可以消除该间隔,如下图所示。
图 4.16 rewind功能
但当函数中有多个循环时,rewind不能使用。
自动添加流水
在config_compile中,可以设置自动添加流水操作,如果循环次数小于我们设定的pipeline loops时,HLS就会自动为for循环添加流水。
在使用config_compile后,如果不想对某些for循环做流水,就可以在pipeline下面的选项中选中disable Loop pipeline。
变量边界的解决方法
当循环边界为变量时,通常可以采用下面的方式进行处理。
1. 使用tripcount directive;
2. 对于边界变量的定义使用ap_int
3. 在C代码中使用assert宏。
Tripcount directive不会对综合有任何的影响,它只会对报告的显示有影响。
使用ap_int和assert方法后,综合后的资源会有明显的减少。采用assert的方式的资源和延迟是最少的。
inline是针对函数,flatten是针对嵌套的循环。