в OpenCL оптимальный размер группы

я запускаю генератор Мандельброта (2D-изображение из статических параметров) на OpenCL. Программа проста:

__kernel
void mandelbrot(__global uchar * output, 
                const float xstep,
                const float xoffset,
                const float ystep,
                const float yoffset,
                const int maxiter)
{
    int gid_y = get_global_id(1);
    int gid_x = get_global_id(0);

    //calculate x and y on the fly for every pixel. 
    //This is just as fast as reading precalculated rulers from global memory.
    float x = gid_x * xstep + xoffset;
    float y = gid_y * ystep + yoffset;

    float real = 0;
    float imag = 0;

    int out = 0;

    for(int curiter = 0; curiter < maxiter; curiter++) {
        float nreal = real*real - imag*imag + x;
        imag = 2* real*imag + y;
        real = nreal;

        if (real*real + imag*imag > 4.0f) {
            out = curiter;
            break;
        }
    }

    //normalize output
    out *= 256.0 / (float)maxiter;
    output[gid_y * get_global_size(0) + gid_x] = out;

}

[EDIT] [опубликовано полное ядро и заменены строки и столбцы, как предложено. Таким образом, я получил производительность 18% на AMD, но 0% на NVidia. Исходный код был

output[get_global_id(0) * get_global_size(1) + get_global_id(1)] = out;

[/EDIT]

я запускаю его на моем Nvidia Quadro 1000M, который имеет 2 вычислительных блока и 96 ядер CUDA (48 ядер на вычисление блок.)

Я играю, изменяя размер локальной группы при запросе ядра. Эти результаты я получаю с различными размерами при создании изображения 400Mpixel. Все номера из профилировщика OpenCL и исключают окончательную копию памяти обратно в ОС. Изображение 40992x10272-высота и ширина делятся на 48.

rows x columns
8x8: 397 MPixel/s
8x12: 505 MPixel/s
8x16: 523 MPixel/s
8x24: 521 MPixel/s
8x32: 520 MPixel/s
8x48: 520 MPixel/s

1x48: 321 MPixel/s
2x32: 424 MPixel/s
2x48: 523 MPixel/s
4x24: 519 MPixel/s
3x32: 525 MPixel/s
4x32: 525 MPixel/s
4x48: 525 MPixel/s

12x8: 490 MPixel/s
12x12:464 MPixel/s
12x24:505 MPixel/s
12x32:508 MPixel/s
12x48:433 MPixel/s

16x8: 499 MPixel/s
16x12:499 MPixel/s
16x16:472 MPixel/s
16x24:450 MPixel/s
16x32:440 MPixel/s
16x48:418 MPixel/s

некоторые из этих чисел оставляют меня в недоумении. Хотя понятно, почему я получаю лучшие результаты с 48 колоннами (спасибо как работают операции SIMD), я не понимаю:

  1. почему производительность резко снижается, когда я использую 16 строк в группе?
  2. почему я получаю плохую производительность с 1x48?
  3. почему на небесах я получаю максимальную производительность с 3x32, 4x32 и 8x32?!? Я бы ожидал, что 33% процессоров SIMD будут простаивать, а вместо этого похоже, что рабочая группа сидит между двумя вычислительными единицами?!?
  4. почему PREFERRED_WORK_GROUP_SIZE_MULTIPLE вернуться 32, а не 48?
  5. есть ли неэмпирический способ выяснить геометрию для максимальной производительности на любом GPU (ATI/Nvidia/Intel HD), учитывая только то, что я получаю от информационных структур OpenCL?

спасибо заранее

3 ответов


Я ответил на аналогичный вопрос здесь что вы можете найти интересным, прежде чем читать следующее.

почему производительность резко снижается, когда я использую 16 строк в группе?

На самом деле он уже ухудшается при использовании 12 строк. Доступ к памяти работает по сделке. Транзакция будет получать определенное количество байтов за один снимок. Теперь, если несколько workitems пытаются получить доступ к нескольким смежным элементам в массиве, это означает, что одна транзакция может достаточно, чтобы служить им всем.

потому что вы получаете доступ к памяти таким образом:

output[get_global_id(0) * get_global_size(1) + get_global_id(1)] = out;

это означает, что чем больше локальный размер в измерении 0, тем больше будет количество транзакций, так как вам нужно получить доступ к несмежным элементам (разделенным элементами get_global_size(1)). И глобальный доступ к памяти стоит дорого.

таким образом, в случае 12/16 строк, у вас, по крайней мере, 12/16 необходимых операций. Это приведет к твоему второму вопрос:

почему я получаю плохую производительность с 1x48?

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

но здесь возникает проблема холостого хода потоков. Информация, которую вы получили относительно 48 ядер на SM, неверна, как уже указывали другие. Потоки выполняются в группе (называемой warp для NVIDIA) 32 на оборудовании NVIDIA. Эти группы называются wavefront и может быть до 64 потоков для AMD. Поскольку в этом случае рабочая группа состоит из 48 потоков (1 на 48), это означает, что 64 потока запланированы. Это всегда несколько потоков, кратных 32, которые запланированы, потому что вы не можете выполнить часть деформации.

поэтому в этом случае у вас есть четвертая часть потоков, которые ничего не делают. И на самом деле, когда вы сравниваете с результатом, который вы получили для 2x32 (все еще 64 потоков - 2 искривления, но полностью используется) 321 MPixel/s в значительной степени 3/4 из 424 MPixel/s.

стоит отметить и такой результат:2x48: 523 MPixel/s. В этом случае размер рабочей группы 96 кратен 32. Так что никаких холостых потоков.

почему на небесах я получаю максимальную производительность с 3x32, 4x32 и 8x32?!?

ну, ответ приходит из двух предыдущих: вы используете несколько 32, и вы держите количество потоков в измерении 0 относительно небольшим. Но давайте поближе посмотрим на ваш результаты:

2x32:  424 MPixel/s
3x32:  525 MPixel/s
4x32:  525 MPixel/s
8x32:  520 MPixel/s
16x32: 440 MPixel/s

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

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

Итак, как это работает? Возьмем случай 3x32. У вас есть рабочая группа, состоящая из 3 искривлений. Поскольку они принадлежат к той же рабочей группе, они запланированы на том же SM, что и стандарт OpenCL (если бы это было не так, синхронизация между потоками в рабочей группе была бы невозможна).

первый warp начинает работать, пока он не получит остановите, потому что необходим доступ к памяти. Между тем warp 1 ждет завершения транзакций памяти, warp 2 может начать работать. Поскольку на SM много регистров, SM может легко и быстро переключать контекст для запуска других искривлений. Все переменные warp 1 остаются на регистрах, выделенных warp 1. Затем warp 2 попадает в строку, где требуется доступ к памяти, и останавливается. В этот момент далее готов к запуску warp может начать работать. Это может будьте warp 3, но и warp 1, если его доступ к памяти завершен. В вашем случае кажется, что это warp 3, который работает, так как у вас есть разница между 2x32 и 3x32. В первом случае не хватает искривлений, запланированных для скрытия доступа к памяти, хотя во втором случае есть.

по сути, это влияет также на плохую производительность для размера 1x48 из вопроса 2.

почему PREFERRED_WORK_GROUP_SIZE_MULTIPLE возвращает 32 вместо 48?

уже ответил.

есть ли неэмпирический способ выяснить геометрию для максимальной производительности на любом GPU (ATI/Nvidia/Intel HD), учитывая только то, что я получаю от информационных структур OpenCL?

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

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

поэтому продолжайте бенчмаркинг;).


все зависит от кода, который вы не показываете. И это ключ. Если ваш код был очень простым ie:out = 8; тогда ваше предположение, вероятно, быть правильным.

однако, как вы сказали, значение REFERRED_WORK_GROUP_SIZE_MULTIPLE возвращает 32. Это означает, что 32-это максимальные параллельные потоки, которые вычислительный блок может запускать параллельно, не влияя на производительность. Например, нет смысла запускать больше 32. Если с 32 вы уже истощаете локальную память хранения, и вы должны повторяться в глобальной памяти (которая dammly медленно).

Если вы попытаетесь перейти рекомендуемый предел, вы получите именно это -> снижение производительности. Это не то, что 32 лучше, это oposite. 48-это плохо.

рекомендую вам:

  1. используйте автоматический размер, если это возможно (передайте null как локальный размер ядру). Это приводит к максимальной производительности, если вы не беспокоитесь о локальной форме рабочего размера.
  2. использовать REFERRED_WORK_GROUP_SIZE_MULTIPLE в качестве ссылки, если вам нужно установить локальный размер вручную.

способ доступа ядра к глобальной памяти имеет решающее значение и определяется рабочей группой и глобальными измерениями:

  • какие адреса будут записываться последовательными рабочими элементами в одной рабочей группе? Здесь шаг get_global_size(1), Вы можете поменять местами X и Y. обычно быстрее адресовать последовательные элементы в последовательных рабочих элементах. Это самый важный фактор.

  • какие адреса будут написаны последовательные рабочие группы? Последовательные рабочие группы часто планируются одновременно на разных вычислительных единицах. Они могут в конечном итоге конкурировать за тот же канал/БАНК, что приводит к потере производительности.

  • желательно писать 32-разрядные целые числа, а не байты.

чтобы максимизировать производительность, я предлагаю вам ввести больше кнопок для поворота: напишите ядра, вычисляющие блок из нескольких пикселей (например, 4x2) внутри один рабочий элемент, а затем проверьте все комбинации (размер блока) x (размер рабочей группы) x (XY swap) x (размер изображения). Затем выберите лучшее для вашего GPU.