vivado HLS硬件化指令(四)卷积相关的指令优化

目的:搞懂与卷积相关的HLS硬件指令。

目录

INLINE指令p316

UNROLL指令P154

    UNROLL停止的判断

LATENCY指令 P164

FUNCTION_INSTANTIATE P174

RESOURCE P178

DATAFLOW P157

    DATAFLOW运用的限制

        DATAFLOW不支持:不是single-producer-consumer格式

        DATAFLOW不支持:条件执行的task

        DATAFLOW不支持:循环中有条件退出

    Dataflow Memory Channels

INTERFACE P489 P95


参考资料 UG902 v2016.4

相关文章:MTCNN的FPGA实现(三)加入HLS预编译指令  https://blog.csdn.net/weixin_36474809/article/details/84580456

检索 P133 优化指令集表

INLINE指令p316

去除子函数层次结构,直接将子函数结构嵌入融入到上一层函数之中,子函数不再是独立的RTL结构。以此来改善时延和II。

HLS会自动的INLINE小的函数,当函数被INLINE的时候,就不会有独立的report和RTL file。

所以,INLINE理解为,子函数硬件结构直接融入到上层函数之中,INLINE OFF理解为子函数硬件结构独立存在,用上层函数来调用此子函数。保险起见,我们需要手动加入相应的优化指令,确定为INLINE或者INLINE off

UNROLL指令P154

HLS默认将循环内设置为单个元素,循环会重复调用同一个结构。UNROLL可以并行化或者部分并行化相应的循环。

文档中的此过程类似于卷积中的相乘,可以UNROLL。文档中将读,相乘,写三个操作一起当作一个时钟周期,确实可以这样吗?第三种完全UNROLL的情况需要将arrays完全partitioned。

例如:

// Array Order : 0 1 2 3 4 5 6 7 8 9 10 etc. 16 etc...
// Sample Order: A0 B0 C0 D0 E0 F0 G0 H0 A1 B1 C2 etc. A2 etc...
// Output Order: A0 B0 C0 D0 E0 F0 G0 H0 A0+A1 B0+B1 C0+C2 etc. A0+A1+A2 etc...
#define CHANNELS 8
#define SAMPLES 400
#define N CHANNELS * SAMPLES
void foo (dout_t d_o[N], din_t d_i[N]) {
	int i, rem;
	// Store accumulated data
	static dacc_t acc[CHANNELS];
	// Accumulate each channel
	For_Loop: for (i=0;i<N;i++) {
		rem=i%CHANNELS;
		acc[rem] = acc[rem] + d_i[i];
		d_o[i] = acc[rem];
	}
}

此程序是设置acc[8],然后每隔8个数字将d_i[i]加入对应的acc[i%8]中。然后将d_o[i]当作acc输出。

此过程是可以8并行的。优化方法:

void foo (dout_t d_o[N], din_t d_i[N]) {
#pragma HLS ARRAY_PARTITION variable=d_i cyclic factor=8 dim=1 partition
#pragma HLS ARRAY_PARTITION variable=d_o cyclic factor=8 dim=1 partition
	int i, rem;
	// Store accumulated data
	static dacc_t acc[CHANNELS];
	// Accumulate each channel
	For_Loop: for (i=0;i<N;i++) {
#pragma HLS PIPELINE rewind
#pragma HLS UNROLL factor=8
		rem=i%CHANNELS;
		acc[rem] = acc[rem] + d_i[i];
		d_o[i] = acc[rem];
	}
}

将d_i与d_o设置为factor为8的cyclic数组分开。相当于8块BRAM,然后设置加入factor为8的UNROLL。问题是为什么前面加了一个pipeline rewind ??

可能为UNROLL的8个并行,但是并行与并行的单元之间还可以pipeline。此例子没有给出合理解释。

    UNROLL停止的判断

原来代码:

for(int i = 0; i < N; i++) {
	a[i] = b[i] + c[i];
}

例如我们将UNROLL factor设置为2,如果循环次数与UNROLL指令不能整除,HLS会自动停止。类似于加一个判断结构。

for(int i = 0; i < N; i += 2) {
	a[i] = b[i] + c[i];
	if (i+1 >= N) break;
	a[i+1] = b[i+1] + c[i+1];
}

如果我们知道N可以被UNROLL factor整除,可以进一步加一个指令 skip_exit_check 避免判断操作,从而节省硬件消耗。

for(int i = 0; i < N; i += 2) {
	a[i] = b[i] + c[i];
	a[i+1] = b[i+1] + c[i+1];
}

LATENCY指令 P164

LATENCY指令用于设定最大或者最小的LATENCY

下面写法用于限定单个循环迭代的LATENCY

Loop_A: for (i=0; i<N; i++) {
#pragma HLS latency max=10
    ..Loop Body...
}

下面这种写法用于限定整个循环所有迭代的LATENCY

Region_All_Loop_A: {
#pragma HLS latency max=10
    Loop_A: for (i=0; i<N; i++)
    {
        ..Loop Body...
    }
}

如果HLS不能达到理想的最大LATENCY,则它会适当放宽条件并且尽可能的满足设定的LATENCY

如果设置最小的LATENCY,那么HLS就会加入dummy clock cycles来满足相应的最小LATENCY。

FUNCTION_INSTANTIATE P174

函数实例化,可以简化函数调用的控制逻辑并且潜在的改善时延和吞吐量。有些函数的输入可能是一个固定的值(constant value),所以可以利用这一点来简化相关的控制结构并且提供更优化的函数块。例如:

void foo_sub(bool mode){
#pragma HLS FUNCTION_INSTANTIATE variable=mode
	if (mode) {
		// code segment 1
	} else {
		// code segment 2
	}
}
void foo(){
#pragma HLS FUNCTION_INSTANTIATE variable=select
	foo_sub(true);
	foo_sub(false);
}

加入FUNCTION_INSTANTIATE 指令之后,相应函数类似转变为下面的格式:

void foo_sub1() {
	// code segment 1
}
void foo_sub1() {
	// code segment 2
}
void A(){
	B1();
	B2();
}

如果函数被许多大层级结构反复调用,则函数需要被密集的INLINE操作,但是许多大结构并不需要,运用FUNCTION_INSTANTIATE 指令可以获取函数许多优化过的小的copies,而不需要使用整个大的函数其中的无用的结构。

RESOURCE P178

指定用于生成具体的硬件core

int foo (int a, int b) {
    int c, d;
#pragma HLS RESOURCE variable=c latency=2
	c = a*b;
	d = a*c;
	return d;
}

例如上面例子中,HLS就自己决定哪个硬件core来实现变量c。

void apint_arith(dinA_t inA, dinB_t inB,dout1_t *out1) {
	dout2_t temp;
#pragma HLS RESOURCE variable=temp core=AddSub_DSP
	temp = inB + inA;
	*out1 = temp;
}

例如,上面例子中,把加法操作和temp变量实现于AddSub_DSP core之中。这说明加法操作被实现于DSP48之中,如果不加此指令则加法操作默认是用LUT来实现。下表为可以用于实现为core的硬件结构:P180

实现于卷积中地址计算的很多变量都是用的MulnS。MulnS表示N阶段的pipelined的乘法器。它的位宽度可以大于标准的DSP48单元。

DATAFLOW P157

 用来确保task level pipeline,让函数或者循环并行化的执行。

 用于一系列的任务序列化的执行,下一个函数的执行不需要上一个函数完成所有的操作。例如下图的例子:

DATAFLOW不会分层的执行,例如子函数和子循环可以运用DATAFLOW进行优化,则必须在子函数和子循环之中加入DATAFLOW指令,或者INLINE子函数。 

    DATAFLOW运用的限制

此步骤之中,数据必须从一个task流向另一个task,下面的情况下不能运用DATAFLOW指令:

  • 不是Single-producer-consumer
  • 避开绕过的task
  • tasks之间有feedback
  • task需要条件语句执行
  • 循环是变上界的循环
  • 循环有一些中间退出条件

        DATAFLOW不支持:不是single-producer-consumer格式

例如下面这个,数据必须从一个task流入到下一个task,但是如果绕过了某个task就不能进行DATAFLOW。

void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
	int temp1[N], temp2[N]. temp3[N];
	Loop1: for(int i = 0; i < N; i++) {
		temp1[i] = data_in[i] * scale;
		temp2[i] = data_in[i] >> scale;
	}
	Loop2: for(int j = 0; j < N; j++) {
		temp3[j] = temp1[j] + 123;
	}
	Loop3: for(int k = 0; k < N; k++) {
		data_out[k] = temp2[k] + temp3[k];
	}
}

例如这里,loop1产生了temp1和temp2,但是temp2绕过了loop2直接到了loop3,所以进行了绕过(bypass)所以不能进行DATAFLOW。我们可以加一步,让temp2产生temp4,这样就可以执行dataflow了。

void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
	int temp1[N], temp2[N]. temp3[N], temp4[N];
	Loop1: for(int i = 0; i < N; i++) {
		temp1[i] = data_in[i] * scale;
		temp2[i] = data_in[i] >> scale;
	}
	Loop2: for(int j = 0; j < N; j++) {
		temp3[j] = temp1[j] + 123;
		temp4[j] = temp2[j];
	}
	Loop3: for(int k = 0; k < N; k++) {
		data_out[k] = temp4[k] + temp3[k];
	}
}

        DATAFLOW不支持:条件执行的task

void foo(int data_in1[N], int data_out[N], int sel) {
	int temp1[N], temp2[N];
	if (sel) {
		Loop1: for(int i = 0; i < N; i++) {
			temp1[i] = data_in[i] * 123;
			temp2[i] = data_in[i];
		}
	} else {
		Loop2: for(int j = 0; j < N; j++) {
			temp1[j] = data_in[j] * 321;
			temp2[j] = data_in[j];
		}
	}
	Loop3: for(int k = 0; k < N; k++) {
		data_out[k] = temp1[k] * temp2[k];
	}
}

这里loop1与loop2是条件执行的,所以不能进行DATAFLOW。若想改成可以DATAFLOW的格式,必须让所有的loop顺序执行,我们将条件语句嵌套到loop1之中,让所有的loop都顺序执行。

void foo(int data_in[N], int data_out[N], int sel) {
	int temp1[N], temp2[N];
	Loop1: for(int i = 0; i < N; i++) {
		if (sel) {
			temp1[i] = data_in[i] * 123;
		} else {
			temp1[i] = data_in[i] * 321;
		}
	}
	Loop2: for(int j = 0; j < N; j++) {
		temp2[j] = data_in[j];
	}
	Loop3: for(int k = 0; k < N; k++) {
		data_out[k] = temp1[k] * temp2[k];
	}
}

        DATAFLOW不支持:循环中有条件退出

#include "ap_cint.h"
#define N 16
typedef int8 din_t;
typedef int15 dout_t;
typedef uint8 dsc_t;
typedef uint1 dsel_t;
void multi_exit(din_t data_in[N], dsc_t scale, dsel_t select, dout_t data_out[N]) {
	dout_t temp1[N], temp2[N];
	int i,k;
	Loop1: for(i = 0; i < N; i++) {
		temp1[i] = data_in[i] * scale;
		temp2[i] = data_in[i] >> scale;
	}
	Loop2: for(k = 0; k < N; k++) {
		switch(select) {
			case 0: data_out[k] = temp1[k] + temp2[k];
			case 1: continue;
			default: break;
		}
	}
}

例如此处就不能执行DATAFLOW,因为loop2有好多循环退出的条件。1. k>=N时退出,2.break语句,3.continue语句

break与continue语句不能出现在DATAFLOW优化之中。

    Dataflow Memory Channels

HLS会将tasks之间的通道设为ping-pong或者FIFO buffer,主要取决于数据的consumer与producer。

对于标量(最大的channel size是1)、指针、函数return的参数,HLS会将channel设为FIFO

如果是数组,顺序接入,则channel设为depth为1的FIFO

HLS若不知道数组是顺序接入还是乱序接入,则将数据设置为ping-pong buffer(两块BRAM,每块的大小都为array的最大值)

INTERFACE P489 P95

运用ap_ctrl_none指令时,它属于block level的IO协议。ap_ctrl_none表示不进行handshake signal。

If you select this option, all pass-by-value reads are performed in the first cycle of
operation. For output ports, the register option guarantees the output is registered. You
can apply the register option to any function in the design. For memory, FIFO, and AXI4
interfaces, the register option has no effect.

运用了此指令,所有的通过此处的数值都会在第一个时钟周期被读取。对于输出的端口,register指令使得输出存储在reigster上。对于内存来说,FIFO和AXI4接口运用register指令是没有用的。

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祥瑞Coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值