指令级并行和线程级并行如何在GPU上运行?

时间:2013-08-05 21:42:07

标签: cuda opencl nvidia

假设我正在尝试对数组大小n进行简单的缩减,比如保持在一个工作单元内...说添加所有元素。一般策略似乎是在每个GPU上生成许多工作项,这会减少树中的项目。天真地看起来似乎采取了log n步骤,但它并不像第一波线程所有这些线程一次性进行,是吗?他们在经线上安排。

for(int offset = get_local_size(0) / 2;
      offset > 0;
      offset >>= 1) {
     if (local_index < offset) {
       float other = scratch[local_index + offset];
       float mine = scratch[local_index];
       scratch[local_index] = (mine < other) ? mine : other;
     }
     barrier(CLK_LOCAL_MEM_FENCE);
   }

因此并行添加了32个项目,然后该线程在屏障处等待。另外32人去,我们在障碍物等待。另外32个,我们在屏障处等待,直到所有线程都完成了必要的n / 2次添加以进入树的最顶层,然后我们绕过循环。凉。

这看起来不错,但也许很复杂?我理解指令级并行是一个大问题,所以为什么不生成一个线程并执行类似

的操作
while(i<array size){
    scratch[0] += scratch[i+16]
    scratch[1] += scratch[i+17]
    scratch[2] += scratch[i+17]
    ...
    i+=16
}
...
int accum = 0;
accum += scratch[0]
accum += scratch[1]
accum += scratch[2]
accum += scratch[3]
...

这样所有的添加都发生在经线内。现在你有一个线程可以让gpu保持忙碌状态。

现在假设指令级并行性不是真的。如下所示,工作大小设置为32(warp数)。

for(int i = get_local_id(0);i += 32;i++){
    scratch[get_local_id(0)] += scratch[i+get_local_id(0)]
}

然后将前32个项目添加到一起。我想这32个线程会一次又一次地停止射击。

如果你不放弃放弃OpenCL的普遍性,那么当你知道每个循环会有多少加法时,为什么还要在树上减少呢?

1 个答案:

答案 0 :(得分:5)

一个线程无法保持GPU忙碌。这与说一个线程可以使8核CPU忙碌大致相同。

为了最大限度地利用计算资源以及可用内存带宽,必须使用整个机器(即可以执行线程的所有可用资源)。

对于大多数较新的GPU,通过使线程代码按顺序具有多个独立指令,您当然可以通过指令级并行性来提高性能。但是你不能把所有这些都扔进一个线程并期望它能提供良好的性能。

按顺序有2条指令时,如下所示:

scratch[0] += scratch[i+16]
scratch[1] += scratch[i+17]

这对ILP有利,因为这两个操作完全相互独立。但是,由于GPU发出内存事务的方式,第一行代码将参与特定的内存事务,第二行代码必然参与不同内存事务。

当我们有一个warp一起工作时,有一行代码如下:

float other = scratch[local_index + offset];

将导致warp的所有成员生成请求,但这些请求将全部合并到单个或两个内存事务中。这就是如何实现全带宽利用率。

尽管大多数现代GPU具有缓存,并且缓存将倾向于弥合这两种方法之间的差距,但它们决不会弥补所有warp成员发出组合请求之间的事务中的巨大差异,与单个warp成员按顺序发出一组请求。

您可能想要了解GPU内存合并。由于您的问题似乎以OpenCL为中心,因此您可能对this document感兴趣。