Улучшение производительности ядра за счет увеличения загруженности? - PullRequest
6 голосов
/ 12 октября 2011

Вот вывод Compute Visual Profiler для моего ядра на GT 440:

  • Подробности ядра: Размер сетки: [100 1 1], Размер блока: [256 1 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 1], Размер блока: [256 1 1]
  • Соотношение регистров: 1 (32768/32768) [30 регистров на поток]
  • Коэффициент общей памяти: 0,451823 (22208/49152) [5552 байта на блок]
  • Активных блоков на SM: 4 (Максимальное количество активных блоков на SM: 8)
  • Активных потоков на SM: 1024 (Максимально активных потоков на SM: 1536)
  • Потенциальная вместимость: 0,666667 (32/48)
  • Фактор ограничения занятости: Регистры

Следовательно, теперь 4 блоки выполняются одновременно на одном SM по сравнению с 3 блоками в предыдущей версии. Однако время выполнения составляет 115756 us, что почти одинаково! Зачем? Разве блоки не являются полностью независимыми, будучи выполненными на разных ядрах CUDA?

Ответы [ 2 ]

14 голосов
/ 12 октября 2011

Вы подразумеваете, что более высокая занятость автоматически приводит к повышению производительности.Чаще всего это не так.

Архитектуре NVIDIA требуется определенное количество активных деформаций на MP, чтобы скрыть задержку конвейера команд графического процессора.Для вашей карты на основе Fermi это требование соответствует минимальной загрузке около 30%.Стремление к более высокой занятости, чем этот минимум, не обязательно приведет к более высокой пропускной способности, так как узкое место задержки может переместиться в другую часть графического процессора.Ваш GPU начального уровня не имеет большой пропускной способности памяти, и вполне возможно, что 3 блока на MP достаточно для того, чтобы ограничить пропускную способность памяти кода, и в этом случае увеличение количества блоков не повлияет на производительность.(он может даже отключиться из-за увеличения конкуренции за контроллер памяти и отсутствия кеша).Кроме того, вы сказали, что вылили переменные в общую память, чтобы уменьшить отпечаток регистра ядра.В Fermi общая память имеет только около 1000 Гбит / с пропускной способности, по сравнению с около 8000 Гбит / с для регистров (см. Ссылку ниже для результатов микробенчмаркинга, которые демонстрируют это).Таким образом, вы переместили переменные в более медленную память, что также может оказать негативное влияние на производительность, компенсируя любые преимущества, которые дает высокая занятость.

Если вы еще не видели этого, я настоятельно рекомендую выступление Василия Волкова на GTC 2010«Лучшая производительность при низкой загруженности» (pdf) .Здесь показано, как использование параллелизма на уровне команд может повысить пропускную способность графического процессора до очень высокого уровня при очень и очень низком уровне занятости.

2 голосов
/ 12 сентября 2014

talonmies уже ответили на ваш вопрос, поэтому я просто хочу поделиться кодом, вдохновленным первой частью презентации В. Волкова, упомянутой в ответе выше.

Это код:

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

}

На моем GeForce GT540M результат равен

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

, что означает, что ядра с меньшей загрузкой могут по-прежнему демонстрировать высокую производительность, если используется Параллелизм уровня команд (ILP) .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...