Cuda толкает глобальную память, пишет очень медленно

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

Поэтому я выделяю непрерывный блок памяти устройства, который я постоянно обновляю с помощью специального функтора.

Проблема в том, что запись в память устройства очень медленная, но чтение на самом деле в порядке.

Базовая настройка следующая:

struct HistogramCreation
{
    HistogramCreation(
    ...
    // pointer to memory
    ...
    ){}

    /// The actual summation operator
    __device__ void operator()(int index){
       .. do the calculations ..
       for(int j=0;j<30;j++){

       (1)  *_memoryPointer =  values (also using reads to such locations) ;

       }
  }
}

void foo(){

  cudaMalloc(_pointer,size);

  HistogramCreation initialCreation( ... _pointer ...);
  thrust::for_each(
    thrust::make_counting_iterator(0),
    thrust::make_counting_iterator(_imageSize),
    initialCreation);
}

если я изменю запись в (1) на следующую >

unsigned int val = values;

Производительность намного лучше. Это единственная глобальная запись в память, которую я имею.

Используя запись в память, я получаю около 2 с для HD-видео. с использованием локальной переменной это занимает около 50 мс, то есть примерно в 40 раз меньше.

Почему это так медленно? как я могу улучшить его?


person Sleeme    schedule 21.12.2012    source источник
comment
Это не ответ на ваш вопрос, но недавно мне понадобилась кумулятивная гистограмма, и я нашел этот пример кода полезен. Может быть интересно, если вы еще не видели.   -  person Robert Crovella    schedule 21.12.2012


Ответы (3)


Как сказал @OlegTitov, следует максимально избегать частой загрузки/сохранения с глобальной памятью. Когда возникает ситуация, когда это неизбежно, объединенный доступ к памяти может помочь процессу выполнения не стать слишком медленным; однако в большинстве случаев расчет гистограммы довольно сложен для реализации объединенного доступа.

Хотя большая часть вышеизложенного в основном просто повторяет ответ @OlegTitov, я просто хотел бы поделиться исследованием, которое я провел по поиску суммирования с помощью NVIDIA CUDA. На самом деле результат довольно интересный, и я надеюсь, что он будет полезен другим разработчикам xcuda.

В основном эксперимент состоял в том, чтобы запустить тест скорости нахождения суммирования с различными шаблонами доступа к памяти: с использованием глобальной памяти (1 поток), кеша L2 (атомарные операции — 128 потоков) и кеша L1 (общая память — 128 потоков).

В этом эксперименте использовались: Kepler GTX 680, 1546 ядер, 1,06 ГГц, GDDR5, 256 бит, 3 ГГц.

Вот ядра:

__global__
void glob(float *h) {
    float* hist = h;
    uint sd = SEEDRND;
    uint random;
    for (int i = 0; i < NUMLOOP; i++) {
        if (i%NTHREADS==0) random = rnd(sd);
        int rind = random % NBIN;
        float randval = (float)(random % 10)*1.0f ;
        hist[rind] += randval;
    }
}

__global__
void atom(float *h) {
    float* hist = h;
    uint sd = SEEDRND;
    for (int i = threadIdx.x; i < NUMLOOP; i+=NTHREADS) {
        uint random = rnd(sd);
        int rind = random % NBIN;
    float randval = (float)(random % 10)*1.0f ;
        atomicAdd(&hist[rind], randval);
    }
}

__global__
void shm(float *h) {
    int lid = threadIdx.x;
    uint sd = SEEDRND;

    __shared__ float shm[NTHREADS][NBIN];
    for (int i = 0; i < NBIN; i++) shm[lid][i] = h[i];

    for (int i = lid; i < NUMLOOP; i+=NTHREADS) {
        uint random = rnd(sd);
        int rind = random % NBIN;
        float randval = (float)(random % 10)*1.0f ;
        shm[lid][rind] += randval;
    }

    /* reduction here */
    for (int i = 0; i < NBIN; i++) {
        __syncthreads();
        if (threadIdx.x < 64) {
            shm[threadIdx.x][i] += shm[threadIdx.x+64][i];
        }
        __syncthreads();
        if (threadIdx.x < 32) {
            shm[threadIdx.x][i] += shm[threadIdx.x+32][i];
        }
        __syncthreads();
        if (threadIdx.x < 16) {
            shm[threadIdx.x][i] += shm[threadIdx.x+16][i];
        }
        __syncthreads();
        if (threadIdx.x < 8) {
            shm[threadIdx.x][i] += shm[threadIdx.x+8][i];
        }
        __syncthreads();
        if (threadIdx.x < 4) {
            shm[threadIdx.x][i] += shm[threadIdx.x+4][i];
        }
        __syncthreads();
        if (threadIdx.x < 2) {
            shm[threadIdx.x][i] += shm[threadIdx.x+2][i];
        }
        __syncthreads();
        if (threadIdx.x == 0) {
            shm[0][i] += shm[1][i];
        }
    }

    for (int i = 0; i < NBIN; i++) h[i] = shm[0][i];
}

ВЫВОД

atom:  102656.00 shm:  102656.00 glob:  102656.00
atom:  122240.00 shm:  122240.00 glob:  122240.00
... blah blah blah ...

  One Thread: 126.3919 msec
      Atomic:   7.5459 msec
      Sh_mem:   2.2207 msec

Соотношение между этими ядрами составляет 57:17:1. Здесь можно проанализировать многое, и это действительно не означает, что использование пространства памяти L1 или L2 всегда даст вам более чем 10-кратное ускорение всей программы.

А вот основные и другие функции:

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;

#define NUMLOOP 1000000
#define NBIN 36
#define SEEDRND 1

#define NTHREADS 128
#define NBLOCKS 1

__device__ uint rnd(uint & seed) {
#if LONG_MAX > (16807*2147483647)
    int const a    = 16807;
    int const m    = 2147483647;
    seed = (long(seed * a))%m;
    return seed;
#else
    double const a    = 16807;
    double const m    = 2147483647;

    double temp = seed * a;
    seed = (int) (temp - m * floor(temp/m));
    return seed;
#endif
}

... the above kernels ...

int main()
{
    float *h_hist, *h_hist2, *h_hist3, *d_hist, *d_hist2,
    *d_hist3;
    h_hist = (float*)malloc(NBIN * sizeof(float));
    h_hist2 = (float*)malloc(NBIN * sizeof(float));
    h_hist3 = (float*)malloc(NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist, NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist2, NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist3, NBIN * sizeof(float));

    for (int i = 0; i < NBIN; i++) h_hist[i] = 0.0f;
    cudaMemcpy(d_hist, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);
    cudaMemcpy(d_hist2, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);
    cudaMemcpy(d_hist3, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);

    cudaEvent_t start, end;
    float elapsed = 0, elapsed2 = 0, elapsed3;
    cudaEventCreate(&start);
    cudaEventCreate(&end);

    cudaEventRecord(start, 0);

    atom<<<NBLOCKS, NTHREADS>>>(d_hist);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed, start, end);

    cudaEventRecord(start, 0);

    shm<<<NBLOCKS, NTHREADS>>>(d_hist2);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed2, start, end);

    cudaEventRecord(start, 0);

    glob<<<1, 1>>>(d_hist3);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed3, start, end);

    cudaMemcpy(h_hist, d_hist, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);
    cudaMemcpy(h_hist2, d_hist2, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);
    cudaMemcpy(h_hist3, d_hist3, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);

    /* print output */
    for (int i = 0; i < NBIN; i++) {
        printf("atom: %10.2f shm: %10.2f glob:
    %10.2f¥n",h_hist[i],h_hist2[i],h_hist3[i]);
    }

    printf("%12s: %8.4f msec¥n", "One Thread", elapsed3);
    printf("%12s: %8.4f msec¥n", "Atomic", elapsed);
    printf("%12s: %8.4f msec¥n", "Sh_mem", elapsed2);

    return 0;
}
person ardiyu07    schedule 21.12.2012

При написании кода GPU следует избегать чтения и записи в/из глобальной памяти. Глобальная память очень медленная на GPU. Это аппаратная особенность. Единственное, что вы можете сделать, это заставить соседние треды читать/записывать соседние адреса в глобальной памяти. Это вызовет слияние и ускорит процесс. Но в целом прочитайте свои данные один раз, обработайте их и запишите один раз.

person Oleg Titov    schedule 21.12.2012
comment
Ну, чтение в моем случае по-прежнему очень быстрое, и только письмо медленное. Проблема в том, что мне действительно нужна глобальная память, так как в принципе пиксель будет обновляться в произвольном другом месте (конечно, на устройстве). Поскольку запись местоположения происходит медленно, но чтение происходит быстро, я предполагаю некую форму механизма блокировки/сериализации, который мне не нужен, поскольку у меня нет условий гонки, все может действовать само по себе. - person Sleeme; 21.12.2012
comment
@user1913946 user1913946 Вы выполняете одно чтение и 30 операций записи... Или вы пропустили несколько строк кода? В этом случае вы можете показать свой код цикла for? - person Oleg Titov; 21.12.2012
comment
в цикле for есть что-то вроде *_p1++ = *_p2++ + *_p3++ + *_p4++ *_p5++, поэтому 4 чтения и 1 запись за итерацию. Если я заменяю *_p1++ локальной переменной, появляется эффект - person Sleeme; 21.12.2012
comment
Если ваш графический процессор имеет вычислительную мощность ›= 2.0, значит, у вас есть кеш для глобальной памяти, и эффект может быть связан с ним. Когда вы читаете только из глобальной памяти, у вас могут кэшироваться хиты, и все в порядке. Когда вы начинаете писать, это может сделать строки кэша недействительными и вызвать фактическое чтение. Но я не уверен, что это так. Попробуйте изменить алгоритм. 150 обращений к памяти на поток почти без вычислений — это что-то крайне странное. - person Oleg Titov; 21.12.2012
comment
Как я уже сказал, это не полный код, а все, что действительно важно для задачи. Если вы вычисляете интегральную гистограмму изображения, вы должны делать это поэтапно, если хотите получить хорошее распараллеливание. Сначала я рассчитываю гистограмму для каждого отдельного пикселя размера 1, увеличивая патчи, пока не получу гистограмму для всех пикселей. Все, что он делает в общей сложности, - это около log (n) * 150 обращений к потоку, что должно выполняться быстрее, чем за две секунды. - person Sleeme; 21.12.2012

Обратите внимание, что NVCC может оптимизировать большую часть вашего кода после внесения изменений — он обнаруживает, что запись в глобальную память не производится, и просто удаляет «ненужный» код. Таким образом, это ускорение может исходить не от глобального писателя как такового.

Я бы порекомендовал использовать профилировщик в вашем фактическом коде (тот, который имеет глобальную запись), чтобы увидеть, есть ли что-то вроде невыровненного доступа или другой проблемы с производительностью.

person Eugene    schedule 21.12.2012