OpenCL快速入门教程

OpenCL快速入门教程

原文地址:http://opencl.codeplex.com/wikipage?title=OpenCL%20Tutorials%20-%201

翻译日期:2012年6月4日星期一

 

这是第一篇真正的OpenCL教程。这篇文章不会从GPU结构的技术概念和性能指标入手。我们将会从OpenCL的基础API开始,使用一个小的kernel作为例子来讲解基本的计算管理。

首先我们需要明白的是,OpenCL程序是分成两部分的:一部分是在设备上执行的(对于我们,是GPU),另一部分是在主机上运行的(对于我们,是CPU)。在设备上执行的程序或许是你比较关注的。它是OpenCL产生神奇力量的地方。为了能在设备上执行代码,程序员需要写一个特殊的函数(kernel函数)。这个函数需要使用OpenCL语言编写。OpenCL语言采用了C语言的一部分加上一些约束、关键字和数据类型。在主机上运行的程序提供了API,所以i可以管理你在设备上运行的程序。主机程序可以用C或者C++编写,它控制OpenCL的环境(上下文,指令队列…)。

设备(Device)

我们来简单的说一下设备。设备,像上文介绍的一样,OpenCL编程最给力的地方。

我们必须了解一些基本概念:

Kernel:你可以把它想像成一个可以在设备上执行的函数。当然也会有其他可以在设备上执行的函数,但是他们之间是有一些区别的。Kernel是设备程序执行的入口点。换言之,Kernel是唯一可以从主机上调用执行的函数。

现在的问题是:我们如何来编写一个Kernel?在Kernel中如何表达并行性?它的执行模型是怎样的?解决这些问题,我们需要引入下面的概念:

    SIMT:单指令多线程(SINGLE INSTRUCTION MULTI THREAD)的简写。就像这名字一样,相同的代码在不同线程中并行执行,每个线程使用不同的数据来执行同一段代码。

    Work-item(工作项):Work-item与CUDA Threads是一样的,是最小的执行单元。每次一个Kernel开始执行,很多(程序员定义数量)的Work-item就开始运行,每个都执行同样的代码。每个work-item有一个ID,这个ID在kernel中是可以访问的,每个运行在work-item上的kernel通过这个ID来找出work-item需要处理的数据。

    Work-group(工作组):work-group的存在是为了允许work-item之间的通信和协作。它反映出work-item的组织形式(work-group是以N维网格形式组织的,N=1,2或3)。

Work-group等价于CUDA thread blocks。像work-items一样,work-groups也有一个kernel可以读取的唯一的ID。

    ND-Range:ND-Range是下一个组织级别,定义了work-group的组织形式(ND-Rang以N维网格形式组织的,N=1,2或3);

clip_image002

这是ND-Range组织形式的例子

Kernel

现在该写我们的第一个kernel了。我们写一个小的kernel将两个向量相加。这个kernel需要四个参数:两个要相加的向量,一个存储结果的向量,和向量个数。如果你写一个程序在cpu上解决这个问题,将会是下面这个样子:

复制代码
void vector_add_cpu (const float* src_a,const float* src_b,float*  res,const int num)
{for (int i = 0; i < num; i++)res[i] = src_a[i] + src_b[i];
}
复制代码

 

在GPU上,逻辑就会有一些不同。我们使每个线程计算一个元素的方法来代替cpu程序中的循环计算。每个线程的index与要计算的向量的index相同。我们来看一下代码实现:

复制代码
__kernel void vector_add_gpu (__global const float* src_a,__global const float* src_b,__global float* res,const int num)
{/* get_global_id(0) 返回正在执行的这个线程的ID。许多线程会在同一时间开始执行同一个kernel,每个线程都会收到一个不同的ID,所以必然会执行一个不同的计算。*/const int idx = get_global_id(0);/* 每个work-item都会检查自己的id是否在向量数组的区间内。如果在,work-item就会执行相应的计算。*/if (idx < num)res[idx] = src_a[idx] + src_b[idx];
}
复制代码

 

有一些需要注意的地方:

1. Kernel关键字定义了一个函数是kernel函数。Kernel函数必须返回void。

2. Global关键字位于参数前面。它定义了参数内存的存放位置。

另外,所有kernel都必须写在“.cl”文件中,“.cl”文件必须只包含OpenCL代码。

主机(Host)

我们的kernel已经写好了,现在我们来写host程序。

建立基本OpenCL运行环境

有一些东西我们必须要弄清楚:

Plantform(平台):主机加上OpenCL框架管理下的若干设备构成了这个平台,通过这个平台,应用程序可以与设备共享资源并在设备上执行kernel。平台通过cl_plantform来展现,可以使用下面的代码来初始化平台:

// Returns the error code

cl_int oclGetPlatformID (cl_platform_id *platforms) // Pointer to the platform object

 

Device(设备):通过cl_device来表现,使用下面的代码:

复制代码
// Returns the error code

cl_int clGetDeviceIDs (cl_platform_id platform,cl_device_type device_type, // Bitfield identifying the type. For the GPU we use CL_DEVICE_TYPE_GPU

cl_uint num_entries, // Number of devices, typically 1

cl_device_id *devices, // Pointer to the device object

cl_uint *num_devices) // Puts here the number of devices matching the device_type
复制代码

 

Context(上下文):定义了整个OpenCL化境,包括OpenCL kernel、设备、内存管理、命令队列等。上下文使用cl_context来表现。使用以下代码初始化:

复制代码
// Returs the context

cl_context clCreateContext (const cl_context_properties *properties, // Bitwise with the properties (see specification)

cl_uint num_devices, // Number of devicesconst cl_device_id *devices, // Pointer to the devices objectvoid (*pfn_notify)(const char *errinfo, const void *private_info, size_t cb, void *user_data), // (don't worry about this)void *user_data, // (don't worry about this)

cl_int *errcode_ret) // error code result
复制代码

 

Command-Queue(指令队列):就像它的名字一样,他是一个存储需要在设备上执行的OpenCL指令的队列。“指令队列建立在一个上下文中的指定设备上。多个指令队列允许应用程序在不需要同步的情况下执行多条无关联的指令。”

复制代码
cl_command_queue clCreateCommandQueue (cl_context context,cl_device_id device,cl_command_queue_properties properties, // Bitwise with the properties

cl_int *errcode_ret) // error code result
复制代码

 

下面的例子展示了这些元素的使用方法:

复制代码
cl_int error = 0;   // Used to handle error codes
cl_platform_id platform;
cl_context context;
cl_command_queue queue;
cl_device_id device;// Platform
error = oclGetPlatformID(&platform);
if (error != CL_SUCCESS) {cout << "Error getting platform id: " << errorMessage(error) << endl;exit(error);
}
// Device
error = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
if (err != CL_SUCCESS) {cout << "Error getting device ids: " << errorMessage(error) << endl;exit(error);
}
// Context
context = clCreateContext(0, 1, &device, NULL, NULL, &error);
if (error != CL_SUCCESS) {cout << "Error creating context: " << errorMessage(error) << endl;exit(error);
}
// Command-queue
queue = clCreateCommandQueue(context, device, 0, &error);
if (error != CL_SUCCESS) {cout << "Error creating command queue: " << errorMessage(error) << endl;exit(error);
}
复制代码

 

分配内存

主机的基本环境已经配置好了,为了可以执行我们的写的小kernel,我们需要分配3个向量的内存空间,然后至少初始化它们其中的两个。

在主机环境下执行这些操作,我们需要像下面的代码这样去做:

复制代码
const int size = 1234567
float* src_a_h = new float[size];
float* src_b_h = new float[size];
float* res_h = new float[size];
// Initialize both vectors
for (int i = 0; i < size; i++) {src_a_h = src_b_h = (float) i;
}
复制代码

 

在设备上分配内存,我们需要使用cl_mem类型,像下面这样:

复制代码
// Returns the cl_mem object referencing the memory allocated on the device

cl_mem clCreateBuffer (cl_context context, // The context where the memory will be allocated

cl_mem_flags flags,size_t size, // The size in bytesvoid *host_ptr,cl_int *errcode_ret)
复制代码

 

flags是逐位的,选项如下:

CL_MEM_READ_WRITE

CL_MEM_WRITE_ONLY

CL_MEM_READ_ONLY

CL_MEM_USE_HOST_PTR

CL_MEM_ALLOC_HOST_PTR

CL_MEM_COPY_HOST_PTR – 从 host_ptr处拷贝数据

我们通过下面的代码使用这个函数:

复制代码
const int mem_size = sizeof(float)*size;// Allocates a buffer of size mem_size and copies mem_size bytes from src_a_h

cl_mem src_a_d = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, mem_size, src_a_h, &error);cl_mem src_b_d = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, mem_size, src_b_h, &error);cl_mem res_d = clCreateBuffer(context, CL_MEM_WRITE_ONLY, mem_size, NULL, &error);
复制代码

 

程序和kernel

到现在为止,你可能会问自己一些问题,比如:我们怎么调用kernel?编译器怎么知道如何将代码放到设备上?我们怎么编译kernel?

下面是我们在对比OpenCL程序和OpenCL kernel时的一些容易混乱的概念:

Kernel:你应该已经知道了,像在上文中描述的一样,kernel本质上是一个我们可以从主机上调用的,运行在设备上的函数。你或许不知道kernel是在运行的时候编译的!更一般的讲,所有运行在设备上的代码,包括kernel和kernel调用的其他的函数,都是在运行的时候编译的。这涉及到下一个概念,Program。

Program:OpenCL Program由kernel函数、其他函数和声明组成。它通过cl_program表示。当创建一个program时,你必须指定它是由哪些文件组成的,然后编译它。

你需要用到下面的函数来建立一个Program:

复制代码
// Returns the OpenCL program

cl_program clCreateProgramWithSource (cl_context context,cl_uint count, // number of filesconst char **strings, // array of strings, each one is a fileconst size_t *lengths, // array specifying the file lengths
cl_int *errcode_ret) // error code to be returned
复制代码

 

当我们创建了Program我们可以用下面的函数执行编译操作:

复制代码
cl_int clBuildProgram (cl_program program,cl_uint num_devices,const cl_device_id *device_list,const char *options, // Compiler options, see the specifications for more detailsvoid (*pfn_notify)(cl_program, void *user_data),void *user_data)
复制代码

 

查看编译log,必须使用下面的函数:

复制代码
cl_int clGetProgramBuildInfo (cl_program program,cl_device_id device,cl_program_build_info param_name, // The parameter we want to know
size_t param_value_size,void *param_value, // The answer
size_t *param_value_size_ret)
复制代码

 

最后,我们需要“提取”program的入口点。使用cl_kernel:

cl_kernel clCreateKernel (cl_program program, // The program where the kernel isconst char *kernel_name, // The name of the kernel, i.e. the name of the kernel function as it's declared in the code

cl_int *errcode_ret)

 

注意我们可以创建多个OpenCL program,每个program可以拥有多个kernel。

以下是这一章节的代码:

复制代码
// Creates the program
// Uses NVIDIA helper functions to get the code string and it's size (in bytes)
size_t src_size = 0;
const char* path = shrFindFilePath("vector_add_gpu.cl", NULL);
const char* source = oclLoadProgSource(path, "", &src_size);
cl_program program = clCreateProgramWithSource(context, 1, &source, &src_size, &error);
assert(error == CL_SUCCESS);// Builds the program
error = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
assert(error == CL_SUCCESS);// Shows the log
char* build_log;
size_t log_size;
// First call to know the proper size
clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);
build_log = new char[log_size+1];
// Second call to get the log
clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, build_log, NULL);
build_log[log_size] = '\0';
cout << build_log << endl;
delete[] build_log;// Extracting the kernel
cl_kernel vector_add_kernel = clCreateKernel(program, "vector_add_gpu", &error);
assert(error == CL_SUCCESS);
复制代码

 

运行kernel

一旦我们的kernel建立好,我们就可以运行它。

首先,我们必须设置kernel的参数:

复制代码
cl_int clSetKernelArg (cl_kernel kernel, // Which kernel
cl_uint arg_index, // Which argument
size_t arg_size, // Size of the next argument (not of the value pointed by it!)const void *arg_value) // Value
复制代码

 

每个参数都需要调用一次这个函数。

当所有参数设置完毕,我们就可以调用这个kernel:

复制代码
cl_int  clEnqueueNDRangeKernel (cl_command_queue command_queue, cl_kernel kernel, cl_uint  work_dim,    // Choose if we are using 1D, 2D or 3D work-items and work-groupsconst size_t *global_work_offset,const size_t *global_work_size,   // The total number of work-items (must have work_dim dimensions)const size_t *local_work_size,     // The number of work-items per work-group (must have work_dim dimensions)
                            cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)
复制代码

 

下面是这一章节的代码:

复制代码
// Enqueuing parameters
// Note that we inform the size of the cl_mem object, not the size of the memory pointed by it
error = clSetKernelArg(vector_add_k, 0, sizeof(cl_mem), &src_a_d);
error |= clSetKernelArg(vector_add_k, 1, sizeof(cl_mem), &src_b_d);
error |= clSetKernelArg(vector_add_k, 2, sizeof(cl_mem), &res_d);
error |= clSetKernelArg(vector_add_k, 3, sizeof(size_t), &size);
assert(error == CL_SUCCESS);// Launching kernel
const size_t local_ws = 512;    // Number of work-items per work-group
// shrRoundUp returns the smallest multiple of local_ws bigger than size
const size_t global_ws = shrRoundUp(local_ws, size);    // Total number of work-items
error = clEnqueueNDRangeKernel(queue, vector_add_k, 1, NULL, &global_ws, &local_ws, 0, NULL, NULL);
assert(error == CL_SUCCESS);
复制代码

 

读取结果

读取结果非常简单。与之前讲到的写入内存(设备内存)的操作相似,现在我们需要存入队列一个读取缓冲区的操作:

复制代码
cl_int  clEnqueueReadBuffer (cl_command_queue command_queue, cl_mem buffer,   // from which buffercl_bool blocking_read,   // whether is a blocking or non-blocking readsize_t offset,   // offset from the beginningsize_t cb,   // size to be read (in bytes)void *ptr,   // pointer to the host memory
                      cl_uint num_events_in_wait_list,const cl_event *event_wait_list, cl_event *event)
复制代码

 

使用方法如下:

// Reading back
float* check = new float[size];
clEnqueueReadBuffer(queue, res_d, CL_TRUE, 0, mem_size, check, 0, NULL, NULL);

 

清理

作为一名牛X的程序员我们肯定要考虑如何清理内存!

你需要知道最基本东西:使用clCreate申请的(缓冲区、kernel、队列)必须使用clRelease释放。

代码如下:

复制代码
// Cleaning up

delete[] src_a_h;delete[] src_b_h;delete[] res_h;delete[] check;clReleaseKernel(vector_add_k);clReleaseCommandQueue(queue);clReleaseContext(context);clReleaseMemObject(src_a_d);clReleaseMemObject(src_b_d);clReleaseMemObject(res_d);
复制代码

 

这是文章的全部内容了,码农们,作者最后说,如果你有任何问题,都可以马上联系他。

 

译者注:对文章内容有任何疑问或建议可以加opencl cuda新手群 242337476 一起讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/448765.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git使用教程-idea系列中git使用教程

一、新建项目 新建项目后记得复制git仓库的地址。 二、上传项目到git仓库 在你的idea里新建git仓库&#xff0c;这是新建本地仓库&#xff0c;等会会同步到线上git仓库 新建后如果代码不是文件名不是绿色的表示没有加入到git索引中 将需要上传的文件按照下图方式add 添加后&…

分布式开放 消息系统 (RocketMQ) 的原理与实践

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 分布式消息系统作为实现分布式系统可扩展、可伸缩性的关键组件&#xff0c;需要具有高吞吐量、高可用等特点。而谈到消息系统的设计&…

使用 OpenCL.Net 进行 C# GPU 并行编程

在 初探 C# GPU 通用计算技术 中&#xff0c;我使用 Accelerator 编写了一个简单的 GPU 计算程序。也简单看了一些 Brahma 的代码&#xff0c;从它的 SVN 最新代码看&#xff0c;Brahma 要转移到使用 OpenCL.Net 作为底层了&#xff0c;于是也去网上搜索了一下&#xff0c;发现…

模拟真实环境之内网漫游

0x00 前言 目标ip&#xff1a;192.168.31.55&#xff08;模拟外网&#xff09; 目的&#xff1a;通过一个站点渗透至内网&#xff0c;发现并控制内网全部主机 0x01 信息收集 用nmap进行端口探测 浏览站点时查看元素发现该站点是DotNetCMS v2.0 该版本cms存在SQL注入漏洞&#x…

超简单:解析 yml 类型(application.yml)配置文件 、springboot 工程读取 yml 文件中的值

方法三是我觉得最简单的。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 工程结构&#xff1a; 2. 我要读取 application.yml 中属性 &#xff1a;spring.rocketmq.namesrvAddr …

初探 C# GPU 通用计算技术

GPU 的并行计算能力高于 CPU&#xff0c;所以最近也有很多利用 GPU 的项目出现在我们的视野中&#xff0c;在 InfoQ 上看到这篇介绍 Accelerator-V2 的文章&#xff0c;它是微软研究院的研究项目&#xff0c;需要注册后才能下载&#xff0c;感觉作为我接触 GPU 通用运算的第一…

Jenkins 详细安装、构建部署 使用教程

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Jenkins是一个开源软件项目&#xff0c;是基于Java开发的一种持续集成工具&#xff0c;用于监控持续重复的工作&#xff0c;功能包括&…

GPU并行计算版函数图像生成器

前几天技术大牛Vczh同学开发了一个函数图像绘制程序&#xff0c;可以画出方程f(x,y)0的图像。他的原理是用图像上每一点的坐标带入函数f得到针对x和y的两个方程&#xff0c;再用牛顿迭代法求解得到一组点集&#xff0c;然后画到图像上。用他的程序可以画出各种各样令人惊叹的方…

完全平方公式、平方差公式、一个数负次方

1.完全平方公式&#xff1a; 两数和&#xff08;或差&#xff09;的平方&#xff0c;等于它们的平方和&#xff0c;加上&#xff08;或减去&#xff09;它们的积的2倍即完全平方公式 (ab)2a2b22ab 两数和的完全平方公式&#xff08;完全平方和&#xff09; 与(a-b)2a2b2-2ab …

java 复制文件

2019独角兽企业重金招聘Python工程师标准>>> public class copyFIle { public static void main(String[] args) throws IOException { File source new File("d:/test/1.xml");File des new File("d:/test/ma.txt");InputStream input null;…

Docker最全教程之MySQL容器化 (二十四)

Docker最全教程之MySQL容器化 &#xff08;二十四&#xff09; 原文:Docker最全教程之MySQL容器化 &#xff08;二十四&#xff09;前言 MySQL是目前最流行的开源的关系型数据库&#xff0c;MySQL的容器化之前有朋友投稿并且写过此块&#xff0c;本篇仅从笔者角…

CODING 受邀参加《腾讯全球数字生态大会》

近日&#xff0c;腾讯全年最重要的一场活动——《腾讯全球数字生态大会》于昆明滇池国际会展中心正式举办。此次全球数字生态大会是腾讯战略升级后&#xff0c;整合互联网数字经济峰会、云未来峰会、腾讯全球合作伙伴三大行业大会&#xff0c;全新升级打造的行业创新大会。大会…

第二章:08运算符[比较运算符]

①比较运算符②实例/* 运算符之三&#xff1a;比较运算符 ! > < > < instanceof 结论&#xff1a; 1.比较运算符的结果是boolean类型 2.区分 和 */ class CompareTest { public static void main(String[] args) { int i 10; int j 20; System.out.pr…

Apache版本兼容性问题

Apache 版本2.2.31 版本对于谷歌浏览器不兼容、IE8版本可以正常使用 当使用了Apache 高版本的话就解决了 出现以下现象 转载于:https://www.cnblogs.com/tzhyy/p/10931084.html

米尔电子Zynq UltraScale MPSoC核心板资料介绍

米尔Zynq UltraScale MPSoC核心板&#xff08;MYC-CZU3EG&#xff09;是采用Xilinx新一代Zynq处理器。该核心板是业界最小尺寸Zynq UltraScale 核心板&#xff0c;采用16纳米制程&#xff0c;相比Znyq7000系列每瓦性能提升5倍&#xff0c;且单芯片融合4核心Cortex-A53&#xff…

解决:If the number of processors is expected to increase from one, then you should configure the numbe

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 按官方说明用 docker 部署 rocketmq ,启动 broker 报错&#xff1a; OpenJDK 64-Bit Server VM warning: If the number of processo…

Docker 方式安装部署 rocketMQ 、部署 图形化界面控制台、rocketMQ 控制台

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 直接上官网&#xff0c;找到工程&#xff0c;clone 到本地&#xff0c;地址&#xff1a;https://github.com/apache/rocketmq-extern…

【随想】每日两题Day.22

题目&#xff1a;102. 二叉树的层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[…

实体类(VO,DO,DTO)的划分

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 从领域建模中的实体划分、项目中的实际应用情况两个角度&#xff0c;对这几个概念进行简析。 得出的主要结论是&#xff1a;在项目应用…

IIS新建站点服务器,localhost能登录但是IP访问登录不了。

IIS服务器新建站点之后&#xff0c;浏览页面&#xff0c;服务器本地是可以登录&#xff0c;但是localhost换成IP就无法访问。其他站点IP却可以访问。 1.如果浏览直接失败&#xff0c;说明端口号需要更换。 2.如果出现IP不能访问&#xff0c;localhost能访问&#xff0c;需要在高…