2011-10-12 20 views
6

Aquí es una potencia de cálculo gráfico de perfiles para mi núcleo en GT 440:¿Mejora el rendimiento del kernel al aumentar la ocupación?

  • Kernel detalles: Tamaño de la cuadrícula: [100 1 1], el tamaño de bloque: [256 1 1]
  • Registro Ratio: 0,84375 (27648/32768) [35 registros por hilo]
  • memoria compartida Ratio: 0,336914 (16560/49152) [5520 bytes por Bloquear]
  • bloques activos por SM: 3 (Bloques activa máxima por SM: 8)
  • hilos activos por SM: 768 (hilos activa máxima por SM: 1536)
  • Uso Potencial: 0,5 (24/48)
  • Ocupación factor limitante: Registros

Por favor, preste atención a la viñetas marcadas en negrita. El tiempo de ejecución del Kernel es 121195 us.

Reduje un número de registros por hilo moviendo algunas variables locales a la memoria compartida. La salida Visual Profiler Compute convirtió:

  • Kernel detalles: Tamaño de la cuadrícula: [100 1 1], tamaño de bloque: [256 1 1]
  • Registro Ratio: 1 (32768/32768) [30 registros por hilo]
  • memoria compartida Ratio: 0,451823 (22208/49152) [5552 bytes por bloque]
  • bloques activos por SM: 4 (bloques activos máximos por SM: 8)
  • hilos activos por SM: 1024 (Hilos activos máximos por SM: 153 6)
  • Ocupación Potencial: 0,666667 (32/48) factor limitante
  • Ocupación: Registra

Por lo tanto, ahora 4 bloques se ejecutan simultáneamente en un único SM frente 3 bloques en la versión anterior. Sin embargo, el tiempo de ejecución es 115756 us, ¡que es casi lo mismo! ¿Por qué? ¿No se ejecutan los bloques totalmente independientes en diferentes núcleos CUDA?

Respuesta

14

Está asumiendo implícitamente que una mayor ocupación se traduce automáticamente en un mayor rendimiento. Eso a menudo no es el caso.

La arquitectura NVIDIA necesita un cierto número de warps activos por MP para ocultar la latencia de la interconexión de instrucciones de la GPU. En su tarjeta basada en Fermi, ese requisito se traduce en una ocupación mínima de aproximadamente 30%. Apuntar a ocupaciones más altas que ese mínimo no necesariamente dará como resultado un mayor rendimiento, ya que el cuello de botella de latencia se puede haber movido a otra parte de la GPU. Su GPU de nivel de entrada no tiene mucho ancho de banda de memoria, y es bastante posible que 3 bloques por MP sean suficientes para limitar el ancho de banda de la memoria del código, en cuyo caso aumentar el número de bloques no tendrá ningún efecto en el rendimiento (incluso puede disminuir debido a una mayor contención del controlador de memoria y errores de caché). Además, dijo que derramó variables en la memoria compartida para reducir la huella de registro del kernel.En Fermi, la memoria compartida solo tiene alrededor de 1000 Gb/s de ancho de banda, en comparación con alrededor de 8000 Gb/s para los registros (consulte el enlace a continuación para conocer los resultados de microbenchmarking que lo demuestran). Por lo tanto, ha movido las variables a una memoria más lenta, lo que también puede tener un efecto negativo en el rendimiento, lo que compensa cualquier beneficio que la alta ocupación le permita.

Si aún no lo ha visto, recomiendo ampliamente la presentación de Vasily Volkov de GTC 2010 "Mejor rendimiento en ocupaciones más bajas" (pdf). Aquí se muestra cómo la explotación del paralelismo del nivel de instrucción puede aumentar el rendimiento de la GPU a niveles muy altos con niveles muy, muy bajos de ocupación.

+1

Buena respuesta. La ocupación es solo una preocupación seria por ocultar la latencia de acceso a la memoria global; para hilos de cálculo, algunos hilos activos por SP deberían ser suficientes. ¿Es eso tu entendimiento también? – Patrick87

+0

Realmente no lo creo, Patrick. Eso no es cierto para todos los tipos de kernels. Para kernels vinculados a la computación, una mayor ocupación aún podría aumentar el rendimiento. La cantidad de warps activos necesarios para ocultar la latencia aritmética no es tan simple de decir. Depende de los tipos de operaciones y de cómo se intercalan entre sí. – Zk1001

2

talonmies ya ha respondido su pregunta, así que solo quiero compartir un código inspirado en la primera parte de la presentación de V. Volkov mencionada en la respuesta anterior.

Este es el código:

#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(); 

} 

En mi GT540M GeForce, el resultado es

kernel0 GFlops = 21.069281 Occupancy = 66% 
kernel1 GFlops = 21.183354 Occupancy = 33% 
kernel2 GFlops = 21.224517 Occupancy = 16.7% 

que significa que los granos con menor ocupación todavía pueden exhibir un alto rendimiento, si Instrucción Nivel Paralelismo (ILP) se explota.

Cuestiones relacionadas