OpenCL案例研究之一<矩阵乘法>
2014-02-24 15:00
411 查看
一、 案例简述
本案例讲述使用OpenCL计算矩阵乘法:C = A * B 。设A、B、C分别是大小为N*P、P*M和N*M的矩阵,那么顺序实现的C代码可以如下所示:
// C Function void mat_mul( int Ndim, int Mdim, int Pdim, float* A, float* B, float* C) { int i, j, k; float tmp; for (i = 0; i < Ndim; i++) { for (j = 0; j < Mdim; j++) { tmp = 0.0; for (k = 0; k < Pdim; k++) tmp += A[i*Pdim + k] * B[k*Mdim + j]; C[i*Mdim + j] = tmp; } } }
二、 OpenCL实现矩阵乘法
1. 内核函数实现
// OpenCL Kernel Function __kernel void HelloOpenCL( 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; } }为每个工作项分配一个要计算的乘法矩阵的元素。将针对i,j的外层循环删除,替换为函数调用,查找这两维中对应工作项的全局ID。要特别当心,必须保证得到的工作项ID在矩阵C的范围内。这三个矩阵都留在全局内存中。
2. 宿主机代码实现
下面是在《基于CUDA的OpenCL开发环境搭建与入门程序示例》中main.cpp宿主机代码为基础的补丁文件。测量运行时间的部分:首先,在clCreateCommandQueue()函数中设置CL_QUEUE_PROFILING_ENABLE标志;然后,在clEnqueueNDRangeKernel()函数中设置事件对象;最后,通过clGetEventProfilingInfo()函数获取命令入队时间和命令执行结束时间。注意:时间的单位是纳秒,在最后打印时转换为秒显示。--- /root/Desktop/main.cpp +++ /root/Desktop/main_new.cpp @@ -143,8 +143,10 @@ } // 4. Choose the first device - commandQueue = clCreateCommandQueue(context, - devices[0], 0, NULL); + commandQueue = clCreateCommandQueue(context, + devices[0], + CL_QUEUE_PROFILING_ENABLE, + NULL); if (commandQueue == NULL) { perror("Failed to create commandQueue for device 0."); exit(1); @@ -183,14 +185,33 @@ /******** 第四部分 创建内核和内存对象 ********/ - #define ARRAY_SIZE 10 + const int Ndim = 3; + const int Mdim = 4; + const int Pdim = 5; + + int szA = Ndim * Pdim; + int szB = Pdim * Mdim; + int szC = Ndim * Mdim; cl_kernel kernel = 0; cl_mem memObjects[3] = {0, 0, 0}; - float a[ARRAY_SIZE]; - float b[ARRAY_SIZE]; - float result[ARRAY_SIZE]; + 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] = i + 1; + + for (i = 0; i < szB; i++) + B[i] = i + 1; + // 8. Create the kernel kernel = clCreateKernel(program, "HelloOpenCL", NULL); @@ -200,23 +221,18 @@ } // 9. Create memory objects - for (int i = 0; i < ARRAY_SIZE; i++) { - a[i] = (float)i + 1; - b[i] = (float)i + 1; - } - memObjects[0] = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, - sizeof(float) * ARRAY_SIZE, - a, NULL); + sizeof(float) * szA, + A, NULL); memObjects[1] = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, - sizeof(float) * ARRAY_SIZE, - b, NULL); + sizeof(float) * szB, + B, NULL); memObjects[2] = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, - sizeof(float) * ARRAY_SIZE, - result, NULL); + sizeof(float) * szC, + C, NULL); if (memObjects[0] == NULL || memObjects[1] == NULL || memObjects[2] == NULL) { perror("Error in clCreateBuffer.\n"); @@ -225,48 +241,98 @@ /******** 第五部分 执行内核 ********/ - size_t globalWorkSize[1] = { ARRAY_SIZE }; - size_t localWorkSize[1] = { 1 }; // 10. Set the kernel arguments - errNum = clSetKernelArg(kernel, 0, sizeof(cl_mem), &memObjects[0]); - errNum |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &memObjects[1]); - errNum |= clSetKernelArg(kernel, 2, sizeof(cl_mem), &memObjects[2]); + errNum = clSetKernelArg(kernel, 0, sizeof(int), &Ndim); + errNum |= clSetKernelArg(kernel, 1, sizeof(int), &Mdim); + errNum |= clSetKernelArg(kernel, 2, sizeof(int), &Pdim); + errNum |= clSetKernelArg(kernel, 3, sizeof(cl_mem), &memObjects[0]); + errNum |= clSetKernelArg(kernel, 4, sizeof(cl_mem), &memObjects[1]); + errNum |= clSetKernelArg(kernel, 5, sizeof(cl_mem), &memObjects[2]); if (errNum != CL_SUCCESS) { perror("Error in clSetKernelArg.\n"); exit(1); } // 11. Queue the kernel up for execution across the array - errNum = clEnqueueNDRangeKernel(commandQueue, kernel, 1, NULL, - globalWorkSize, localWorkSize, - 0, NULL, NULL); + 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; + + errNum = clEnqueueNDRangeKernel(commandQueue, kernel, 2, NULL, + global, NULL, 0, NULL, &prof_event); if (errNum != CL_SUCCESS) { perror("Error in clEnqueueNDRangeKernel.\n"); exit(1); } + clFinish(commandQueue); + errNum = clWaitForEvents(1, &prof_event); + if (errNum != CL_SUCCESS) { + perror("Error in clWaitForEvents.\n"); + exit(1); + } + + errNum = clGetEventProfilingInfo(prof_event, + CL_PROFILING_COMMAND_QUEUED, + sizeof(cl_ulong), + &ev_start_time, + NULL); + + errNum |= clGetEventProfilingInfo(prof_event, + CL_PROFILING_COMMAND_END, + sizeof(cl_ulong), + &ev_end_time, + NULL); + + if (errNum != CL_SUCCESS) { + perror("Error in clGetEventProfilingInfo.\n"); + while(1); + exit(1); + } + // 12. Read the output buffer back to the Host errNum = clEnqueueReadBuffer(commandQueue, memObjects[2], CL_TRUE, 0, - ARRAY_SIZE * sizeof(float), result, + sizeof(float) * szC, C, 0, NULL, NULL); if (errNum != CL_SUCCESS) { perror("Error in clEnqueueReadBuffer.\n"); exit(1); } + rum_time = (double)(ev_end_time - ev_start_time); + /******** 第六部分 测试结果 ********/ - printf("\nTest: a * b = c\n\n"); - - printf("Input numbers:\n"); - for (int i = 0; i < ARRAY_SIZE; i++) - printf("a[%d] = %f, b[%d] = %f\n", i, a[i], i, b[i]); - - printf("\nOutput numbers:\n"); - for (int i = 0; i < ARRAY_SIZE; i++) - printf("a[%d] * b[%d] = %f\n", i, i, result[i]); + + 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"); + } + + printf("\n\nRunning Time: %f s\n", rum_time*1.0e-9); while(1);
3. 运行结果
(1). N = 3,M = 4,P = 5。(2).
N = 1000,M = 1000,P = 1000。
三、 代码优化 <工作项分组和减少数据移动>
矩阵乘法的核心是一个乘加计算。大多数处理器中的ALU都有足够的带宽,可以保证这个计算可以接近峰值性能运行,不过只有隐藏数据移动时的开销时才能做到这一点。因此,优化矩阵运算的根本就是尽量减少数据移动。上面的代码中的矩阵乘法内核,三个矩阵都保留在全局内存中。这意味着每次乘法都要通过内存层次结构反复地传递行和列(全局内存到私有内存)。1. 第一次优化
在此优化版本中,每个工作项计算矩阵中的一行。NDRange从一个2Drange(分区)集(匹配矩阵C的维度)变为一个1D range(分区)集(匹配矩阵C的行数)。如下内核代码中,每个工作项管理C中一整行的更新,不过在完成这个更新之前,要把矩阵A中相关的行从全局内存复制到私有内存。注:优化代码以(N = 1000,M = 1000,P = 1000)为例。
// OpenCL Kernel Function __kernel void HelloOpenCL( 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 j, k; float tmp; float Awrk[1000]; if (i < Ndim) { for (k = 0; k < Pdim; k++) Awrk[k] = A[i*Pdim + k]; for (j = 0; j < Mdim; j++) { tmp = 0.0; for (k = 0; k < Pdim; k++) tmp += Awrk[k] * B[k*Mdim + j]; C[i*Mdim + j] = tmp; } } }在宿主机代码中,只需将clEnqueueNDRangeKernel()函数中的维度数由"2"改为"1"。
errNum = clEnqueueNDRangeKernel(commandQueue, kernel, 1, NULL, global, NULL, 0, NULL, &prof_event);
最终运行结果: 0.295128 秒,是上面代码中0.692887秒的42.6% !
本次优化通过增加内核工作量,减少数据移动来提高运行速度。其实如果是在GPU中而不是CPU中,单独使用上述方式增加内核工作量,运行时间反而变为原来几倍。
2. 第二次优化
首先获取平台上的设备数及其最大计算单元数,使用以下代码。//OpenCL设备信息 cl_uint numDevices; cl_device_id deviceIds[1]; size_t maxComputeUnits; errNum = clGetDeviceIDs (platformIds[0], CL_DEVICE_TYPE_GPU, 0, NULL, &numDevices); if ((errNum != CL_SUCCESS) || (numDevices < 1)) { perror("Error in clGetDeviceIDs or no GPU deivce."); exit(1); } errNum = clGetDeviceIDs (platformIds[0], CL_DEVICE_TYPE_GPU, 1, &deviceIds[0], NULL); if ((errNum != CL_SUCCESS) || (numDevices < 1)) { perror("Error in clGetDeviceIDs."); exit(1); } errNum = clGetDeviceInfo(deviceIds[0], CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(cl_uint), &maxComputeUnits, NULL); printf("numDevices = %d, maxComputeUnits = %d\n", numDevices, maxComputeUnits);
运行结果: 平台中由一个设备,最大计算单元数为7。在此,我们划分为4个工作组,每个工作组大小设置为250。具体代码如下:
size_t global[1]; size_t local[1]; global[0] = (size_t)Ndim; local[0] = (size_t)250; errNum = clEnqueueNDRangeKernel(commandQueue, kernel, 1, NULL, global, local, 0, NULL, &prof_event);最终运行时间为: 0.208867 秒,约为原有基础上的2/3 !
4. 附录:关于二维数据的工作组分组
在宿主机代码中,根据clGetDeviceInfo()函数查找CL_DEVICE_MAX_WORK_GROUP_SIZE标志所对应工作组内最多元素个数,在我的电脑上,为1024。工作组的大小通常是64的倍数,最好不超过256。localx,localy也有要求,根据我的试验,必须是4的倍数。size_t local[2]; local[0] = 8; local[1] = 128; errNum = clEnqueueNDRangeKernel(commandQueue, kernel, 2, NULL, global, local, 0, NULL, &prof_event);由于globalx与globaly必须恰好整除localx与localy,因此通过下述写法确保在图像宽与高不能整除对应localx和localy值时,能够满足条件。
global[0] = (nWidth + local[0] - 1) / local[0]; global[1] = (nHeight + local[1] - 1) / local[1]; global[0] *= local[0]; global[1] *= local[1];为此,在相应内核代码中,必须判断所计算出的globalx与globaly的值是否还在图像范围之内。如下:
if((x < nWidth) && (y < nHeight)) { ... }
相关文章推荐
- (noip 模拟 Matrix)<逃避矩阵乘法的好方法#滑稽>
- 【DP】poj1651 <矩阵链乘法>
- 51nod oj 1137 1242 <结构体内重载运算符求矩阵乘法>
- 数据结构之(动态规划二)之<矩阵链乘法>
- 利用CUDA的矩阵乘法1 <利用 Kahan's Summation Formula 来提高CUDA 的浮点数运算精确度>
- <Python> Numpy中的矩阵乘法问题
- .net集合类的研究--哈希表(二)--HashSet<T>
- Spring模拟aop静态代理案例<一>
- Java基础知识强化之集合框架笔记57:Map集合之HashMap集合(HashMap<Student,String>)的案例
- 用Eslipse开发Android案例(1)——计算器<part2>
- <脱机手写汉字识别若干关键技术研究>
- leetcode<四> 改变矩阵的形状
- .net集合类的研究--Array,ArrayList,List<T>
- Day03 三个案例 <输出九九乘法表+表格> <去掉字符串两边的空格> <window.opener的应用>
- <PY>今天研究了一下批处理,py拖动生成单文件exe
- android 自定义组件< 七> :对VelocityTracker的研究
- Kafka组件研究<四>----Kafka API使用
- .net集合类的研究--链表—ListDictionary,LinkedList<T>
- 类的索引器<索引器可以像方法一样重载>(一步步集合案例分析)
- Android实战简易教程<十>(画廊组件Gallery实用研究)