Распараллеливаясь с 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, и я пытаюсь вычислить массивы величины и угла (на графическом процессоре, чтобы сэкономить время).
Просмотрев ценные комментарии и ответы от 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);
}
Единственные потерянные потоки здесь относятся к случаям, когда ширина или высота не являются кратными количеству потоков на блок.
cudaMallocPitch
, вы должны сделать то же самое, чтобы пропустить заполнение. Я не понимаю, как вы могли бы избежать этого.