• OpenCL


    OpenCL

    一、 CUDA vs OpenCL

    1. 简介

    OpenCL: Open Computing Language,开放计算语言。
    OpenCL和CUDA是两种异构计算(此异构平台可由CPU,GPU或其他类型的处理器组成。)的编程模型。

    1. CUDA只支持NVIDIA自家的GPU。
    2. OpenCL最早是由Apple提出,后来交给了Khronos这个开放标准组织。OpenCL 1.0 在2008年底正式由Khronos发布,比CUDA晚了整整一年。

    2012年移动图形处理器市场份额,imagenation失去苹果后一落千丈,已被别的公司收购:

    enter description here
    enter description here

    2. 操作步骤

    CUDA C加速步骤:

    1. 在device (也就是GPU) 上申请内存
    2. 将host (也就是CPU) 上的数据拷贝到device
    3. 执行CUDA kernel function
    4. 将device上的计算结果传回host
    5. 释放device上的内存

    OpenCL操作步骤:

    1. 检测申请计算资源
      • 检测platform, clGetPlatformIDs
      • 检测platform对应的device, clGetDeviceInfo
      • 建立context, clCreateContextFromType
      • 建立command queue, clCreateCommandQueue
      • 在context内申请存储空间, clCreateBuffer
    2. 将host (也就是CPU) 上的数据拷贝到device, clCreateBuffer
    3. OpenCL代码编译
      • 读入OpenCL (kernel function) 源代码,创立program 句柄, clCreateProgramWithSource
      • 编译program, clBuildProgram
      • 创立一个 OpenCL kernel 句柄, clCreateKernel
      • 申明设置 kernel 的 参数, clSetKernelArg
      • 设置NDRange
    4. 运行kernel , clEnqueueNDRangeKernel
    5. 将device上的计算结果传回host, clEnqueueReadBuffer
    6. 释放计算资源
      • 释放kernel, clReleaseKernel
      • 释放program, clReleaseProgram
      • 释放device memory, clReleaseMemObject
      • 释放command queue, clReleaseCommandQueue
      • 释放context, clReleaseContext

    procedure
    procedure

    整体架构如下:

    enter description here
    enter description here

    CUDA C语言与OpenCL的定位不同,或者说是使用人群不同。CUDA C是一种高级语言,那些对硬件了解不多的非专业人士也能轻松上手;而OpenCL则是针对硬件的应用程序开发接口,它能给程序员更多对硬件的控制权,相应的上手及开发会比较难一些。

    OpenCL, CUDA C
    OpenCL, CUDA C

    3. 名词比较

    Block: 相当于opencl 中的work-group
    Thread:相当于opencl 中的work-item
    SP: 相当于opencl 中的PE
    SM: 相当于opencl 中的CU
    warp: 相当于opencl 中的wavefront(简称wave),基本的调试单位

    4. system tradeoff

    各种硬件形态的开发效率与执行效率,而opencl在FPGA上作用就是绿色箭头的方向,可以有效提高FPGA开发效率。

    system
    system

    二、常用API

    1. clEnqueueNDRangeKernel

    clEnqueueNDRangeKernel
    clEnqueueNDRangeKernel

    参数:

    1. command_queue,
    2. kernel,
    3. work_dim,使用多少维的NDRange,可以设为1, 2, 3, ..., CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS。
    4. global_work_offset(GWO), 每个维度的偏移,如果不设置默认为0
    5. global_work_size(GWS),每个维度的索引长度,GWS(1) * GWS(2) * ... * GWS(N) 应该大于等于需要处理的数据量
    6. local_work_size(LWS), 每个维度work-group的大小,如果不设置,系统会自己选择一个合适的大小
    7. num_events_in_wait_list: 执行kernel前需要等待的event个数
    8. event_wait_list: 需要等待的event列表
    9. event: 当前这个命令会返回一个event,以供后面的命令进行同步
      返回:

    函数返回执行状态。如果成功, 返回CL_SUCCESS

    2. clCreateBuffer

    clCreateBuffer
    clCreateBuffer

    1. context

    2. flags参数共有9种:

      device权限,默认为可读写:
      CL_MEM_READ_WRITE: kernel可读写
      CL_MEM_WRITE_ONLY: kernel 只写
      CL_MEM_READ_ONLY: kernel 只读

      创建方式:
      CL_MEM_USE_HOST_PTR: device端会对host_ptr位置内存进行缓存,如果有多个命令同时使用操作这块内存的行为是未定义的
      CL_MEM_ALLOC_HOST_PTR: 新开辟一段host端可以访问的内存
      CL_MEM_COPY_HOST_PTR: 在devices新开辟一段内存供device使用,并将host上的一段内存内容copy到新内存上

      host权限,默认为可读写:
      CL_MEM_HOST_WRITE_ONLY:host 只写
      CL_MEM_HOST_READ_ONLY: host只读
      CL_MEM_HOST_NO_ACCESS: host没有访问权限

    3. size是buffer的大小

    4. host_ptr只有在CL_MEM_USE_HOST_PTR, CL_MEM_COPY_HOST_PTR时才有效。

    一般对于kernel函数的输入参数,使用CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR可以将host memory拷贝到device memory,表示device只读,位置在device上并进行内存复制,host权限为可读写;
    对于输出参数,使用CL_MEM_WRITE_ONLY表示device只写,位置在device上,host权限为可读可写。

    如果进行host与device之间的内存传递,可以使用clEnqueueReadBuffer读取device上的内存到host上, clEnqueueWriteBuffer可以将host上内存写到device上。

    3. clEnqueueWriteBuffer

    clEnqueueWriteBuffer
    clEnqueueWriteBuffer

    1. command_queue,
    2. buffer, 将内存写到的位置
    3. blocking_write, 是否阻塞
    4. offset, 从buffer的多少偏移处开始写
    5. size, 写入buffer大小
    6. ptr, host端buffer地址
    7. num_events_in_wait_list, 等待事件个数
    8. event_wait_list, 等待事件列表
    9. event, 返回的事件

    4. clCreateImage

    创建一个ImageBuffer:

    clCreateImage
    clCreateImage

    1. context
    2. flags, 同clCreateBuffer里的flags
    3. image_format, 图像的属性,包含两个变量: image_channel_order, 指定通道数和形式,通常为RGBA;image_channel_data_type, 定义数据类型, CL_UNORM_INT8表示为unsigned规一化的INT8,CL_UNSIGNED_INT8
      表示 为非规一化的unsigned int8
    4. image_desc, 定义图像的维度大小,
    5. host_ptr, 输入图像地址
    6. errorce_ret, 返回状态

    5. clEnqueueWriteImage

    clEnqueueWriteImage
    clEnqueueWriteImage

    1. command_queue
    2. image, 目标图像
    3. block_writing, 是否阻塞,如果TRUE,则阻塞
    4. origin, 图像的偏移,通常为(0, 0, 0)
    5. region, 图像的区域,(width, height, depth)
    6. input_row_pitch,每行字节数,可能有对齐;如果设为0,则程序根据每个像素的字节数 乘以 width 计算
    7. input_slice_pitch,3D图像的2D slice块,如果是1D或2D图像,这个值必须为0
    8. ptr, host端输入源图像地址
    9. num_events_in_wait_list, 需等待事件个数
    10. evnet_wait_list, 需要等待的事件列表
    11. event, 返回这个命令的事件,用于后续使用

    Map buffer

    将cl_mem映射到CPU可访问的指针:

    clEnqueueMapBuffer
    clEnqueueMapBuffer

    1. command_queue
    2. buffer, cl_mem映射的源地址
    3. blocking_map, 是否阻塞
    4. map_flags, CL_MAP_READ,映射的地址为只读;CL_MAP_WRITE,向映射的地址写东西;CL_MAP_WRITE_INVALIDATE_REGION, 向映射的地址为写东西,host不会使用这段地址的内容,这时返回的地址处的内容不保证是最新的
    5. offset, cl_mem的偏移
    6. size, 映射的内存大小
    7. num_events_in_wait_list, 等待事件个数
    8. event_wait_list, 等待事件列表
    9. event, 返回事件
    10. errorcode_ret, 返回状态

    返回值是CPU可访问的指针。

    注意:

    1. 当flag为CL_MAP_WRITE时,如果不使用unmap进行解映射,device端无法保证可以获取到最新写的值。
    2. 如果不用unmap,那么device端无法释放这部分内存

    所以写完内容后,要立马解映射。

    buffer

    clEnqueueCopyBuffer: 从一个cl buffer拷贝到另一个cl buffer

    event

    cl_int clWaitForEvents(cl_uint num_events, const cl_event *event_list)
    等待事件执行完成才返回,否则会阻塞

    cl_int clEnqueueWaitForEvents(cl_command_queue command_queue, cl_uint num_events, const cl_event *event_list)
    和 clWaitForEvents 不同的是该命令执行后会立即返回,线程可以在不阻塞的情况下接着执行其它任务。而 clWaitForEvents 会进入阻塞状态,直到事件列表 event_list 中对应的事件处于 CL_COMPLETE 状态。

    cl_int clFlush(cl_command_queue command_queue)
    只保证command_queue中的command被commit到相应的device上,不保证当clFlush返回时这些command已经执行完。

    cl_int clFinish(cl_command_queue command_queue)
    clFinish直到之前的队列命令都执行完才返回。clFinish is also a synchronization point.

    cl_int clEnqueueBarrier(cl_command_queue command_queue)
    屏障命令保证在后面的命令执行之前,它前面提交到命令队列的命令已经执行完成。
    和 clFinish 不同的是该命令会异步执行,在 clEnqueueBarrier 返回后,线程可以执行其它任务,例如分配内存、创建内核等。而 clFinish 会阻塞当前线程,直到命令队列为空(所有的内核执行/数据对象操作已完成)。

    cl_int clEnqueueMarker(cl_command_queue command_queue, cl_event *event)
    将标记命令提交到命令队列 command_queue 中。当标记命令执行后,在它之前提交到命令队列的命令也执行完成。该函数返回一个事件对象 event,在它后面提交到命令队列的命令可以等待该事件。例如,随后的命令可以等待该事件以确保标记之前的命令已经执行完成。如果函数成功执行返回 CL_SUCCESS。

    三、架构

    1. Platform Model

    1个host加上1个或多个device,1个device由多个compute unit组成,1个compute unit又由多个Processing Elemnet组成。

    Platform Model
    Platform Model

    2. Execution Model

    执行模型:

    一个主机要使得内核运行在设备上,必须要有一个上下文来与设备进行交互。 一个上下文就是一个抽象的容器,管理在设备上的内存对象,跟踪在设备上 创建的程序和内核。

    主机程序使用命令队列向设备提交命令,一个设备有一个命令队列,且与上下文 相关。命令队列对在设备上执行的命令进行调度。这些命令在主机程序和设备上 异步执行。执行时,命令间的关系有两种模式:(1)顺序执行,(2)乱序执行。

    内核的执行和提交给一个队列的内存命令会生成事件对象,可以用来控制命令的执行、协调宿主机和设备的运行。

    有3种命令类型:
    • Kernel-enqueue commands: Enqueue a kernel for execution on a device.(执行kernel函数)
    • Memory commands: Transfer data between the host and device memory, between memory objects, or map and unmap memory objects from the host address space.(内存传输)
    • Synchronization commands: Explicit synchronization points that define order constraints between commands.(同步点)

    命令执行经历6个状态:

    1. Queued: 将command放到CommandQueue
    2. Submitted: 将command从CommandQueue提交到Device
    3. Ready: 当所有运行条件满足,放到Device的WorkPool里
    4. Running: 命令开始执行
    5. Ended: 命令执行结束
    6. Complete: command以及其子command都结束执行,并设置相关的事件状态为CL_COMPLETE

    Execution Model
    Execution Model

    Mapping work-items onto an NDRange:

    与CUDA里的grid, block, thread类似,OpenCL也有自己的work组织方式NDRange。NDRange是一个N维的索引空间(N为1, 2, 3...),一个NDRange由三个长度为N的数组定义,与clEnqueueNDRangeKernel几个参数对应:

    1. global_work_size(GWS),每个维度的索引长度,GWS(1) * GWS(2) * ... * GWS(N) 应该大于等于需要处理的数据量
    2. global_work_offset(GWO), 每个维度的偏移,如果不设置默认为0
    3. local_work_size(LWS), 每个维度work-group的大小,如果不设置,系统会自己选择较好的结果

    如下图所示,整个索引空间的大小为,每个work-group大小为,全局偏移为
    对于一个work-item,有两种方式可以索引:

    1. 直接使用global id
    2. 或者使用work-group进行相关计算,设当前group索引为,group里的local id分别为(s_x, s_y),那么便有

    NDRange index space
    NDRange index space

    3. Memory Model

    不同平台的内存模型不一样,为了可移植性,OpenCL定义了一个抽象模型,程序的实现只需要关注抽象模型,而具体的向硬件的映射由驱动来完成。

    Memory Model
    Memory Model

    主要分为host memory和device memory。而device memory 一共有4种内存:
    private memory:是每个work-item各自私有
    local memory: 在work-group里的work-item共享该内存
    global memory: 所有memory可访问
    constant memory: 所有memory可访问,只读,host负责初始化

    enter description here
    enter description here

    4. Program Model

    OpenCL支持数据并行,任务并行编程,同时支持两种模式的混合。
    分散收集(scatter-gather):数据被分为子集,发送到不同的并行资源中,然后对结果进行组合,也就是数据并行;如两个向量相加,对于每个数据的+操作应该都可以并行完成。
    分而治之(divide-and-conquer):问题被分为子问题,在并行资源中运行,也就是任务并行;比如多CPU系统,每个CPU执行不同的线程。还有一类流水线并行,也属于任务并行。流水线并行,数据从一个任务传送到另外一个任务中,同时前一个任务又处理新的数据,即同一时刻,每个任务都在同时运行。

    enter description here
    enter description here

    并行编程就要考虑到数据的同步与共享问题。

    in-order vs out-of-order:
    创建命令队列时,如果没有为命令队列设置 CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE 属性,提交到命令队列的命令将按照 in-order 的方式执行。

    OpenCL支持两种同步:
    同一工作组内(work-group)工作项(work-item)的同步(实现方式barrier):
    reduction的实现中,需要进行数据同步,所谓reduction就是使用多个数据生成一个数据,如tensorflow中的reduce_mean, reduce_sum等。在执行reduce之前,必须保证这些数据已经是有效的,执行过的,

    命令队列中处于同一个上下文中的命令的同步(使用clWaitForEvents,clEnqueueMarker, clEnqueueBarrier 或者执行kernel时加入等待事件列表)。

    有2种方式同步:
    锁(Locks):在一个资源被访问的时候,禁止其他访问;
    栅栏(Barriers):在一个运行点中进行等待,直到所有运行任务都完成;(典型的BSP编程模型就是这样)

    数据共享:
    (1)shared memory
    当任务要访问同一个数据时,最简单的方法就是共享存储shared memory(很多不同层面与功能的系统都有用到这个方法),大部分多核系统都支持这一模型。shared memory可以用于任务间通信,可以用flag或者互斥锁等方法进行数据保护,它的优缺点:
    优点:易于实现,编程人员不用管理数据搬移;
    缺点:多个任务访问同一个存储器,控制起来就会比较复杂,降低了互联速度,扩展性也比较不好。
    (2)message passing
    数据同步的另外一种模型是消息传递模型,可以在同一器件中,或者多个数量的器件中进行并发任务通信,且只在需要同步时才启动。
    优点:理论上可以在任意多的设备中运行,扩展性好;
    缺点:程序员需要显示地控制通信,开发有一定的难度;发送和接受数据依赖于库方法,因此可移植性差。

    Experiment

    1. 向量相加

    guru_ge@dl:~/opencl/test$ ./cuda_vector_add
    SUCCESS
    copy input time: 15438.000000
    CUDA time: 23.000000
    copy output time: 17053.000000
    CPU time: 16259.000000
    result is right!
    guru_ge@dl:~/opencl/test$ ./main
    Device: GeForce GTX 1080 Ti
    create input buffer time: 7
    create output buffer time: 1
    write buffer time: 4017
    OpenCL time: 639
    read buffer time: 30337
    CPU time: 16197
    result is right!

    guru_ge@dl:~/opencl/test$ ./cuda_vector_add
    SUCCESS
    copy input time: 59825.000000
    CUDA time: 36.000000
    copy output time: 67750.000000
    CPU time: 64550.000000
    result is right!
    guru_ge@dl:~/opencl/test$ ./main
    Device: GeForce GTX 1080 Ti
    create input buffer time: 7
    create output buffer time: 1
    write buffer time: 52640
    OpenCL time: 1634
    read buffer time: 80206
    CPU time: 66502
    result is right!
    guru_ge@dl:~/opencl/test$

    Reference

    1. https://www.cnblogs.com/wangshide/archive/2012/01/07/2315830.html
    2. https://www.cnblogs.com/hlwfirst/p/5003504.html
    3. http://blog.csdn.net/leonwei/article/details/8909897
    4. https://blog.csdn.net/babyfacer/article/details/6863572
    5. https://blog.csdn.net/xbinworld/article/details/45949629
    6. https://blog.csdn.net/Bob_Dong/article/details/70172165?locationNum=11&fps=1
  • 相关阅读:
    年尾最有可能被老板“干掉”的十类人
    jQuery学习笔记
    HttpServletRequest的应用(一)
    getRequestDispatcher()与sendRedirect()的区别
    RequestDispatcher介绍
    原子类
    多线程基础
    asp.net Treeview节点保存成XML文件
    推荐小说给大家
    http://msdn.microsoft.com/zhcn/library/system.web.ui.webcontrols.gridview.rowediting.aspx
  • 原文地址:https://www.cnblogs.com/gr-nick/p/9379352.html
Copyright © 2020-2023  润新知