部署方案

我们选择yolo v3 Tiny,这是一个轻量级的网络(仅13层),先前已有在7z020上部署了yolo v3 Tiny的整个网络的开源案例[1]。但7020逻辑资源数量是7010的将近1倍,所以要在7010上部署就不得不把一些层放在arm核上计算,在FPGA部分实例化更多计算密集层的单元。

前提

参数量化

参数的量化是必须的,PL部分逻辑资源受限,高精度的浮点计算消耗资源太多,所以把参数量化成了16位(一位符号位、7位整数和8位小数)。在内存中存储的参数为short类型(16位),但只是以short类型存储,实际值为有符号浮点数。Short类型的值传入PL端卷积模块会以有符号浮点数计算,输出的值依然是16位有符号浮点数。

ap_fixed<16,8>

部署

原则上,实例化单元越多,并行计算加速越多,资源消耗也会越大。反之,资源消耗也就少。由于片上逻辑资源受限,我们并不能使用HLS进行最大限度的实例化。要合理的实例化数据结构和计算单元,保证片上逻辑资源得到充分使用。

此外,尽可能提高PL的使用效率是重要的:资源有限的情况下,不一定把更多的层放在PL获得的效益最高。把使用频繁的层和计算密集的层放在PL侧并将内容进行最大的实例化,使用次数较少的层放在arm侧,收益最高。

结果

测试多种部署方案:卷积+池化、深度可分离卷积?。最优结果是在PL部署卷积计算,并利用了片上资源对其进行了最大限度的加速优化(见下文)。

HLS数据结构

// AXI_HP 接口数据结构
typedef struct quad_fp_pack{  //四个16位并行传输,共64位
	fp_data_type sub_data_0;
	fp_data_type sub_data_1;
	fp_data_type sub_data_2;
	fp_data_type sub_data_3;
}quad_fp_pack;
template<int D,int U,int TI,int TD>   // 数据传输信号
  struct ap_axi_fp{
	quad_fp_pack   data;   // 数据
    ap_uint<(D+7)/8> keep;
    ap_uint<(D+7)/8> strb;
    ap_uint<U>       user;
    ap_uint<1>       last;
    ap_uint<TI>      id;
    ap_uint<TD>      dest;
};
typedef ap_axi_fp<64,2,5,6> quad_fp_side_channel;
typedef hls::stream<quad_fp_side_channel> yolo_quad_stream; //接口,FIFO实现

// 计算数据结构
typedef hls::Window<3,3,fp_data_type> window_type; //感受野
typedef hls::LineBuffer<3,MAX_LINE_BUFF_WIDTH,fp_data_type> line_buff_type; //三行特征图缓存
typedef struct local_weight_type  // 卷积核
{
	fp_weight_type data[3*3];
}local_weight_type;

HLS资源优化策略和协调仿真验证

下文一次计算最大in/out通道为16,经过测试不行。尝试8也不行,改为了4。最终运行在200Mhz

数据结构

资源受限,一次计算输入输出最大通道数为16。

卷积核缓存local_mem_group第三维complete卷积核的9个值都有单独的输入输出接口。

卷积核缓存local_mem_group第一维16个卷积核组被分为4个block,4个卷积核组存一起,每个能单独输入输出

特征图缓存line_buff_group[4]实例化四个,每个存四个通道(每四个有一个输入输出接口)

#define MAX_KERNEL_NUM 16  定义conv ip进行一次计算最大输入/输出通道数

	line_buff_type line_buff_group_0[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_1[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_2[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_3[MAX_KERNEL_NUM/4];

local_weight_type local_mem_group[MAX_KERNEL_NUM][MAX_INPUT_CH];
#pragma HLS ARRAY_PARTITION variable=local_mem_group block factor=4 dim=1
#pragma HLS ARRAY_PARTITION variable=local_mem_group complete dim=3

函数和循环 

pipline后,先9个并行读入卷积核数据,4个并行读入特征图数据

macc卷积计算模块实例化4个,slide_window实例化了2个

暂时未验证计算瓶颈,待学习透视图分析

基于MAX_KERNEL_NUM = 32的分析

权重读入

weight [MAX_KERNEL_NUM][MAX_INPUT_CH][9]   被例化成72个部分

第三个维度是complete block ,每个block有独立的读写接口,能够被并行处理(pipline后,实现并行读入),而且每个都被单独存放!

第二个维度只能串行读取,循环嵌套方式(内层第二维度,外层第一维度)导致pipline无法展开一二层循环(第一维虽然有8个接口,也只能串行读取)。但是第一维被分为8个block,所以每四个可以进行单独存储(4个卷积核组存一起)。

读取时序(只列出前三个)
// 读取并存放权重,数据结构为 32 * 32 个 3*3 的卷积核。
	local_weight_type local_mem_group[MAX_KERNEL_NUM][MAX_INPUT_CH];
#pragma HLS ARRAY_PARTITION variable=local_mem_group block factor=8 dim=1  // 卷积核分8组,每组四个
#pragma HLS ARRAY_PARTITION variable=local_mem_group complete dim=3  // 每个kernel的9个值独立,可以被并行处理

	for(int k=0; k<output_ch; k++) //卷积核个数(最大32)= 输出通道数
	{
		for(int i=0;i<input_ch;i++) //每个kernel的depth(最大32)= 输入通道数
		{
			for(int j=0; j<fold_win_area; j++) //文件中以4*3存储kernel,读取3*3
			{
#pragma HLS PIPELINE
				curr_input = inStream.read();
				local_mem_group[k][i].data[4*j] = curr_input.data.sub_data_0;
				if(j!=(MAX_KERNEL_DIM*MAX_KERNEL_DIM+3)/4-1)
				{
					local_mem_group[k][i].data[4*j+1] = curr_input.data.sub_data_1;
					local_mem_group[k][i].data[4*j+2] = curr_input.data.sub_data_2;
					local_mem_group[k][i].data[4*j+3] = curr_input.data.sub_data_3;
				}
			}
		}
	}

特征图读入

数据在内存中的存储方式 是CHW,这是能进行四通道并行读入的前提!!!

4个line_buf每个有独立接口(读入被例化四次),pipline后4个buf并行读入 418 * 3 个数据(每行也是并行读入,且每行单独存储)。下图表示3通道图片输入读取的过程(第四通道为0),如果是32通道输入则四个四个通道依次读取。

数据持续读入第三行第三列(能够进行一次计算),之后每次读入一个数,抛弃一个数。之所以数组元素为8是要在输入为32通道时记录每个通道的输入数据,以便计算每个通道。

// 缓存感受野(被卷积的window):数据结构为 3 行宽度为 418 的缓冲区。
// 加速计算是进行四个一组的并行计算,所以定义的四个。最大32个输出对应8个并行计算组。
	line_buff_type line_buff_group_0[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_1[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_2[MAX_KERNEL_NUM/4];
	line_buff_type line_buff_group_3[MAX_KERNEL_NUM/4];

	for(int row_idx=0;row_idx<input_h+1;row_idx++) //extra one row to send rest data
	{
		for(int col_idx=0;col_idx<input_w;col_idx++)
		{
			for(int input_ch_idx=0;input_ch_idx<fold_input_ch;input_ch_idx++)  
			{	//四个输入一组的并行计算,input_ch_idx是计算组的index
#pragma HLS PIPELINE
/*--------------------------------------------------------------------------------------------------------------------*/
				if(row_idx != input_h)
				{
					if(((row_idx == 0)|| (row_idx == real_input_h-1)||
					   (col_idx == 0)|| (col_idx == input_w-1)))
					{ // 原本是416*416    进行首尾行列的pading得到418*418
						curr_input.data.sub_data_0 = 0;
						curr_input.data.sub_data_1 = 0;
						curr_input.data.sub_data_2 = 0;
						curr_input.data.sub_data_3 = 0;
					}
					else  curr_input = inStream.read();
/*--------------------------------------------------------------------------------------------------------------------*/
// 每个输入通道的三行数据缓存到每个输入通道的buf中
yolo_line_buffer(curr_input.data.sub_data_0,&line_buff_group_0[input_ch_idx],col_idx);
yolo_line_buffer(curr_input.data.sub_data_1,&line_buff_group_1[input_ch_idx],col_idx);					
yolo_line_buffer(curr_input.data.sub_data_2,&line_buff_group_2[input_ch_idx],col_idx);				
yolo_line_buffer(curr_input.data.sub_data_3,&line_buff_group_3[input_ch_idx],col_idx);
// 从 linebuf 中取一个 window
kernel_window_0 = slide_window(conv_col_count,&line_buff_group_0[input_ch_idx]);
kernel_window_1 = slide_window(conv_col_count,&line_buff_group_1[input_ch_idx]);
kernel_window_2 = slide_window(conv_col_count,&line_buff_group_2[input_ch_idx]);
kernel_window_3 = slide_window(conv_col_count,&line_buff_group_3[input_ch_idx]);

计算

四个输入通道可以并行计算,先循环计算kernel0-32(可以被pipline并行,取决于kernel接口的个数) ,再循环计算输入通道组(串行)。

卷积模块被例化了16次,后处理4次(被pipline展开,kernel_idx :0,1,2,3又进行了并行计算)

row_idx == 2      col_idx == 2 时进行第一次conv计算(416*416)
row_idx == 2      col_idx == 3 时进行第一次输出  

void yolo_conv_top(yolo_quad_stream &inStream, yolo_quad_stream &outStream 。。。)
{

// 输出FIFO,每个卷积核会有一个输出,网络单层最多32个输出,所以定义了32个用于输出FIFO的数组 。 
	yolo_inter_stream out_stream_group[MAX_KERNEL_NUM];
#pragma HLS ARRAY_PARTITION variable=out_stream_group complete dim=1 // complete:元素独立,因此每个输出通道(每个元素)可以单独进行输入输出。
#pragma HLS STREAM variable=out_stream_group depth=2 dim=1 // fifo深度为2

	fp_mid_type val_output[MAX_KERNEL_NUM];   //用来暂存输出
#pragma HLS ARRAY_PARTITION variable=val_output complete dim=1

/*-------------------------------------------------------------------------------------*/
	for(int row_idx=0;row_idx<input_h+1;row_idx++) //extra one row to send rest data
	{
		for(int col_idx=0;col_idx<input_w;col_idx++)
		{
			for(int input_ch_idx=0;input_ch_idx<fold_input_ch;input_ch_idx++)  
			{	//四个输入一组的并行计算,input_ch_idx是计算组的index
#pragma HLS PIPELINE
				int conv_row_count=0,conv_col_count=0;
				if((row_idx>MAX_KERNEL_DIM-2)&&(col_idx>MAX_KERNEL_DIM-2))	
				{   conv_row_count = row_idx - (MAX_KERNEL_DIM-1);
					conv_col_count = col_idx - (MAX_KERNEL_DIM-1);}
				else
				{conv_row_count = 0;conv_col_count = 0;}

 //条件判断使得linebuffer中先填充3行,才能开始计算
if((row_idx>MAX_KERNEL_DIM-2)&&(col_idx>MAX_KERNEL_DIM-2))
{
	// kernel_idx 输出的索引值。每个输出 = Σ( 所有输入*输入通道对应的卷积核 )
    //进行所有输出通道一个计算组的计算
	for(int kernel_idx=0; kernel_idx<MAX_KERNEL_NUM; kernel_idx++) 
	{	
//window_macc :卷积计算
		sub0_val_output = window_macc(kernel_window_0,local_mem_group[kernel_idx][4*input_ch_idx]);
		sub1_val_output = window_macc(kernel_window_1,local_mem_group[kernel_idx][4*input_ch_idx+1]);
		sub2_val_output = window_macc(kernel_window_2,local_mem_group[kernel_idx][4*input_ch_idx+2]);
		if(input_ch==3)
			sub3_val_output = 0;
		else
			sub3_val_output = window_macc(kernel_window_3,local_mem_group[kernel_idx][4*input_ch_idx+3]);

// 累加操作		
val_output[kernel_idx]=post_process(sub0_val_output,sub1_val_output,sub2_val_output,sub3_val_output,input_ch_idx,val_output[kernel_idx]); 

//完成所有的计算组,最终结果接入输出流fifo
if(input_ch_idx == fold_input_ch-1)
{
	if(kernel_idx<output_ch)
	{
		ap_fixed<16,8,AP_RND_CONV,AP_SAT> output_rec = val_output[kernel_idx];
		if(!(out_stream_group[kernel_idx].full()))
		write_output(output_rec,out_stream_group[kernel_idx]);
	}
}
	}
}

if(!((conv_row_count == 0)&&(conv_col_count ==0)))
{
	if((row_idx==input_h)&&(input_ch_idx==fold_input_ch-1))
		last = 1;
	else
		last = 0;
	// input_ch_idx 值为0、1时才能输出。
	out_stream_merge(out_stream_group,outStream,input_ch_idx,curr_input,last,output_ch,fold_output_ch);
}

HLS资源占用

仿真结果频率达到100Mhz

hls资源使用情况表并不准确,vivado综合后的资源占用率较低

Tips--使用方法

 

新建工程:定义top函数、定义tb文件(包含测试向量)、设置器件型号、定义端口输入时钟、定义输出配置(默认:导出IP、语言verilog)

模拟仿真

第一步:C simulation      软件层面,对代码功能进行逻辑验证(需要使用tb和测试向量)

                主要检查功能是否符合预期,不考虑硬件实现的时序、资源等因素

第二步:C synthsis  (hls会自动优化资源占用空间)

目的:高级语言综合成RTL 、计算 FPGA 资源使用情况、生成时序信息

结果示例:

第三步    Co-simulation协同仿真

目的:检查 RTL 代码是否与原始 C 代码的功能匹配

使用dunp trace 选项可以得到波形图(可以使用仿真工具vivado 、modelsim打开)

可以在仿真工具中查看时序情况、并行情况、函数实例化个数

第四步:导出IP

Solution --->Export RTL

Ref

[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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐