我的Cuda程序可以显着提升性能(平均而言),具体取决于块的大小和数量。块数;其中"线程的总数"保持原样。 (我不确定线程是否是正确的术语......但是我将在这里使用它;每个内核的总线程数是(块数)*(块大小)) 。我制作了一些图表来说明我的观点。
但首先请允许我解释一下我的算法是什么,然而我不确定它的相关性,因为我认为这适用于所有GPGPU程序。但是也许我错了。
基本上我会遇到逻辑上被视为2D数组的大型数组,其中每个线程从数组中添加一个元素,并将该值的平方添加到另一个变量,然后在最后将值写入另一个数组,在每次读取期间,所有线程都以某种方式移位。这是我的内核代码:
__global__ void MoveoutAndStackCuda(const float* __restrict__ prestackTraces, float* __restrict__ stackTracesOut,
float* __restrict__ powerTracesOut, const int* __restrict__ sampleShift,
const unsigned int samplesPerT, const unsigned int readIns,
const unsigned int readWidth, const unsigned int defaultOffset) {
unsigned int globalId = ((blockIdx.x * blockDim.x) + threadIdx.x); // Global ID of this thread, starting from 0 to total # of threads
unsigned int jobNum = (globalId / readWidth); // Which array within the overall program this thread works on
unsigned int readIndex = (globalId % readWidth) + defaultOffset; // Which sample within the array this thread works on
globalId = (jobNum * samplesPerT) + readIndex; // Incorperate default offset (since default offset will also be the offset of
// index we will be writing to), actual globalID only needed for above two variables.
float stackF = 0.0;
float powerF = 0.0;
for (unsigned int x = 0; x < readIns; x++) {
unsigned int indexRead = x + (jobNum * readIns);
float value = prestackTraces[readIndex + (x * samplesPerT) + sampleShift[indexRead]];
stackF += value;
powerF += (value * value);
}
stackTracesOut[globalId] = stackF;
powerTracesOut[globalId] = powerF;
}
现在为了这篇文章的内容,在调用此代码时
MoveoutAndStackCuda<<<threadGroups, threadsPerGroup>>>(*prestackTracesCudaPtr,
*stackTracesOutCudaPtr, *powerTracesOutCudaPtr,
*sampleShiftCudaPtr, samplesPerT, readIns,
readWidth, defaultOffset);
我所做的只是&lt;&lt;&lt;&gt;&gt;&gt;内的threadGroups和threadsPerGroup不同,其中threadGroups.x * threadsPerGroup.x保持不变。 (如前所述,这是一维问题)。
我将块大小增加64,直到达到1024.我预计没有变化,因为我认为只要块大小大于32,我相信它是核心中的ALU数量,它会运行得那么快尽可能。看看我制作的这张图:
对于此特定大小,线程总数为5000 * 5120,因此,例如,如果块大小为64,则存在((5000 * 5120)/ 64)个块。 出于某种原因,块大小为896,768和512时性能会有显着提升。为什么?
我知道这看起来是随机的,但是这个图中的每个点都是50个测试平均值!
这是另一个图表,这次是线程总数为(8000 * 8192)的时间。这次的提升是768和960。
又一个例子,这次是一个小于其他两个问题的工作(总线程数为2000 * 2048):
事实上,这是我用这些图表制作的相册,每张图表代表问题的不同大小:graph album。
我正在运行这个Quadro M5000,它有2048个Cuda核心。我相信每个Cuda Core都有32个ALU,所以我认为在任何给定时间可能发生的计算总数是(2048 * 32)?
那么是什么解释了这些神奇的数字?我认为它可能是线程的总数除以cuda核心的数量,或者除以(2048 * 32),但到目前为止,我发现没有与我的相册中所有图形的任何内容相关联。是否还有其他测试可以帮助缩小范围?我想找出运行此程序的块大小以获得最佳结果。
此外,我没有包含它,但我也做了一个测试,其中块大小从32减少了1并且事情变得指数级慢。这对我来说很有意义,因为我们每组的局部线程数比给定多处理器中的ALU少。
答案 0 :(得分:5)
基于此声明:
我将块大小增加64,直到达到1024.我预计没有变化,因为我认为只要块大小大于32,我相信它是核心中的ALU数量,它会运行得那么快尽可能。
我想说有一个关于GPU的重要概念你可能不知道:GPU是一个“延迟隐藏”机器。它们主要通过暴露大量可用(并行)工作来隐藏延迟。这可以粗略地概括为“许多线程”。使用GPU时,如果有足够的线程来覆盖“核心”或执行单元的数量,这是一个完全错误的想法,这就足够了。 不是。
作为(初学者)GPU程序员,您应该忽略GPU中的核心数量。你想要很多的线程。在内核级别和每个GPU SM。
通常,当您为每个SM提供更多线程时,GPU在执行其他有用工作时隐藏延迟的能力会增加。这解释了所有图表的一般趋势,即斜率通常从左向右向下(即平均性能增加,通常,因为您为每个SM提供了更多的暴露工作)。
然而,这并未涉及高峰和低谷。 GPU具有大量可能影响性能的架构问题。我不会在这里提供完整的治疗方法。但是我们来看一个案例:
为什么第一个图表中的性能增加到512个线程,然后突然减少到576个线程?
这很可能是占用效果。 GPU中的SM最多可以补充2048个线程。基于前面的讨论,当我们最大化线程补码时,SM将具有隐藏延迟(因此通常提供最大平均性能)的最大能力,最高可达2048。
对于512个线程的块大小,我们可以在SM上准确地匹配这些线程块中的4个,然后它将具有2048个线程的补充,可以从中选择工作和延迟隐藏。
但是当您将线程块大小更改为576时,4 * 576&gt; 2048,所以我们不能再在每个SM上放置4个线程块。这意味着,对于该内核配置,每个SM将以3个线程块运行,即2048个中的1728个线程可能。从SM的角度来看,这实际上是更糟,而不是之前允许2048个线程的情况,因此它可能是性能从512减少到576个线程的指标(就像它增加了一样)从448到512,其中涉及瞬时占用的类似变化。)
由于上述原因,当我们改变每个块的线程时,看到你所展示的性能图表并不罕见。
具有粒度(量化)效果的其他占用限制器可能会在性能图中产生类似的峰值行为。例如,你的问题中没有足够的信息来推测每线程寄存器的使用情况,但占用限制器可能是每个线程使用的寄存器。当您更改线程补码时,您会发现每个SM可能会有类似的块替换,这可能会导致占用率(上下)变化,从而改变性能。
为了进一步深入研究,我建议您花些时间了解各种分析器的占用率,每个线程的寄存器和性能分析功能。有很多关于这些主题的信息;谷歌是你的朋友,并注意上面评论中链接的question/answers作为一个合理的起点。要充分研究占用率及其对性能的影响,需要比您在此处提供的信息更多的信息。它基本上需要MCVE以及确切的编译命令行,以及您运行的平台和CUDA版本。编译器的每线程寄存器使用情况受到所有这些因素的影响,其中大部分都没有提供。