如何为邻居访问优化OpenCL代码?

时间:2013-07-02 08:10:42

标签: optimization opencl gpgpu memory-access

修改:问题末尾会添加建议的解决方案结果。

我开始使用OpenCL编程,我已经创建了一个天真的问题实现。

理论是:我有一个3D网格元素,其中每个元素都有一堆信息(大约200字节)。每一步,每个元素都访问其邻居信息并累积此信息以准备更新自身。之后,有一个步骤,每个元素使用之前收集的信息更新自己。这个过程是迭代执行的。

我的OpenCL实现是:我创建一个1维的OpenCL缓冲区,用表示元素的结构填充它,它有一个“int neighbors [6]”,其中我将邻居的索引存储在Buffer中。我启动了一个内核,它咨询邻居并将他们的信息累积到这个步骤中没有参考的元素变量中,然后我启动另一个使用这个变量来更新元素的内核。这些内核仅使用__global变量。

示例代码:

typedef struct{
  float4 var1;
  float4 var2;
  float4 nextStepVar1;
  int neighbors[8];
  int var3;
  int nextStepVar2;
  bool var4;
} Element;

__kernel void step1(__global Element *elements, int nelements){
  int id = get_global_id(0);
  if (id >= nelements){
    return;
  }
  Element elem = elements[id];

  for (int i=0; i < 6; ++i){
    if (elem.neighbors[i] != -1){
      //Gather information of the neighbor and accumulate it in elem.nextStepVars
    }
  }
  elements[id] = elem;
}

__kernel void step2(__global Element *elements, int nelements){
  int id = get_global_id(0);
  if (id >= nelements){
    return;
  }
  Element elem = elements[id];

  //update elem variables by using elem.nextStepVariables
  //restart elem.nextStepVariables
}

现在,我的OpenCL实现基本上与我的C ++实现相同。

所以,问题是:你(专家:P)如何解决这个问题? 我已经阅读了有关3D图像的信息,通过将NDRange更改为3D图像来存储信息并更改邻居访问模式。另外,我已经阅读了__本地内存,首先加载工作组中的所有邻域,与屏障同步然后使用它们,以便减少对内存的访问。

您能否给我一些优化过程的提示,就像我描述的那样,如果可能的话,给我一些片段?

感谢名单。

修改Huseyin Tugrul提议的第三次和第五次优化已在代码中。如上所述here,为了使结构行为正常,它们需要满足一些限制,因此值得理解,以避免头痛。

编辑1 :将Huseyin Tugrul性能提出的第七个优化应用从7 fps增加到60 fps。在更一般的实验中,性能增益约为x8。

编辑2 :应用Huseyin Tugrul性能提议的第一个优化增加了大约x1.2。我认为真正的收益更高,但由于另一个尚未解决的瓶颈而隐藏。

编辑3 :应用Huseyin Tugrul提出的第8和第9次优化并没有改变性能,因为缺乏利用这些优化的重要代码,值得在其他内核中尝试虽然。

编辑4 :将不变参数(例如n_elements或workgroupsize)作为#DEFINEs而非内核args传递给内核,如here所述,提高了x3.33左右的性能。正如文档中所解释的那样,这是因为在编译时知道变量时编译器可以进行积极的优化。

编辑5 :应用Huseyin Tugrul提出的第二个优化,但每个邻居使用1位并使用按位运算来检查邻居是否存在(所以,如果邻居和1! = 0,存在顶部邻居,如果邻居&amp; 2!= 0,则存在bot邻居,如果邻居&amp; 4!= 0,存在右邻居等,则性能提高x1.11倍。我认为这主要是因为数据传输减少,因为数据移动是,并一直是我的瓶颈。很快我将尝试摆脱用于向我的结构添加填充的虚拟变量。

编辑6 :通过消除我正在使用的结构,并为每个属性创建单独的缓冲区,我消除了填充变量,节省了空间,并且能够优化全局内存访问和本地内存分配。性能提高了x1.25倍,非常好。值得这样做,尽管编程的复杂性和不可读性:P。

1 个答案:

答案 0 :(得分:15)

根据你的step1和step2,你没有让你的gpu核心努力工作。你内核的复杂性是什么?你的gpu用法是什么?您是否检查过加力燃烧室等监控程序?中档桌面游戏卡可以获得10k个线程,每个线程执行10k次迭代。

由于您只与邻居合作,因此数据大小/计算大小太大而您的内核可能会受到vram bandiwdth的瓶颈。您的主系统RAM可能与您的pci-e带宽一样快,这可能就是问题所在。

1)使用专用缓存可以将线程的实际网格单元转换为最快的私有寄存器。然后邻居进入__local数组,因此比较/计算仅在芯片中完成。

将当前单元格加载到__private

将邻居加载到__local

开始为本地数组

循环

从__local

获取__private的下一个邻居

计算

结束循环

(如果它有很多邻居,&#34之后的行;将邻居加载到__local&#34;可以在另一个通过补丁从主存储器获取的循环中)

你的gpu是什么?不错的是GTX660。每个计算单元应该有64kB可控缓存。 CPU只有1kB的寄存器,不能用于阵列操作。

2)更短的索引可能使用单个字节作为邻居存储的索引而不是int。保存珍贵的L1缓存空间来自&#34; id&#34;提取很重要,以便其他线程可以更多地访问L1缓存!

示例:

 0=neighbour from left
 1=neighbour from right
 2=neighbour from up
 3=neighbour from down
 4=neighbour from front
 5=neighbour from back
 6=neighbour from upper left
 ...
 ...

所以你可以从单个字节而不是4字节int派生邻居索引,这减少了主存储器访问,至少是邻居访问。您的内核将使用其计算能力从上层派生邻居索引,而不是内存功率,因为​​您将从核心寄存器(__ privates)中获取此值。如果您的总网格大小是常量,这很容易,例如只添加1个实际的单元格ID,向id添加256或向id添加256 * 256。

3)最佳对象大小可能使您的struct / cell-object大小为4个字节的倍数。如果您的总对象大小约为200字节,您可以填充它或用一些空字节对其进行扩充,以准确地生成200字节,220字节或256字节。

4)无分支代码编辑:取决于!)使用较少的if语句。使用if语句会使计算速度变慢。您可以使用其他方式,而不是将-1检查为neightbour索引的结尾。因为轻量级核心不具备重量级能力。您可以使用表面缓冲单元来包裹表面,因此计算单元格将始终具有6个邻居,因此您可以摆脱if(elem.neighbors [i]!= -1)。值得一试尤其是GPU。

只计算所有邻居比使用if语句更快。当结果变化不是有效邻居时,只需将结果变化乘以零。我们怎么知道它不是一个有效的邻居呢?通过使用每个单元格6个元素的字节数组(与邻居id数组并行)(无效= 0,有效= 1 - &gt;将结果与此相乘)

if语句位于循环内,计数六次。如果循环中的工作负载相对容易,则循环展开会提供类似的加速。

但是,如果同一个warp中的所有线程都进入相同的if-or-else分支,那么它们就不会失去性能。所以这取决于你的代码是否有所不同。

5)数据元素重新排序你可以将int [8]元素移动到struct的最上层,这样内存访问可能会变得更加让步,因此可以在单个读取中读取较小的元素到较低的一侧-operation。

6)工作组的大小尝试不同的本地工作组大小可以提供2-3倍的性能。从16到512开始给出不同的结果。例如,AMD GPU类似于64的整数倍,而NVIDIA GPU则是32的整数倍.INTEL在8处可以很好地处理任何事情,因为它可以将多个计算单元合并在一起工作在同一个工作组上。

7)变量分离(只有当你无法摆脱if语句时)才能从struct中分离比较元素。这样你就不需要从主内存加载一个完整的结构来比较一个int或一个布尔值。当比较需要时,然后从主内存加载结构(如果你已经有本地mem优化,那么你应该把它放在它之前,所以加载到本地mem只对选定的邻居进行)

这种优化使得最佳情况(无邻居或仅有一个邻居)的速度更快。不影响最坏情况(最大邻居情况)。

8a)魔术使用移位而不是除以2的幂。对模数进行类似操作。放&#34; f&#34;在浮动文字的末尾(1.0f而不是1.0),以避免从double到float的自动转换。

8b)Magic-2 -cl-mad-enable编译器选项可以增加乘法+添加操作速度。

9)延迟隐藏执行配置优化。您需要隐藏内存访问延迟并负责占用。

 Get maximum cycles of latency for instructions and global memory access.
 Then divide memory latency by instruction latency.
 Now you have the ratio of: arithmetic instruction number per memory access to hide latency. 
 If you have to use N instructions to hide mem latency and you have only M  instructions in your code, then you will need N/M warps(wavefronts?) to hide latency because a thread in gpu can do arithmetics while other thread getting things from mem.

10)混合类型计算优化内存访问后,交换或移动一些适用的指令以获得更好的占用率,使用半类型来帮助精度不重要的浮点运算。

11)延迟再次隐藏仅使用算术来尝试内核代码(注释掉所有内存访问并使用0或者你喜欢的方式启动它们)然后尝试仅使用内存访问指令的内核代码(注释)计算/ ifs)

将内核时间与原始内核时间进行比较。哪个更能影响原始时间?专注于......

12)Lane&amp;银行冲突纠正任何LDS通道冲突和全局存储库冲突,因为相同的地址访问可以以连续的方式减慢进程(较新的卡具有减少此功能的广播能力)

13)使用寄存器尝试用私有替换任何独立的本地,因为你的GPU可以使用寄存器提供近10TB / s的吞吐量。

14)不使用寄存器不要使用太多的寄存器,否则它们会溢出到全局内存并减慢进程。

15)职业的简约方法查看本地/私人用途以了解职业。如果你使用更多的本地和私有,则可以在同一计算单元中使用更少的线程并导致更少的占用。较少的资源使用会导致更高的占用机会(如果您有足够的总线程数)

16)聚集分散当邻居是来自内存的随机地址的不同粒子(如nNS NNS)时,它可能难以应用但聚集读取优化可以给出在优化之前的2x-3x速度(需要本地内存优化才能工作),因此它从内存而不是随机读取顺序,并根据需要在本地内存中重新排序以在(分散)到线程之间共享。

17)分而治之以防万一缓冲区太大并在主机和设备之间复制,使得gpu等待空闲,然后将其分成两部分,分别发送,尽快开始计算一到,最后同时发回结果。即使是进程级并行,也可以通过这种方式将gpu推向极限。 GPU的L2缓存对于整个数据来说可能还不够。缓存平铺计算,但隐式完成而不是直接使用本地内存。

18) 来自内存限定符的带宽。当内核需要一些额外的“阅读”时。带宽,您可以在一些尺寸较小且仅用于读取的参数上使用&#39; __ constant&#39;(而不是__global)关键字。如果这些参数太大,那么你仍然可以通过&#39; __ read_only&#39;限定符(在&#39; __全局&#39;限定符之后)。相似的&#39; __ write_only&#39;增加吞吐量但这些主要提供特定于硬件的性能。如果是Amd的HD5000系列,则常数很好。也许GTX660的缓存速度更快,所以__read_only可能会变得更有用(或者Nvidia使用__constant缓存?)。

具有相同缓冲区的三个部分,其中一个作为__global __read_only,一个作为__constant,一个作为__global(如果构建它们不会比读取更多的惩罚和好处)。

使用AMD APP SDK示例测试我的卡,LDS带宽显示2TB / s,而常量为5TB / s(相同的索引而不是线性/随机),主内存为120 GB / s。

另外,请不要忘记尽可能将限制添加到内核参数中。这使编译器可以对它们进行更多优化(如果你没有对它们进行别名化)。

19) 现代硬件超越功能比旧版本更快(如Quake-3快速反向平方根)版本

20) 现在有了Opencl 2.0,可以在内核中生成内核,这样你就可以在2d网格点进一步提高分辨率,并在需要时将其卸载到工作组(比如增加涡量细节)动态流体的边缘

分析器可以为所有这些提供帮助,但是如果每步只进行一次优化,任何FPS指标都可以。

即使基准测试不适用于依赖于体系结构的代码路径,您也可以尝试在计算空间中每行使用多个192个点,因为您的gpu具有多个核心数和基准数,如果它使gpu更多占用并且每秒有更多的gigafloatingpoint操作。

在所有这些选项之后,仍然必须有一些优化空间,但如果它损坏了您的卡或者您的项目的生产时间可行,那就是idk。例如:

21) 查找表当内存带宽余量增加10%但没有计算能力余量时,将这些工作项的10%卸载到LUT版本,以便获得表中的预先计算的值。我没有尝试,但这样的事情应该有效:

  • 8个计算组
  • 2 LUT组
  • 8个计算组
  • 2 LUT组

所以它们均匀分布到飞行中的线程中#34;并利用延迟隐藏的东西。我不确定这是否是一种更好的科学方法。

21) Z顺序模式对于旅行邻居会增加缓存命中率。缓存命中率为其他作业节省了一些全局内存带宽,从而提高了整体性能。但这取决于缓存的大小,数据布局以及其他一些我不记得的事情。

22) 异步邻居遍历

  • iteration-1:加载邻居2 +计算邻居1 +存储邻居0
  • iteration-2:加载邻居3 +计算邻居2 +存储邻居1
  • iteration-3:加载邻居4 +计算邻居3 +存储邻居2

因此,每个循环体都没有任何依赖链并且在GPU处理元素上完全流水线化,OpenCL具有使用工作组的所有核心异步加载/存储全局变量的特殊指令。检查一下:

https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/xhtml/async_work_group_copy.html

也许你甚至可以将计算部分分成两部分,一部分使用transcandental函数,另一部分使用add / multiply,这样加/减操作不会等待一个缓慢的sqrt。如果traveerse至少有几个邻居,这应该隐藏其他迭代背后的一些延迟。