不可能使用同一个内核对象来并行执行同一个内核的两个实例。
要并行执行同一个内核的多个实例,需要从同一个程序对象创建多个内核对象,并将其排入不同的命令队列。
即使主机代码是并行化的,两个 CPU 线程保留相同的内核对象也是没有用的。那么“clRetainKernel”API 的用途是什么?
不可能使用同一个内核对象来并行执行同一个内核的两个实例。
要并行执行同一个内核的多个实例,需要从同一个程序对象创建多个内核对象,并将其排入不同的命令队列。
即使主机代码是并行化的,两个 CPU 线程保留相同的内核对象也是没有用的。那么“clRetainKernel”API 的用途是什么?
那么“clRetainKernel”API 的用途是什么?
来自源https://www.khronos.org/registry/OpenCL/specs/opencl-1.2.pdf
第 18 页:
引用计数:OpenCL 对象的生命周期由其引用计数决定——对对象的引用数量的内部计数。在 OpenCL 中创建对象时,其引用计数设置为 1。对适当的retainAPI(例如clRetainContext、clRetainCommandQueue)的后续调用会增加引用计数。调用适当的 releaseAPI(例如 clReleaseContext、clReleaseCommandQueue)会减少引用计数。在引用计数达到零后,OpenCL 将释放对象的资源。
它增加其相关 opencl 对象的内部计数器,并可在某些RAII块之外使用。我没有使用它,因为 RAII 已经足够了。但是,如果有一个“共享”问题,这个保留会有所帮助,在它的范围之外使用它。因此,如果他们共享超出其范围的任何内容(尤其是使用 C api 代替时),每个人都应该做自己的保留和释放部分。在 C++ 绑定中,https://github.khronos.org/OpenCL-CLHPP/cl2_8hpp_source.html#l05668你可以看到构造函数
explicit Kernel(const cl_kernel& kernel, bool retainObject = false) : ...
确实取得所有权而不是增加引用计数器。(保留=假)。然后,在几行代码之后,
(带保留)
2447 // We must retain things we obtain from the API to avoid releasing
2448 // API-owned objects.
2449 if (devices) {
2450 devices->resize(ids.size());
2451
2452 // Assign to param, constructing with retain behaviour
2453 // to correctly capture each underlying CL object
2454 for (size_type i = 0; i < ids.size(); i++) {
2455 (*devices)[i] = Device(ids[i], true); // true: retain
2456 }
2457 }
(无保留)
6457 kernels->resize(value.size());
6458
6459 // Assign to param, constructing with retain behaviour
6460 // to correctly capture each underlying CL object
6461 for (size_type i = 0; i < value.size(); i++) {
6462 // We do not need to retain because this kernel is being created
6463 // by the runtime
6464 (*kernels)[i] = Kernel(value[i], false); // false: no retain
6465 }
6466 }
清楚地说,“如果你创造了它,你就不需要保留它”。
如果它是 API 拥有的东西,它会在里面释放,所以,如果你需要使用它,那就保留。如果你创造了一些东西,你只是创造和释放。
不可能使用同一个内核对象来并行执行同一个内核的两个实例。
不,如果您在每次 nd-range 启动时使用不同的偏移量,这是可能的。
cl_event evt;
clEnqueueWriteBuffer(queue,buffer,CL_FALSE,0,100,myCharArray.data(),0,NULL,&evt);
size_t global_work_size = 50;
clEnqueueNDRangeKernel(queue,kernel,1,NULL,&global_work_size,NULL,0, NULL, NULL);
size_t global_work_size_2 = 50;
size_t global_offset_2 = 50;
cl_event evt2; clEnqueueNDRangeKernel(queue2,kernel,1,&global_offset_2,&global_work_size_2,NULL,1, &evt, &evt2);
clEnqueueReadBuffer(queue,buffer,CL_FALSE,0,100,myCharArray.data(),1,&evt2,NULL);
clFlush(queue);
clFlush(queue2);
clFinish(queue2);
clFinish(queue);
确保队列之间存在事件同步,以便能够在内核中看到数据的“最新位”,但在执行时具有不同的偏移量。
第二个队列与第一个的数据复制命令(evt 参数)同步。复制数据后,它的事件向另一个队列 (queue2) 发出信号,以便它现在可以计算。但是在第一个队列上,同步是隐式的,因此在没有事件的数据复制入队之后立即将计算入队是可以的,因为这里使用的队列是有序队列。queue2 完成计算后,向 readBuffer 发出信号(通过 evt2);
这是来自单个 GPU 样本,对于多 GPU,您还需要复制数据。
即使主机代码被并行化,两个 CPU 线程也没有用
如果同步是通过事件轮询自旋等待循环完成的,它会完全占用它的线程。如果您有多个具有相同自旋等待循环的命令队列,则需要这两个线程;您也可以在同一个循环中一个接一个地轮询两个事件,但这需要您在动态更改命令队列数量的情况下处理事件。使用每线程轮询,可以更轻松地管理代码行的可伸缩性。
要并行执行同一个内核的多个实例,需要创建多个内核对象
如果内核要同时在多个 GPU 上使用,或者在同一个 GPU 但具有不同的缓冲区,则必须有不同的内核对象。因为设置内核参数不是入队操作。它在完成时返回,并且不应该在内核运行时完成,并且您无法知道内核运行的确切时间而在完成后没有得到事件。但是您可以在内核执行之前添加一个锁存器,并在那里进行回调以及时设置参数。这一定很慢,因此拥有多个对象既更快又更简单。