0

不可能使用同一个内核对象来并行执行同一个内核的两个实例。

要并行执行同一个内核的多个实例,需要从同一个程序对象创建多个内核对象,并将其排入不同的命令队列。

即使主机代码是并行化的,两个 CPU 线程保留相同的内核对象也是没有用的。那么“clRetainKernel”API 的用途是什么?

4

1 回答 1

1

那么“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 但具有不同的缓冲区,则必须有不同的内核对象。因为设置内核参数不是入队操作。它在完成时返回,并且不应该在内核运行时完成,并且您无法知道内核运行的确切时间而在完成后没有得到事件。但是您可以在内核执行之前添加一个锁存器,并在那里进行回调以及时设置参数。这一定很慢,因此拥有多个对象既更快又更简单。

于 2019-10-03T19:05:15.267 回答