CUDA会自动为您进行负载平衡吗?

时间:2013-01-02 20:45:48

标签: cuda load-balancing gpgpu

我希望就CUDA C中的负载平衡最佳实践提供一些一般性建议和说明,特别是:

  • 如果warp中的1个线程比其他31个线程更长,那么它会阻止其他31个线程完成吗?
  • 如果是,是否将备用处理能力分配给另一个经线?
  • 为什么我们需要warp 块的概念?在我看来,warp只是一小块32个线程。
  • 一般来说,对于给定内核的调用,我需要负载均衡吗?
    • 每个经线中的线程?
    • 每个区块中的线程?
    • 跨越所有街区的线程?

最后,举一个例子,您将使用哪种负载平衡技术来实现以下功能:

  1. 我有一个x0N的向量:[1, 2, 3, ..., N]
  2. 我随机选择5%的分数和log分(或一些复杂的功能)
  3. 我将结果向量x1(例如[1, log(2), 3, 4, 5, ..., N])写入内存
  4. 我在x1上重复上述2个操作以产生x2(例如[1, log(log(2)), 3, 4, log(5), ..., N]),然后再进行8次迭代以产生x3 ... {{ 1}}
  5. 我返回x10
  6. 非常感谢。

5 个答案:

答案 0 :(得分:6)

线程分为三个级别,这三个级别的计划方式不同。 Warps利用SIMD获得更高的计算密度。线程块利用多线程来实现延迟容忍。网格提供独立的粗粒度工作单元,用于跨SM的负载平衡。

warp中的线程

硬件一起执行warp的32个线程。它可以使用不同的数据执行单个指令的32个实例。如果线程采用不同的控制流,那么它们并非都执行相同的指令,那么这32个执行资源中的一些将在指令执行时空闲。这在CUDA引用中称为控制差异

如果内核表现出很多控制差异,那么在这个级别重新分配工作可能是值得的。这通过使所有执行资源在warp中保持忙碌来平衡工作。您可以在线程之间重新分配工作,如下所示。

// Identify which data should be processed
if (should_do_work(threadIdx.x)) {
  int tmp_index = atomicAdd(&tmp_counter, 1); 
  tmp[tmp_index] = threadIdx.x;
}
__syncthreads();

// Assign that work to the first threads in the block
if (threadIdx.x < tmp_counter) {
  int thread_index = tmp[threadIdx.x];
  do_work(thread_index); // Thread threadIdx.x does work on behalf of thread tmp[threadIdx.x]
}

块中的变形

在SM上,硬件计划会扭曲到执行单元。某些指令需要一段时间才能完成,因此调度程序会对多个warp的执行进行交错,以使执行单元保持忙碌状态。如果某些warp尚未准备好执行,则会跳过它们而不会降低性能。

此级别通常不需要负载平衡。只需确保每个线程块有足够的warp可用,以便调度程序始终可以找到准备执行的warp。

网格中的块

运行时系统将块调度到SM上。可以在SM上同时运行多个块。

此级别通常不需要负载平衡。只需确保有足够的螺纹块可以多次填充所有SM。当一些SM空闲且没有更多线程块准备好执行时,过度配置线程块以最小化内核末端的负载不平衡是很有用的。

答案 1 :(得分:5)

正如其他人已经说过的那样,warp中的线程使用称为单指令,多数据(SIMD)的方案.SIMD意味着硬件中有一个指令解码单元控制多个算术和逻辑单元(ALU)。 CUDA'核心'基本上只是一个浮点ALU,而不是与CPU核心相同意义上的完整核心。虽然CUDA核心到指令解码器的确切比率在不同的CUDA Compute Capability版本之间有所不同,但它们都使用这种方案。由于它们都使用相同的指令解码器,因此线程扭曲中的每个线程将在每个时钟周期执行完全相同的指令。分配给该warp中不遵循当前正在执行的代码路径的线程的内核将在该时钟周期内不执行任何操作。没有办法避免这种情况,因为它是故意的物理硬件限制。因此,如果在warp中有32个线程,并且这32个线程中的每个线程遵循不同的代码路径,那么在该warp中根本没有并行性的加速。它将按顺序执行这32个代码路径中的每一个。这就是为什么warp中的所有线程尽可能遵循相同的代码路径是理想的,因为warp中的并行性只有在多个线程遵循相同的代码路径时才有可能。

以这种方式设计硬件的原因是它节省了芯片空间。由于每个内核都没有自己的指令解码器,因此内核本身占用的芯片空间更少(并且功耗更低。)拥有更小的内核,每个内核使用更少的功率意味着可以将更多内核封装到芯片上。像这样的小内核允许GPU在每个芯片上拥有数百或数千个内核,而CPU只有4或8个,即使在保持类似的芯片尺寸和功耗(和散热)水平时也是如此。与SIMD的权衡是你可以将更多的ALU打包到芯片上并获得更多的并行性,但是当这些ALU都执行相同的代码路径时,你才能获得加速。这种权衡取决于GPU的如此高的程度是因为3D图形处理中涉及的大部分计算都是简单的浮点矩阵乘法。 SIMD非常适合矩阵乘法,因为计算结果矩阵的每个输出值的过程是相同的,只是在不同的数据上。此外,每个输出值可以完全独立于每个其他输出值计算,因此线程根本不需要彼此通信。顺便提一下,类似的模式(通常甚至是矩阵乘法本身)也恰好出现在科学和工程应用中。这就是GPU(GPGPU)上的通用处理诞生的原因。 CUDA(以及GPGPU)基本上是对于已经为游戏行业大规模生产的现有硬件设计如何用于加速其他类型的并行浮点处理应用程序的事后想法。

答案 2 :(得分:4)

  

如果warp中的1个线程比其他31个线程更长,它会阻止其他31个线程完成吗?

是。一旦你在Warp中出现分歧,调度程序需要采取所有不同的分支并逐个处理它们。不在当前执行的分支中的线程的计算容量将丢失。你可以查看CUDA编程指南,它很好地解释了究竟发生了什么。

  

如果是,是否将备用处理能力分配给另一个经线?

不,不幸的是,这完全失去了。

  

为什么我们需要经线和阻塞的概念?在我看来,warp只是一小块32个线程。

因为Warp必须是SIMD(单指令,多数据)以实现最佳性能,所以块内的Warps可能完全不同,但是,它们共享一些其他资源。 (共享记忆,登记等)

  

一般来说,对于给定内核的调用,我需要负载均衡吗?

我不认为负载平衡在这里是正确的词。只要确保你总是有足够的线程被执行,并避免在warp内发散。同样,CUDA编程指南对于类似的东西也是一个很好的阅读。

现在举例:

你可以用m = 0..N * 0.05执行m个线程,每个线程选择一个随机数并将“复杂函数”的结果放在x1 [m]中。 但是,在大面积上随机读取全局内存并不是GPU可以做到的最有效的事情,因此您还应该考虑是否真的需要完全随机。

答案 3 :(得分:2)

其他人为理论问题提供了很好的答案。

对于您的示例,您可以考虑按如下方式重构问题:

  1. xN点的向量[1, 2, 3, ..., N]x
  2. y的每个元素上计算一些复杂的函数,产生y
  3. 随机抽样y0的子集,以生成y10y*
  4. 步骤2仅对每个输入元素进行一次操作,而不考虑是否需要该值。如果步骤3的采样是在没有替换的情况下完成的,那么这意味着您将计算实际需要的元素数量的2倍,但是您将在没有控制差异的情况下计算所有内容,并且所有内存访问都是连贯的。这些通常是GPU上速度比计算本身更重要的驱动因素,但这取决于复杂功能的实际作用。

    步骤3将具有非连贯的内存访问模式,因此您必须决定在GPU上执行此操作是否更好,或者将其传输回CPU并在那里进行采样是否更快。

    根据下一次计算的不同,您可以重构步骤3,而不是在[0,N]中为每个元素随机绘制一个整数。如果值在[N / 2,N]中,则在下一次计算中忽略它。如果它在[0,N / 2]中,则将其值与该虚拟{{1}}数组的累加器相关联(或适用于您的计算的任何内容)。

答案 4 :(得分:1)

你的例子是展示减少的一种非常好的方式。

I have a vector x0 of N points: [1, 2, 3, ..., N]
I randomly pick 50% of the points and log them (or some complicated function) (1)
I write the resulting vector x1 to memory (2)
I repeat the above 2 operations on x1 to yield x2, and then do a further 8 iterations to  yield x3 ... x10 (3)
I return x10 (4)

说| x0 | = 1024,你选择了50%的分数。

第一阶段可能是您必须从全局记忆中读取的唯一阶段,我将告诉您原因。

512个线程从内存(1)读取512个值,它将它们存储到共享内存(2)中,然后对于步骤(3),256个线程将从共享内存中读取随机值并将它们也存储在共享内存中。你这样做直到你最终得到一个线程,它将把它写回全局存储器(4)。

您可以在初始步骤中进一步扩展这一点,其中256个线程读取两个值,或者128个线程读取4个值等等...