opencl 加速 c语言程序_在AlveoU200加速卡上实现简单手写数字识别
最近实验室租了块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加速卡上实现简单手写数字识别相关推荐
- k近邻算法_图穷匕见:K近邻算法与手写数字识别
机器学习算法是从数据中产生模型,也就是进行学习的算法.我们把经验提供给算法,它就能够根据经验数据产生模型.在面对新的情况时,模型就会为我们提供判断(预测)结果.例如,我们根据"个子高.腿长. ...
- python cnn代码详解图解_基于TensorFlow的CNN实现Mnist手写数字识别
本文实例为大家分享了基于TensorFlow的CNN实现Mnist手写数字识别的具体代码,供大家参考,具体内容如下 一.CNN模型结构 输入层:Mnist数据集(28*28) 第一层卷积:感受视野5* ...
- 我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
"我的Go+语言初体验" | 征文活动进行中- 我的Go+语言初体验--Go+语言构建神经网络实战手写数字识别 0. 前言 1. 神经网络相关概念 2. 构建神经网络实战手写数字识 ...
- pytorch 预测手写体数字_深度学习之PyTorch实战(3)——实战手写数字识别
如果需要小编其他论文翻译,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/DeepLearningNote 上一节,我们已经 ...
- C语言底层搭建CNN实现MNIST手写数字识别
手写数字识别 手写数字识别是指使用计算机自动识别手写体阿拉伯数字的技术.作为光学字符识别OCR的一个分支,它可以被广泛应用到手写数据的自动录入场景中.传统的识别方法如最近邻算法k-NN.支持向量机SV ...
- python手写多个字母识别_一个带界面的CNN手写数字识别,使用Python(tensorflow, kivy)实现...
CNN_Handwritten_Digit_Recognizer (CNN手写数字识别) A CNN handwritten digit recognizer with graphical UI, i ...
- matlab 对mnist手写数字数据集进行判决分析_人工智能TensorFlow(十四)MINIST手写数字识别...
MNIST是一个简单的视觉计算数据集,它是像下面这样手写的数字图片: MNIST 每张图片还额外有一个标签记录了图片上数字是几,例如上面几张图的标签就是:5.0.4.1. MINIST数据 MINIS ...
- 深度学习数字仪表盘识别_深度学习之手写数字识别项目(Sequential方法amp;Class方法进阶版)...
此项目使用LeNet模型针对手写数字进行分类.项目中我们分别采用了顺序式API和子类方法两种方式构建了LeNet模型训练mnist数据集,并编写了给图识物应用程序用于手写数字识别. 一.LeNet模型 ...
- matlab朴素贝叶斯手写数字识别_从“手写数字识别”学习分类任务
机器学习问题可以分为回归问题和分类问题,回归问题已经在线性回归讲过,本文学习分类问题.分类问题跟回归问题有明显的区别,回归问题是连续的数值,而分类问题是离散的类别,比如将性别分为[男,女],将图片分为 ...
最新文章
- 如何将一键还原精灵备份文件复制出来?
- python小案例_Python的应用小案例
- 奇怪吸引子---Aizawa
- jenkins api_接触Jenkins(Hudson)API,第1部分
- ASP面向对象编程探讨及比较
- linux mysql 实战_linux实用实战
- html树形结构主从命名,HAP_头⾏/主从结构的实现
- 20162305《程序设计与数据结构》第1周学习总结
- 巩固有私有VLAN和VLAN访问控制列表的网络
- Java TCP实现文件传输
- 共享计算机桌面,DeskTopShare桌面屏幕共享软件
- 弹弹堂手游语音服务器怎么连接,腾讯弹弹堂手游空间怎么进去 互动玩法攻略介绍...
- Tensor for argument #2 ‘mat1‘ is on CPU, but expected it to be on GPU (while checking arguments for
- B站风清扬-Java面试总结
- 领英常见问题—如何提高邀请通过率与账号曝光量
- # 2021-01-03 #「Jenkins Pipeline」- expected to call xxx but wound up catching xxx
- RTSP实时音视频传输介绍
- easyRtc设置视频清晰度的方法
- Linux 安装Redis 图解教程
- 软件工程导论概念集合
热门文章
- 从世界五百强及中国五百强企业网站设计风格看当前WEB设计潮流
- \\u559c\\u6b22\\u4e00\\u4e2a\\u4eba unicode编码问题
- 如何安装adb(Android Debug Bridge)
- 【程序人生】拿到offer就稳了?实习期你还得“忍”一下
- 曲线救国!通过VirtualBox让Windows Server 2008 R2也用上蓝牙
- 煤炭企业内部调拨物资称重问题如何管理(二)
- 如何快速查看你的笔记本电池健康报告
- C语言新建文件写入数据
- 小猿日记(14)- 阿里云ACE是什么
- java 判断是不是图片_java判断是否是图片