目的:搞懂与卷积相关的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 优化指令集表
去除子函数层次结构,直接将子函数结构嵌入融入到上一层函数之中,子函数不再是独立的RTL结构。以此来改善时延和II。
HLS会自动的INLINE小的函数,当函数被INLINE的时候,就不会有独立的report和RTL file。
所以,INLINE理解为,子函数硬件结构直接融入到上层函数之中,INLINE OFF理解为子函数硬件结构独立存在,用上层函数来调用此子函数。保险起见,我们需要手动加入相应的优化指令,确定为INLINE或者INLINE off
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
此程序是设置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
将d_i与d_o设置为factor为8的cyclic数组分开。相当于8块BRAM,然后设置加入factor为8的UNROLL。问题是为什么前面加了一个pipeline rewind ??
可能为UNROLL的8个并行,但是并行与并行的单元之间还可以pipeline。此例子没有给出合理解释。
原来代码:
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指令用于设定最大或者最小的LATENCY
下面写法用于限定单个循环迭代的LATENCY
Loop_A: for (i=0; i
下面这种写法用于限定整个循环所有迭代的LATENCY
Region_All_Loop_A: {
#pragma HLS latency max=10
Loop_A: for (i=0; i
如果HLS不能达到理想的最大LATENCY,则它会适当放宽条件并且尽可能的满足设定的LATENCY
如果设置最小的LATENCY,那么HLS就会加入dummy clock cycles来满足相应的最小LATENCY。
函数实例化,可以简化函数调用的控制逻辑并且潜在的改善时延和吞吐量。有些函数的输入可能是一个固定的值(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,而不需要使用整个大的函数其中的无用的结构。
指定用于生成具体的硬件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单元。
用来确保task level pipeline,让函数或者循环并行化的执行。
用于一系列的任务序列化的执行,下一个函数的执行不需要上一个函数完成所有的操作。例如下图的例子:
DATAFLOW不会分层的执行,例如子函数和子循环可以运用DATAFLOW进行优化,则必须在子函数和子循环之中加入DATAFLOW指令,或者INLINE子函数。
此步骤之中,数据必须从一个task流向另一个task,下面的情况下不能运用DATAFLOW指令:
例如下面这个,数据必须从一个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];
}
}
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];
}
}
#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优化之中。
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的最大值)
运用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指令是没有用的。