学习使用HLS 加速YOLO v3 Tiny(待更新)

HLS是一种高级综合技术,它允许开发人员使用高级语言(如C、C++和SystemC)描述数字电路的行为和功能。(从定义上看,hls的代码虽然是c++但其描述电路的本质并没有变,所以hls生成ip的c++代码和普通c++代码有较大区别,并不能替换使用)

优化指令

HLS Interface与Hls数据流库

通过工程案例分析:

/*主函数通过DMA一次性传入数据*/
XAxiDma_SimpleTransfer(&axiDMA_1,\
                        (u32)&(l.biases[l.output_ch*i]),l.output_ch*sizeof(short),\
                        XAXIDMA_DMA_TO_DEVICE);
示例:
/*hls中接口必须按引用传递 ,如下yolo_quad_stream &inStream_a。
  接口能输入不同含义的数据(按c++理解为对象的引用有些抽象,接口更容易理解)。*/
void yolo_acc_top(yolo_quad_stream &inStream_a, yolo_quad_stream &inStream_b,
		          yolo_quad_stream &outStream,
				  ap_uint<9> input_h, ap_uint<9> input_w)
{
#pragma HLS INTERFACE s_axilite port=bias_en bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=leaky bundle=CTRL_BUS  //bundle选项对信号进行分组
#pragma HLS INTERFACE s_axilite port=fold_input_ch bundle=CTRL_BUS 
#pragma HLS INTERFACE axis register both port=outStream 
#pragma HLS INTERFACE axis register both port=inStream_a
#pragma HLS INTERFACE axis register both port=inStream_b
/*把inStream_a实现为一个AXI4-Stream接口,并且还表明输入应该被寄存*/

	for(ap_uint<MAX_FOLD_CH_BIT> i=0;i<fold_input_ch;i++)
	{
#pragma HLS LOOP_TRIPCOUNT min=4 max=4
#pragma HLS PIPELINE
		if(bias_en==1)
		{
		quad_fp_side_channel curr_input;
		curr_input = inStream_b.read();      //对inStream_b接口的循环读取
		kernel_bias_fp[4*i] = curr_input.data.sub_data_0;
		kernel_bias_fp[4*i+1] = curr_input.data.sub_data_1;
		kernel_bias_fp[4*i+2] = curr_input.data.sub_data_2;
		kernel_bias_fp[4*i+3] = curr_input.data.sub_data_3;
		}
	}
}

注意:hls::stream<> 的行为与无限深度的 FIFO 相似。

c++理解数据的传输(其他c++实现的模块):数据存入对象hls::stream<>,对象作为参数传入计算函数,函数中使用hls::stream<>.read()在该对象中循环读取数据,每次读取4个值,直到读空。传入下一类数据该接口就又可以读取下一类数据,所以实现了一个接口的两类数据的输入。

高层次综合:hls::stream<>被综合为FIFO、read()会被综合对FIFO的一次读操作

对于其硬件实现的数据传输过程(conv模块)的猜测:DMA传入,从inStream_a接口进入FIFO,每个循环进行一次对FIFO的读操作,每次读取四个值,直到读空。传入下一类数据,就又可以从FIFO读取下一类数据,所以实现了一个接口的两类数据的输入。

From Xilinx UG902

“register”

如果选择该选项,则将在首个操作周期内执行所有按值传递读取操作。对于输出端口,“register”选项可保证对输 出进行寄存。

hls流传输库

设计内部的 hls::stream<> 作为 FIFO 来实现,深度为 2。

按顺序对其执行读取和写入。即,从 hls::stream<> 读取数据之后,无法再次对其进行读取。

hls::stream 类应始终在函数之间作为 C++ 参考实参来进行传递。例如,&my_stream。

对应 hls::stream<> 对象的基本访问为阻塞读取和阻塞写入(API:read/write)。这些访问是使用类方法完成的。这些方法会停止(阻 止)对空的流传输 FIFO 尝试执行的读操作以及对满的流传输 FIFO 尝试执行的写操作,直至针对映射到 ap_hs 接口协 议的流传输完成完整握手为止。

pragma HLS stream

如果存储在阵列中的数据以顺序方式使用或生成,则更有效的通信机制是使用 STREAM pragma 指定的流数据,其中使用 FIFO 而不是 RAM。当顶层函数的参数指定为 INTERFACE 类型 ap_fifo 时,该数组将自动实现为流。

#pragma HLS stream variable=<variable> depth=<int> dim=<int> off

depth=<int>:仅与DATAFLOW 通道中的数据流相关。 默认情况下,RTL 中实现的 FIFO 深度与 C 代码中指定的数组大小相同。 此选项可让您修改 FIFO 的大小并指定不同的深度。

pragma HLS pipeline

PIPELINE pragma 通过允许并发执行操作来减少函数或循环的启动间隔。

#pragma HLS pipeline II=<int> enable_flush rewind  

II=<int>:指定流水操作的所需启动间隔Initial Interval。默认值II为1。

pragma HLS loop_tripcount

TRIPCOUNT指令可应用于循环,以手动指定循环执行的总迭代次数。TRIPCOUNT指令仅用于分析,不影响合成结果。

#pragma HLS loop_tripcount min=<int>max=<int>avg=<int>

指定循环的次数

pragma HLS array_partition

将数组划分为更小的数组或单个元素。

#pragma HLS array_partition variable=<name> <type> factor=<int> dim=<int>

<type>

cyclic 循环分区通过交错原始数组中的元素来创建更小的数组。 

block 块分区从原始数组的连续块创建更小的数组。

complete 完整分区将数组分解为单个元素。

factor:指定要创建的数组的数量。

如果type是complete,此时参数factor不起作用

dim:指定要分区的多维数组的哪个维度。

当dim=0时,表示对整个数组进行完全分区,不考虑具体的维度。这意味着数组的每个元素都将被单独存储,完全独立地占用资源。对于A[2][5],这将导致所有10个元素都被独立存储,每个元素可以被单独访问,这最大化了并行访问能力,但也大量占用资源

pragma HLS BIND_OP

#pragma HLS bind_op variable=<variable> op=<type> impl=<value> latency=<int>


Vitis HLS 使用特定 impl 来实现代码中的运算。BIND_OP 编译指示用于指定针对每个特定变量,都应将一项运算(mul、add、div)映射到特定器件资源,以便在 RTL 内实现 (impl)。如果不指定 BIND_OP 编译指示,Vitis HLS 会自动判定用于运算的资源(一般是LUT)。

op=<type>:用于定义要绑定到特定实现资源的运算。受支持的函数运算包括:mul、add 和 sub 

impl=<value>:定义用于指定运算的实现。受支持的函数运算实现包括 fabric 和 dsp。

latency=<int>:定义运算的实现的默认时延。有效的时延值因指定的 op 和 impl 而异。

案例:
In the following example, the BIND_OP pragma specifies that the add operation for variable temp is implemented using the dsp implementation. 
This ensures that the operation is implemented using a DSP module primitive in the final design.
By default, add operations are implemented using LUTs.

void apint_arith(dinA_t  inA, dinB_t  inB,
          dout1_t *out1
  ) {

 dout2_t temp;

#pragma HLS BIND_OP variable=temp op=add impl=dsp

 temp = inB + inA;
 *out1 = temp;
}

pragma HLS allocation

指定实例限制以限制已实现内核中的资源分配。这定义并可以限制用于实现特定功能、循环、操作或内核的 RTL 实例和硬件资源的数量。

#pragma HLS allocation instances=<list>  limit=<value> <type>

instances=<list>:指定函数、运算符或内核的名称。

limit=<value>:可选地指定要在内核中使用的实例的限制。

<type>:指定分配应用于用于创建设计(例如加法器、乘法器、流水线乘法器和块 RAM)的功能、操作或内核(硬件组件)。

pragma HLS resource

#pragma HLS resource variable=<variable> core=<core> latency=<int>

指定特定的库资源(核心)用于在RTL中实现变量(数组、算术运算或函数参数)。

  • variable=<variable>:指定要将资源pragma分配到的数组、算术运算或函数参数的必需参数。
  • core=<core>:指定核心的必需参数,如技术库中所定义。
  • latency=<int>:指定核心的延迟。

C++模块 coding

接口

typedef hls::stream<quad_fp_side_channel> yolo_quad_stream;
//quad_fp_side_channel是数据流传输结构体,内含4个fp_data_type类型数据、一些标志位

void yolo_conv_top(yolo_quad_stream &inStream, yolo_quad_stream &outStream,
		           ap_uint<MAX_CH_BIT> output_ch, ap_uint<MAX_CH_BIT> input_ch, ap_uint<MAX_FOLD_CH_BIT> fold_output_ch, ap_uint<MAX_FOLD_CH_BIT> fold_input_ch, 
		           ap_uint<9> input_h, ap_uint<9> input_w, ap_uint<9> real_input_h,
				   ap_uint<3> fold_win_area)
{
/*控制接口:输入配置参数*/
#pragma HLS INTERFACE s_axilite port=fold_input_ch bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=fold_output_ch bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=input_ch bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=output_ch bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=fold_win_area bundle=CTRL_BUS
//#pragma HLS INTERFACE s_axilite port=kernel_dim bundle=CTRL_BUS
//#pragma HLS INTERFACE s_axilite port=leaky bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=real_input_h bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=input_w bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=input_h bundle=CTRL_BUS
#pragma HLS INTERFACE s_axilite port=return bundle=CTRL_BUS
/*数据流接口*/
#pragma HLS INTERFACE axis register both port=outStream
#pragma HLS INTERFACE axis register both port=inStream
}

数据流接口使用了hls::stream流传输库,输入的数据一次读入FIFO,每次通过调用hls::stream.read()读取quad_fp_side_channel结构体中的四个数据进行并行运算。

计算参数

input_ch:输入通道数,最初的输入通道数取决于图片的类型,之后的取决于上一层输出的通道数

output_ch:输出通道数,取决于本层过滤器的数量(filter),几个过滤器就会生成几个特征图也就会生成几个通道的结果

每四个通道分为1个fold_input_ch(并行计算单元),总计算量:高度*宽度*四通道(计算单元)数目。同一input_fold_ch的通道会并行计算。

每次传入四个值进行计算,计算完输出4个值,总共要进行g_acc_input.input_h * g_acc_input.input_w * g_acc_input.fold_input_ch次。

计算完把四个值写回输出FIFO,全部写完后给一个信号,整合输出到DMA(自动实现)。

资源优化

资源介绍

1、LUT:查找表,来实现组合逻辑电路的功能
2、FF 触发器
3、BRAM :block ram 嵌入fpga内部。BRAM可以用来实现多种不同位宽和深度的RAM/ROM/FIFO。相比于DRAM这种分布式的ram,Bram只存了1 bit也要占用一个BRAM。
4、DSP:高位加法和乘法主要是由DSP数字信号处理器承担

Xilinx 7系列FPGA中的可编程逻辑单元叫可配置逻辑块(Configurable Logic Block,CLB),它是最小的可编辑逻辑单元,每个CLB 里包含两个逻辑片( Slice),每个 Slice由 4 个查找表( LUT)、8个触发器(FF)和其他一些逻辑所组成的。

资源优化

原则上,加速多,资源消耗就会大。加速少,资源消耗也就少。

zynq7010逻辑资源受限,需要放弃单个模块的加速性能,从而换取片上资源,以在pl能部署更多的模块。(二则也需要找到一个平衡点,才能达到最优化的加速性能提升,初步思路是把神经网络中使用较多的层和并行计算需求量大的层进行部署,使用次数较少的层放在PS端进行串行计算)

1、平衡资源使用,平衡Bram、dsp、LUT的使用情况

(hls资源使用情况表并不准确?与vivado综合后的资源占用表不一致,vivado显示的占用率较低)

减少LUT的方法主要 有对数据位宽,数组实现方式(BRAM),计算资源的选择(DSP),函数的内联等等

2、最优化分配PL端网络的结构

资源有限的情况下,不一定把整个网络都放在PL获得的效益最高

选择使用率高、计算量大的层放在PL。

3、最优化PL侧资源的分配

给最需要加速的PL模块以最大的资源。

优化平衡过程

下文是参考案例中对所有的层都进行的优化(我们只保留了conv)

数组优化

N_max :MAX_KERNEL_NUM           模块数组的大小       // 取值{4,8,16,32}

yolo_acc模块
P_acc  :                                 // 取值{2,4}
#pragma HLS ARRAY_PARTITION variable=kernel_bias_fp cyclic factor=(P_acc>>1)   dim=1
或 #pragma HLS RESOURCE variable=kernel_bias_fp core=RAM_1P_BRAM

yolo_conv模块
P_mem :     P_mem<=N_max/2   N_max % P_mem == 0
#pragma HLS ARRAY_PARTITION variable=local_mem_group block factor=P_mem  dim=1
#pragma HLS ARRAY_PARTITION variable=local_mem_group complete dim=3

#pragma HLS ARRAY_PARTITION variable=out_stream_group complete dim=1
#pragma HLS STREAM variable=out_stream_group depth=2 dim=1

#pragma HLS ARRAY_PARTITION variable=val_output complete dim=1

实例限制

HLS会自动探测算法中的并行性,尽可能将函数或逻辑并行执行以降低整体的Latency。

如下:hls会实例化四份该函数,以提高并行度。

使用ALLOCATION,其作用对象为此函数。limit的值为1表明该函数对应的RTL模块只会被实例化一份。实现了资源的共享(降低了资源的消耗),但降低了并行度。

ALLOCATION还可以指定某类资源的用量。通过指定操作,可以限定LUT、DSP等的用量。

yolo_maxpool模块(移到PS端)
P_pool  :      {1,2}
#pragma HLS ALLOCATION instances=window_max_pool limit=P_pool  function

yolo_yolo模块(移到PS端)
P_yolo :           {1,2,4}
"#pragma HLS ALLOCATION instances=logistic_activate limit=P_yolo function

流水线优化

#pragma HLS PIPELINE       ///在循环的最内层

参考方案

参考的github上整体网络部署方案[1].

尝试了很多种优化方案(包括保留acc,并最小化其资源占用,但资源仍然无法满足部署需求。保留maxpool也不行) ,由于7z010PL侧资源太少,最终只在PL侧保留了卷积模块,并利用了片上资源对其进行了最大限度的加速优化。

参考文献

[1] Yu, Z., Bouganis, CS. (2020). A Parameterisable FPGA-Tailored Architecture for YOLOv3-Tiny. In: Rincón, F., Barba, J., So, H., Diniz, P., Caba, J. (eds) Applied Reconfigurable Computing. Architectures, Tools, and Applications. ARC 2020. Lecture Notes in Computer Science(), vol 12083. Springer, Cham. https://doi.org/10.1007/978-3-030-44534-8_25

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值