Как определить максимальный размер изображений, которые я могу безопасно передать в/из ядра OpenCL?

Я разрабатываю приложение OpenCL 1.2, которое имеет дело с большими изображениями. На данный момент изображение, которое я тестирую, составляет 16507x21244 пикселей. Ядро запускается в цикле, который работает с фрагментами изображения. Ядро занимает 32bpp (rgba) куски изображения в и пропускает float4-пиксельные фрагменты.

Определите одну сторону (квадрата) куска в пикселях, чтобы быть размером куска. То есть размер 8192x8192 пикселей имеет размер 8192. Конечно, с правой и нижней стороны у нас есть меньшие прямоугольные куски, если изображение не чисто делится на размер куска. Мой код обрабатывает это, но для остальной части этого сообщения мы будем игнорировать это для простоты.

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

Для справки вот информация, указанная утилитой clinfo на моей машине. Я запускаю свое ядро ​​на Geforce GTX 560 Ti с платформой Nvidia, используя их собственные Linux-драйверы.

Мое первоначальное наивное предположение заключалось в том, что я мог работать с максимальным размером изображения 2d. Однако это приводит к тому, что clEnqueueNDRangeKernel возвращает код ошибки -4 (CL_MEM_OBJECT_ALLOCATION_FAILURE).

Размышляя об этом, это имеет смысл для меня. С 1 Гбайт видеопамяти можно было бы ожидать, чтобы иметь возможность содержать одну текстуру 16384x16384 пикселей (32bpp) или текстуру 8192x8192 пикселей (float4). Если оба они должны быть кэшированы на карточке во время работы ядра, мы можем рассчитывать на использование следующего объема памяти:

   4 bytes-per-pixel * chunk size^2 (input image) 
+ 16 bytes-per-pixel * chunk size^2 (output image) 
= 1 GiB total video memory

Решение для размера блока мы получаем

chunk size = sqrt(1GiB/20)

Включение объема памяти, сообщенного OpenCL (который немного меньше, чем 1GiB - 1023 MiB) и настил результата, получаем:

floor(sqrt(1072889856/20)) = 7324

Однако размер куска 7324 по-прежнему приводит к CL_MEM_OBJECT_ALLOCATION_FAILURE.

Мое следующее предположение состояло в том, что мы не можем передать изображение, большее размера max, которое OpenCL сообщает как 268222464 байт для моей карты. Поскольку мое выходное изображение имеет большую ширину пикселя, оно будет определять размер моего блока.

floor(sqrt(268222464/16)) = 4094

Эй, это действительно работает! Теперь, что, если мы попытаемся пойти больше? К моему удивлению, это не подводит. Сквозь проб и ошибок я сузился на 6784 как фактический максимальный размер куска. В 6785 году он начинает жаловаться на CL_MEM_OBJECT_ALLOCATION_FAILURE. Я не знаю, почему max составляет 6784, и я не знаю, является ли это повторяемым или если значение колеблется (например, другое состояние, существующее в видеопамяти, влияющее на то, сколько он может удерживать.) Я также считаю, что работа с размер куска 6784 на несколько секунд медленнее, чем работа с размером, основанным на максимальном распределении. Интересно, это потому, что OpenCL необходимо выполнить несколько (дорогостоящих) распределений под капотом? Я также заметил "максимальный размер аргумента ядра", который OpenCL может сообщить (CL_DEVICE_MAX_PARAMETER_SIZE). Однако эта ценность кажется фиктивной. Если бы я мог пропускать только 4096 байт, это ограничило бы меня 16x16 пикселей!

Итак, у меня остались два фундаментальных вопроса:

  • Как определить абсолютный максимальный размер блока?
  • Как определить самый быстрый размер куска? (Есть ли метод, отличный от проб и ошибок?)

В качестве бонусного вопроса есть ли хорошие ресурсы, на которые я мог бы обратиться в будущих вопросах такого характера относительно низкоуровневых OpenCL-аппаратных взаимодействий?

И, наконец, я дам несколько фрагментов кода для экспертной оценки; Я был бы чрезвычайно благодарен за любую конструктивную критику!

Как всегда, заранее за помощь!

Ответ 1

Чтобы ответить на ваши прямые вопросы:

1) Чтобы определить абсолютный максимальный размер куска, который можно использовать для одной операции с ядром, нужно знать, к какому "размеру куска" относится. Например, в структуре памяти OpenCL имеется пять определенных моделей памяти. Одна из них - память хоста, которую мы будем игнорировать. Остальные четыре являются глобальными, постоянными, локальными и частными.

Чтобы получить какую-либо информацию о вашем оборудовании в отношении того, что он может поддержать, я настоятельно рекомендую перейти к документам API Khronos, записанным внизу. На вашем устройстве собрано множество метаданных, которые вы можете собрать. Например, есть запросы для максимальной высоты и максимальной ширины изображения в 2D и/или 3D, которые устройство может поддерживать. Я также предложил бы взглянуть на CL_DEVICE_LOCAL_MEM_SIZE и CL_DEVICE_MAX_COMPUTE_UNITS, чтобы определить ваши рабочие группы. Допускается даже запрос CL_DEVICE_MAX_MEM_ALLOC_SIZE.

Чтобы подчеркнуть свою озабоченность по поводу производительности, причина в том, что размер памяти, предоставляемый вам для работы, - это самый оптимальный размер для рабочей группы или элемента (в зависимости от запроса). То, что может происходить, - это переполнение памяти в глобальное пространство. Это требует большего объема памяти для разных работников, что приводит к снижению производительности. На этом утверждении не 100%, но это может быть очень важно, если вы превысите рекомендуемый размер буфера.

2) Чтобы определить самую быструю версию размера блока и ошибку, не требуется. В книге "Руководство по программированию OpenCL", опубликованной Addison-Wesley, есть раздел об использовании событий для профилирования в главном приложении. Есть множество функций, которые разрешены для профилирования. Эти функции следующие:

  • clEnqueue {Read | Write | Карта} Buffer
  • clEnqueue {Read | Write} BufferRect
  • clEnqueue {Read | Написать | Карта} Изображение
  • clEnqueueUnmapMemObject
  • clEnqueueCopyBuffer
  • clEnqueueCopyBufferRect
  • clEnqueueCopyImage
  • clEnqueueCopyImageToBuffer
  • clEnqueueCopyBufferToImage
  • clEnqueueNDRangeKernel
  • clEnqueueTask
  • clEnqueueNativeKernel
  • clEnqueueAcquireGLObjects
  • clEnqueueReleaseGLObject

Чтобы включить это профилирование, при создании очереди необходимо установить флаг CL_QUEUE_PROFILING_ENABLE. Затем функция clGetEventProfilingInfo (cl_event event, cl_profiling_info param_name, size_t param_value_size, void * param_value, size_t * param_value_size_ret); может использоваться для извлечения временных данных. Затем вы можете использовать приложение-хост с этими данными, как вам удобно, например:

  • Записать в журнал профилирования
  • Записать в выходной буфер
  • Load-Balance

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

Бонусный вопрос Некоторые хорошие ресурсы будут "Руководством по программированию OpenCL", опубликованным Аддисоном Уэсли, написанным Афтабом Мунши, Бенедиктиком Р. Гастером, Тимоти Г. Маттсоном, Джеймсом Фунгом и Дэн Гинзбургом. Я также хотел бы сказать, что Khronos docs содержит много информации.

В качестве дополнительной заметки. Вы запускаете это ядро ​​внутри двоичного вложенного цикла в главном коде... этот вид разбивает всю причину использования параллельного программирования. Особенно на изображении. Я бы предложил рефакторинг вашего кода и исследование моделей параллельного программирования для операций GPU. Также сделайте некоторые исследования по настройке и использованию Memory Barriers в OpenCL. У Intel и Nvidia есть отличные документы и примеры в отношении этого. Наконец, документы API всегда доступны