Cuda 通过增加占用率来提高内核性能?
以下是GT 440上我的内核的Compute Visual Profiler的输出: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) 占用限制因素:寄存器 请注意标
- 内核详细信息:网格大小:[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%