一、引言
涉及内容包括:多位宽并行,动态定点数运算,设置LOOP_TRIPCOUNT来性能分析等。
二、概念
1、功能定义。
为了让卷积运算单元更具灵活性,在输入图层的长宽高、卷积核的大小、Stride的大小等很多功能上可设置。
2、feature的内存排布。
之前的池化模块的feature,是一个四维数组。而卷积核没有分块前,也是四维的。这边按问答方式对一些概念进行整理。
为什么未分块前,卷积核(feature、kernel)为四维的?
从下面这张图上看,卷积核可以表示为2x3x3x3,即:个数x长x宽x通道数,不同的地方将这四个参数的位置可能颠倒了下,但都只是这四个概念。下面对这四个参数做个理解,①个数:表示不同卷积核的数目,对于一张完整的图(RGB都有),要提取里面的不同特征,需要多个不同的卷积核来进行提取,从之前的学习中,可以知道,卷积核本质就是特征feature,用一个特征怎么可能得到不同特征嘛,所以需要多个卷积核。②长x宽:这个为卷积核的size,很好理解,是和局部特征单独运算相关的。③通道数:相同卷积核的数目(权值共享),与一张完整图片中准备卷积处理的通道数相同,一般处理R、G、B通道,一个卷积核的通道数,也就为3了,同时可以得到3张feature map。在下面这张图的例子中,输入6x6,kernel为3x3,输出4x4,首先确定不是same模式,为valid模式,padding为0。只看H,有4-1=(6-3+2xpadding)/stride,则stride为1。通过在通道方向上相乘求累加后,9个点再做一次累加,得到输出feature map上的一个点。
为什么分块后,卷积核(feature、kernel)为五维的?
从下面这张图上看,可以把kernel上任意个数据表示为kernel: [CHout][Ky][Kx][CHin/K][K]
。CHout表示的是有多少个kernel,Ky和Kx为一个kernel的宽和长,CHin/K为在输入图层方向切了多少个子块,K为每个子块的索引。因此,本设计中,由一个四维的输入图层feature map和五维的feature(卷积核、权重),得到一个输出的feature map输出图层。
3、卷积运算的full、same和vaild模式是啥?
这个参考了前人的一篇文章,讲的很好,非常感谢,Add在最后边了。橙色为image,蓝色为kernel。
1°full mode模式:从image和kernel刚相交,就开始做卷积运算。而其余白色的地方全部填0,这样一张橙色的image先补了两行两列的0后,如果stride为1的卷积运算后,图片反而变大了,这模式很少用。
2°same mode模式:当kernel的中心K与image的边角重叠时,开始做卷积运算。这种模式下,如果stride为1的卷积运算后,输出的feature map尺寸是保持不变的(相当于输入图)。相比full mode,这模式就用的多了。该模式可以在前向传播的过程中,让特征图的大小保持不变,调参师不需要精准计算尺寸的变化,因为根本就没有变化。
由于卷积核的size尺寸一般为奇数,故可以总结一个padding公式,用来计算需要补多少行列的0数据,才能满足same mode模式。具体公式为:padding_x=(kx-1)/2,padding_y=(ky-1)/2。举个例子,这里size为33,那么,padding_x=padding_y=1。
3°valid mode模式:当kernel全在image里时,进行卷积运算,明显地,会丢掉一部分的数据。
如何计算卷积运算后的feature map的尺寸大小?
也参考了前人的博文,但网上貌似没有发现推导的过程,举几个例子应该也能总结下,这里也直接作为经验来使用了。
计算的公式为:output_h =(originalSize_h+padding x 2-kernelSize_h)/stride +1。
其中,originalSize_h为原始输入image的H或W,padding为填充的行或列数,kernelSize_h为kernel的size大小,stride为x或y方向上的步长。
拿一道面试的题来试试手吧。(卷积向下取整,池化向上取整)
input image:200×200
first layer_conv:kernel size 5×5,padding 1,stride 2
second layer_pooling:kernel size 3×3,padding 0,stride 1
third layer_conv:kernel size 3×3,padding 1,stride 1
output feature map?
答:
1°(200-5+21)/2+1 为99.5,取99。
2°(99-3)/1+1 为97。
3°(97-3+2*1)/1+1 为97。
分块时为什么是沿输入图层的方向,而不是输出图层方向?
为了并行地做子块的矩阵乘累加运算,得到结果(和之前FIR滤波器案例类似)。要时刻把握住,是取出输入feature map的一个子块,和权重的一个子块,来进行相乘累加的操作。
三、程序
1、conv_core.h文件。
#ifndef __CONV_CORE_H__
#define __CONV_CORE_H__
#include <ap_int.h>
#include <iostream>
using namespace std;
#define K 8
// 单个数据是16bit位宽,一个子块数据是16*K位宽,用bus表示。
// 输入图层和权重都是ap_int<16>
typedef ap_int<16> dtype_dat;
typedef ap_int<16*K> dtype_bus;
// 两个INT16数据相乘,得到的结果会溢出,用32位来存。根据经验,用40位来作为累加的结果位宽。
typedef ap_int<32> dtype_mul;
typedef ap_int<32*K> dtype_mul_bus;
typedef ap_int<40> dtype_acc;
// C语言无法传可变索引的数组的,但在硬件内存中,都按照一维分布,故可以把地址传进去。
// 后续得自己计算存放的地址来取数据
// 定点化后的数据由三个小数点的参数来计算得到,INT16,小数点由四位来表示
void Conv(
ap_uint<16> CHin, // 输入通道数
ap_uint<16> Hin, // 输入特征的高度
ap_uint<16> Win, // 输入特征的宽度
ap_uint<16> CHout, // 输出特征的通道数
ap_uint<8> Kx, // 卷积核的宽度
ap_uint<8> Ky, // 卷积核的高度
ap_uint<8> Sx, // 卷积核扫描时,宽度方向的步进
ap_uint<8> Sy, // 卷积核扫描时,高度方向的步进
ap_uint<1> mode, // 卷积的模式,valid还是same
ap_uint<1> relu_en, // 激活非线性反激活层标志
dtype_bus feature_in[], // 输入feature map的数据,由于C中无法使用可变数组,故使用指针,传地址来定位具体数据,四维数组按一维分布
// 之前池化使用的是固定大小的,可以按照固定数组来传,但这里数组长度是变的
ap_uint<4> feature_in_precision, // 输入feature map数据小数点位置
dtype_bus W[], // 输入权重的数据,也是指针索引
ap_uint<4> W_precision, // 输入权重数据小数点位置
dtype_bus feature_out[], // 输出feature map的数据,也是指针索引
ap_uint<4> feature_out_precision // 输出feature map数据小数点位置
);//mode: 0:VALID, 1:SAME
#endif
2、conv_core.cpp文件。
#include "conv_core.h"
// 输入特征图四维:[C/K][H][W][K]
// 输入权重值五维:[CHout][Ky][Kx][CHin/K][K]
// 输出特征图四维:[C/K][H][W][K],均可以计算出来
void Conv(
ap_uint<16> CHin, // 输入通道数(输入图层方向上的数目)(卷积核与特征图参量)
ap_uint<16> Hin, // 输入特征的高度(卷积核与特征图参量)
ap_uint<16> Win, // 输入特征的宽度(卷积核与特征图参量)
ap_uint<16> CHout, // 输出特征的通道数(卷积核参量)
ap_uint<8> Kx, // 卷积核的宽度
ap_uint<8> Ky, // 卷积核的高度
ap_uint<8> Sx, // 卷积核扫描时,宽度方向的步进
ap_uint<8> Sy, // 卷积核扫描时,高度方向的步进
ap_uint<1> mode, // 卷积的模式,valid还是same
ap_uint<1> relu_en, // 激活非线性反激活层标志
dtype_bus feature_in[], // 输入feature map的数据,由于C中无法使用可变数组,故使用指针,传地址来定位具体数据,四维数组按一维分布
// 之前池化使用的是固定大小的,可以按照固定数组来传,但这里数组长度是变的
ap_uint<4> feature_in_precision, // 输入feature map数据小数点位置
dtype_bus W[], // 输入权重的数据,也是指针索引
ap_uint<4> W_precision, // 输入权重数据小数点位置
dtype_bus feature_out[], // 输出feature map的数据,也是指针索引
ap_uint<4> feature_out_precision // 输出feature map数据小数点位置
)//mode: 0:VALID, 1:SAME
{
ap_uint<8> pad_x,pad_y; // padding数据,表示x和y方向待填补的行列数
ap_uint<16> CHin_div_K=(CHin+K-1)/K; // 输入图层方向上,切块后每个的长度
ap_uint<5> out_truncate;
out_truncate=feature_in_precision+W_precision-feature_out_precision;
if(mode==0) // valid下不进行padding
{
pad_x=0;pad_y=0;
}
else // same模式下进行padding,具体padding的数据值,在之前概念部分补充了
{
pad_x=(Kx-1)/2;pad_y=(Ky-1)/2;
}
// 通过输入的参数数据,计算输出图层的高度和宽度,这个的公式也是固定的
ap_uint<16> Hout,Wout;
Wout=(Win+2*pad_x-Kx)/Sx+1;
Hout=(Hin+2*pad_y-Ky)/Sy+1;
dtype_acc sum=0;
dtype_bus out_tp=0;
// 对输出feature map的数据进行计算
// 关于是输出方向上需要多少重循环的定位:输出是一个四维的数据,需要前三重做迭代,第四重做处理
// 一是输出图像的高度,二是输出图像的宽度,三是输入图层方向上切块了多少,四是具体索引到哪个子块
LOOP_i:for(int i=0;i<Hout;i++)
{
// 输出高度和宽度方向上的循环
LOOP_j:for(int j=0;j<Wout;j++)
{
// 输出通道方向上的循环,CHout表示有多少个kernel,是卷积核专门的一个参数,与输入特征图无关
LOOP_cout:for(int cout=0;cout<CHout;cout=cout+1)
{
// 第四维上的数据处理
// 把卷积核size上一个点,在所有子块上的结果都求出来,再循环size上所有点
LOOP_ii:for(int ii=0;ii<Ky;ii++)
{
LOOP_jj:for(int jj=0;jj<Kx;jj++)
{
// CHin_div_K输入方向上切块的循环
// 输入通道方向上被切块了,需要做一个循环,按输入通道上子块的数目
LOOP_cin:for(int cin=0;cin<CHin_div_K;cin=cin+1)
{
ap_int<16> h=i*Sy-pad_y+ii;
ap_int<16> w=j*Sx-pad_x+jj;
dtype_mul_bus tp;
dtype_bus dat;
dtype_bus wt;
// 判断是否在padding的范围内
// 这段if代码是获取特征图数据和权重数据的
if(h>=0 && w>=0 && h<Hin && w<Win)
{
// 输入数据dat=feature_in[cin][h][w][K]
// 根据四维数据的这个索引,来算出一维的数据
// INT16*K feature_in[C/K][H][W]为feature_in的形状,[K]相当于INT16*K了
// dat=feature_in[cin][h][w]为这个形状里取的索引
dat=feature_in[cin*Hin*Win+h*Win+w];
// INT16*K weight[CHout][Ky][Kx][CHin/K]为权重的形状,[K]相当于INT16*K了
// wt=weight[cout][ii][jj][cin]为这个形状里取的索引
wt=W[cout*CHin_div_K*Kx*Ky+ii*CHin_div_K*Kx+jj*CHin_div_K+cin];
//std::cout<<"dat="<<dat<<",W="<<wt<<std::endl;
}
else
{
dat=0;
wt=0;
}
// 每次取出一个特征图子块和权重子块,来进行累加
for(int k=0;k<K;k++)
{
// 具体子块的累加过程,对应元素相乘
// tp是一个32*k的数据,位宽32*k
tp.range(k*32+31,k*32)=(dtype_dat)dat.range(k*16+15,k*16)*(dtype_dat)wt.range(k*16+15,k*16);
}
for(int k=0;k<K;k++)
{
// 累加,40位的数据,这个累加是移位前的数据
sum+=(dtype_mul)tp.range(k*32+31,k*32);
//std::cout<<"sum="<<sum<<std::endl;
}
//std::cout<<"sum="<<sum<<std::endl;
if((cin==CHin_div_K-1) && (jj==Kx-1) && (ii==Ky-1) )
{
// 如果使用了非线性激活层,小于0就设为0
if(relu_en & sum<0)
sum=0;
// 数据右移
// 右移位数为输入feature map小数点位数+权重小数点位数-输出feature map小数点位数
dtype_acc res=sum>>out_truncate;
// 移位如果范围,进行前后的钳位
if(res>32767)
res=32767;
else
if(res<-32768)
res=-32768;
dtype_dat res_16=res;
out_tp.range((cout%K)*16+15,(cout%K)*16)=res;sum=0;
// feature_out[cout][i][j][cout]
if( ((cout%K)==K-1) || (cout==(CHout-1)) )
{
feature_out[(cout/K)*Wout*Hout+i*Wout+j]=out_tp;out_tp=0;
}
}
}
}
}
}
}
}
}
3、main.cpp文件。
C仿真是必须的,用来保证C语言这边的设计是没有问题的。
#include "stdio.h"
#include "conv_core.h"
#define IN_WIDTH 10
#define IN_HEIGHT 10
#define IN_CH 1
#define IN_CH_DIV_K ((IN_CH+K-1)/K)
#define KERNEL_WIDTH 5
#define KERNEL_HEIGHT 5
#define X_STRIDE 1
#define Y_STRIDE 1
#define RELU_EN 0
#define MODE 1 //0:VALID, 1:SAME
#define X_PADDING (MODE?(KERNEL_WIDTH-1)/2:0)
#define Y_PADDING (MODE?(KERNEL_HEIGHT-1)/2:0)
#define OUT_CH 1
#define OUT_CH_DIV_K ((OUT_CH+K-1)/K)
#define OUT_WIDTH ((IN_WIDTH+2*X_PADDING-KERNEL_WIDTH)/X_STRIDE+1)
#define OUT_HEIGHT ((IN_HEIGHT+2*Y_PADDING-KERNEL_HEIGHT)/Y_STRIDE+1)
int main(void)
{
dtype_bus feature_in[IN_CH_DIV_K][IN_HEIGHT][IN_WIDTH];
dtype_bus W[OUT_CH][KERNEL_HEIGHT][KERNEL_WIDTH][IN_CH_DIV_K];
dtype_bus feature_out[OUT_CH_DIV_K][OUT_HEIGHT][OUT_WIDTH];
dtype_dat temp;
for(int cin=0;cin<IN_CH_DIV_K;cin++)
for(int i=0;i<IN_HEIGHT;i++)
for(int j=0;j<IN_WIDTH;j++)
for(int k=0;k<K;k++)
if((cin*K+k)<IN_CH)
feature_in[cin][i][j].range(16*k+15,16*k)=(1<<14);//i*IN_WIDTH+j;
else
feature_in[cin][i][j].range(16*k+15,16*k)=0;
/*打印测试
for(int cin=0;cin<IN_CH_DIV_K;cin++)
for(int i=0;i<IN_HEIGHT;i++)
for(int j=0;j<IN_WIDTH;j++)
for(int k=0;k<K;k++){
temp = feature_in[cin][i][j].range(16*k+15,16*k);
//printf("feature_in[%d][%d][%d][%d] = %d \n", cin, i, j, k, temp);
std::cout<<"feature_in["<<cin<<"]["<<i<<"]["<<j<<"]["<<k<<"]="<<(dtype_dat)temp<<std::endl;
}
*/
for(int i=0;i<KERNEL_HEIGHT;i++)
for(int j=0;j<KERNEL_WIDTH;j++)
for(int cin=0;cin<IN_CH_DIV_K;cin++)
for(int cout=0;cout<OUT_CH;cout++)
for(int k=0;k<K;k++)
W[cout][i][j][cin].range(16*k+15,16*k)=(1<<14);//(i*KERNEL_WIDTH+j);//(cout==0)?(i*KERNEL_WIDTH+j):0;
/*打印测试
for(int i=0;i<KERNEL_HEIGHT;i++)
for(int j=0;j<KERNEL_WIDTH;j++)
for(int cin=0;cin<IN_CH_DIV_K;cin++)
for(int cout=0;cout<OUT_CH;cout++)
for(int k=0;k<K;k++){
temp = W[cout][i][j][cin].range(16*k+15,16*k);
std::cout<<"W["<<cin<<"]["<<i<<"]["<<j<<"]["<<k<<"]="<<(dtype_dat)temp<<std::endl;
}
*/
for(int cout=0;cout<OUT_CH_DIV_K;cout++)
for(int i=0;i<OUT_HEIGHT;i++)
for(int j=0;j<OUT_WIDTH;j++)
feature_out[cout][i][j]=0;
printf("1234\n");
Conv(IN_CH,IN_HEIGHT,IN_WIDTH,OUT_CH,
KERNEL_WIDTH,KERNEL_HEIGHT,X_STRIDE,Y_STRIDE,MODE,RELU_EN,
&feature_in[0][0][0],14,
&W[0][0][0][0],14,
&feature_out[0][0][0],10
);//mode: 0:VALID, 1:SAME
for(int i=0;i<OUT_HEIGHT;i++)
for(int j=0;j<OUT_WIDTH;j++)
for(int cout=0;cout<OUT_CH_DIV_K;cout++)
{
std::cout<<"OUT["<<cout<<"]["<<i<<"]["<<j<<"]="<<(dtype_dat)feature_out[cout][i][j].range(15,0)<<std::endl;
}
return 0;
}
输入feature map打印出来的数据,可以看出,只有当具体的高度i和宽度j发生变化时,才有数据,其他都为0。而weight数据全都是16384,这里就不贴出来了。
验证数据。
四、优化
1、第一次没加任何优化。
综合结果是直接报错了。原因是之前几个数组采用的是指针来索引的,HLS工具并没能很好知道这几个数组在内存中的具体位置和大小。
回想之前HP口那一次lab(池化),那里为了给自己的模块说明,数据是来源于DDR的具体哪个位置,使用了m_axi和offset=slave的方式。
#pragma HLS INTERFACE m_axi depth=99999 port=feature_in offset=slave
#pragma HLS INTERFACE m_axi depth=99999 port=W offset=slave
#pragma HLS INTERFACE m_axi depth=99999 port=feature_out offset=slave
更改完,也确实可以综合过了,但Latency为“?”,这个是没有控制循环次数造成的。
2、循环次数设置。
按照所写的main函数,来对模块的循环变量进行控制。
3、端口部分优化。
除了feature in、feature out和weight数据,其他的都得CPU来进行配置,因此都是用GP口的s_axi。但feature in、feature out和weight数据得用HP,来与DDR内存进行交互。另外,单总线有一个缺陷,就是feature in和weight数据,得两个周期才能读取出来,但axi接口上有一个bundle捆版的操作,就是一个模块,可以产生两个接口。
#pragma HLS INTERFACE s_axilite port=return
#pragma HLS INTERFACE s_axilite port=feature_out_precision
#pragma HLS INTERFACE s_axilite port=feature_in_precision
#pragma HLS INTERFACE s_axilite port=Sy
#pragma HLS INTERFACE s_axilite port=Kx
#pragma HLS INTERFACE s_axilite port=Win
#pragma HLS INTERFACE s_axilite port=Sx
#pragma HLS INTERFACE s_axilite port=Hin
#pragma HLS INTERFACE s_axilite port=W_precision
#pragma HLS INTERFACE s_axilite port=relu_en
#pragma HLS INTERFACE s_axilite port=Ky
#pragma HLS INTERFACE s_axilite port=CHin
#pragma HLS INTERFACE s_axilite port=mode
#pragma HLS INTERFACE s_axilite port=CHout
#pragma HLS INTERFACE m_axi depth=100 port=feature_in offset=slave bundle=bus1
#pragma HLS INTERFACE m_axi depth=25 port=W offset=slave bundle=bus2
#pragma HLS INTERFACE m_axi depth=100 port=feature_out offset=slave bundle=bus1
4、关于latency和II的优化。
这个需要非常理解HLS工具,HLS工具在处理for循环的时候,会做一个Loop Flattening的操作,就是外层的若干个循环中,若没有语句截断,就当成一个for循环,并把Loop的次数给计算出来。在代码设计时,就需要很注意了,如果把语句放在不同的for循环层,会导致出现:消耗周期=(内层Loop次数 x II+latency)x 外层Loop次数。因此,需要做一件事,就是把外层for循环的语句,全部搬移到内部趣,再通过if语句来限制啥时候触发。代码中的这句,就是做了这个工作。
if( (cin==CHin_div_K-1) && (jj==Kx-1) && (ii==Ky-1) )
5、关于feature out数据的优化。
feature out是一个axi的接口,输出数据不能使用range的方式来输出,这样会导致CPU先将最完整的数据读取出来,再使用截断的方式改写后写回(以前学嵌入式的时候,好像是位带的操作)。这操作,直接导致输出的latency变长。
五、测试
1、实验平台搭建。
使用到了两个HP口,具体平台如下。
实验环境,用了Zynq这块板子。
2、数据预备测试。
把feature_in、feature_out和weight数据先存储到内存中,并作打印测试先。结果和HLS的C仿真是一样的,没啥问题。另外,SDK的%d可以直接打印short、int等不同类型,不用单独使用hd来打印,这和c++的输出流很像。
// 初始化feature_in数据
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
for(v=0; v<8; v++)
if((i*8+v)<1)
feature_in[i][j][k][v]=(1<<14);//i*IN_WIDTH+j;
else
feature_in[i][j][k][v]=0;
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
for(v=0; v<8; v++)
xil_printf( "the input_buffer is [%d][%d][%d][%d] : %d \n\r" , i,j,k,v, feature_in[i][j][k][v]);
// 初始化weight数据
for(i=0; i<1; i++)
for(j=0; j<5; j++)
for(k=0; k<5; k++)
for(v=0; v<1; v++)
for(w=0; w<8; w++)
weight[i][j][k][v][w]=(1<<14);
for(i=0; i<1; i++)
for(j=0; j<5; j++)
for(k=0; k<5; k++)
for(v=0; v<1; v++)
for(w=0; w<8; w++)
xil_printf( "the weight is [%d][%d][%d][%d][%d] : %d \n\r" , i,j,k,v,w, weight[i][j][k][v][w]);
测试的结果。
通过debug的方式,检查寄存器配置的值。
3、SDK部分测试代码。
可能部分有些更改,详细可看工程。
#include <stdio.h>
#include <stdlib.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xparameters_ps.h"
#include "xil_io.h"
#include "xConv.h"
#include "xConv_hw.h"
#include "sleep.h"
#define DDR_BASEARDDR XPAR_DDR_MEM_BASEADDR + 0x10000000
int main()
{
init_platform();
int i,j,k,v,w;
//int rev;
int data,state;
short feature_in[1][10][10][8];
short weight[1][5][5][1][8];
short feature_out[1][10][10][8];
XConv xconv;
XConv_Config *ConfigPtr;
xil_printf("feature_in address is %p \n\r", feature_in);
xil_printf("weight address is %p \n\r", weight);
xil_printf("feature_out address is %p \n\r", feature_out);
print("Hello World\n\r");
// 关闭cache,保证内存缓存读写的一致性
Xil_DCacheDisable();
// 初始化feature_in数据
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
for(v=0; v<8; v++)
if((i*8+v)<1)
feature_in[i][j][k][v]=(1<<14);//i*IN_WIDTH+j;
else
feature_in[i][j][k][v]=0;
/*
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
for(v=0; v<8; v++)
xil_printf( "the input_buffer is [%d][%d][%d][%d] : %d \n\r" , i,j,k,v, feature_in[i][j][k][v]);
*/
// 初始化weight数据
for(i=0; i<1; i++)
for(j=0; j<5; j++)
for(k=0; k<5; k++)
for(v=0; v<1; v++)
for(w=0; w<8; w++)
weight[i][j][k][v][w]=(1<<14);
/*
for(i=0; i<1; i++)
for(j=0; j<5; j++)
for(k=0; k<5; k++)
for(v=0; v<1; v++)
for(w=0; w<8; w++)
xil_printf( "the weight is [%d][%d][%d][%d][%d] : %d \n\r" , i,j,k,v,w, weight[i][j][k][v][w]);
*/
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
for(v=0; v<8; v++)
feature_out[i][j][k][v]=0;
ConfigPtr = XConv_LookupConfig(XPAR_CONV_0_DEVICE_ID);
state = XConv_CfgInitialize(&xconv, ConfigPtr);
// 判断初始化pool模块是否成功
//state = XConv_Initialize(&xconv, XPAR_CONV_0_DEVICE_ID);
if(state != XST_SUCCESS)
{
print("XConv_Initialize fail!!\n\r");
return XST_FAILURE;
}
//XConv_DisableAutoRestart(&xconv);
// 众多参数配置,得参照slave的优化和C仿真时Conv调用传入的形参
// 设置通道数
XConv_Set_CHin_V(&xconv, 1);
// 设置输入特征的高度
XConv_Set_Hin_V(&xconv, 10);
// 设置输入特征的宽度
XConv_Set_Win_V(&xconv, 10);
// 设置输出特征的通道数
XConv_Set_CHout_V(&xconv, 1);
// 设置卷积核的宽度、高度
XConv_Set_Kx_V(&xconv, 5);
XConv_Set_Ky_V(&xconv, 5);
// 设置卷积核扫描水平与竖直的步进
XConv_Set_Sx_V(&xconv, 1);
XConv_Set_Sy_V(&xconv, 1);
// 设置卷积的模式
XConv_Set_mode_V(&xconv, 1);
// 设置是否要relu非线性激活层
XConv_Set_relu_en_V(&xconv, 0);
// 设置feature_in、feature_out和weight的地址,以及各自小数点的位置
XConv_Set_feature_in_V(&xconv, (u32)feature_in);
XConv_Set_feature_in_precision_V(&xconv, 14);
XConv_Set_W_V(&xconv, (u32)weight);
XConv_Set_W_precision_V(&xconv, 14);
XConv_Set_feature_out_V(&xconv, (u32)feature_out);
XConv_Set_feature_out_precision_V(&xconv, 10);
// 启动电路,将ap_start置为1,开始计算。
XConv_EnableAutoRestart(&xconv);
XConv_Start(&xconv);
print("Test Start!!!\n\r");
// 数据输出
// 判断计算完成的条件是ap_done为1,当不是1时说明还尚未完成,就一直读取判断,直至算完。
/* data = XConv_IsDone(&xconv);
while(data != 1)
{
data = XConv_IsDone(&xconv);
}*/
sleep(20);
print("Test Done!!!\n\r");
for(i=0; i<1; i++)
for(j=0; j<10; j++)
for(k=0; k<10; k++)
//for(v=0; v<8; v++)
xil_printf( "the feature_out is [%d][%d][%d][2] : %d \n\r" , i,j,k, feature_out[i][j][k][2]);
cleanup_platform();
return 0;
}
4、测试结果。
出现一个问题,数据不在k为0时有,而是k为2时才有数据,这是和C仿真冲突的,已经做了下面一些尝试:调试发现,问题关键在于XConv_IsDone函数没有成功返回1,即模块功能没正常完成。
1°调整了conv模块时钟为20MHz,不行。
2°generator output再生成比特流,不行。
3°使用单总线验证,降低效率来验证,同样问题,不行。
4°更改了feature和权重数据存储的方式,不行。
5°尝试了Disable模块后再配置,不行。
5、猜测与后续验证。
1°内存问题,使用了双HP口,如果模块想同时对DDR访问,是不是有些细节没有注意到。可用单端验证过,这问题双HP口应该可以处理,那内存上的问题,可能是地址与存储形式上,需要后续验证。
2°Conv模块配置问题,这部分也是还没验证的,对比了很多文章里的配置,基本上都是一个套路,可依旧不行。要说细节,就是双HP口的模块配置,会不会需要打开interrupt这东西,也需要后续验证,不过得先理解下按几个interrupt是用来干啥的。
就很无语,C语言只有那几行代码,多维数组测试也没啥问题,但数据发生了漂移,而且前面有几个是错的,但后面基本都是对的,前面做的几次测试,也都是这个结果,除了能排除薛定谔的Conv现象外,这个Conv模块是没啥问题的,Bug集中在“SDK测试的配置中”。算了去南亭吃碗面,今天天气太好了,适合出去玩,打球啥的,后续再填这个坑。
六、补充
一些关键时间点的记录:
2021年04月13日:卷积的一些概念。
2021年04月14日:动态定点数概念、网络定点化概念、权重和特征的排布方式、子块间并行、卷积运算单元需支持哪些参数(卷积规模等)、代码的理解。
2021年04月20日:分析模块需要如何工作,约束优化等。
2021年04月21日早上:板级测试,但失败了,原因是数据前几个出错,并且发生漂移。
参考的资料:
为什么卷积核是四维的:https://blog.csdn.net/qq_30763385/article/details/103094391。
卷积的三种模式:https://blog.csdn.net/leviopku/article/details/80327478
面试中常考的feature map的大小计算:https://zhuanlan.zhihu.com/p/49913137
feature map大小计算方法:https://blog.csdn.net/qq_28424679/article/details/78665273
后续可能做的工作:(非常的多)
1、填坑调Bug。
2、卷积相关部分的加深学习。
3、该实验在PynqZ2上的验证。这得等有Pynq板卡后了。
4、HLS工具中Analysis的学习。是一个分析综合后latency和II的组件,值得学习。
5、C语言部分的提高。C语言长期不用带来的后遗症,后续回看朱老师的课,并研究下算法,卷积相关的Lab难点在于多重for循环设计与多维数组。
Some Notes:
1、多重for循环的一个技巧。如果想把外层循环的多条语句放到内部,只需要剪切后,在外面套一层if来判断,就行了。
2、关于程序下载的一个点。之前好像application是默认有勾上的,这次不知为啥,生成的平台没有勾上,下载程序后,啥反应都没有,主要原因就是elf程序没有下载到CPU里边。