Ядро OpenCL выполняется медленнее, чем один поток

Все, я написал очень простое ядро ​​OpenCL, которое преобразует RGB-изображение в оттенки серого с помощью простого усреднения.

Немного фона:

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

Я запускаю это на изображении 1280x960. Серийная версия алгоритма в среднем 60 мс, ядро ​​OpenCL в среднем 200 мс!!! Я делаю что-то не так, но я понятия не имею, как действовать, что оптимизировать. (Рассчитывая время чтения/записи без вызова ядра, алгоритм работает за 15 мс)

Я прилагаю настройку ядра (размеры и аргументы), а также ядро


РЕДАКТИРОВАТЬ: Итак, я написал еще более тупое ядро, в котором нет доступа к глобальной памяти внутри него, и это было всего 150 мс... Это все еще смехотворно медленно. Я подумал, может быть, я ошибаюсь с чтением глобальной памяти, они должны быть выровнены по 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 user57368 На самом деле, вы хотите загрузить планировщик тысячами ожидающих потоков, вот как работает OpenCL. На графическом процессоре, если вы этого не сделаете, вы получите ужасную производительность. На процессоре это менее важно, но вы все равно хотите, иначе вы будете тратить все свое время на вызовы ядра. И да, реализация OpenCL не глупа и не будет порождать один поток ОС для каждого экземпляра ядра, она умнее этого.   -  person Thomas    schedule 30.03.2013
comment
Тысяча потоков для тысячи ядер — вот как следует использовать OpenCL на графическом процессоре. Это не то же самое, что тысяча потоков на ядро, о чем говорилось выше.   -  person user57368    schedule 30.03.2013
comment
@ user57368 У графических процессоров нет тысячи ядер, это далеко не так. В общем, вы хотите поставить в очередь столько рабочих элементов, сколько вам нужно для выполнения вашей задачи, например, если вы хотите оттенить изображение 2048x2048 в градациях серого, вы запускаете рабочие элементы 2048x2048, и каждый из них обрабатывает 1 пиксель (в простейшем случае). . Реализация OpenCL не запускает один поток для каждого рабочего элемента. Например, на ЦП он запускает столько потоков, сколько у вас есть исполнительных блоков, и выполняет итерацию ядра с другим globalID/localID, распределяя работу по этим потокам. Что касается графического процессора, то здесь все практически так же.   -  person Thomas    schedule 30.03.2013
comment
Планировщик рабочих элементов также выполняет некоторую дополнительную работу по логической группировке рабочих элементов в соответствии с выбранным размером локальной рабочей группы.   -  person Thomas    schedule 30.03.2013
comment
1) Каков ваш файл local_work_size. 2) вы запускаете ядро ​​​​на процессоре или графическом процессоре? Я передаю еще большие структуры по значению, и это нормально. Странно, если из-за этого он был таким медленным (хотя, возможно, это так, если вы запускаете ядра на процессоре, не уверен).   -  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.

Ваша производительность должна быть далека от оптимальной. Измените локальный рабочий размер, глобальный рабочий размер, попробуйте обрабатывать два или четыре пикселя на протектор. Можете ли вы изменить способ хранения пикселей до лечения? Затем разбейте свою структуру на древовидные массивы, чтобы более эффективно объединить доступ к памяти.

Tou может скрыть ваши передачи памяти с помощью работы GPU: это будет проще сделать с профилировщиком рядом с вами.

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