Cuda 通过增加占用率来提高内核性能?

Cuda 通过增加占用率来提高内核性能?,cuda,Cuda,以下是GT 440上我的内核的Compute Visual Profiler的输出: 内核详细信息:网格大小:[100 1],块大小:[256 1] 寄存器比率:0.84375(27648/32768)[每个线程35个寄存器] 共享内存比率:0.336914(16560/49152)[5520字节/秒 块] 每个SM的活动块数:3(每个SM的最大活动块数:8) 每个SM的活动线程数:768(每个SM的最大活动线程数:1536) 潜在入住率:0.5(24/48) 占用限制因素:寄存器 请注意标

以下是GT 440上我的内核的Compute Visual Profiler的输出:

  • 内核详细信息:网格大小:[100 1],块大小:[256 1]
  • 寄存器比率:0.84375(27648/32768)[每个线程35个寄存器]
  • 共享内存比率:0.336914(16560/49152)[5520字节/秒 块]
  • 每个SM的活动块数:3(每个SM的最大活动块数:8)
  • 每个SM的活动线程数:768(每个SM的最大活动线程数:1536)
  • 潜在入住率:0.5(24/48)
  • 占用限制因素:寄存器
请注意标有粗体的子弹。内核执行时间为
121195 us

通过将一些局部变量移动到共享内存,我减少了每个线程的寄存器数量。Compute Visual Profiler输出变为:

  • 内核详细信息:网格大小:[100 1],块大小:[256 1]
  • 寄存器比率:1(32768/32768)[每个线程30个寄存器]
  • 共享内存比率:0.451823(22208/49152)[每个块5552字节]
  • 每个SM的活动块数:4(每个SM的最大活动块数:8)
  • 每个SM的活动线程数:1024(每个SM的最大活动线程数:1536)
  • 潜在入住率:0.666667(32/48)
  • 占用限制因素:寄存器

因此,与以前版本中的
3
块相比,现在在单个SM上同时执行
4
块。但是,执行时间是
115756us
,几乎相同!为什么?在不同的CUDA内核上执行的块不是完全独立的吗?

您隐含地假设更高的占用率自动转换为更高的性能。通常情况并非如此

NVIDIA体系结构需要每个MP具有一定数量的活动扭曲,以隐藏GPU的指令管道延迟。在你的基于费米的卡上,这一要求转化为大约30%的最低占用率。以比最小值更高的占用率为目标并不一定会导致更高的吞吐量,因为延迟瓶颈可能已经转移到GPU的另一部分。入门级GPU没有太多的内存带宽,很可能每MP 3个块就足以限制代码内存带宽,在这种情况下,增加块数不会对性能产生任何影响(甚至可能会因为内存控制器争用和缓存未命中增加而下降)。此外,您说过您将变量溢出到共享内存以减少内核的寄存器足迹。在费米上,共享内存只有约1000 Gb/s的带宽,而寄存器的带宽约为8000 Gb/s(请参阅下面的链接,以了解演示这一点的微基准标记结果)。因此,您将变量移到了较慢的内存中,这也可能对性能产生负面影响,抵消了高占用率带来的任何好处


如果您还没有看过,我强烈推荐Vasily Volkov在GTC 2010“低占用率下更好的性能”中的演讲。这里展示了如何利用指令级并行性,在占用率非常非常低的情况下将GPU吞吐量提高到非常高的水平。

Talonmes已经回答了您的问题,所以我只想分享一段代码,它的灵感来自于上面回答中提到的V.Volkov演示的第一部分

代码如下:

#include<stdio.h>

#define N_ITERATIONS 8192

//#define DEBUG

/********************/
/* CUDA ERROR CHECK */
/********************/
#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, char *file, int line, bool abort=true)
{
    if (code != cudaSuccess) 
    {
        fprintf(stderr,"GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
        if (abort) exit(code);
    }
}

/********************************************************/
/* KERNEL0 - NO INSTRUCTION LEVEL PARALLELISM (ILP = 0) */
/********************************************************/
__global__ void kernel0(int *d_a, int *d_b, int *d_c, unsigned int N) {

    const int tid = threadIdx.x + blockIdx.x * blockDim.x ;

    if (tid < N) {

        int a = d_a[tid];
        int b = d_b[tid];
        int c = d_c[tid];

        for(unsigned int i = 0; i < N_ITERATIONS; i++) {
            a = a * b + c;
        }

        d_a[tid] = a;
    }

}

/*****************************************************/
/* KERNEL1 - INSTRUCTION LEVEL PARALLELISM (ILP = 2) */
/*****************************************************/
__global__ void kernel1(int *d_a, int *d_b, int *d_c, unsigned int N) {

    const int tid = threadIdx.x + blockIdx.x * blockDim.x;

    if (tid < N/2) {

        int a1 = d_a[tid];
        int b1 = d_b[tid];
        int c1 = d_c[tid];

        int a2 = d_a[tid+N/2];
        int b2 = d_b[tid+N/2];
        int c2 = d_c[tid+N/2];

        for(unsigned int i = 0; i < N_ITERATIONS; i++) {
            a1 = a1 * b1 + c1;
            a2 = a2 * b2 + c2;
        }

        d_a[tid]        = a1;
        d_a[tid+N/2]    = a2;
    }

}

/*****************************************************/
/* KERNEL2 - INSTRUCTION LEVEL PARALLELISM (ILP = 4) */
/*****************************************************/
__global__ void kernel2(int *d_a, int *d_b, int *d_c, unsigned int N) {

    const int tid = threadIdx.x + blockIdx.x * blockDim.x;

    if (tid < N/4) {

        int a1 = d_a[tid];
        int b1 = d_b[tid];
        int c1 = d_c[tid];

        int a2 = d_a[tid+N/4];
        int b2 = d_b[tid+N/4];
        int c2 = d_c[tid+N/4];

        int a3 = d_a[tid+N/2];
        int b3 = d_b[tid+N/2];
        int c3 = d_c[tid+N/2];

        int a4 = d_a[tid+3*N/4];
        int b4 = d_b[tid+3*N/4];
        int c4 = d_c[tid+3*N/4];

        for(unsigned int i = 0; i < N_ITERATIONS; i++) {
            a1 = a1 * b1 + c1;
            a2 = a2 * b2 + c2;
            a3 = a3 * b3 + c3;
            a4 = a4 * b4 + c4;
        }

        d_a[tid]        = a1;
        d_a[tid+N/4]    = a2;
        d_a[tid+N/2]    = a3;
        d_a[tid+3*N/4]  = a4;
    }

}

/********/
/* MAIN */
/********/
void main() {

    const int N = 1024;

    int *h_a                = (int*)malloc(N*sizeof(int));
    int *h_a_result_host    = (int*)malloc(N*sizeof(int));
    int *h_a_result_device  = (int*)malloc(N*sizeof(int));
    int *h_b                = (int*)malloc(N*sizeof(int));
    int *h_c                = (int*)malloc(N*sizeof(int));

    for (int i=0; i<N; i++) {
        h_a[i] = 2;
        h_b[i] = 1;
        h_c[i] = 2;
        h_a_result_host[i] = h_a[i];
        for(unsigned int k = 0; k < N_ITERATIONS; k++) {
            h_a_result_host[i] = h_a_result_host[i] * h_b[i] + h_c[i];
        }
    }

    int *d_a; gpuErrchk(cudaMalloc((void**)&d_a, N*sizeof(int)));
    int *d_b; gpuErrchk(cudaMalloc((void**)&d_b, N*sizeof(int)));
    int *d_c; gpuErrchk(cudaMalloc((void**)&d_c, N*sizeof(int)));

    gpuErrchk(cudaMemcpy(d_a, h_a, N*sizeof(int), cudaMemcpyHostToDevice));
    gpuErrchk(cudaMemcpy(d_b, h_b, N*sizeof(int), cudaMemcpyHostToDevice));
    gpuErrchk(cudaMemcpy(d_c, h_c, N*sizeof(int), cudaMemcpyHostToDevice));

    // --- Creating events for timing
    float time;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    /***********/
    /* KERNEL0 */
    /***********/
    cudaEventRecord(start, 0);
    kernel0<<<1, N>>>(d_a, d_b, d_c, N);
#ifdef DEBUG
    gpuErrchk(cudaPeekAtLastError());
    gpuErrchk(cudaDeviceSynchronize());
#endif
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("GFlops = %f\n", (1.e-6)*(float)(N*N_ITERATIONS)/time);
    gpuErrchk(cudaMemcpy(h_a_result_device, d_a, N*sizeof(int), cudaMemcpyDeviceToHost));
    for (int i=0; i<N; i++) if(h_a_result_device[i] != h_a_result_host[i]) { printf("Error at i=%i! Host = %i; Device = %i\n", i, h_a_result_host[i], h_a_result_device[i]); return; }

    /***********/
    /* KERNEL1 */
    /***********/
    gpuErrchk(cudaMemcpy(d_a, h_a, N*sizeof(int), cudaMemcpyHostToDevice));
    cudaEventRecord(start, 0);
    kernel1<<<1, N/2>>>(d_a, d_b, d_c, N);
#ifdef DEBUG
    gpuErrchk(cudaPeekAtLastError());
    gpuErrchk(cudaDeviceSynchronize());
#endif
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("GFlops = %f\n", (1.e-6)*(float)(N*N_ITERATIONS)/time);
    gpuErrchk(cudaMemcpy(h_a_result_device, d_a, N*sizeof(int), cudaMemcpyDeviceToHost));
    for (int i=0; i<N; i++) if(h_a_result_device[i] != h_a_result_host[i]) { printf("Error at i=%i! Host = %i; Device = %i\n", i, h_a_result_host[i], h_a_result_device[i]); return; }

    /***********/
    /* KERNEL2 */
    /***********/
    gpuErrchk(cudaMemcpy(d_a, h_a, N*sizeof(int), cudaMemcpyHostToDevice));
    cudaEventRecord(start, 0);
    kernel2<<<1, N/4>>>(d_a, d_b, d_c, N);
#ifdef DEBUG
    gpuErrchk(cudaPeekAtLastError());
    gpuErrchk(cudaDeviceSynchronize());
#endif
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("GFlops = %f\n", (1.e-6)*(float)(N*N_ITERATIONS)/time);
    gpuErrchk(cudaMemcpy(h_a_result_device, d_a, N*sizeof(int), cudaMemcpyDeviceToHost));
    for (int i=0; i<N; i++) if(h_a_result_device[i] != h_a_result_host[i]) { printf("Error at i=%i! Host = %i; Device = %i\n", i, h_a_result_host[i], h_a_result_device[i]); return; }

    cudaDeviceReset();

}

这意味着,如果利用指令级并行(ILP),占用率较低的内核仍然可以表现出高性能。

回答得好。占用率只是隐藏全局内存访问延迟的一个严重问题;对于绑定计算的线程,每个SP有几个活动线程就足够了。这也是你的理解吗?我真的不这么认为,帕特里克。并非所有类型的内核都是如此。对于计算绑定内核,更高的占用率可能仍然会提高性能。隐藏算术延迟所需的活动扭曲量并不是那么简单。这取决于操作的类型以及它们如何相互交错。
kernel0   GFlops = 21.069281    Occupancy = 66%
kernel1   GFlops = 21.183354    Occupancy = 33%
kernel2   GFlops = 21.224517    Occupancy = 16.7%