Как вы перебираете переданный массив CUDA?

0

Распараллеливаясь с OpenMP раньше, я пытаюсь обернуть голову вокруг CUDA, что для меня не кажется слишком интуитивным. На этом этапе я пытаюсь понять, как перемещаться по массиву параллельно.

Cuda by Example - отличный старт.

Фрагмент на стр. 43 показывает:

__global__ void add( int *a, int *b, int *c ) {
  int tid = blockIdx.x; // handle the data at this index
  if (tid < N)
     c[tid] = a[tid] + b[tid];
  }

Принимая во внимание, что в OpenMP программист выбирает количество циклов, которые будет выполняться циклом, и OpenMP разбивается на потоки для вас, в CUDA вы должны это рассказать (через количество блоков и количество потоков в <<<...>>>), чтобы запустить его достаточное количество времени для итерации по вашему массиву с использованием идентификатора потока в качестве итератора. Другими словами, вы можете иметь ядро CUDA, которое всегда запускается 10 000 раз, что означает, что приведенный выше код будет работать для любого массива до N = 10 000 (и, конечно, для меньших массивов, которые вы теряете в циклах, выпадающих при if (tid < N)),

Для скатной памяти (2D и 3D-массивы) руководство по программированию CUDA имеет следующий пример:

// Host code
int width = 64, height = 64; 
float* devPtr; size_t pitch; 
cudaMallocPitch(&devPtr, &pitch, width * sizeof(float), height);

MyKernel<<<100, 512>>>(devPtr, pitch, width, height); 

// Device code 
__global__ void MyKernel(float* devPtr, size_t pitch, int width, int height) 
{ 
    for (int r = 0; r < height; ++r) {
        float* row = (float*)((char*)devPtr + r * pitch); 
        for (int c = 0; c > width; ++c) { 
            float element = row[c]; 
        }
    }
}

Этот пример не кажется мне слишком полезным. Сначала они объявляют массив размером 64 x 64, тогда ядро будет исполнять 512 x 100 раз. Это прекрасно, потому что ядро ничего не делает, кроме итерации по массиву (поэтому он запускает 51 200 циклов через массив 64 x 64).

В соответствии с этим ответом итератор, когда есть блоки потоков, будет

int tid = (blockIdx.x * blockDim.x) + threadIdx.x;

Поэтому, если бы я хотел запустить первый фрагмент в моем вопросе для разбитого массива, я мог бы просто убедиться, что у меня достаточно блоков и потоков, чтобы охватить каждый элемент, включая дополнение, которое мне неинтересно. Но это кажется расточительным.

Итак, как я могу проходить через массив, не пройдя через элементы прокладки?

В моем конкретном приложении у меня есть 2D FFT, и я пытаюсь вычислить массивы величины и угла (на графическом процессоре, чтобы сэкономить время).

  • 1
    Ваш вопрос мне не понятен. В фрагменте кода Руководства по программированию CUDA C, которое вы цитируете, вы не просматриваете элементы заполнения, а пропускаете их. Аналогично, если вы выделяете массивы, включенные в параллельное суммирование CUDA By Example cudaMallocPitch , вы должны сделать то же самое, чтобы пропустить заполнение. Я не понимаю, как вы могли бы избежать этого.
  • 1
    Если вам нужно использовать cuFFT в связи с массивами с тональным набором, вы можете взглянуть на CUFFT: Как рассчитать fft, когда входной сигнал представляет собой тональный массив .
Показать ещё 3 комментария
Теги:
arrays
memory
cuda

1 ответ

1
Лучший ответ

Просмотрев ценные комментарии и ответы от JackOLantern и перечитав документацию, я смог разобраться. Конечно, ответ теперь "тривиальный", когда я это понимаю.

В приведенном ниже коде я определяю CFPtype (Комплексная CFPtype точка) и FPtype чтобы я мог быстро изменить одну и двойную точность. Например, #define CFPtype cufftComplex.

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

Ключ в том, чтобы заставить всю мою программу работать (2D БПФ на скатной памяти и вычислять величину и аргумент), понимал, что, хотя CUDA дает вам много "очевидной" помощи в распределении 2D и 3D-массивов, все по-прежнему находится в единицах байтов. Очевидно, что в вызове malloc необходимо включить sizeof(type), но я полностью пропустил его в вызовах типа allocate(width, height). Вероятно, ошибка Noob. Если бы я написал библиотеку, я бы сделал размер шрифта отдельным параметром, но что бы то ни было.

Поэтому, учитывая изображение width x height в пикселях, это то, как он объединяется:

Выделение памяти

Я использую закрепленную память на стороне хоста, потому что она должна быть быстрее. Это выделено cudaHostAlloc что просто. Для разбитой памяти вам нужно сохранить высоту тона для каждой разной ширины и типа, потому что это может измениться. В моем случае размеры все одинаковы (сложное комплексное преобразование), но у меня есть массивы, которые являются действительными числами, поэтому я храню complexPitch и realPitch. Разбитая память выполняется следующим образом:

cudaMallocPitch(&inputGPU, &complexPitch, width * sizeof(CFPtype), height);

Чтобы скопировать память в/из разбитых массивов, вы не можете использовать cudaMemcpy.

cudaMemcpy2D(inputGPU, complexPitch,  //destination and destination pitch
inputPinned, width * sizeof(CFPtype), //source and source pitch (= width because it not padded).
width * sizeof(CFPtype), height, cudaMemcpyKind::cudaMemcpyHostToDevice);

План FFT для разбитых массивов

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

int n[] = {height, width};
int nembed[] = {height, complexPitch/sizeof(CFPtype)};
result = cufftPlanMany(
    &plan, 
    2, n, //transform rank and dimensions
    nembed, 1, //input array physical dimensions and stride
    1, //input distance to next batch (irrelevant because we are only doing 1)
    nembed, 1, //output array physical dimensions and stride
    1, //output distance to next batch
    cufftType::CUFFT_C2C, 1);

Выполнение БПФ тривиально:

cufftExecC2C(plan, inputGPU, outputGPU, CUFFT_FORWARD);

До сих пор мне не удалось оптимизировать. Теперь я хотел получить амплитуду и фазу из преобразования, следовательно, вопрос о том, как проходить поперечный массив параллельно. Сначала я определяю функцию для вызова ядра с "правильными" потоками на блок и достаточным количеством блоков для покрытия всего изображения. Как было предложено в документации, создание 2D-структур для этих чисел является большой помощью.

void GPUCalcMagPhase(CFPtype *data, size_t dataPitch, int width, int height, FPtype *magnitude, FPtype *phase, size_t magPhasePitch, int cudaBlockSize)
{
    dim3 threadsPerBlock(cudaBlockSize, cudaBlockSize);
    dim3 numBlocks((unsigned int)ceil(width / (double)threadsPerBlock.x), (unsigned int)ceil(height / (double)threadsPerBlock.y));

    CalcMagPhaseKernel<<<numBlocks, threadsPerBlock>>>(data, dataPitch, width, height, magnitude, phase, magPhasePitch);
}

Установка блоков и потоков на блок эквивалентна записи (до 3), вложенных for -loops. Таким образом, у вас должно быть достаточно блоков * потоков для покрытия массива, а затем в ядре вы должны убедиться, что вы не превышаете размер массива. Используя 2D-элементы для threadsPerBlock и numBlocks, вы избегаете проходить через элементы дополнения в массиве.

Прохождение параллельного массива параллельно

Ядро использует стандартную арифметику указателя из документации:

__global__ void CalcMagPhaseKernel(CFPtype *data, size_t dataPitch, int width, int height,
                                   FPtype *magnitude, FPtype *phase, size_t magPhasePitch)
{
    int threadX = threadIdx.x + blockDim.x * blockIdx.x;
    if (threadX >= width) 
        return;

    int threadY = threadIdx.y + blockDim.y * blockIdx.y;
    if (threadY >= height)
        return;

    CFPtype *threadRow = (CFPtype *)((char *)data + threadY * dataPitch);
    CFPtype complex = threadRow[threadX];

    FPtype *magRow = (FPtype *)((char *)magnitude + threadY * magPhasePitch);
    FPtype *magElement = &(magRow[threadX]);

    FPtype *phaseRow = (FPtype *)((char *)phase + threadY * magPhasePitch);
    FPtype *phaseElement = &(phaseRow[threadX]);

    *magElement = sqrt(complex.x*complex.x + complex.y*complex.y);
    *phaseElement = atan2(complex.y, complex.x);
}

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню