OpenCL并行编程

看了mnn、mace和ncnn的源码,对于深度学习手机gpu计算的优化,mace选择了opencl,而ncnn选择了vulkan,mnn选择了opencl、vulkan、metal、opengl。那本篇就来介绍一下嵌入式(手机)下opencl的异构编程。下篇vulkan。
每年看各手机厂商发布会的时候,都会提到该手机除了牛逼的某某处理器之外,还配备了某某协处理器,其实这个协处理器就相当于手机上的GPU(图形处理单元)。除了手机,还有其他的一些嵌入式设备也配备了其他类型的协处理器,比如Xilinx的Zynq用FPGA来补充ARM Cortex-A9处理器的不足,再比如Texas Instruments的Keystone II用DSP补充了ARM Cortex A15的不足。这些协处理器都支持通用编程使用开发计算语言(OpenCL)。说回到手机,现在主流的手机上用的GPU主要是以下两种:1)ARM Mali GPU;2)Qualcomm Adreno GPU。这俩都支持opencl。

  • OpenCL规范
    OpenCL规范由四个模型组成,总结如下:
  1. 平台模型:描述了协同执行的单个处理器(宿主机)及一个或多个能执行OpenCL代码的处理器(设备)。它定义了一个抽象的硬件模型,供编程人员用于编写能够再这些设备上执行的OpenCL C函数(称作kernel)。
  2. 执行模型:定义了在主机上如何配置OpenCL环境以及如何在设备上执行kernel。这包括在主机端建立OpenCL上下文,提供主机-设备之间的交互机制,定义一个并发模型供在设备上执行kernel所用。
  3. 内存模型: 定义被kernel所用的抽象内存层次,无需考虑实际的底层内存架构。尽管内存模型十分接近当前的GPU内存层次,但同样也适用于其他硬件加速器。
  4. 编程模型:定义了如何将并发模型映射到物理硬件上。
  • OpenCL应用步骤
    一个基于异构平台的应用一般会包含以下步骤:
  1. 找出组成异构平台的所有组件
  2. 考察组件的特征,这样就能使得软件根据不同的硬件特征来实现。
  3. 创建在平台上运行的一组内核。
  4. 设置与计算相关的存储对象
  5. 在合适的组件上以正确的顺序执行内核。
  6. 收集结果。
  • OpenCL编程


    20181105142838114.png

    OpenCl的编程框架组成包括:
    平台API:平台API定义了宿主机程序发现OpenCL设备所用的函数以及这些函数的功能,另外还定义为OpenCL应用创建上下文(上下文表示的是程序运行时所拥有的所有软硬件资源+内存+处理器)的函数。这里的平台指的是宿主机、OpenCL设备和OpenCL框架的组合。
    运行时API:平台API主要用来创建上下文,运行时API则强调使用这个上下文满足应用需求的函数集,用来管理上下文来创建命令队列以及运行时发生的其它操作。例如,将命令提交到命令队列的函数。
    编程语言:用来编写内核代码的编程语言,基于ISO C99标准的一个扩展子集,通常称为OpenCL C编程语言。

  • OpenCL的编程步骤如下:
  1. Discover and initialize the platforms
    调用两次clGetPlatformIDs函数,第一次获取可用的平台数量,第二次获取一个可用的平台。
  2. Discover and initialize the devices
    调用两次clGetDeviceIDs函数,第一次获取可用的设备数量,第二次获取一个可用的设备。
  3. Create a context(调用clCreateContext函数)
    上下文context可能会管理多个设备device。
  4. Create a command queue(调用clCreateCommandQueue函数)
    一个设备device对应一个command queue。
    上下文conetxt将命令发送到设备对应的command queue,设备就可以执行命令队列里的命令。
  5. Create device buffers(调用clCreateBuffer函数)
    Buffer中保存的是数据对象,就是设备执行程序需要的数据保存在其中。
    Buffer由上下文conetxt创建,这样上下文管理的多个设备就会共享Buffer中的数据。
  6. Write host data to device buffers(调用clEnqueueWriteBuffer函数)
  7. Create and compile the program
    创建程序对象,程序对象就代表你的程序源文件或者二进制代码数据。
  8. Create the kernel(调用clCreateKernel函数)
    根据你的程序对象,生成kernel对象,表示设备程序的入口。
  9. Set the kernel arguments(调用clSetKernelArg函数)
  10. Configure the work-item structure(设置worksize)
    配置work-item的组织形式(维数,group组成等)
  11. Enqueue the kernel for execution(调用clEnqueueNDRangeKernel函数)
    将kernel对象,以及 work-item参数放入命令队列中进行执行。
  12. Read the output buffer back to the host(调用clEnqueueReadBuffer函数)
  13. Release OpenCL resources(至此结束整个运行过程)
  • OpenCL实战之矩阵相乘
    了解了OpenCL的的理论知识,这里演示一个MP的矩阵和PN的矩阵相乘的例子。
    首先我们知道我们的程序分为两部分,一部分是我们的kernel运行再gpu,一部分是我们的主程序运行再cpu。kernel可以这样写:
__kernel void matrix_mult(
    const int Ndim,
    const int Mdim,
    const int Pdim,
    __global const float* A, 
    __global const float* B, 
    __global float* C)
{
    int i = get_global_id(0);
    int j = get_global_id(1);

    int k;
    float tmp;

    if ((i < Ndim) && (j < Mdim)) {
        tmp = 0.0;
        for (k = 0; k < Pdim; k++)
            tmp += A[i*Pdim + k] * B[k*Mdim + j];
        C[i*Mdim + j] = tmp;
    }
}

那么主程序就可以按照上面的理论步骤来写:

#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>
#include <fstream>

using namespace std;

#define NWITEMS 6

#pragma comment (lib,"OpenCL.lib")

//把文本文件读入一个 string 中
int convertToString(const char *filename, std::string& s)
{
    size_t size;
    char* str;
    std::fstream f(filename, (std::fstream::in | std::fstream::binary));
    if (f.is_open())
    {
        size_t fileSize;
        f.seekg(0, std::fstream::end);
        size = fileSize = (size_t)f.tellg();
        f.seekg(0, std::fstream::beg);
        str = new char[size + 1];
        if (!str)
        {
            f.close();
            return NULL;
        }
        f.read(str, fileSize);
        f.close();
        str[size] = '\0';
        s = str;
        delete[] str;
        return 0;
    }
    printf("Error: Failed to open file %s\n", filename);
    return 1;
}

int main()
{
    cl_uint status;
    cl_platform_id platform;

    //创建平台对象
    status = clGetPlatformIDs(1, &platform, NULL);
    cl_device_id device;
    //创建 GPU 设备
    clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU,
        1,
        &device,
        NULL);
    //创建context
    cl_context context = clCreateContext(NULL,
        1,
        &device,
        NULL, NULL, NULL);
    //创建命令队列
    cl_command_queue commandQueue = clCreateCommandQueue(context,
        device,
        CL_QUEUE_PROFILING_ENABLE, NULL);

    if (commandQueue == NULL) 
            perror("Failed to create commandQueue for device 0.");

    //建立要传入从机的数据
    /********  创建内核和内存对象 ********/

    const int Ndim = 4;
    const int Mdim = 5;
    const int Pdim = 3;
    int szA = Ndim * Pdim;
    int szB = Pdim * Mdim;
    int szC = Pdim * Mdim;

    float *A;
    float *B;
    float *C;

    A = (float *)malloc(szA * sizeof(float));
    B = (float *)malloc(szB * sizeof(float));
    C = (float *)malloc(szC * sizeof(float));
    int i, j;
    for (i = 0; i < szA; i++)
        A[i] = (float)((float)i + 1.0);
    for (i = 0; i < szB; i++)
        B[i] = (float)((float)i + 1.0);

    //创建三个 OpenCL 内存对象,并把buf1 的内容通过隐式拷贝的方式
    //拷贝到clbuf1, buf2 的内容通过显示拷贝的方式拷贝到clbuf2
    cl_mem memObjects[3] = { 0, 0, 0 };
    memObjects[0] = clCreateBuffer(context, CL_MEM_READ_ONLY |  CL_MEM_COPY_HOST_PTR,
        sizeof(float)* szA, A, NULL);
    memObjects[1] = clCreateBuffer(context, CL_MEM_READ_ONLY |  CL_MEM_COPY_HOST_PTR,
        sizeof(float)* szB, B, NULL);
    memObjects[2] = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR,
        sizeof(float)* szC, C, NULL);
    if (memObjects[0] == NULL || memObjects[1] == NULL ||memObjects[2] == NULL) 
        perror("Error in clCreateBuffer.\n");

    const char * filename = "Vadd.cl";
    std::string sourceStr;
    status = convertToString(filename, sourceStr);
    if (status)
        cout << status << "  !!!!!!!!" << endl;
    const char * source = sourceStr.c_str();
    size_t sourceSize[] = { strlen(source) };
    //创建程序对象
    cl_program program = clCreateProgramWithSource(
        context,
        1,
        &source,
        sourceSize,
        NULL);
    //编译程序对象
    status = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
    if (status)
        cout << status << "  !!!!!!!!" <<endl;
    if (status != 0)
    {
        printf("clBuild failed:%d\n", status);
        char tbuf[0x10000];
        clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0x10000, tbuf,
            NULL);
        printf("\n%s\n", tbuf);
        //return −1;
    }

    //创建 Kernel 对象
    cl_kernel kernel = clCreateKernel(program, "matrix_mult", NULL);

    //设置 Kernel 参数
    cl_int clnum = NWITEMS;
    status = clSetKernelArg(kernel, 0, sizeof(int), &Ndim);
    status = clSetKernelArg(kernel, 1, sizeof(int), &Mdim);
    status = clSetKernelArg(kernel, 2, sizeof(int), &Pdim);
    status = clSetKernelArg(kernel, 3, sizeof(cl_mem), &memObjects[0]);
    status = clSetKernelArg(kernel, 4, sizeof(cl_mem), &memObjects[1]);
    status = clSetKernelArg(kernel, 5, sizeof(cl_mem), &memObjects[2]);
    if (status)
        cout << "参数设置错误" << endl;

    //执行 kernel
    size_t global[2];
    cl_event prof_event;
    cl_ulong ev_start_time = (cl_ulong)0;
    cl_ulong ev_end_time = (cl_ulong)0;
    double rum_time;
    global[0] = (size_t)Ndim;
    global[1] = (size_t)Mdim;
    status = clEnqueueNDRangeKernel(commandQueue, kernel, 2, NULL,
             global, NULL, 0, NULL, &prof_event);
    if (status)
        cout << "执行内核时错误" << endl;
    clFinish(commandQueue);

    //读取时间
    status = clGetEventProfilingInfo(prof_event,CL_PROFILING_COMMAND_QUEUED,
        sizeof(cl_ulong),&ev_start_time,NULL);
    status = clGetEventProfilingInfo(prof_event,CL_PROFILING_COMMAND_END,
        sizeof(cl_ulong),&ev_end_time,NULL);
    if (status) 
        perror("读取时间的时候发生错误\n");
    rum_time = (double)(ev_end_time - ev_start_time);
    cout << "执行时间为:" << rum_time << endl;

    //数据拷回 host 内存
    status = clEnqueueReadBuffer(commandQueue, memObjects[2],CL_TRUE, 0,
            sizeof(float)* szC, C,0, NULL, NULL);
    if (status) 
        perror("读回数据的时候发生错误\n");

    //结果显示
    printf("\nArray A:\n");
    for (i = 0; i < Ndim; i++) {
        for (j = 0; j < Pdim; j++)
             printf("%.3f\t", A[i*Pdim + j]);
        printf("\n");
    }
    printf("\nArray B:\n");
    for (i = 0; i < Pdim; i++) {
        for (j = 0; j < Mdim; j++)
             printf("%.3f\t", B[i*Mdim + j]);
        printf("\n");
    }
    printf("\nArray C:\n");
    for (i = 0; i < Ndim; i++) {
        for (j = 0; j < Mdim; j++)
             printf("%.3f\t", C[i*Mdim + j]);
        printf("\n");
    }

    cout << endl;

    if (A)
        free(A);
    if (B)
        free(B);
    if (C)
        free(C);

    //删除 OpenCL 资源对象
    clReleaseMemObject(memObjects[2]);
    clReleaseMemObject(memObjects[1]);
    clReleaseMemObject(memObjects[0]);
    clReleaseProgram(program);
    clReleaseCommandQueue(commandQueue);
    clReleaseContext(context);
    system("pause");

    return 0;
}

参考链接:
https://www.cnblogs.com/wangshide/archive/2012/01/07/2315830.html
http://www.cnblogs.com/xudong-bupt/p/3582780.html
https://blog.csdn.net/c602273091/article/details/45418129

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352