2D卷积C/C++实现(高级综合HLS)

2D卷积C/C++实现

void _2DConv(int8_t *in,int8_t *out,int8_t *filter,uint32_t nrows,uint32_t ncols,uint8_t k,uint8_t stride)
/* 
in:输入图像
out:输出图像
filter:滤波器
nrows:输出图像的行数
ncols:输出图像的列数
k:滤波器大小(k*k)
stride:滤波窗口步进长度
 */

{
uint16_t r,c,i,j;
uint32_t nrows_i=nrows*stride+k-1;//输入图像的行数
uint32_t ncols_i=ncols*stride+k-1;//输入图像的列数

for(r=0;r<nrows;r++)
	{
	for(c=0;c<=ncols;c++)
		{
		for(i=0;i<k;i++)
			{
			for(j=0;j<k;j++)
				{
				uint32_t offset=(r*stride+i)*ncols_i+(c*stride+j);
				int32_t in_tmp=*(in+offset);
				int32_t filter_tmp=*(filter+i*k+j);
				*(out+r*ncols+c)+=in_tmp *filter_tmp;
				}
			}
		}
	}

} 

把代码封装成基于AXI4的形式

void HW_2DConv_Mmap_1(int8_t *pixel_in,int8_t *pixel_out,int32_t addr_reserved)
{
	/*
	AXI4数据总线(m_axi):
	pixel_in用于FPGA加速器主动读取DDR的输入图像和滤波器
	pixel_out用于FPGA加速器主动写入输出结果到DDR中
		
	AXI4Lite控制总线(s_axilite):
		用于输入输出图像访存基地址
	*/
	#pragma HLS INTERFACE m_axi depth =482*272+3*3 port=pixel_in offset=slave bundle=user_axi_in register 
	#pragma HLS INTERFACE m_axi depth =482*270     port=pixel_out offset=slave bundle=user_axi_out register
	
	#pragma HLS INTERFACE s_axilite port=pixel_in bundle=user_axi4lite_config register
	#pragma HLS INTERFACE s_axilite port=pixel_out bundle=user_axi4lite_config register
	#pragma HLS INTERFACE s_axilite port=addr_reserved offset=0xFFF0  bundle=user_axi4lite_config register
	#pragma HLS INTERFACE s_axilite port=return  bundle=user_axi4lite_config register
	
	
	int8_t *filter_base=pixel_in+ROWS_I*COLS_I;
	
	_2DConv(pixel_in,pixel_out,filter_base,ROWS_O,COLS_O,FILTER_SIZE,STRIDE);
}

实现结果的时间和资源

  • 时间(latency):21.25M clock cycles
  • 主要资源开销:1个dsp 84个乘法器
  • 瓶颈:四层迭代 串行运行

里面的两层迭代需要展开
外面的两层迭代需要流水

迭代优化

void _2DConv(int8_t *in,int8_t *out,int8_t *filter,uint32_t nrows,uint32_t ncols,uint8_t k,uint8_t stride)
{
uint16_t r,c,i,j;
uint32_t nrows_i=nrows*stride+k-1;//输入图像的行数
uint32_t ncols_i=ncols*stride+k-1;//输入图像的列数

for(r=0;r<nrows;r++)
	{
	for(c=0;c<=ncols;c++)
		{
		#pragma HLS PIPELINE II=1//流水
		for(i=0;i<k;i++)
			{
			for(j=0;j<k;j++)
				{
				uint32_t offset=(r*stride+i)*ncols_i+(c*stride+j);
				int32_t in_tmp=*(in+offset);
				int32_t filter_tmp=*(filter+i*k+j);
				*(out+r*ncols+c)+=in_tmp *filter_tmp;
				}
			}
		}
	}

} 	

实现结果的时间和资源

  • 时间(latency):2.33M clock cycles 提高了9倍多
  • 主要资源开销:6个dsp 365个乘法器
  • 瓶颈:目标流水线是1个clock cycles ,但是目前只能达到18个clock cycles,访存瓶颈

访存及核心计算优化

void _2DConv——Op(int8_t *in,int8_t *out,int8_t *filter,uint32_t nrows,uint32_t ncols,uint8_t k,uint8_t stride)
{
uint16_t r,c,i,j;
uint32_t nrows_i=nrows*stride+k-1;//输入图像的行数
uint32_t ncols_i=ncols*stride+k-1;//输入图像的列数

int8_t *filter_based=in+ROWS_I*COLS_I;

int8_t filter_buffer[FILTER_SIZE][FILTER_SIZE]
//*pragma HLS ARRAY_PARTITION variable=filter_buffer complete dim=0
memcpy (&filter_buffer[0][0],filter_base,FILTER_SIZE*FILTER_SIZE*sizeof(int8_t));//提前将卷积核放到片上


for(r=0;r<nrows;r++)
	{
	for(c=0;c<=ncols;c++)
		{
		#pragma HLS PIPELINE II=1
		int8_t store_data=0;
		for(i=0;i<k;i++)
			{
			for(j=0;j<k;j++)
				{
				uint32_t offset=(r*stride+i)*ncols_i+(c*stride+j);
				int8_t val=*(in+offset);
				int8_t res=0;
				
				if(filter_buffer[i][j]==0)//针对二值卷积核进行优化
					res=0;
				else if(filter_buffer[i][j]==1)
					res=val;
				else if(filter_buffer[i][j]==-1)
					res=-val;
				
				store_data+=res
				*(out+r*ncols+c)+=store_data;
				}
			}
		}
	}

} 	

卷积核缓存到片上ram

实现结果的时间和资源

  • 时间:1.16M clock cycles 提高了18倍多
  • 主要资源开销:0个dsp 365个乘法器
  • 瓶颈:目标流水线是1个clock cycles ,但是目前只能达到9个clock cycles,图片还是从ddr读的

2D卷积操作必须对k*k大小的窗口内的所有像素点进行计算
字节从DDR中取像素进行计算的话,访存延迟大,访存周期不确定,无法实现高效的流水处理

图片数据缓冲到片上

行缓冲(line buffer)
窗口缓冲(windows buffer)

在这里插入图片描述

算法硬件结构优化:行缓冲

  • 由于DEMO中filter大小为3*3,因此,建立一个3行的行缓冲,用于缓存当前滤波窗口正在处理的三行像素
  • 行缓冲通过ARRAY_PARTITION指示对二维数组的第一维(行)进行展开,每一维通过独立的片上BRAM实现,支持同时访问不同行中的像素点
template<typename T,int LROW,int LCOL>
class ap_linebuffer{
   public:
   	TM[LROW][LCOL];
   	#pragma AP ARRAY_PARTITION variable=M dim=1 complete
   	ap_linebuffer(){
   		};
}	

算法硬件结构优化:窗口缓冲

  • 建立一个3*3的窗口缓存,通过ARRAY_PARTITION对所有维度进行全部展开,窗口缓存中所有9个元素都由LUT实现,支持同时对所有元素并发访问
  • 算法从DDR读取下一个像素,添加到行缓冲中。同时,行缓冲已经缓存了前两行的像素。通过访问行缓存,正好可以得到当前滤波窗口所在位置的所有像素点,这些像素点存入窗口缓存中,并送到流水线中进行处理。
template<typename T,int LROW,int LCOL>
class ap_window{
   public:
   	TM[LROW][LCOL];
   	#pragma AP ARRAY_PARTITION variable=M dim=1 complete
   	ap_window(){
   		};
}	

完整代码

void _2DConv_Op(int8_t *in,int8_t *out,int8_t *filter,uint32_t nrows,uint32_t ncols,uint8_t k,uint8_t stride)
{
uint16_t r,c,i,j;
uint32_t nrows_i=nrows*stride+k-1;//输入图像的行数
uint32_t ncols_i=ncols*stride+k-1;//输入图像的列数

int8_t *filter_based=in+ROWS_I*COLS_I;

int8_t filter_buffer[FILTER_SIZE][FILTER_SIZE]
//*pragma HLS ARRAY_PARTITION variable=filter_buffer complete dim=0
memcpy (&filter_buffer[0][0],filter_base,FILTER_SIZE*FILTER_SIZE*sizeof(int8_t));//提前将FILTER_SIZE*FILTER_SIZE个像素放到片上

ap_linebuffer<int8_t,FILTER_SIZE,COLS_I> line_buffer;

ROW_LOOP:for(r=0;r<ROWS_I;r++)
	{
	ap_window<int8_t,FILTER_SIZE,FILTER_SIZE> window_buffer;
	
	//fill the line buffer
	COL_LOOP:for(c=0;c<=COLS_I;c++)
		{
#pragma HLS PIPELINE II=1
		int8_t store_data=0;
		int8_t load_data=*(in_base+r*COLS_I+c);
		
		line_buffer.shift_up(c);
		line_buffer.insert_bottom(load_data,c);//行缓冲,保证数据从DDR里面流水的取出
		
		if(r>=2)
			{
			window_buffer.shift_right();
			window_buffer,insert(line_buffer.getval(2,c),0,2);
			window_buffer,insert(line_buffer.getval(1,c),1,2);
			window_buffer,insert(line_buffer.getval(0,c),2,2);
			}//从行缓冲中获取数据,构建卷积计算的窗口缓冲
		
		if(r>=2&&c>=2)
			{
			for(i=0;i<FILTER_SIZE;i++)
				{
				for(j=0;j<FILTER_SIZE;j++)
					{
					int8_t res=0;
					int8_t val=window_buffer.getval(i,j);//从窗口中获取像素值,进行卷积计算									
					
					if(filter_buffer[i][j]==0)//针对二值卷积核进行优化
						res=0;
					else if(filter_buffer[i][j]==1)
						res=val;
					else if(filter_buffer[i][j]==-1)
						res=-val;
					
					store_data+=res
					
					}
				}
			*(out_base+(r-2)*COLS_O+c(c-2))+=store_data;
			}
		}

	}
	
}	

实现结果的时间和资源

  • 时间(latency):131k clock cycles 提高了164倍多
  • 综合后流水线达到目标流水线要求,1个clock cycles
  • latency 降低到1311143个时钟周期,比优化前降低了约99.5%
  • 理论分析,实现COL_LOOP全流水后,每个时钟周期能完成一个滤波窗口的计算。理论上,总的时钟周期开销约为270*480=129600.最终优化后的131143仅与理论值相差1.1%。
  • 11
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
HLS(High-Level Synthesis)是一种将高级语言(如C/C++)转换为硬件描述语言(如Verilog和VHDL)的技术,以方便进行FPGA设计。在HLS实现F.conv2d(二维卷积运算)的方法如下: 1. 定义输入和输出数据类型:首先需要定义输入和输出数据类型。假设输入数据为3通道的图像,每个通道有16位的像素值,输出数据也是16位。则可以使用以下代码定义数据类型: ```c++ typedef short data_t; typedef struct { data_t data[3][H][W]; } input_t; typedef struct { data_t data[H][W]; } output_t; ``` 其中,H和W分别表示图像的高度和宽度。 2. 实现卷积核:卷积核是一个3维数组,用于对输入图像进行卷积运算。可以使用以下代码定义卷积核: ```c++ typedef struct { data_t data[K][K][C][F]; } weight_t; ``` 其中,K表示卷积核的大小,C表示输入图像的通道数,F表示输出图像的通道数。 3. 编写卷积函数:卷积函数的输入包括输入图像、卷积核和偏置项(可选),输出为卷积后的图像。可以使用以下代码实现卷积函数: ```c++ void conv2d(input_t input, weight_t weight, data_t bias, output_t output) { for (int i = 0; i < H-K+1; i++) { for (int j = 0; j < W-K+1; j++) { for (int f = 0; f < F; f++) { data_t sum = 0; for (int c = 0; c < C; c++) { for (int ii = 0; ii < K; ii++) { for (int jj = 0; jj < K; jj++) { sum += input.data[c][i+ii][j+jj] * weight.data[ii][jj][c][f]; } } } sum += bias; output.data[i][j][f] = sum; } } } } ``` 该函数使用4层循环,分别遍历输出图像的每个像素、每个通道和卷积核的每个参数。 4. 使用HLS编译器生成硬件描述语言:使用HLS编译器将C/C++代码转换为硬件描述语言(如Verilog或VHDL),并在FPGA上实现卷积运算。 以上是使用HLS实现F.conv2d的一般流程,具体实现可能因不同的HLS工具和FPGA平台而有所不同。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值