FPGA基础知识(十)HLS针对循环的操作

背景:循环结构是c语言中经常出现的结构。HLS会对循环结构作出具体的优化。

目的:目的,搞懂HLS对循环的操作。

UG902: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


1.对循环的操作

  • Pipelined
  • Unrolled
  • Partially unrolled
  • Merged
  • Flattened.

其中对循环结构改变的操作:unrolled,partially unrolled,merged

2.循环上限变动

当循环的上限变动时,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; x++) {
        out_accum += A[x];
    }
    return out_accum;
}

循环的次数边界取决于变量width,这个变量是从最高部分输入的,因此这是一个变上界的循环。

2.1  无法确定latency与performance

因为HLS不知道相应的循环上界,所以无法确定时延(运行循环所需要的周期)

2.2 产生报告方法:运用tripcount指令

可以运用tripcount指令,或者将上限定义为c中的宏。

tripcount指令可以定义一个最小或者平均或者最大的循环上限,它表示循环迭代的次数。例如将上例的最大tripcount 定义为32,则运行结果为:

tripcount指令对综合的结果没有影响,只对报告的结果产生影响。

2.3 无法运行的优化

减小 initiation interval的方法:

  • Unroll the loop and allow accumulations to occur in parallel
  • Partition the array input, or the parallel accumulations are limited,by a single memory port.

如果想要进行上述优化,HLS就会给出下面的报错:

2.4 优化方案,定上界加判断

在循环中加入定值的判断结构,例如上例中,循环上界可以被确定为一个值,loop body就被条件的执行。上面的循环上界因为确定了,所以可以被执行。

#define N 32

LOOP_X:for (x=0;x<N; x++) {
    if (x<width) {
        out_accum += A[x];
    }
}

3. Loop pipeline

对循环进行pipeline时,最优化的基于area和performance的banlance会被执行,针对的是最内层的循环(most inner loop)。这同样会获得最快的运行时间,下面这个代码例子显示了对loop和函数进行的pipeline

// 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<N; x++) {
		if (x<width) {
			out_accum += A[x];
		}
	}
	return out_accum;
}

3.1如果对最内层的LOOP_J进行pipeline

硬件上只有一个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)

3.2 如果对外层的LOOP_I进行pipeline

(注意:当loop或者function被pipelined的时候,被pipeline结构中的loop必须被unroll)

内层的loop会被unrolled,创建20个乘法器,和20个array access。单个的LOOP_I可以被scheduled as a single entity.

3.3 如果对top-level的函数进行pipeline

所有的循环结构都会被unroll,400 multiplier和400 arryas accessd,会被scheduled

3.4 Data dependencies

data dependencies会prevent parallelism。例如,这个例子中,运用dual-port的RAM用于A[N],则相应设计在一个时钟周期中只能获得两个A[N]的值

3.5 pipeline的总结

pipeline高层的loop会unroll底层的loop,但会获得highest的performance。

  • pipeline LOOP_J

latency大概是20*20cycles,但是需要小于100个LUT和寄存器(IO control和FSM are always present)

  • pipeline LOOP_I

latency大概为20cycles,需要上百个LUT与register,大约是上面那种方法的20倍。

  • pipeline function loop_pipeline

latency大概为10(20 dual-port accesses),但是需要上千个LUT与registers,是第一种方法的400倍

3.6 Imperfect nested loops

当内层的loop被pipelined时,vivado HLS会对nested loops进行flattens的操作,从而减少时延和改善总体的吞吐量。(删去loop 中的对循环参数和相应的checks的操作)

imperfect loop nests或者不能flatten loop的结构,会对结果加入一些额外的clock cycles来进入或者退出loop。尽量的减少nested loops的结构。

4. Loop parallelism

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<xlimit; i++) {
		X_accum += A[i];
		X[i] = X_accum;
	}
	SUM_Y:for (i=0;i<ylimit; i++) {
		Y_accum += B[i];
		Y[i] = Y_accum;
	}
}

在这个代码中,循环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<limit; i++) {
		accum += I[i];
		O[i] = accum;
	}
}
void loop_functions(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N],
dsel_t xlimit, dsel_t ylimit) {
	sub_func(A,X,xlimit);
	sub_func(B,Y,ylimit);
}

3-14的时延就比3-13少了将近一半,因为loops可以被并行的执行。但是在3-13中可以进行dataflow的优化,而这里则不行。dataflow的优化只能在top-level的函数和loop中运行。

5. Loop dependencies

loop dependencies即data dependencies,它会让对loop的优化变得困难,特别是pipeline的时候。例如:

在这个循环中,下个循环迭代必须有上个迭代的结果送出。这个会在array中讨论。

6.Unrool loops in c++ classes

在运用c++的classes的时候,应当小心,不要让loop induction variable作为clss中的data member,这样loop就不能unroll。

例如,下面loop induction variable的k就是foo_class的成员。

template <typename T0, typename T1, typename T2, typename T3, int N>
class foo_class {
	private:
		pe_mac<T0, T1, T2> 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 <typename T0, typename T1, typename T2, typename T3, int N>
class foo_class {
	private:
		pe_mac<T0, T1, T2> 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);
};

猜你喜欢

转载自blog.csdn.net/weixin_36474809/article/details/81479551