使用OpenCL在RTX3070上运行exp内核,如何降低20µs固定开销?
使用C++结合OpenCL在NVIDIA GeForce RTX 3070上运行如下简单内核:
__kernel void op_exp_f(global float* vOut) { const uint i = get_global_id(0); vOut[i] = exp(vOut[i]); }
通过clCreateBuffer CL_MEM_READ_WRITE分配GPU内存后,调用以下函数执行内核:
inline void op_exp(cl_mem buffer, size_t n) { auto& instance = gpuInstance(); auto& kernel = instance._kernel_op_exp; clSetKernelArg(kernel, 0, sizeof(cl_mem), &buffer); auto queue = instance.GetCommandQueue(); clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &n, NULL, 0, NULL, NULL); clFinish(queue); }
测试发现:数据量在1~130000之间时,op_exp执行时间稳定在20µs(不含内核构建、内存分配及传输耗时);数据量超过130000后执行时间随数据量线性增长。需要降低这20µs的固定开销。
移除强制同步的
clFinish
当前每次调用op_exp都用clFinish等待队列所有命令完成,这会带来不必要的CPU-GPU同步开销。如果后续操作不需要立即获取计算结果,可移除clFinish,改用事件(cl_event)在需要结果时再同步:inline void op_exp(cl_mem buffer, size_t n, cl_event* out_event = nullptr) { auto& instance = gpuInstance(); auto& kernel = instance._kernel_op_exp; clSetKernelArg(kernel, 0, sizeof(cl_mem), &buffer); auto queue = instance.GetCommandQueue(); // 异步提交内核,返回事件用于后续同步 clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &n, NULL, 0, NULL, out_event); }当需要读取计算结果时,再调用
clWaitForEvents等待事件完成,或者在读取前调用clFinish,避免每次执行都同步。避免重复设置内核参数
如果op_exp调用时复用同一个cl_membuffer,可将clSetKernelArg移到内核初始化阶段(比如gpuInstance的初始化函数中),无需每次执行都设置参数,减少驱动层的参数配置开销。若每次处理不同的buffer,则此优化不适用,可转向其他方案。合并小批量任务
小数据量下,GPU计算耗时远小于内核启动的固定开销。可将多个小批次的exp计算任务合并到一个大的buffer中,一次性提交内核执行,分摊固定启动开销。例如,攒够多个小数据块后,统一拷贝到一个大buffer,调用一次内核处理所有数据。复用命令队列与内核实例
确保全程复用同一个命令队列和内核实例,避免频繁创建/销毁资源带来的额外开销。NVIDIA OpenCL驱动会对持续复用的内核进行调度优化,降低重复启动的延迟。升级NVIDIA驱动版本
新版本NVIDIA驱动通常会优化OpenCL内核的启动调度逻辑,减少固定延迟。确保使用最新的官方驱动,能直接获得底层优化收益。
内容的提问来源于stack exchange,提问作者Joseph Perez

