java中使用opencl操作GPU
需要管理GPU资源,使用java编写,选用opencl框架,并且选择org.jocl包(<dependency><groupId>org.jocl</groupId><artifactId>jocl</artifactId><version>2.0.5</version></dependency>)。具体opencl原理此处不涉及,仅记录使用java该如何做基本操作。
最少要以下几步,详细可以参看:https://blog.51cto.com/u_16175492/6735362和https://www.cnblogs.com/jzhoucdc/p/14346418.html
1、获取平台(cl_platform_id)信息
// 定义一个一维、一位的整型数组,用于下一步接收平台数量。因opencl函数调用需要用指针,java中用数组的引用来实现 int numberOfClPlatforms[] = new int[1]; // 第一次调用获取平台数量:num_entries传0、platforms传null,将在numberOfClPlatforms[0]中返回平台数量 CL.clGetPlatformIDs(0, null, numberOfClPlatforms); // 根据平台数量生成平台列表数组,用于下一步接收平台id的清单 cl_platform_id[] clPlaforms = new cl_platform_id[numberOfClPlatforms[0]]; // 再次调用clGetPlatformIDs,获取平台id的列表,platforms中是所有平台的编码,第一个参数num_entries存放参数clPlatforms中最多可以接收的平台数量,clPlatforms用于接收平台列表(可以理解为硬件编码,不会变化),numberOfClPlatforms[0]用于接收平台数量 CL.clGetPlatformIDs(clPlaforms.length, clPlaforms, numberOfClPlatforms); log.info("got {} platforms", numberOfClPlatforms[0]);
2、根据平台信息获取设备(cl_device_id)信息
// 定义一个一维、一位的整型数组,用于下一步接收设备(显卡)数量。因opencl函数调用需要用指针,java中用数组的引用来实现 int[] numberOfClDevices={0}; // 第一次调用获取设备数量:num_entries传0、devices传null,将在numberOfClDevices[0]中返回设备数量 CL.clGetDeviceIDs(clPlaforms[0],CL.CL_DEVICE_TYPE_GPU,0,null,numberOfClDevices); // 根据设备数量初始化设备清单数组,用于下一步接收设备id的清单 cl_device_id[] clDevices = new cl_device_id[numberOfClDevices[0]]; // 再次调用clGetDeviceIDs,获取设备id列表,clDevices中是所有设备的编码,num_entries存放clDevices中最多可以接收的设备数量,clDevices用于接收设备列表(可以理解为硬件编码,不会变化),counts[0]用于接收设备数量 CL.clGetDeviceIDs(clPlaforms[0],CL.CL_DEVICE_TYPE_GPU,numberOfClDevices[0],clDevices,numberOfClDevices); log.info("got {} GPUs", numberOfClDevices[0]);
3、创建设备的上下文(cl_context)
cl_context_properties contextProperties = new cl_context_properties(); contextProperties.addProperty(CL.CL_CONTEXT_PLATFORM, clPlatforms[0]); cl_context clContext = CL.clCreateContext(contextProperties, clDevices.length, clDevices, null, null, null);
4、根据上下文创建队列(cl_command_queue)
cl_command_queue clCommandQueues = CL.clCreateCommandQueueWithProperties(clContexts[i], clDevices[i], null, null);
5、根据上下文准备程序(OpenCL C语言,类C99,cl_program)
// 定义C99语法的内核程序源码 String clSource = "__kernel void runDefault() { for(long a=0;a<10;a++);}\n" + "__kernel void runTillDeath(){while(1);}\n" + "__kernel void matrixMultiple(__global float* A, __global float* B, __global float* C," + " int M, int N, int K)" + "{" + " int row = get_global_id(0);" + " int col = get_global_id(1);" + " if (row < M && col < N) {" + " float sum = 0.0f;" + " for (int k = 0; k < K; ++k) {" + " sum += A[row * K + k] * B[k * N + col];" + " }" + " C[row * N + col] = sum;" + " }" + "}\n"; // 创建程序 cl_program clPrograms[i] = CL.clCreateProgramWithSource(clContexts[i], 1, new String[]{clSource}, null, null); // 编译生成内核可执行代码(很快) CL.clBuildProgram(clPrograms[i], 0, null, null, null, null);
6、根据程序创建内核(cl_kernel)
// 用指定内核函数创建内核,需要单独定义每个出入参,此处以矩阵乘法为例 /** * 矩阵乘法:第一个矩阵的列数与第二个矩阵的行数相等才能进行合法的乘法运算,即 A 的维度是 MxK,B 的维度是 KxN,操作如下:A (M x K) * B (K x N) = C (M x N) */ // 第一个矩阵(通常记为 A)的行数,也是结果矩阵(记为 C)的行数 int M = 8000; // 第二个矩阵(通常记为 B)的列数,也是结果矩阵(记为 C)的列数 int N = 8000; // 第二个矩阵(B)的行数,同时也是第一个矩阵(A)的列数 int K = 8000; // 创建矩阵乘法所需出入参的内存对象,包括放置2个输入矩阵以及1个输出结果矩阵数据缓存 cl_mem inBufferA = CL.clCreateBuffer(clContext, CL.CL_MEM_READ_ONLY | CL.CL_MEM_COPY_HOST_PTR, Sizeof.cl_float * M * K, Pointer.to(A), null); cl_mem inBufferB = CL.clCreateBuffer(clContext, CL.CL_MEM_READ_ONLY | CL.CL_MEM_COPY_HOST_PTR, Sizeof.cl_float * K * N, Pointer.to(B), null); cl_mem outBufferC = CL.clCreateBuffer(clContext, CL.CL_MEM_WRITE_ONLY | CL.CL_MEM_COPY_HOST_PTR, Sizeof.cl_float * M * N, Pointer.to(C), null); // 创建内核 clKernel = CL.clCreateKernel(clPrograms[i], "matrixMultiple", null); // 设置内核运行所需的参数,共6个,3个矩阵缓存区(cl_mem类型),3个矩阵定义(整形),java传递指针给opencl比较麻烦,需要用到opencl封装出来的几个方法 IntBuffer intValueBufferM = IntBuffer.allocate(1); intValueBufferM.put(M); intValueBufferM.flip(); IntBuffer intValueBufferN = IntBuffer.allocate(1); intValueBufferN.put(N); intValueBufferN.flip(); IntBuffer intValueBufferK = IntBuffer.allocate(1); intValueBufferK.put(K); intValueBufferK.flip(); CL.clSetKernelArg(clKernel, 0, Sizeof.cl_mem, Pointer.to(inBuffersA[devId])); CL.clSetKernelArg(clKernel, 1, Sizeof.cl_mem, Pointer.to(inBuffersB[devId])); CL.clSetKernelArg(clKernel, 2, Sizeof.cl_mem, Pointer.to(outBuffersC[devId])); CL.clSetKernelArg(clKernel, 3, Sizeof.cl_int, Pointer.to(intValueBufferM)); CL.clSetKernelArg(clKernel, 4, Sizeof.cl_int, Pointer.to(intValueBufferN)); CL.clSetKernelArg(clKernel, 5, Sizeof.cl_int, Pointer.to(intValueBufferK));
7、让设备执行内核代码
// 定义输入输出数据 private float A[] = new float[M * K]; private float B[] = new float[K * N]; private float C[] = new float[M * N]; // 定义工作空间 long global_work_size[] = new long[]{M, N}; long local_work_size[] = new long[]{8, 8}; // 假设选择8x8的工作组尺寸 // 指定队列(设备),运行内核 CL.clEnqueueNDRangeKernel(clCommandQueue, clKernel, 1, global_work_size, local_work_size, null, 0, null, null); // 等待内核执行完成。此时线程会被挂起,直到内核运行完成并退出。需要注意的是,设备在执行内核程序过程中,主机线程无法直接干预该过程。主机进程退出时,操作系统会终止设备上的内核执行。 int status = CL.clFinish(clCommandQueue); if (status != CL.CL_SUCCESS) { // 执行错误处理 }
8、等待设备内核执行完(内核一旦开始执行,主机进程无法直接干预该过程)
9、释放内核、程序、队列、上下文对象
CL.clReleaseMemObject(inBufferA)
...
CL.clReleaseKernel(clKernel);
CL.clReleaseDevice(clDevice);
CL.clReleaseCommandQueue(clCommandQueue);
CL.clReleaseProgram(clProgram);
CL.clReleaseContext(clContext);