【问题标题】:OpenCL: loop kernel?OpenCL:循环内核?
【发布时间】:2019-05-26 14:24:20
【问题描述】:

我正在运行一个 OpenCL 内核,它一遍又一遍地处理和重新处理相同的数据集(它是一个迭代物理求解器)。

在我的测试中,调用 clEnqueueNDRangeKernel 需要付出不小的代价。例如,当运行模拟的 1000 个子步骤时(需要对 clEnqueueNDRangeKernel 进行 1000 次相同的调用来处理相同的数据),这些对 clEnqueueNDRangeKernel 的调用似乎实际上成为了瓶颈。我的(伪)代码如下所示:

[create buffers]
[set kernel arguments]

for (int i = 0; i < 1000; i++) //queuing the kernels takes a while
{
    clEnqueueNDRangeKernel(queue, kernel, args...); 
}

clFinish(queue); //waiting for the queue to complete doesn't take much time
[read buffers]

我知道第一次调用 clEnqeueuNDRangeKernel 将初始化任何延迟到 GPU 的缓冲区传输......所以第一次调用可能会产生额外的费用。然而,在我的测试中,10 次迭代的循环比 1000 次迭代要快得多,这让我相信数据传输不是瓶颈。

我还认为 clEnqueueNDRangeKernel 是非阻塞的,因为它在内核完成之前不会阻塞,因此内核的复杂性不应该是瓶颈(在我的情况下,内核在调用 clFinish()) 之前,执行不应阻塞。

但是,当我分析我的代码时,大部分时间都花在处理 for 循环上,在调用 clFinish() 之前......所以看起来内核本身的排队是花费最多时间的在这里。

我的问题:有没有办法告诉 GPU 重新运行先前排队的内核 N 次,而不必手动将内核排队 N 次?在我的情况下,每次迭代都不需要更改或更新内核的参数……只需要重新运行内核即可。重复调用它可以提高效率吗?

【问题讨论】:

  • 您是否尝试过每说 100 次迭代调用 clFlush() 来触发内核处理?
  • @doqtor 刚才试过了...没有明显的速度增加,并且 flush 命令显然有它自己的开销,实际上进一步减慢了速度...
  • 由于您没有传递任何新数据或缓冲区,您是否尝试过在 1 个内核中循环处理而不是启动多个内核?也正如 doqtor 所说,尝试发布 100 个实例。但是这次每 50 个实例放置一个事件,然后不要将任何进一步的内核排入队列。当第 50 个实例完成时,再入队一百个,依此类推。这有助于避免过度填充缓冲区
  • @gallickgunner “在一个循环中在 1 个内核中进行处理”是什么意思?这样做我不会失去并行性吗?我正在处理数百万个元素,因此我需要最大化线程使用率。我尝试每隔几百次调用调用一次 clFinish(而不是如上所述的 clFlush),但再次没有获得性能提升。
  • 我在一个循环中处理 1 个内核的意思是你只是在一个 for 循环或其他东西中进行处理。您也不会以这种方式获得任何并行性。 GPU 一次可以处理 1 个内核,其他的只是排队。分析 1 个内核在循环中进行处理所花费的时间以及您的方法,即,而不是多次循环入队。然后看看哪个更好

标签: c++ performance queue kernel opencl


【解决方案1】:

OpenCL 2.x 支持动态并行性,允许 1 个工作项启动新内核。如果每次内核启动不需要任何 gpu-cpu 数据传输,您可以让 1 个工作项启动 1000 个内核并等待每个工作项完成。使用事件让所有子内核一个接一个地运行。

在 OpenCL 1.2 中,您可以使用原子和循环来执行“运行中线程”内核同步,但这不会比新内核启动 imo 更快,而且它不是一种可移植的同步方式。

如果每个内核启动所花费的时间都比每个内核运行的时间长,那么 GPU 上完成的工作就不够了。您可以简单地在 GPU 上执行 c=a+b,但这还不够快,因为 gpu 管道上的内核调度比执行 c=a+b 需要更多时间。

但是,您仍然可以使用clEnqueueWaitForEvents 和有序命令队列执行以下方法:

thread-0:
    enqueue user event, not triggered
    enqueue 1000 kernels, they don't start yet because of untriggered wait
thread-1:
    nothing

下一个时间步:

thread-0:
    enqueue new user event on a new command queue, not triggered
    enqueue 1000 kernels on new command queue so they don't start yet
thread-1:
    run the old command queue from last timestep by triggering the user event

这样排队和运行至少可以“重叠”。如果你需要更多的 enqueue 来运行重叠率,

thread-0 to thread-N-2:
    enqueue new user event on a new command queue, not triggered
    enqueue 1000 kernels on new command queue so they don't start yet
thread-N-1:
    iterate all command queues
          run currently selected command queue from last timestep  by triggering the user event

现在您的入队速度提高了 N-1 倍,运行它们将仅在 GPU 端调度开销上。如果您的 GPU 端调度开销很大(1M 工作项用于 1M c=a+b 计算),那么您应该为每个工作项做更多的工作。

也许让生产者-消费者风格的内核启动更好,其中 7 个线程产生填充的命令队列等待在他们自己的用户事件上触发,8 个线程通过触发它们来消耗它们。即使他们需要将下载数据上传到 GPU 或从 GPU 上传,这也可以工作。

即使像 HD7870 这样的旧 GPU 同时支持 32 个以上的命令队列(每个 GPU),因此您可以通过高端 CPU 扩展排队性能。

如果 pci-e 桥(提升器的高延迟?)造成瓶颈,那么 OpenCL2.x 动态并行性必须优于 CPU 端生产者-消费者模式。

【讨论】:

  • 我喜欢你的主机线程想法,虽然我有几个关于 enqueue_kernel 的问题,因为我的设备与 OCL2.0 兼容:设备端入队是利用所有工作组,还是只利用排队的工作组线?在设备端排队时,我是否应该注意其他任何陷阱,或者是否相当简单明了?在排队数百万个工作项时进行设备端排队是否可行,还是更适合较小的工作? (换句话说,enqueue_kernel 是否与 clEnqueueNDRangeKernel 非常相似,还是仅适用于特定类型的任务?)
  • 设备端排队可以有一个完整的内核启动。但是当子内核是非常少的工作项时,它们通常是有利的,这样 cpu 可以比入队更快地运行它们。有时设备端排队会增加设备端开销,具体取决于驱动程序的错误,但它们至少没有 CPU 端开销。您可以在从工作项启动它们时使用不同的同步标志,但我会使用事件让它们一个接一个地运行,至少可以确保。设备端排队也需要在主机端进行一些小的编码,用于不同的“主”命令队列。
  • 数百万个工作项应该无关紧要,但由于分配给该主队列的资源有限,数千个内核启动可能会卡在一些有问题的驱动程序上。是的,它是一个你给它自己的数组参数和本地/全局范围的启动。
  • 我的意思是内核事件,就像内核内核启动一样。 khronos.org/registry/OpenCL/sdk/2.0/docs/man/xhtml/…
  • 感谢您提供的额外信息...我一定会尝试一下,看看他们是否可以提高性能!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-04-06
  • 1970-01-01
  • 2022-11-01
  • 1970-01-01
  • 2017-12-19
  • 2011-12-06
  • 1970-01-01
相关资源
最近更新 更多