如何为CUDA内核选择网格和块尺寸?


112

这是关于如何确定CUDA网格,块和线程大小的问题。这是在此处发布的问题的另一个问题。

在此链接之后,来自爪子的答案包含一个代码段(请参见下文)。我不理解“通常是通过调整和硬件限制选择的值”的注释。

我在CUDA文档中找不到很好的解释或说明来解释这一点。总而言之,我的问题是blocksize给定以下代码,如何确定最佳(线程数):

const int n = 128 * 1024;
int blocksize = 512; // value usually chosen by tuning and hardware constraints
int nblocks = n / nthreads; // value determine by block size and total work
madd<<<nblocks,blocksize>>>mAdd(A,B,C,n);

Answers:


148

这个答案有两部分(我写了)。一部分易于量化,而另一部分则更具经验。

硬件限制:

这是易于量化的部分。当前CUDA编程指南的附录F列出了许多硬性限制,这些硬性限制限制了内核启动每个块可以拥有多少个线程。如果超过以上任何一项,您的内核将永远无法运行。它们可以大致概括为:

  1. 每个块总共不能具有超过512/1024个线程(分别具有1.x或2.x和更高的计算能力
  2. 每个块的最大尺寸限制为[512,512,64] / [1024,1024,64](计算1.x / 2.x或更高版本)
  3. 每个块的总消耗量不能超过8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64k寄存器(计算1.0、1.1 / 1.2、1.3 / 2.x- / 3.0 / 3.2 / 3.5-5.2 / 5.3 / 6-6.1 / 6.2 / 7.0)
  4. 每个块不能占用超过16kb / 48kb / 96kb的共享内存(计算1.x / 2.x-6.2 / 7.0)

如果您在这些限制之内,则可以成功编译的任何内核都将启动而不会出错。

性能调优:

这是经验部分。在上面概述的硬件限制内选择的每个块的线程数可以并且确实会影响在硬件上运行的代码的性能。每种代码的行为方式将有所不同,并且量化它的唯一真实方法是仔细进行基准测试和性能分析。但是,再次非常概括:

  1. 每个块的线程数应为扭曲大小的整数倍,在当前所有硬件上均为32。
  2. GPU上的每个流式多处理器单元必须具有足够的活动扭曲,以充分隐藏架构的所有不同内存和指令流水线延迟,并实现最大吞吐量。这里的正统方法是尝试达到最佳的硬件占用率(Roger Dahl的答案所指)。

第二点是一个巨大的话题,我怀疑有人会尝试将其覆盖在一个StackOverflow答案中。有人在围绕问题的各个方面进行定量分析时撰写博士学位论文(请参阅UC Berkley的Vasily Volkov的本次演讲以及多伦多大学的Henry Hong的本白皮书,以了解问题的真正复杂性示例)。

在入门级,您应该最了解选择的块大小(在上述约束定义的合法块大小范围内)可以而且确实会影响代码的运行速度,但取决于硬件您拥有和正在运行的代码。通过基准测试,您可能会发现大多数非平凡的代码在每个块范围内的128-512个线程中都有一个“甜蜜点”,但是您需要进行一些分析才能找出问题所在。好消息是,因为您要使用的是扭曲大小的倍数,所以搜索空间非常有限,并且相对容易找到给定代码段的最佳配置。


2
“每个块的线程数必须是扭曲大小的整数倍”,这不是必须的,但是如果不是这样,则会浪费资源。我注意到在内核启动的块太多之后,cudaGetLastError返回cudaErrorInvalidValue(看起来,compute 2.0无法处理10亿个块,compute 5.0可以处理十亿个块),因此这里也有限制。
masterxilo

4
您的Vasili Volkov链接已死。我认为您喜欢他的2010年9月:《降低占用率性能更好》一文(当前位于nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf),这里有一个带有代码的bitbucket:bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

上面的答案指出了块大小如何影响性能,并建议了基于占用率最大化的常见启发式选择。在不希望提供选择块大小标准的情况下,值得一提的是CUDA 6.5(现在为Release Candidate版本)包括了几个新的运行时函数,以帮助进行占用率计算和启动配置,请参见

CUDA Pro提示:占用API简化了启动配置

有用的功能之一是cudaOccupancyMaxPotentialBlockSize启发式地计算可达到最大占用率的块大小。然后,该功能提供的值可用作启动参数手动优化的起点。下面是一个小例子。

#include <stdio.h>

/************************/
/* TEST KERNEL FUNCTION */
/************************/
__global__ void MyKernel(int *a, int *b, int *c, int N) 
{ 
    int idx = threadIdx.x + blockIdx.x * blockDim.x; 

    if (idx < N) { c[idx] = a[idx] + b[idx]; } 
} 

/********/
/* MAIN */
/********/
void main() 
{ 
    const int N = 1000000;

    int blockSize;      // The launch configurator returned block size 
    int minGridSize;    // The minimum grid size needed to achieve the maximum occupancy for a full device launch 
    int gridSize;       // The actual grid size needed, based on input size 

    int* h_vec1 = (int*) malloc(N*sizeof(int));
    int* h_vec2 = (int*) malloc(N*sizeof(int));
    int* h_vec3 = (int*) malloc(N*sizeof(int));
    int* h_vec4 = (int*) malloc(N*sizeof(int));

    int* d_vec1; cudaMalloc((void**)&d_vec1, N*sizeof(int));
    int* d_vec2; cudaMalloc((void**)&d_vec2, N*sizeof(int));
    int* d_vec3; cudaMalloc((void**)&d_vec3, N*sizeof(int));

    for (int i=0; i<N; i++) {
        h_vec1[i] = 10;
        h_vec2[i] = 20;
        h_vec4[i] = h_vec1[i] + h_vec2[i];
    }

    cudaMemcpy(d_vec1, h_vec1, N*sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vec2, h_vec2, N*sizeof(int), cudaMemcpyHostToDevice);

    float time;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start, 0);

    cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, MyKernel, 0, N); 

    // Round up according to array size 
    gridSize = (N + blockSize - 1) / blockSize; 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Occupancy calculator elapsed time:  %3.3f ms \n", time);

    cudaEventRecord(start, 0);

    MyKernel<<<gridSize, blockSize>>>(d_vec1, d_vec2, d_vec3, N); 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Kernel elapsed time:  %3.3f ms \n", time);

    printf("Blocksize %i\n", blockSize);

    cudaMemcpy(h_vec3, d_vec3, N*sizeof(int), cudaMemcpyDeviceToHost);

    for (int i=0; i<N; i++) {
        if (h_vec3[i] != h_vec4[i]) { printf("Error at i = %i! Host = %i; Device = %i\n", i, h_vec4[i], h_vec3[i]); return; };
    }

    printf("Test passed\n");

}

编辑

cudaOccupancyMaxPotentialBlockSize被中定义cuda_runtime.h文件,并定义如下:

template<class T>
__inline__ __host__ CUDART_DEVICE cudaError_t cudaOccupancyMaxPotentialBlockSize(
    int    *minGridSize,
    int    *blockSize,
    T       func,
    size_t  dynamicSMemSize = 0,
    int     blockSizeLimit = 0)
{
    return cudaOccupancyMaxPotentialBlockSizeVariableSMem(minGridSize, blockSize, func, __cudaOccupancyB2DHelper(dynamicSMemSize), blockSizeLimit);
}

参数的含义如下

minGridSize     = Suggested min grid size to achieve a full machine launch.
blockSize       = Suggested block size to achieve maximum occupancy.
func            = Kernel function.
dynamicSMemSize = Size of dynamically allocated shared memory. Of course, it is known at runtime before any kernel launch. The size of the statically allocated shared memory is not needed as it is inferred by the properties of func.
blockSizeLimit  = Maximum size for each block. In the case of 1D kernels, it can coincide with the number of input elements.

请注意,从CUDA 6.5开始,需要根据API建议的1D块大小来计算自己的2D / 3D块尺寸。

还要注意,CUDA驱动程序API包含功能上等效的用于占用率计算的API,因此可以按照cuOccupancyMaxPotentialBlockSize上面示例中针对运行时API所示的相同方式在驱动程序API代码中使用。


2
我有两个问题。首先,何时应该选择网格大小作为minGridSize而不是手动计算的gridSize。其次,您提到“该功能提供的值可以用作手动优化启动参数的起点。”-您是否还需要手动优化启动参数?
nurabha 2015年

是否有关于如何计算2D / 3D块尺寸的指南?就我而言,我正在寻找2D块尺寸。当乘以x和y因子给出原始块大小时,是否只是计算x和y因数的一种情况?
格雷厄姆·道斯

1
@GrahamDawes,可能很有趣。
罗伯特·克罗维拉

9

通常选择块大小以最大化“占用率”。在CUDA占用率上搜索以获取更多信息。特别是,请参阅CUDA占用率计算器电子表格。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.