背景:循环结构是c语言中经常出现的结构。HLS会对循环结构作出具体的优化。
目的:目的,搞懂HLS对循环的操作。
UG902 v2016.4 P318:HLS用户指南中Loops内容
目录
1.对循环的操作
2.循环上限变动
2.1 无法确定latency与performance
2.2 产生报告方法:运用tripcount指令
2.3 无法运行的优化
2.4 优化方案,定上界加判断
3. Loop pipeline
3.1如果对最内层的LOOP_J进行pipeline
3.2 如果对外层的LOOP_I进行pipeline
3.3 如果对top-level的函数进行pipeline
3.4 Data dependencies
3.5 pipeline的总结
3.6 Imperfect nested loops
4. Loop parallelism
5. Loop dependencies
6.Unrool loops in c++ classes
其中对循环结构改变的操作:unrolled,partially unrolled,merged
当循环的上限变动时,HLS难以对循环作出优化。比如下面这个例子中,
Example 3-10
#include "ap_cint.h"
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint5 dsel_t;
dout_t code028(din_t A[N], dsel_t width) {
dout_t out_accum=0;
dsel_t x;
LOOP_X:for (x=0;x
循环的次数边界取决于变量width,这个变量是从最高部分输入的,因此这是一个变上界的循环。
因为HLS不知道相应的循环上界,所以无法确定时延(运行循环所需要的周期)
可以运用tripcount指令,或者将上限定义为c中的宏。
tripcount指令可以定义一个最小或者平均或者最大的循环上限,它表示循环迭代的次数。例如将上例的最大tripcount 定义为32,则运行结果为:
tripcount指令对综合的结果没有影响,只对报告的结果产生影响。
减小 initiation interval的方法:
如果想要进行上述优化,HLS就会给出下面的报错:
在循环中加入定值的判断结构,例如上例中,循环上界可以被确定为一个值,loop body就被条件的执行。上面的循环上界因为确定了,所以可以被执行。
#define N 32
LOOP_X:for (x=0;x
// Example 3-11: Variable Loop Bounds Rewritten
#include "ap_cint.h"
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint5 dsel_t;
dout_t loop_max_bounds(din_t A[N], dsel_t width) {
dout_t out_accum=0;
dsel_t x;
LOOP_X:for (x=0;x
这里我们存在一个疑问,为什么out_accum是一个累加的变量,但是我们可以进行UNROLL,即并行化 ?那相应的时钟周期到底是什么样的?
对循环进行pipeline时,最优化的基于area和performance的banlance会被执行,针对的是最内层的循环(most inner loop)。这同样会获得最快的运行时间,下面这个代码例子显示了对loop和函数进行的pipeline
#include "loop_pipeline.h"
dout_t loop_pipeline(din_t A[N]) {
int i,j;
static dout_t acc;
LOOP_I:for(i=0; i < 20; i++){
LOOP_J: for(j=0; j < 20; j++){
acc += A[i] * j;
}
}
return acc;
}
硬件上只有一个LOOP_J的copy(单个的乘法器)。vivado HLS会自动的flatten the loops。例如这个程序里面,对单个的loop进行了20*20次迭代。只有一个乘法操作和一个array access会被scheduled。整个loop iteration会被scheduled为single loop-body entity(20*20个loop iteration)
(注意:当loop或者function被pipelined的时候,被pipeline结构中的loop必须被unroll)
内层的loop会被unrolled,创建20个乘法器,和20个array access。单个的LOOP_I可以被scheduled as a single entity.
所有的循环结构都会被unroll,400 multiplier和400 arryas accessd,会被scheduled
data dependencies会prevent parallelism。例如,这个例子中,运用dual-port的RAM用于A[N],则相应设计在一个时钟周期中只能获得两个A[N]的值
pipeline高层的loop会unroll底层的loop,但会获得highest的performance。
latency大概是20*20cycles,但是需要小于100个LUT和寄存器(IO control和FSM are always present)
latency大概为20cycles,需要上百个LUT与register,大约是上面那种方法的20倍。
latency大概为10(20 dual-port accesses),但是需要上千个LUT与registers,是第一种方法的400倍
当内层的loop被pipelined时,vivado HLS会对nested loops进行flattens的操作,从而减少时延和改善总体的吞吐量。(删去loop 中的对循环参数和相应的checks的操作)
imperfect loop nests或者不能flatten loop的结构,会对结果加入一些额外的clock cycles来进入或者退出loop。尽量的减少nested loops的结构。
这个指的是不同的loop之间的并行化。vivado HLS会尽可能的将logic operation和function进行并行化处理,但是它并不会对相应的loops进行并行化处理。
//Example 3-13: Sequential Loops
#include "loop_sequential.h"
void loop_sequential(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N],
dsel_t xlimit, dsel_t ylimit) {
dout_t X_accum=0;
dout_t Y_accum=0;
int i,j;
SUM_X:for (i=0;i
在这个代码中,循环SUM_X和循环SUM_Y会被sheduled,尽管SUM_Y并不需要等到SUM_X结束再开始,但是它依然会被scheduled after SUM_X。
由于这两个loop的bounds(xlimit与ylimit)是不同的,因此他们不能进行merged。将两个loop放在两个分开的function中,如下面所示,则loops就可以被parallel处理。
//Example 3-14: Sequential Loops as Functions
#include "loop_functions.h"
void sub_func(din_t I[N], dout_t O[N], dsel_t limit) {
int i;
dout_t accum=0;
SUM:for (i=0;i
3-14的时延就比3-13少了将近一半,因为loops可以被并行的执行。但是在3-13中可以进行dataflow的优化,而这里则不行。dataflow的优化只能在top-level的函数和loop中运行。
loop dependencies即data dependencies,它会让对loop的优化变得困难,特别是pipeline的时候。例如:
在这个循环中,下个循环迭代必须有上个迭代的结果送出。这个会在array中讨论。
在运用c++的classes的时候,应当小心,不要让loop induction variable作为clss中的data member,这样loop就不能unroll。
例如,下面loop induction variable的k就是foo_class的成员。
template
class foo_class {
private:
pe_mac mac;
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
int k; // Class Member
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col)
{
Function_label0:;
#pragma HLS inline off
SRL:for (k = N-1; k >= 0; --k) {
#pragma HLS unroll// Loop will fail UNROLL
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};
如果想让vivado HLS对loop进行UNROLL的pragma指令,则代码需要改为不要把k作为class member成员。
template
class foo_class {
private:
pe_mac mac;
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col)
{
Function_label0:;
int k; // Local variable
#pragma HLS inline off
SRL:for (k = N-1; k >= 0; --k) {
#pragma HLS unroll// Loop will unroll
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};