OpenCL ядрото се изпълнява по-бавно от единична нишка

Всичко, написах много просто OpenCL ядро, което трансформира RGB изображение в сива скала, използвайки просто осредняване.

Малко предистория:

  1. Изображението се съхранява в картографирана памет като 24-битов, неподплатен блок памет
  2. Изходният масив се съхранява в фиксирана памет (картирана с clEnqueueMapBuffer) и е 8 bpp
  3. Има два буфера, разпределени на устройството (clCreateBuffer), единият е специално прочетен (в който ние clWriteBuffer влизаме преди стартирането на ядрото), а другият е специално записван (който ние clReadBuffer след като ядрото приключи)

Пускам това на изображение 1280x960. Серийната версия на алгоритъма е средно 60ms, OpenCL ядрото е средно 200ms!!! Правя нещо нередно, но нямам идея как да продължа, какво да оптимизирам. (Времето ми чете/записва без извикване на ядрото, алгоритъмът работи за 15 ms)

Прилагам настройката на ядрото (размери и аргументи), както и ядрото


РЕДАКТИРАНЕ: Затова написах още по-глупаво ядро, което няма глобален достъп до паметта вътре в него и беше само 150 ms... Това все още е абсурдно бавно. Помислих си, че може би бъркам с четенията на глобалната памет, те трябва да са подравнени в 4 байта или нещо подобно? Не...

Редактиране 2: Премахването на всички параметри от моето ядро ​​ми даде значително ускорение... Объркан съм, мислех си, че тъй като съм clEnqueueWriteBuffer, ядрото не трябва да извършва прехвърляне на памет от хост->устройство и устройство->хост....

Редактиране 3: Разбрах го, но все още не разбирам защо. Ако някой може да го обясни, ще се радвам да му дам правилния отговор. Проблемът беше предаването на персонализираните структури по стойност. Изглежда, че ще трябва да заделя глобална памет за тях и да предам техните cl_mems


Извикване на ядрото:

//Copy input to device
result = clEnqueueWriteBuffer(handles->queue, d_input_data, CL_TRUE, 0, h_input.widthStep*h_input.height, (void *)input->imageData, 0, 0, 0);
if(check_result(result, "opencl_rgb_to_gray", "Failed to write to input buffer on device!")) return 0;

//Set kernel arguments
result = clSetKernelArg(handles->current_kernel, 0, sizeof(OpenCLImage), (void *)&h_input);
if(check_result(result, "opencl_rgb_to_gray", "Failed to set input struct.")) return 0;
result = clSetKernelArg(handles->current_kernel, 1, sizeof(cl_mem), (void *)&d_input_data);
if(check_result(result, "opencl_rgb_to_gray", "Failed to set input data.")) return 0;
result = clSetKernelArg(handles->current_kernel, 2, sizeof(OpenCLImage), (void *)&h_output);
if(check_result(result, "opencl_rgb_to_gray", "Failed to set output struct.")) return 0;
result = clSetKernelArg(handles->current_kernel, 3, sizeof(cl_mem), (void *)&d_output_data);
if(check_result(result, "opencl_rgb_to_gray", "Failed to set output data.")) return 0;

//Determine run parameters
global_work_size[0] = input->width;//(unsigned int)((input->width / (float)local_work_size[0]) + 0.5);
global_work_size[1] = input->height;//(unsigned int)((input->height/ (float)local_work_size[1]) + 0.5);

printf("Global Work Group Size: %d %d\n", global_work_size[0], global_work_size[1]);

//Call kernel
result = clEnqueueNDRangeKernel(handles->queue, handles->current_kernel, 2, 0, global_work_size, local_work_size, 0, 0, 0);
if(check_result(result, "opencl_rgb_to_gray", "Failed to run kernel!")) return 0;

result = clFinish(handles->queue);
if(check_result(result, "opencl_rgb_to_gray", "Failed to finish!")) return 0;

//Copy output
result = clEnqueueReadBuffer(handles->queue, d_output_data, CL_TRUE, 0, h_output.widthStep*h_output.height, (void *)output->imageData, 0, 0, 0);
if(check_result(result, "opencl_rgb_to_gray", "Failed to write to output buffer on device!")) return 0;

Ядро:

typedef struct OpenCLImage_t
{
    int width;
    int widthStep;
    int height;
    int channels;
} OpenCLImage;

__kernel void opencl_rgb_kernel(OpenCLImage input, __global unsigned char*  input_data, OpenCLImage output, __global unsigned char * output_data)
{
    int pixel_x = get_global_id(0);
    int pixel_y = get_global_id(1);
    unsigned char * cur_in_pixel, *cur_out_pixel;
    float avg = 0;

    cur_in_pixel = (unsigned char *)(input_data + pixel_y*input.widthStep + pixel_x * input.channels);
    cur_out_pixel = (unsigned char *)(output_data + pixel_y*output.widthStep + pixel_x * output.channels);

    avg += cur_in_pixel[0];
    avg += cur_in_pixel[1];
    avg+= cur_in_pixel[2];
    avg /=3.0f;

    if(avg > 255.0)
        avg = 255.0;
    else if(avg < 0)
        avg = 0;

    *cur_out_pixel = avg;
}

person Constantin    schedule 29.03.2013    source източник
comment
Не сте споменали какъв тип устройство работи с CL ядрото, но е вероятно то да няма 1280*960=1,2 милиона изчислителни ядра. Опитайте се да напишете ядро, което работи с повече от един пиксел наведнъж, вместо да натоварвате планировчика с хиляди нишки на ядро.   -  person user57368    schedule 30.03.2013
comment
Това не беше проблемът, но аз преминах към по-голям локален размер, като използвах clGetWorkGroupInfo, за да определя размерите на моите локални работни групи.   -  person Constantin    schedule 30.03.2013
comment
@user57368 Всъщност искате да натоварите планировчика с хиляди чакащи нишки, така работи OpenCL. На GPU, ако не направите това, ще получите ужасна производителност. За CPU това е по-малко важно, но все пак искате, в противен случай ще прекарвате цялото си време в извикване на ядрото. И, да, внедряването на OpenCL не е глупаво и няма да създаде една OS нишка на екземпляр на ядрото, по-умно е от това.   -  person Thomas    schedule 30.03.2013
comment
Хиляда нишки за хиляда ядра е начинът, по който OpenCL трябва да се използва на GPU. Това не е същото като хиляда нишки на ядро, което е публикуваното по-горе.   -  person user57368    schedule 30.03.2013
comment
@user57368 Графичните процесори нямат хиляда ядра, далеч от това. По принцип искате да поставите в опашката толкова работни елементи, колкото са ви необходими, за да изпълните задачата си, например, ако искате да промените в сивата скала изображение с размери 2048x2048, стартирате работни елементи с размери 2048x2048 и всеки от тях обработва 1 пиксел (в най-простия случай) . Реализацията на OpenCL не стартира една нишка на работен елемент. На процесора, например, той стартира толкова нишки, колкото имате изпълнителни единици, и итерира ядрото с различен globalID/localID, разпределяйки работата върху тези нишки. За GPU нещата по същество са същите.   -  person Thomas    schedule 30.03.2013
comment
Планировчикът на работни елементи извършва допълнителна работа, за да групира логически заедно работните елементи според избрания размер на локалната работна група.   -  person Thomas    schedule 30.03.2013
comment
1) Какъв е вашият local_work_size. 2) стартирате ли ядрото на CPU или GPU? Предавам дори по-големи структури по стойност и е добре. Странно е, ако беше толкова бавен поради това (все пак може би е така, ако стартирате ядра на CPU, не съм сигурен).   -  person alariq    schedule 05.04.2013
comment
Работех на GPU. Има и ситуацията, че използвам вграден профил OpenCL, така че може да има разлики в изпълнението. Моят local_work_size беше 16x8, тъй като всичко по-високо хвърляше грешки.   -  person Constantin    schedule 05.04.2013


Отговори (2)


Разходите за копиране на стойността във всички нишки, които ще бъдат създадени, може да са възможната причина за времето; където за глобална памет препратката ще бъде достатъчна в другия случай. Единственият изпълнител на SDK ще може да отговори точно.. :)

person Chanakya.sun    schedule 04.04.2013
comment
И така, защо се предлага да предаваме прости типове директно? - person Constantin; 04.04.2013
comment
простите типове имат същите допълнителни разходи за копиране като указателите към глобалната памет (която е много малка), така че няма причина да се разпределят буфери за тях. - person alariq; 05.04.2013

Може да искате да опитате local_work_size като [64, 1, 1], за да обедините вашите извиквания на паметта. (обърнете внимание, че 64 е делител на 1280).

Както беше казано по-рано, трябва да използвате профильор, за да получите повече информация. Използвате ли nvidia карта? След това изтеглете CUDA 4 (не 5), тъй като съдържа openCL профильор.

Вашето представяне трябва да е далеч от оптималното. Променете локалния работен размер, глобалния работен размер, опитайте се да третирате два или четири пиксела на протектор. Можете ли да промените начина, по който пикселите се съхраняват в зависимост от вашето лечение? След това разбийте структурата си за дървовидни масиви, за да обедините по-ефективно достъпа до паметта.

Можете да скриете вашите прехвърляния на памет с работата на графичния процесор: ще бъде по-лесно да го направите с профилиращ близо до вас.

person simon.denel    schedule 06.04.2013
comment
Вграден графичен процесор, Vivante, за да бъдем по-конкретни. Играх си с четене на 4 байта (както беше предложено на няколко места), скоростта беше около 10%. Основното ми ускоряване беше от прехвърляне на персонализирани структури чрез cl_mem, а не по стойност. - person Constantin; 08.04.2013
comment
Опитахте ли да създадете по един масив на канал? Ако не можете да скриете времето си за прехвърляне с изпълнението на други ядра, опитахте ли да закачите паметта на хоста си? - person simon.denel; 08.04.2013