最近实验室租了块xilinx家的AlveoU200加速卡,过去几天被这块板吸引了注意力。刚开始了解,做点什么来试试水呢?一想,可以把曾经学 @蔡宇杰 大佬在pynq-z2上做的那个手写数字识别工程在这块板上复现一下。

数字识别的基础知识在我曾经pynq-z2的总结里讲过了,陈年老文章链接在这:

花火同学:使用PYNQ搭建手写数字识别工程小白级说明(完整版)​zhuanlan.zhihu.com

接下来就专门来说说加速卡的开发是个什么名堂,以我自己的理解先来比较一下zedboard纯PL开发,pynq使用,以及U200加速卡的区别吧。

1.纯PL开发没啥可说的,写好RTL,烧进去,跑就完事了。

2.pynq的开发跟zedboard的PS开发本质是一回事,只不过pynq是一个先装好了linux的zedboard,同时还有个overlay让这个板子可以支持python。这个在调用的时候,虽然ip核的调用是用python(在zedboard就是用C)来控制的,但ip核之间的调用流程,还有内存的使用方式等细节,我们是控制不了的。

3.U200走的路子跟前面就不同了,它用openCL来写主函数, openCL是专门为异构平台写程序的语言。我们可以实现对ip核调用过程,内存使用等方法的控制。emmm,说实话我简单使用下来的感觉是opencl使用起来并不方便,但起码它提供了方法完成以前做不到的事情2333。


整个事情的流程是这样的:

(1)测试读取目标图像的bmp文件、卷积核以及偏置数据的代码。

(2)使用VITIS的HLS工具将c代码封装成核

(3)用openCL编写host程序,跑仿真,这里面我还分了9步。

3-1:读取main函数的argv[](也就是读取ip核)
3-2:读取图像
3-3:读取卷积核以及偏置参数
3-4: 在host上分配各种ip核处理完后放结果的内存
3-5:用opencl做配置操作
3-6:接下来将device跟host上已经写好的内存位置连接起来
3-7:设置第一次卷积和池化的参数
3-8:将第一次卷积操作压入queue,开始执行
3-9:GUI 操作


(1)读取目标图像的bmp文件、卷积核以及偏置数据

1-1:读bmp

读取目标图像在pynq上面是通过cv2的读取jpeg库函数一步到位完成的。但是U200不支持python,所以这个过程咱们要自己写段简单的C程序完成,为了方便我们将目标函数从jepg格式改成bmp格式。

代码在下面,逻辑很简单,就是bmp整个格式实际有效的数据在文件最后,文件前面有一堆标注格式和文件内容的"废话",我们要做的就是跳过前面的部分,直接把后面的内容读出来。

这里多说一句,这种读文件的操作,写代码的时候还是多花几秒钟把各步的错误信号设置好,说不定就可以为debug省去不少时间。

uint8_t* readbmpfile(const char* filepath) {FILE* img_pFile;size_t img_size;size_t result;img_pFile = fopen(filepath, "rb");if (img_pFile == NULL) { fputs("File error", stderr); exit(1); }// 读bmp大小fseek(img_pFile, 0, SEEK_END);//把指针放去末尾img_size = ftell(img_pFile);//读出指针的位置,也就是这个文件的byte数量rewind(img_pFile);//将指针放回开头// 开块动态数组uint8_t* ptr_buffer;ptr_buffer = new uint8_t[img_size - BMP_OFFSET];if (ptr_buffer == NULL) { fputs("Memory error", stderr); exit(2); }//跳1078fseek(img_pFile, BMP_OFFSET, SEEK_SET);//将bmp从1078位以后的内容读去动态数组ptr_bufferresult = fread(ptr_buffer, sizeof(uint8_t), img_size - BMP_OFFSET, img_pFile);//result是正确读到的元素数量if (result != img_size - BMP_OFFSET) { fputs("Reading error", stderr); exit(3); }cout << result << endl;fclose(img_pFile);return ptr_buffer;
}

效果是这样的:

刚读进来是个反的2,简单处理了一下,没啥可说的。

1-2:接下来是要读取卷积函数用到的卷积核以及偏置数据:

首先先来复习一下整个流程用到的数据情况,不做过多解释了,了解流程的一看就懂了,不了解的可以回去翻一下我过去在pynq那篇里的总结。

(2)使用VITIS的HLS工具将c代码封装成核

关于HLS,简单介绍一下三部分内容。

2-1:HLS对循环的处理。

下面这个图就是HLS跑完综合后,对程序里面循环的处理报告,拿下面这个报告中的WR_Loop_Col循环做为例子,画图说明一下其中各个参数的含义。

HLS默认情况下会自动对64次以下的循环做优化,优化的目标是要让循环在1周期内完成,这个目标有时候会太苛刻了,完成不了,那么HLS在跑完综合之后就会在报告里提出issue,issue type是II violation 这里的II 是iteration interval的缩写,并不是"第二类冲突"的意思。

2-2:HLS中进程细节查看以及处理优化冲突

接着上面的点,比如我在自己要做HLS的函数中出现了II violation,咋办呢,可以右键,先到进程调度操作里面看一看。

当然我们也可以在这里界面里退一步,看看全局的调度是什么情况,比如下面这样。

这样就可以清楚地看到,这个循环主要是从gmem(global memory,跟CUDA编程里的global memory类似的概念)读了两个数,这就是图里面两个很长的readreq操作,然后做了一点处理之后,在sum_4这个位置做了fadd操作,也就是浮点加法,我们的问题就出在这个浮点加法上,HLS告诉我们它没有办法把浮点加法压缩到一个周期完成,臣妾做不到啦,最多就压缩到3个周期了(这就是报告图中inteval=3的含义)。

看清楚这个问题之后,我们还能怎么办呢,我们暂时就先摸摸HLS的狗头,微笑地告诉它,3周期就3周期吧,先这么着吧。

具体做法在cpp的那个循环处,就加个directive,手动添加pipeline的directive,同时指定其inteval参数为3。

2-3:HLS对于传入函数的参数会采用什么协议来传数据

默认协议有下面这么几类

详细一点的解释

(3)编写host代码,跑软件仿真

3-1:读取main函数的argv[](也就是读取ip核)

在我的工程里一共有两个参数,argv[0]是地址,argv[1]是两个kernel的二进制文件。

int main(int argc, char* argv[]) {cout<<"setting address of xclbin"<<endl<<endl;/**********annotation*********** ↓ 通过argv把xo文件的xclbin传进来* ********************/if (argc != 2) {std::cout << "Usage: " << argv[0] << " <XCLBIN File>" << std::endl;return EXIT_FAILURE;}std::string binaryFile = argv[1];//传入卷积kernel的地址//std::string binaryFile_2 = argv[2];//传入池化kernel的地址cout<<argv[0]<<endl;//地址    cout<<argv[1]<<endl;//第一个xclbin

3-2:读取图像,这个部分在前面visual stuido的测试程序里讲过了

3-3:读取卷积核以及偏置参数,函数实现基本跟前面读取bmp部分是一样的

这里只是把读取出来东西重新放进了我们声明的一个vector里面,这是多此一举吗,并不,因为后面我们要把host上的这个vector里的参数传去给device的内存(也就是加速卡上的内存),这一步直接用vector的系统函数比较方便,所以这里做下这种简单处理。

cout<<"#######read w_conv1#######"<<endl;size_t size_w_conv1_byte = sizeoffile("../data/W_conv1.bin");cout<<"size_w_conv1_byte"<<size_w_conv1_byte<<endl;size_t size_w_conv1_float = size_w_conv1_byte/4;cout<<"size_w_conv1_float"<<size_w_conv1_float<<endl;std::vector<float,aligned_allocator<float>> din_w_conv1(size_w_conv1_float);cout<<"allocation of memory finished"<<endl;float* ptr_buffer_w_conv1 = readfilterfile_to_float("../data/W_conv1.bin",IN_HEIGHT1,IN_WIDTH1,IN_CH1,OUT_CH1);//把数据从我们的读取函数取出来,放到vector里面cout<<"readfilterfile_to_float (W_conv1.bin) finished"<<endl;for(size_t i = 0 ; i < size_w_conv1_float ; i++){din_w_conv1[i] = ptr_buffer_w_conv1[i];}cout<<"setting din_w_conv1 finished"<<endl;delete[] ptr_buffer_w_conv1;for (int i = 0; i < 16; i++) {for (int j = 0; j < 3; j++) {for (int k = 0; k < 3; k++) {cout << setw(5) << (din_w_conv1[i*3*3 + j*3+k]);}//          cout << endl;}}

3-4: 在host上分配各种ip核处理完后放结果的内存,跟前面差不多。

/**********annotation*********** ↓分配输出第一次卷积后,dout_featureout1的内存,并且初始化为0* ********************/size_t size_featureout1 = OUT_CH1* IN_WIDTH1* IN_HEIGHT1;//16*28*28std::vector<float,aligned_allocator<float>> dout_featureout1(size_featureout1);for(size_t i = 0 ; i < size_featureout1 ; i++){dout_featureout1[i] = 0;}cout<<"allocation and setting of dou_featureout1"<<endl<<endl;

3-5:重头戏来了,用opencl做配置操作。

cout << "##########step3:OPENCL configuration##############" << endl << endl;/**********annotation**** ↓OPENCL的操作:* one device* one context* one queue* two binary files* two programs* two kernels* ********************/// Step 0: Device //配置器件std::vector<cl::Device> devices = get_devices("Xilinx");devices.resize(1);cl::Device device = devices[0];// Step 1: Create Context//配个contextOCL_CHECK(err, cl::Context context(device, NULL, NULL, NULL, &err));cout<<"create context"<<endl;// Step 2: Create Command Queue//生产指令列表//OCL_CHECK(err, cl::CommandQueue q(context, device, CL_QUEUE_PROFILING_ENABLE, &err));OCL_CHECK(err, cl::CommandQueue q(context, device, 0, &err));cout<<"create commandqueue"<<endl;// Step 3: Load Binary File from disk//读取二进制xo文件unsigned fileBufSize;//fileBufSize在下面的read_binary_file函数里面被赋了文件大小值char* fileBuf = read_binary_file(binaryFile, fileBufSize);//这里用到了最开始的argv,所以把上面的语句放这应该会更好点cl::Program::Binaries bins{{fileBuf, fileBufSize}};cout<<"read conv xclbin"<<endl;// Step 4: Create the program object from the binary and program the FPGA device with it//个人理解:相当于烧bitstreamOCL_CHECK(err, cl::Program program(context, devices, bins, NULL, &err));cout<<"program xclbin"<<endl;// Step 5: Create Kernels//个人理解:相当于给烧了bitstream的PL部分配上PS的寄存器OCL_CHECK(err, cl::Kernel krnl_Conv(program,"Conv", &err));//kernel1//这里用的名字应该是hls选的那个TOP函数的函数名cout<<"create kernel:Conv finished"<<endl<<endl;...其它ip核同理

3-6:接下来将device跟host上已经写好的内存位置连接起来

 cout<<"step4:setting parameters for kernel"<<endl;// ================================================================// Setup Buffers and run Kernels// Allocate Global Memory for source_in1 //在device上面分配内存,分配的时候,这里应该.data()只是返回指针,所以这里没有真的在做搬运,而只是标了真正在queue里面要搬运时候的地址// 自己做个规范,host的内存,用下划线,global的内存,用驼峰命名法/ 在device上配读img的位置,读第一次卷积核与偏置的位置,读输出doutFeatureOut1和输出doutFeatureOut11的位置OCL_CHECK(err, cl::Buffer dinImage   (context,CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY,img_size, din_img.data(), &err));OCL_CHECK(err, cl::Buffer dinWeightConv1   (context,CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY,size_w_conv1_byte, din_w_conv1.data(), &err));OCL_CHECK(err, cl::Buffer dinBiasConv1   (context,CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY,size_b_conv1_byte, din_b_conv1.data(), &err));OCL_CHECK(err, cl::Buffer doutFeatureOut1   (context,CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY,size_featureout1*4, dout_featureout1.data(), &err));OCL_CHECK(err, cl::Buffer doutFeatureOut11   (context,CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY,size_featureout11*4, dout_featureout11.data(), &err));....读取其它参数同理

3-7:设置第一次卷积和池化的参数

(这里最后4个参数是传递指针进去,前面都是传具体的参数,我直接用一个h文件把所有参数都define好了,查找和复制起来都比较方便,当然更好的方法是以后kernel的c函数就不要传这么多参数了,就简简单单传个类进去就好了)

// 设置第一次卷积和池化的参数OCL_CHECK(err, err = krnl_Conv.setArg(0, IN_CH1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(1,IN_HEIGHT1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(2,IN_WIDTH1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(3,OUT_CH1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(4, KERNEL_WIDTH1));//OCL_CHECK(err, err = krnl_Conv.setArg(5,KERNEL_HEIGHT1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(6,X_STRIDE1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(7,Y_STRIDE1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(8,MODE1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(9,RELU_EN1 ));//OCL_CHECK(err, err = krnl_Conv.setArg(10, dinImage));//OCL_CHECK(err, err = krnl_Conv.setArg(11, dinWeightConv1));//OCL_CHECK(err, err = krnl_Conv.setArg(12, dinBiasConv1));//OCL_CHECK(err, err = krnl_Conv.setArg(13, doutFeatureOut1));//cout<<"setting parameters for first krnl_Conv finished"<<endl;OCL_CHECK(err, err = krnl_Pool.setArg(0, IN_CH11));//OCL_CHECK(err, err = krnl_Pool.setArg(1, IN_HEIGHT11));//OCL_CHECK(err, err = krnl_Pool.setArg(2, IN_WIDTH11));//OCL_CHECK(err, err = krnl_Pool.setArg(3, KERNEL_WIDTH11));//OCL_CHECK(err, err = krnl_Pool.setArg(4, KERNEL_HEIGHT11));//OCL_CHECK(err, err = krnl_Pool.setArg(5, MODE11));//它娘的参数粘帖错了 mmpOCL_CHECK(err, err = krnl_Pool.setArg(6, doutFeatureOut1));//读入上面的结果doutFeatureOut1OCL_CHECK(err, err = krnl_Pool.setArg(7, doutFeatureOut11));//输出为doutFeatureOut11cout << "setting parameters for first krnl_Pool finished" << endl;
...其它核配置方法同理

3-8:将第一次卷积操作压入queue,开始执行。先一图流解释一下

/**********annotation**********
* 准备执行第一次卷积池化
* ****************/cout<<"开始压指令"<<endl;//从stackoverflow上看,在这把kernel压入队列之后,压入的操作就定死了,后面就可以去改argument重新压新操作进队列了cout<<"1.把img,第一次卷积用的filter和bias从host搬去device"<<endl;OCL_CHECK(err, err = q.enqueueMigrateMemObjects({dinImage, dinWeightConv1,dinBiasConv1},0/* 0 means from host*/));    //把device上的global内容给到kernel里面,话说这里应该是写错了,0标志这里做的是从global传去给kernel里面cout<<"2.执行krnl_Conv"<<endl;OCL_CHECK(err, err = q.enqueueTask(krnl_Conv));cout<<"3.搬输出回host"<<endl;OCL_CHECK(err, err = q.enqueueMigrateMemObjects({doutFeatureOut1},CL_MIGRATE_MEM_OBJECT_HOST));//把算完的doutFeatureOut1拿出来
//cout<<"#######再看一遍host上现在到三组数据#######"<<endl;cout<<"转成float的host上到图像内容"<<endl;for (int k = 0; k < 100; k++) {cout << setw(5) << (din_img[k]);}cout<<endl;cout<<"host上的din_w_conv1"<<endl;for (int k = 0; k < 100; k++) {cout << setw(5) << (din_w_conv1[k]);}cout<<endl;cout<<"host上的din_b_conv1"<<endl;for (int k = 0; k < 100; k++) {cout << setw(5) << (din_b_conv1[k]);}cout<<endl;cout<<"#######第一次conv前host上dout_featureout1前100内容#######"<<endl;for (int k = 0; k < 100; k++) {cout << setw(5) << (float)(dout_featureout1[k]);}cout<<endl;cout<<"执行q.finish()"<<endl;q.finish();cout<<"#######第一次conv后host上dout_featureout1前100内容#######"<<endl;for (int k = 0; k < 100; k++) {cout << setw(5) << (dout_featureout1[k]);}cout<<endl;

按照这个方法,把我们要做的2次卷积,2次池化,2次全连接串起来,就可以得到结果了。

多说两句,1.当我们需要让多个核并行执行的时候,我们可以在造queue的时候,声明一个参数来让软件自动把能并行的核做并行处理。2.当几个核之间的数据也是串的,比如核1的结果是核2的输入,我们并不需要让host先存核1的输出,再将这个传给核2,而是可以直接一步到位让核1核2在device里传数据,但这样做需要在函数的hls阶段给参数加hls stream的directive。下次再搞,以后关于优化还有很多要学的,未来学了再输出。

3-9:GUI 操作

把该放进来的文件都放进工程之后,在project界面下,点击Hardware Function处的小闪电,把卷积核池化的函数进行加速。

在Emulation-SW处右键,在run的configuration里面将arguments点选上自动添加二进制文件,就可以了。

下面是最后的运行结果:

四、硬件仿真过程

这步跟软件仿真的主要区别就似乎不再只是读两个核从cpp文件,而是要读hls生成的xo文件。对其它文件的处理也有点小名堂。另外这步要花的时间非常非常长,比如这么个简单的网络,软仿只要几秒钟,但是硬件仿真花了4个多小时...

写累了,后面有空再更吧...

opencl 加速 c语言程序_在AlveoU200加速卡上实现简单手写数字识别相关推荐

  1. k近邻算法_图穷匕见:K近邻算法与手写数字识别

    机器学习算法是从数据中产生模型,也就是进行学习的算法.我们把经验提供给算法,它就能够根据经验数据产生模型.在面对新的情况时,模型就会为我们提供判断(预测)结果.例如,我们根据"个子高.腿长. ...

  2. python cnn代码详解图解_基于TensorFlow的CNN实现Mnist手写数字识别

    本文实例为大家分享了基于TensorFlow的CNN实现Mnist手写数字识别的具体代码,供大家参考,具体内容如下 一.CNN模型结构 输入层:Mnist数据集(28*28) 第一层卷积:感受视野5* ...

  3. 我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别

    "我的Go+语言初体验" | 征文活动进行中- 我的Go+语言初体验--Go+语言构建神经网络实战手写数字识别 0. 前言 1. 神经网络相关概念 2. 构建神经网络实战手写数字识 ...

  4. pytorch 预测手写体数字_深度学习之PyTorch实战(3)——实战手写数字识别

    如果需要小编其他论文翻译,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/DeepLearningNote 上一节,我们已经 ...

  5. C语言底层搭建CNN实现MNIST手写数字识别

    手写数字识别 手写数字识别是指使用计算机自动识别手写体阿拉伯数字的技术.作为光学字符识别OCR的一个分支,它可以被广泛应用到手写数据的自动录入场景中.传统的识别方法如最近邻算法k-NN.支持向量机SV ...

  6. python手写多个字母识别_一个带界面的CNN手写数字识别,使用Python(tensorflow, kivy)实现...

    CNN_Handwritten_Digit_Recognizer (CNN手写数字识别) A CNN handwritten digit recognizer with graphical UI, i ...

  7. matlab 对mnist手写数字数据集进行判决分析_人工智能TensorFlow(十四)MINIST手写数字识别...

    MNIST是一个简单的视觉计算数据集,它是像下面这样手写的数字图片: MNIST 每张图片还额外有一个标签记录了图片上数字是几,例如上面几张图的标签就是:5.0.4.1. MINIST数据 MINIS ...

  8. 深度学习数字仪表盘识别_深度学习之手写数字识别项目(Sequential方法amp;Class方法进阶版)...

    此项目使用LeNet模型针对手写数字进行分类.项目中我们分别采用了顺序式API和子类方法两种方式构建了LeNet模型训练mnist数据集,并编写了给图识物应用程序用于手写数字识别. 一.LeNet模型 ...

  9. matlab朴素贝叶斯手写数字识别_从“手写数字识别”学习分类任务

    机器学习问题可以分为回归问题和分类问题,回归问题已经在线性回归讲过,本文学习分类问题.分类问题跟回归问题有明显的区别,回归问题是连续的数值,而分类问题是离散的类别,比如将性别分为[男,女],将图片分为 ...

最新文章

  1. 如何将一键还原精灵备份文件复制出来?
  2. python小案例_Python的应用小案例
  3. 奇怪吸引子---Aizawa
  4. jenkins api_接触Jenkins(Hudson)API,第1部分
  5. ASP面向对象编程探讨及比较
  6. linux mysql 实战_linux实用实战
  7. html树形结构主从命名,HAP_头⾏/主从结构的实现
  8. 20162305《程序设计与数据结构》第1周学习总结
  9. 巩固有私有VLAN和VLAN访问控制列表的网络
  10. Java TCP实现文件传输
  11. 共享计算机桌面,DeskTopShare桌面屏幕共享软件
  12. 弹弹堂手游语音服务器怎么连接,腾讯弹弹堂手游空间怎么进去 互动玩法攻略介绍...
  13. Tensor for argument #2 ‘mat1‘ is on CPU, but expected it to be on GPU (while checking arguments for
  14. B站风清扬-Java面试总结
  15. 领英常见问题—如何提高邀请通过率与账号曝光量
  16. # 2021-01-03 #「Jenkins Pipeline」- expected to call xxx but wound up catching xxx
  17. RTSP实时音视频传输介绍
  18. easyRtc设置视频清晰度的方法
  19. Linux 安装Redis 图解教程
  20. 软件工程导论概念集合

热门文章

  1. 从世界五百强及中国五百强企业网站设计风格看当前WEB设计潮流
  2. \\u559c\\u6b22\\u4e00\\u4e2a\\u4eba unicode编码问题
  3. 如何安装adb(Android Debug Bridge)
  4. 【程序人生】拿到offer就稳了?实习期你还得“忍”一下
  5. 曲线救国!通过VirtualBox让Windows Server 2008 R2也用上蓝牙
  6. 煤炭企业内部调拨物资称重问题如何管理(二)
  7. 如何快速查看你的笔记本电池健康报告
  8. C语言新建文件写入数据
  9. 小猿日记(14)- 阿里云ACE是什么
  10. java 判断是不是图片_java判断是否是图片