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

Performance 如何为CUDA内核选择网格和块尺寸?,performance,optimization,cuda,gpu,nvidia,Performance,Optimization,Cuda,Gpu,Nvidia,这是一个关于如何确定CUDA网格、块和线程大小的问题。这是一个附加的问题张贴 在这个链接之后,Talonmes的答案包含一个代码片段(见下文)。我不理解“值通常由调优和硬件约束选择”这句话 我还没有在CUDA文档中找到一个很好的解释或澄清来解释这一点。总之,我的问题是,在给定以下代码的情况下,如何确定最佳的块大小(线程数): const int n = 128 * 1024; int blocksize = 512; // value usually chosen by tuning and h

这是一个关于如何确定CUDA网格、块和线程大小的问题。这是一个附加的问题张贴

在这个链接之后,Talonmes的答案包含一个代码片段(见下文)。我不理解“值通常由调优和硬件约束选择”这句话

我还没有在CUDA文档中找到一个很好的解释或澄清来解释这一点。总之,我的问题是,在给定以下代码的情况下,如何确定最佳的
块大小(线程数):

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);
const int n=128*1024;
int blocksize=512;//值通常由调整和硬件约束选择
int nblocks=n/nthreads;//值由块大小和总工时确定
maddmAdd(A,B,C,n);

通常选择区块大小以最大化“占用率”。有关更多信息,请搜索CUDA入住率。具体请参见CUDA入住率计算器电子表格。

答案有两部分(我写的)。一部分很容易量化,另一部分更经验性

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

  • 每个块的线程总数不能超过512/1024(分别为1.x或2.x及更高版本)
  • 每个块的最大尺寸限制为 [512512,64]/[10241024,64](计算1.x/2.x或更高版本)
  • 每个块消耗的寄存器总数不能超过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)
  • 每个块不能消耗超过16kb/48kb/96kb的共享内存(计算) 1.x/2.x-6.2/7.0)
  • 如果您保持在这些限制范围内,您可以成功编译的任何内核都将毫无错误地启动

    性能调整:
    这是实证部分。在上面概述的硬件约束内选择的每个块的线程数可以并且确实影响硬件上运行的代码的性能。每个代码的行为方式都会有所不同,而量化它的唯一真正方法是通过仔细的基准测试和评测。但再一次,非常粗略地总结:


  • 每个块的线程数应该是扭曲大小的整数倍,在所有当前硬件上都是32
  • GPU上的每个流式多处理器单元必须具有足够的活动扭曲,以充分隐藏体系结构的所有不同内存和指令管道延迟,并实现最大吞吐量。这里的传统方法是尝试实现最佳硬件占用率(所指的)
  • 第二点是一个巨大的话题,我怀疑是否有人会试图用一个单一的答案来涵盖它。有人写博士论文围绕问题的各个方面进行定量分析(由加州大学伯克利分校的Vasily Volkov和多伦多大学的Henry Wong来举例说明问题到底有多复杂)。
    在入门级,您应该知道,您选择的块大小(在上述约束定义的合法块大小范围内)可能并且确实会影响代码的运行速度,但这取决于您拥有的硬件和正在运行的代码。通过基准测试,您可能会发现大多数非平凡的代码在每个块128-512个线程的范围内都有一个“最佳点”,但这需要您进行一些分析才能找到它的位置。好消息是,由于您使用的是warp大小的倍数,因此搜索空间非常有限,并且给定代码段的最佳配置相对容易找到。

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

    其中一个有用的函数是
    cudaOccupancyMapPotentialBlockSize
    ,它试探性地计算实现最大占用率的块大小。该功能提供的值可作为手动优化发射参数的起点。下面是一个小例子

    #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");
    
    }
    
    参数的含义如下所示

    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,因此可以以与上面示例中运行时API相同的方式在驱动程序API代码中使用。

    我有两个问题。首先,什么时候应该选择网格大小作为minGridSize而不是手动计算的网格大小。其次,您提到“该功能提供的值可以用作手动优化发射参数的起点。”-您的意思是发射参数仍然需要手动优化?是否有关于如何计算2D/3D块尺寸的指导?在我的例子中,我正在寻找二维块尺寸。这是否只是一个计算x和y因子的例子,当它们相乘时会得到原始的块大小?@GrahamDawes可能会感兴趣。“每个块的线程数必须是扭曲大小的整数倍”这不是必须的,但如果不是必须的话,你会浪费资源。我注意到cudaErrorInvalidValue是在内核启动时包含太多块(看起来Compute2.0无法处理10亿块,compute 5.0可以)后由cudaGetLastError返回的,因此这里也有限制。您的Vasili-Volkov链接已断开。我想你喜欢
    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.