Использование технологии OpenCL для разработки высоконагруженных приложений

Автор: Манушин Игорь Александрович
Опубликовано: 28.05.2012
Версия текста: 1.0
1. Введение.
2. Технические особенности GPU
2.1 Общая информация
2.2 Производители
3 OpenCL
3.1 Общая информация
3.2 Архитектура
3.3 Масштабируемость
4 Примеры программ на OpenCL
4.1 Hello World
4.2 SIMD-операции
4.3 Использование встроенных функций
4.4 Использование различных буферов для памяти
4.5 Оценка времени чтения из памяти
4.6 Вывод
5 Список литературы

1. Введение.

В современных компьютерах есть два наиболее производительных элемента – это центральный процессор и видеокарта. Главное различие в вычислениях на них заключается в том, что в видеокарте процессоры более медленные и поддерживают меньшее количество команд, зато их в разы больше. Например, процессор AMD Phenom II имеет тактовую частоту в единицы гигагерц (2-4 ГГц) и 3-6 ядер. Если брать видеокарты в том же ценовом диапазоне, то у них будет тактовая частота порядка 0.4-1 Ггц, и количество ядер будет от 200 до 1000. Соответственно, подход к решению задач также будет иным.

Кроме этого, видеокарты поддерживают ряд встроенных функций, работающих быстрее, чем на центральном процессоре (об этом будет сказано в разделе ATI/AMD.

Технологии вычисления на GPU (graphics processing unit) можно разделить на несколько категорий:

  1. Прямое использование шейдеров (например, GLSL [1]). В этом случае входные и выходные данные интерпретируются как текстуры, а для вычислений используются шейдеры видеокарт. Этот подход достаточно трудоемок для разработки, зато он может быть наиболее быстрым, если идет разработка под конкретные видеокарты (под конкретную модель), и задача допускает разбиение на независимые локальные участки.
  2. Использование специализированных программных интерфейсов от производителей видеокарт (CUDA от NVidia [2], Fire Stream от AMD/ATI [3]). Эти средства создавались специально для неграфических вычислений на видеокартах. Также они изначально оптимизированы под конкретную архитектуру.
  3. Использование кросс-аппаратных средств: OpenCL, Direct Compute. Обе библиотеки работают не напрямую с устройствами, а используют специальные драйверы под каждого производителя. Например, OpenCL под NVidia работает за счет перенаправления вызовов в CUDA [4]. Основным достоинством этих библиотек является независимость от аппаратной части. Они обе могут работать как на видеокарте, так и на центральном процессоре.

Из вышеописанных средств разработки под графический процессор наиболее распространена CUDA в силу того, что она раньше была введена в использование и за счет активного маркетинга со стороны NVidia. В данной статье представлены основные особенности архитектуры графического контроллера и описаны некоторые подробности разработки под OpenCL.

2. Технические особенности GPU

2.1 Общая информация

На данный момент на рынке присутствует несколько производителей видеокарт для компьютеров: AMD/ATI, NVidia, Intel. Остальные производители, например, Imagination [5] [6], разрабатывают оборудование для мобильных устройств, и их рассмотрение не входит в данную статью, во многом из-за малой производительности видеочипов.

Схематичную архитектуру вычислений на GPU можно представить как иерархию (см. Рисунок 1). Как видно на схеме, если вычисления реализованы для GPU, то они могут также выполняться и на центральном процессоре. Более того, все разработки программных решений с использованием графического ядра имеют важную настройку – на каком устройстве или группе устройств необходимо запускать вычисления. По этой причине одним из возможных режимов работы является автоматический выбор устройства. В этом случае вычисления будут запущены устройстве, располагающем наибольшим количеством ресурсов. Следовательно, если у компьютера маломощная видеокарта, и к тому же часть её ресурсов занята обработкой дисплея, то в качестве устройства может быть выбран центральный процессор.


Рисунок 1. Иерархия модулей

2.2 Производители

2.2.1 Intel

У Intel все видеокарты разрабатывались для низкого энергопотребления [7], поэтому по производительности они пока слабее других участников рынка.

На данный момент (январь 2012) самая мощная видеокарта от Intel – это Intel GMA X4500, которая имеет 10 шейдеров (следовательно, 10 независимых потоков), каждый из которых имеет тактовую частоту 400 МГц.

У Intel есть другая особенность: процессоры Core i имеют поддержку OpenCL [8], а значит, для разработки можно использовать технологию OpenCL. В этом случае приложение в дальнейшем может быть с низкими затратами масштабировано для работы на более новом оборудовании. Подробнее этот вопрос будет рассмотрен в разделе " Масштабируемость" 0.

2.2.2 NVidia

2.2.2.1 Общая информация

Исторически NVidia первая стала развивать и популяризировать использование видеокарт для задач, не связанных с обработкой изображения. NVidia первой выпустила технологию CUDA для создания приложений для видеокарт, без использования шейдеров [2]. В дополнение к этому у NVidia есть ряд видеокарт, изначально приспособленных для профессиональной обработки 3D-графики (см. серию Quadro [9]). Их видеопроцессоры приспособлены не для быстрой работы в реальном времени, а для высокопроизводительной обработки трехмерных объектов. Потом, на их основе, была сделана линейка Tesla – это видеокарты, которые, по замыслу разработчиков, должны использоваться для суперкомпьютеров. Внутри у них используется всё то же ядро GPU, но убраны функции вывода на экран. Вместо этого увеличено количество процессоров и скорость их работы с памятью.

Используя технологию CUDA, можно объединять в единый вычислительный массив не только видеокарты на одном компьютере [10], но и видеокарты нескольких компьютеров, получая при этом вычислительный кластер [11]. На данный момент суперкомпьютер, занимающий второе место по скорости вычислений в мире (Tianhe-1A, Китай), использует именно технологию NVidia Tesla [12].

2.2.2.2 Архитектура вычислительного блока.

Архитектуру вычислительного блока можно представить в виде иерархии:

  1. Разделение на различные компьютеры.
  2. Разделение на различные устройства.
  3. Разделение на рабочие группы (Work Group в контексте OpenCL или Multiprocessor в контексте CUDA).
  4. Разделение на потоки.

Описание разделения вычислений на различные компьютеры (используется топология грид [13]) не входит в рамки данной статьи. Современные видеокарты NVidia, в зависимости от модели, поддерживают от четырех до девяти компьютеров в одном кластере [13].

Разделение на различные устройства. Его можно задать как вручную, так и определять автоматически. Здесь присутствует важный параметр: объединены ли видеокарты с помощью технологии NVidia SLI. Если да, то они могут быть представлены как единый массив, что увеличивает параллелизм вычислений. Если же устройства не объединены, обычно используется самая быстрая видеокарта, вне зависимости от нагрузки на неё.

Разделение на рабочие группы. Все процессоры видеокарт NVidia объединяются в рабочие группы, и одна рабочая группа может выполнять в один момент только одну функцию (которая должна выполниться N раз для N различных параметров). Более того, в момент исполнения одна и та же инструкция выполняется на всех процессорах. Рабочая группа имеет важное преимущество: общую память. Она ограничена 16 килобайтами и имеет 8-48 рабочих потоков (warp size в терминологии CUDA) – эти данные верны для всех устройств NVidia на данный момент [2] [14]. Количество потоков зависит от версии CUDA API (см. Таблица 1) [4].

Версия CUDA API

1.0 – 1.3

2.0

2.1

Количество рабочих потоков

8

32

48

Таблица 1. Количество рабочих потоков в зависимости от CUDA API

Разделение на потоки. Каждому рабочему процессору могут соответствовать один или два потока, в зависимости от того, включено или выключено энергосбережение. Также количество потоков на одном процессоре может меняться в зависимости от трудоемкости задачи (проверялось экспериментально, не подтверждено документацией или иными официальными источниками). Один процессор имеет 8 Кбайт или 16 Кбайт внутренней памяти (Registers Per Kernel в терминологии CUDA и Private Memory в терминологии OpenCL) [2] [14]. В новых моделях процессоры также обладают функцией Hyper Threading, то есть параллельное выполнение нескольких потоков на одном процессоре.

В итоге, архитектура построения вычислений на графическом процессоре у NVidia имеет следующую иерархию:

Разделение задачи на компьютеры: до девяти компьютеров (для серии NVidia Tesla [11]); Разделение задачи на различные устройства: до четырех независимых видеокарт (для серии NVidia Tesla [11]); Разделение задачи между рабочими группами: до 24 рабочих групп на данный момент (см. модель NVidia GeForce GTX 590 [15]); Разделение задачи на потоки: от 8 до 48 потоков в рабочей группе. Этот параметр не фиксирован и может меняться в зависимости от используемой версии CUDA [2].

Тактовая частота одного процессора доходит до 1.5 Ггц (см. модель NVidia Tesla S2050).

2.2.3 ATI/AMD

2.2.3.1 Общая информация

В 2006 году компания ATI Technologies была куплена компанией Advanced Micro Devices, Inc. (AMD), что привело к тому, что сейчас в разных источниках фигурируют разные названия одних и тех же элементов, например, ATI CrossFire вместо нового названия AMD CrossFire. Эта путаница связана с тем, что после покупки ряд технологий для видеокарт маркировался брендом AMD (например, AMD Vision [16]), хотя бренд для видеокарт остался прежним ATI (например, ATI Radeon [17]).

AMD до сих пор не развивает направление кластерных вычислений на графических процессорах, поэтому по популярности вычислений на GPU она отстает от NVidia. Если же рассмотреть технические характеристики отдельных продуктов от AMD и NVidia, близких по стоимости, то оказывается, что производительность видеокарт от AMD в разы превышает производительность аналогичных видеокарт от NVidia (см. табл. Таблица 2).

Параметр

Asus на NVidia GeForce GTX 590 [15]

Asus на ATI Radeon HD6990 [3]

Объем памяти

3072 МБайт

4096 Мбайт

Тип памяти

GDDR5

GDDR5

Частота памяти

3420 MГц

5000 МГц

Частота выборки из памяти

855 MГц

1250 МГц

Частота процессора

612 МГц

830 МГц

Количество процессоров

1024

1536

Hyper Threading

Есть

Отсутствует

Таблица 2. Сравнение видеокарт от NVidia и ATI

На проведенных экспериментах видеокарта от ATI опережала видеокарту от NVidia, если почти полностью исключить работу с памятью и оставить только вычисления. Подобная конфигурация алгоритма используется, например, при взломе хеша MD5 [18], так как в нем сначала считывается небольшой объем данных из памяти, и потом следуют долгие вычисления.

2.2.3.2 Архитектура вычислительного блока

Как уже упоминалось выше, у AMD нет встроенной функциональности для распределенных вычислений, так что все операции могут рассматриваться только в рамках вычислений на одном компьютере.

Иерархия модулей похожа на схему из NVidia:

Разделение на различные устройства. Также поддерживается как в автоматическом режиме, так и в ручном. Разделение на рабочие группы (Work Group в контексте OpenCL или Wavefront в контексте архитектуры видеокарты ATI [17]) Разделение на потоки.

Разделение задач на различные устройства сделано по аналогии с NVidia. Единственное серьезное отличие в том, что в случае работы с платформой AMD, процессор считается также одним из устройств, а значит, он может быть задействован для вычислений (если тип устройства не проставлен явно).

Максимальное количество рабочих групп у AMD малое: до 20 включительно. Обычно его размер изменяется в зависимости от архитектуры ядра видеопроцессора [19]:

В первых ревизиях устанавливается количество рабочих групп как N и фиксируется. Относительно него вычисляется количество процессоров в рабочей группе (обозначим как M); В следующих ревизиях, при увеличении числа процессоров, N остается прежним, увеличивается M; При создании новой версии ядра сбрасывается номер ревизии, и алгоритм повторяется в первого шага.

Таким образом, можно считать, что в следующих версиях количество рабочих групп останется прежним, и будет увеличиваться только их размер [17]. Также следует учитывать факт, что одна рабочая группа может обрабатывать только одну функцию (в контексте OpenCL). Следовательно, вычисление маленького количества параллельных потоков в одной задаче приведет к простою ресурсов.

Объем общей памяти у одной рабочей группы – 16 или 32 Кбайт, в зависимости от модели [20].

3 OpenCL

3.1 Общая информация

Технология OpenCL задумывалась как кроссплатформенный аналог технологии DirectCompute и как кроссаппаратный аналог CUDA. Архитектура её вычислений очень похожа на внутреннюю архитектуру видеокарт. Технология разрабатывается компанией Khronos Group. Эта компания является консорциумом разработчиков, куда входят Microsoft, Apple, AMD, NVidia, Google и ряд других производителей оборудования и программного обеспечения, и занимается такими технологиями как OpenGL и WebGL.

3.2 Архитектура

3.2.1 Аппаратная архитектура

3.2.1.1 Распределенные вычисления

Технология OpenCL не поддерживает распределенных вычислений. Эту проблему можно решить путем подмены библиотек OpenCL. Таким образом, получается архитектура, изображенная на рис. 2 [21]. Пока есть только исследования на эту тему, коммерческое решение еще не создано.


Рисунок 2. Архитектура распределенного OpenCL

3.2.1.2 Потоки

Все потоки на видеокарте делятся на группы (Work Group). В разные промежутки времени размер групп может отличаться. Это связано с Hyper Threading`ом и с функциями энергосбережения. Также их размер различается на устройствах различных производителей, но, например, запуская программу на видеокарте NVidia, можно быть уверенным, что максимальный размер одной рабочей группы будет 48 потоков (см. выше). Аналогично можно подсчитать для AMD (см. выше).

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

3.2.1.3 Память

Всю память в OpenCL можно разделить на три категории:

  1. Глобальная (global). Она доступна всем потокам, и через неё идет обмен данными с центральным процессором. Её размер обычно равен размеру доступной памяти видеокарты.
  2. Локальная (local). Это память рабочей группы. Её размер – 16-32 Кбайт, в зависимости от производителя (у NVidia – 16 Кбайт, у AMD – 16-32 Кбайт). Она общая для всех потоков внутри рабочей группы и недоступна для других рабочих групп.
  3. Внутренняя (private). Это память отдельного потока (не процессора – это важно). Её размер также зависит от производителя и варьируется от 8 до 16 Кбайт. Если процессор выполняет несколько потоков, то память будет разделена. Соответственно, если включить Hyper Threading, то размер памяти для каждого потока будет уменьшен вдвое.

3.2.2 Программная архитектура

3.2.2.1 Функции для программ общего назначения

В данном разделе будут кратко описаны функции для запуска приложений на видеокарте. Всех их можно разделить на две категории:

  1. Подготовка к запуску программ. Вызываются редко, перед вычислениями.
  2. Запуск вычислений. Вызываются каждый раз для запуска вычислений.

3.2.2.1.1 Подготовка к запуску программ.

Перед тем как запускать приложение на OpenCL, необходимо:

  1. Выбрать платформу для запуска (NVidia, AMD, Intel и пр.);
  2. Выбрать устройства, на которых будут выполняться вычисления;
  3. Скомпилировать программу под выбранные устройства;
  4. Определить основную функцию, которая и будет запускаться (kernel).

Пункты 3 и 4 выполняются долго: до нескольких сотен миллисекунд. Желательно вызвать их один раз в течение всей работы программы.

Исходный код для программы, которая будет запускаться на графическом процессоре, может содержать несколько функций, называемых kernel – они будут доступны для запуска вне графического процессора. На данный момент компиляторы NVidia, AMD, Intel поддерживают только один kernel внутри одного файла (проверено экспериментально), хотя в документации утверждается, что их может быть несколько [14].

3.2.2.2 Поддерживаемые типы данных

OpenCL поддерживает следующие типы данных: char, short, int, long, float, double. Также для целочисленных типов поддерживаются их беззнаковые аналоги (с префиксом u – uchar и пр.). Вычисления с типом double поддерживается не всеми устройствами, и программы должны компилироваться с указанием определенного ключа.

OpenCL поддерживает SIMD-операции. Для этого большинство функций поддерживает операции с векторами по 1,2,4,8 и 16 значений базового типа. При этом для типа указывается суффикс, соответствующий размеру вектора. Например, char16.

На данный момент, при выполнении вычислений на видеокарте NVidia SIMD-операции разворачиваются в несколько обыкновенных, хотя CUDA поддерживает SIMD-операции, а OpenCL на этих видеокартах работает над CUDA [2].

При вычислениях на устройствах AMD и Intel (как на видеокартах, так и на центральных процессорах) поддерживаются SIMD-операции для всех типов, если размер вектора не превышает размер шины памяти (для современных видеокарт он равен 128 или 256 битам).

3.2.2.3 Встроенные функции в OpenCL

Функции, которые поддерживаются стандартом OpenCL, можно разделить на две части:

  1. Аналоги из CPU: сложение, умножение и пр.
  2. Специфичные для обработки видеоизображений: например, «a*b+c», «(a+b)/2» и пр. Также поддерживаются математические функции (cos, sin и пр). Основное достоинство дополнительных функций - это высокая скорость работы на числах с плавающей точкой, так как большинство этих функций поддерживается на аппаратном уровне [14].

3.3 Масштабируемость

Учитывая сказанное выше, можно сделать ряд выводов:

  1. Если задача допускает распараллеливание на достаточно большое число потоков (в разы большее, чем количество нитей в рабочей группе), то программа будет масштабироваться на более новых устройствах.
  2. Если видеокарты объединены в единый массив, то их вычислительные мощности могут объединяться.
  3. В OpenCL нет встроенного решения для распределения задач между серверами.

4 Примеры программ на OpenCL

В этом разделе будет рассмотрен ряд программ на OpenCL. В основном, будут преследоваться следующие цели:

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

4.1 Hello World

В этом разделе будет описано приложение, которое будет просто суммировать два целочисленных массива и записывать результат в третий.

Весь код разделен на две части:

  1. GPU-модуль. Он содержит функцию, которая компилируется под конкретную видеокарту и запускается на ней.
  2. CPU-модуль. Он отвечает за конфигурацию и запуск GPU-модуля.

4.1.1 Модуль GPU

Листинг кода для GPU (kernel-функция):

          __kernel
          void hello( 
  __global __read_onlyconstshort* left, 
  __global __read_onlyconstshort* right, 
  __global __write_onlyshort* output )
{
  int index = get_global_id(0);
  
  output[ index ] = left[ index ] + right[ index ];
}

В данной функции выполняются действия:

  1. Объявление трех массивов – два для чтения и один для записи.
  2. Получение номера текущего потока. Будем считать, что каждый поток производит ровно одну операцию, и, следовательно, id потока будет соответствовать индексу в массивах.
  3. Сложение элементов в массиве.

Сразу отмечу ряд отличий синтаксиса кода для GPU:

  1. Функция, которая может быть запущена из CPU, должна иметь модификатор __kernel.
  2. Аргумент, который содержит переменную, доступную для внешней программы, должен иметь модификатор __global.
  3. Аргумент может иметь модификаторы __read_only или __write_only. В этом случае компилятор будет модифицировать код таким образом, чтобы записи в эти переменные не происходило. Функция get_global_id(int) возвращает номер потока. Входной аргумент может иметь значения от нуля до двух. Максимальный номер потока – 255, минимальный – 0. Таким образом, суммарно может быть до 2563 потоков.

4.1.2 Модуль CPU

Выполнение программы для запуска функций на GPU можно представить в виде следующего алгоритма:

  1. Подготовка к запуску kernel-функции: выбор устройства, создание очереди, компиляция программы и т. д.
  2. Последовательные запуски kernel-функций, ожидание их выполнения.
  3. Вывод результата.
  4. Освобождение ресурсов.

Все пункты, кроме второго, не будут представлены, так как их рассмотрение выходит за рамки данной статьи. Соответственно, ниже представлен код, который получает на вход сконфигурированное устройство и созданную очередь.

          int* ArraySumm( 
short * left, 
short * right, 
int size, 
cl::Context context, 
cl:Queue queue )
{
    short* outputData = new short[size];
 
    int arrayItemSize = sizeofshort );
 
    int error = 0;
 
    cl::Buffer leftBuffer( context, CL_MEM_READ_ONLY, size * arrayItemSize, NULL, &error );
    cl::Buffer rightBuffer( context, CL_MEM_READ_ONLY, size * arrayItemSize, NULL, &error );
    cl::Buffer resultBuffer( context, CL_MEM_WRITE_ONLY, size * arrayItemSize, NULL, &error );
 
    error |= queue.enqueueWriteBuffer( leftBuffer, false, 0, size * arrayItemSize, left );
    error |= queue.enqueueWriteBuffer( rightBuffer, false, 0, size * arrayItemSize, right );
 
    error |= rowKernel.setArg( 0, leftBuffer );
    error |= rowKernel.setArg( 1, rightBuffer );
    error |= rowKernel.setArg( 2, resultBuffer );
 
    error |= queue.enqueueBarrier();
 
    error |= queue.enqueueNDRangeKernel( rowKernel, cl::NullRange, cl::NDRange( size ), cl::NullRange );
 
    error |= queue.enqueueBarrier();
 
    error |= queue.enqueueReadBuffer( resultBuffer, true, 0, size * arrayItemSize, outputData );
 
    error |= queue.finish();
 
    return outputData;
}

Суть работы данной функции:

В памяти видеопроцессора выделяется пространство под буферы для чтения или записи данных. В очередь добавляется задание для копирования входных данных в память видеокарты. Заполняются аргументы для kernel-функции. В очередь добавляется вызов kernel-функции, с количеством потоков равным resultSize. В очередь добавляется задача чтения данных с видеокарты. Ожидание завершения всех задач в очереди. Неявный вызов деструкторов для очищения памяти видеокарты (деструкторы объектов cl::Buffer). Выдача результата.

Также в функции присутствует переменная error, которая никак не проверяется. Это сделано для уменьшения количества кода. При запуске с включенным отладчиком можно отслеживать изменение её значения: когда оно перестанет быть равным нулю, в ней будет записан код ошибки.

4.1.3 Вывод

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

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

4.2 SIMD-операции

В этой главе будет рассмотрено использование SIMD-операций. SIMD расшифровывается как single instruction, multiple data. Это означает, что одна инструкция оперирует сразу несколькими значениями, вектором значений.

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

4.2.1 GPU модуль

В качестве функции возьмем следующую:

          __kernel  void simd( 
    __global __read_only const short* input,
    __global __write_only short* output )
{
    int index = get_global_id(0);
 
    short result = 0;
 
    forint i = 0; i < 1048576; i++ )
    {
        result += ( ( ( ( ( input[ index ] - result ) * 5 ) + 2 ) / 7 ) - 4 );
    }
 
    output[ index ] = result;
}

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

После добавления SIMD-операций эта функция выглядит следующим образом:

          __kernel  void simd( 
    __global __read_only const short16* input,
    __global __write_only short16* output )
{
    int index = get_global_id(0);
 
    short16 result = 0;
 
    forint i = 0; i < 1048576; i++ )
    {
        result += ( ( ( ( ( input[ index ] - result ) * (short16)(5) ) + (short16)(2) ) / (short16)(7) ) - (short16)(4) );
    }
 
    output[ index ] = result;
}

Фактически, тип данных short был заменен на short16. И поэтому ряд операций выполняется сразу над вектором значений.

4.2.2 Модуль CPU

Модуль CPU во многом аналогичен модулю из раздела 4.1.2, но у него есть очень важное отличие: так как один поток оперирует сразу с 16-ю значениями, суммарное количество потоков должно быть меньше в 16 раз.

          short* GpuCalculator::CalculateMatrix(short* input, int size)
{
    short* outputData = new short[size];
 
    int arrayItemSize = sizeofshort );
 
    int error = 0;
 
    cl::Buffer inputBuffer( context, CL_MEM_READ_ONLY, size * arrayItemSize, NULL, &error );
    cl::Buffer resultBuffer( context, CL_MEM_WRITE_ONLY, size * arrayItemSize, NULL, &error );
 
    error |= queue.enqueueWriteBuffer( inputBuffer, false, 0, size * arrayItemSize, input );
 
    error |= rowKernel.setArg( 0, inputBuffer );
    error |= rowKernel.setArg( 1, resultBuffer );
 
    error |= queue.enqueueBarrier();
 
    error |= queue.enqueueNDRangeKernel( rowKernel, cl::NullRange, cl::NDRange( size / 16 /*Размер SIMD вектора*/ ), cl::NullRange );
 
    error |= queue.enqueueBarrier();
 
    error |= queue.enqueueReadBuffer( resultBuffer, true, 0, size * arrayItemSize, outputData );
 
    error |= queue.finish();
 
    return outputData;
}

4.2.3 Результаты

Следуя спецификации OpenCL, за счет массовых операций может происходить ускорение вычислений [14]. Все вычисления производились на видеокартах:

  1. NVidia GTS 450 – 192 независимых процессора, тактовая частота одного процессора – 850 МГц;
  2. ATI Radeon 5700 – 800 независимых процессоров, тактовая частота одного процессора – 850 МГц.

В качестве процессора использовалось одно ядро процессора AMD Phenom 9950 (2,6 ГГц).

Во всех тестах запускалось 256 потоков: то есть на видеокарте ATI работала только треть процессоров, на видеокарте NVidia некоторые процессоры обрабатывали последовательно два потока. В случае скалярных операций каждый поток делал вычисления сразу с несколькими элементами: видеокарта делала их параллельно, центральный процессор – последовательно.

Результаты вычислений представлены в таблицах Таблица 3, Таблица 4. Для видеокарт NVidia использовать SIMD не выгодно, так как процессоры внутри – скалярные. Соответственно, для увеличения скорости работы, NVidia советует использовать SIMT-вычисления (single instruction, multiple thread). Суть этого подхода в том, что несколько потоков выполняют одну и ту же инструкцию, например, параллельно копируют данные из глобальной памяти в локальную. ATI отходит от этой схемы: в новых видеокартах (например, в ATI Radeon 7970 [22]) каждое ядро может выполнять инструкции независимо от остальных ядер. Об этом подходе будет подробнее изложено в разделе 4.3.

GPU без SIMD

GPU – SIMD

4 операции

16 операций

CPU

5 990 мс

23 072 мс

91 915 мс

NVidia

94 мс

1 072 мс

4 368 мс

ATI

312 мс

406 мс

1 389 мс

Таблица 3. Статистика вычислений с использованием SIMD-операций: время вычисления

GPU без SIMD

GPU – SIMD

4 операции

16 операций

CPU

100%

100%

100%

NVidia

1.5%

4.6%

4,7%

ATI

5%

1.7%

1.5%

Таблица 4. Статистика вычислений с использованием SIMD-операций: доля по времени вычисления по отношению к времени работы центрального процессора

На видеокарте ATI SIMD-операции над short дают прирост в четыре раза (примерно). Более того, при увеличении размера вектора скорость не уменьшается (как у видеокарт NVidia), так что если в будущем ATI увеличит длину вектора SIMD-операций, то скорость работы программ с большими векторами вырастет. Следовательно, в программах для ATI следует использовать наибольший размер вектора (сейчас для OpenCL он равен 16 [14]).

4.3 Использование встроенных функций

На видеокарте есть набор функций, специфичных для работы с графикой. Эти функции выполняют сразу несколько вычислений за один такт процессора. В соответствии со спецификацией, рекомендуется использовать встроенные функции, так как в дальнейшем скорость их работы может быть увеличена за счет аппаратной поддержки [14].

В данном разделе я буду сравнивать только скорость работы GPU функций: с использованием встроенных функций и без.

4.3.1 GPU модуль

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

          __kernel  void InternalFunctions( 
    __global __read_only const short* input,
    __global __write_only short* output )
{
    int index = get_global_id(0);
 
    short result = 0;
 
    short first = 5; 
    short second = 2; 
    short third = 7; 
    short fourth = 4; 
 
    forint i = 0; i < 1048576; i++ )
    {
  /*внутренний код цикла, повторенный 16 раз*/ 
    }
 
    output[ index ] = result;
}

В случае использования встроенных функций внутренний код цикла будет выглядеть так:

result = mad_sat( result, first, second ); 
result = abs_diff( result, third ); 
result = hadd( result, fourth );

В случае использования стандартных функций код цикла будет выглядеть:

result = result * first + second; 
result = abs( result - third );            
result = ( result + fourth ) >> 1;

Для более корректного результата внутренний код в цикле был повторен 16 раз.

4.3.2 Результаты

Результаты проводились на том же оборудовании, которое использовалось в разделе 4.2.3. Как видно из таблицы Таблица 5, на данный момент ускорение за счет встроенных функций отсутствует. Зато встроенные функции работают по скорости как минимум не медленнее, чем стандартные, следовательно, если верить спецификации [14], в дальнейшем возможен прирост производительности. Возможно, компилятор видеокарты применял оптимизационную свертку, что привело к тому, что скорость работы оставалась неизменной.

NVidia

ATI

Стандартные функции

852 мс

3 494 мс

Встроенные функции

851 мс

3 510 мс

Таблица 5. Сравнение вычислений с использованием встроенных функций и без них.

4.4 Использование различных буферов для памяти

В OpenCL присутствуют функции для работы с общей памятью для GPU и CPU. Если в компьютере используется встроенная видеокарта, то общая память теоретически может быть использована без копирования. Несмотря на это, OpenCL не предоставляет такой функциональности, так как считается, что память для CPU и память для вычислительного устройства (в текущем контексте – GPU) разделена, и не может быть общей [14].

Чтобы всё-таки ассоциировать память на двух устройствах, можно использовать функцию «enqueueMapBuffer», которая делает две операции:

  1. Выделяет на устройстве необходимый блок памяти
  2. Выдает ссылку на массив, который ассоциирован с памятью на устройстве.

Большей информации спецификация OpenCL не дает, а для различных реализаций драйвера существуют свои оптимизации по работе с общей памятью. Например, NVidia позволяет создавать общую память в соответствии со спецификацией в OpenCL, а на платформах х86 может выделяться общая память для видеокарты и процессора, причем при выполнении функций с этим буфером, память блокируется [23]. Суммарно, работа с памятью будет выглядеть следующим образом:

  1. Инициализация (для каждого прикреплённого буфера):
  1. Работа программы:
  1. Удаление использованных объектов.

Схема, описанная выше, используется потому что:

Также рекомендуется еще одна оптимизация: подготовка буферов памяти перед непосредственной работой алгоритма. Чем раньше это будет сделано, тем больше у видеопроцессора будет времени для асинхронного выделения памяти. К тому же происходит экономия на операции непосредственного выделения памяти.

Количество строк обработки

Каждый раз создаем объект

Используем созданные объекты

Используем предварительно-созданные pinned-массивы

2048

15

16

16

4096

47

48

31

8192

78

78

47

16384

156

156

109

32768

328

297

218

65536

655

624

453

Таблица 6. Сравнение скорости работы при использовании разных буферов памяти (мс)

Как видно из таблицы 6, обе оптимизации дают увеличение производительности. Тесты производились на задаче обработки фильтра 9-го порядка (подробнее – см. раздел 4.4). В тестировании участвовали массивы по 1920 байт. 2048 строки соответствуют 2048 таких массивов. Фактически, это два кадра видео формата 1080р.

В дополнение следует отметить, что резервирование большого объема памяти может негативно сказаться на производительности системы в целом, так как другим приложениям эти ресурсы могут потребоваться [23].

4.5 Оценка времени чтения из памяти

В данном разделе я рассмотрю реальную задачу оптимизации, с использованием OpenCL и вычислений на GPU. Особый интерес в ней представляет работа с памятью, так как на неё тратится значительное время.

Постановка задачи: требуется сделать фильтр 9-го порядка для изображений, с помощью которого размер изображения уменьшается в два раза. Размер изображения: 1920 х 1080. Сжатие происходит сначала по всем строкам (параллельно), потом по столбцам (параллельно).

В памяти изображение хранилось как:

  1. До работы алгоритма: хранились последовательно строки.
  2. После масштабирования по строкам и перед сжатием по столбцам: хранились последовательно столбцы (фактически, транспонированная матрица по сравнению с предыдущей схемой)
  3. После работы алгоритма: хранились последовательно строки.

Такой формат был выбран, чтобы чтение данных происходило последовательно из одного блока памяти. Эта схема была самой быстрой для центрального процессора, при условии, что процессор при чтении данных из памяти, помещает в кэш соседние участки памяти.

Масштабирование происходило по строкам и по столбцам одинаковым способом, но использовались разные коэффициенты для фильтров. На каждом шаге считывалось два значения (в формате char), производился подсчет (при условии того, что в памяти хранятся предыдущие результаты), и в результирующий массив записывалось одно значение (char). Само вычисление представляло собой перемножение двух векторов из 9 элементов.

Полный алгоритм представлял собой следующую последовательность действий:

Вычисление краевых условий (9 элементов с каждого края – 1-2% от общего количества пикселов); Цикл: Чтение двух байт из глобальной памяти (подробнее о памяти см. выше0); Сдвиг элементов внутри массива; Вычисление результата; Запись в глобальную память одного байта.

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

Краевые условия: 90 мс; Цикл (суммарно около 1 600 мс): Чтение из памяти: около 1 500 мс; Сдвиг элементов: меньше погрешности (в среднем около 5 мс); Вычисление результата: 110 мс; Запись в память: меньше погрешности (в среднем около 5 мс);

Как видно, основное время работы уходило на чтение данных из глобальной памяти. В предыдущих разделах, чтобы подавить погрешность вызванную обращением к глобальной памяти, каждый поток высчитывал 220 операций на одно чтение из памяти.

Такое большое время чтения из памяти получается из-за того, что каждый процессор использует свой блок памяти, что приводит к тому, что локальный кеш группы процессоров (warp), который общий на 8-48 процессоров, не содержит общих данных.

Алгоритм чтения из памяти на видеокартах NVidia работает по схеме, изображенной на рис. 3. То есть, при чтении в локальную память возникает конфликт, так как каждому ядру требуются разные блоки в памяти, удаленные друг от друга. Соответственно, если запускать обработку так, чтобы ядра в одной рабочей группе работали с близкими данными, то можно увеличить производительность за счет кеширования внутри каждой группы (см. рис. 4). Следовательно, основная цель оптимизации: снизить количество копирований из глобальной памяти в локальную.


Рисунок 3. Алгоритм работы с памятью


Рисунок 4. Оптимизированный алгоритм работы с памятью

В таблице 7 представлено сравнение времени выполнения программ с разными способами распределения ресурсов. В целом, алгоритмы делятся на две категории: независимая работа ядер и групповая работа.

Количество строк

Время работы (мс)

Прямое решение

Групповые решения

С дополнительным выделением памяти

С дополнительным контролем данных

8192

31

16

16

16384

62

47

47

32768

218

93

78

65536

436

250

140

Таблица 7. Сравнение времени выполнения для разных способов обращения к памяти

Из-за специфики работы алгоритма на краях массивов необходим дополнительный учет данных, так как полагается, что для длины L в координатах меньше нуля стоит первое значение из массива, а в координатах от L и больше – последнее значение.

Чтобы подавить эту ситуацию, можно использовать следующие подходы:

  1. В момент, когда ядро обрабатывает одну строку целиком, оно может запускать отдельные функции для начала и конца строки.
  2. В начало строки и в её конец можно добавить фиктивные значения, которые позволят обращаться к ячейкам памяти, выходящим за границу массива.
  3. Для каждого обращения к памяти можно проверять координаты и корректировать их, если они выходят за границы.

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

Как видно из таблицы 7, решение, в котором все процессоры работают с одной областью памяти, увеличивает производительность приложения. На архитектуре х86 наблюдается обратная картина, так как начинаются конфликты кэшей, что приводит к противоположному результату.

Если проанализировать время работы функций (я использовал Profiler из Microsoft Visual Studio), то можно заметить, что наибольшее время было потрачено на подготовку данных (перетасовка массива) и добавление в очередь копирования новых данных на видеокарту. Непосредственно вычисления занимали совсем мало времени. Поэтому было бы логичным нагрузить больше видеокарту и разгрузить центральный процессор.

Имя функции

Количество тактов

Подготовка массива

23

Добавление в очередь чтения данных

8

Ожидание выполнения данных

1

Таблица 8. Время работы функций

Чтобы исправить этот недостаток, программа была модифицирована: теперь видеопроцессор проверяет каждый раз условие попадания в интервал. В общей сложности это делается девять раз для каждого элемента входного массива (так как фильтр девятого порядка). Для сравнения скорости работы см. таблицу 8. После этих оптимизаций основное время в нашей процедуре стало тратиться непосредственно на вычисление.

Здесь стоит отметить, что функция проверки коррекции числа (чтобы оно попадало в заданный интервал) выполняется без ветвления. Поэтому падения производительности в этом случае не будет.

4.6 Вывод

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

Также нельзя забывать, что OpenCL – это надстройка над шейдерной моделью видеокарты, так что различные подходы могут давать совершенно разные результаты для различных видеокарт – эта часть была изложена в разделе 4.2.

Я надеюсь, что эта статья поможет вам быстрее разобраться в основных идеях работы OpenCL, как можно раньше отойти от разработки Hello World и приступить к разработке более востребованных приложений.

5 Список литературы

  1. Алексей, Боресков. Разработка и отладка шейдеров. Петербург : БХВ, 2006. ISBN 5-94157-712-5..
  2. NVidia Inc. Optimizing CUDA - Part II. 2008 г.
  3. Advanced Micro Devices Inc. AMD Radeon HD 6990. AMD. [В Интернете] 2011 г. http://www.amd.com/us/products/desktop/graphics/amd-radeon-hd-6000/hd-6990/pages/amd-radeon-hd-6990-overview.aspx.
  4. NVidia Inc. OpenCL Programming Guide for the CUDA Architecture Version 3.1. б.м. : NVidia Inc.
  5. Imagination Technologies Group plc. Imagination Technologies Group. [В Интернете] http://www.imgtec.com/corporate/.
  6. На рынок может поступить новый Galaxy Nexus с чипом TI OMAP4470? 3D News. [В Интернете] http://www.3dnews.ru/news/623907/.
  7. Intel Inc. Intel Graphics — Built for mainstream Desktop and Mobile PC Users. [В Интернете] http://download.intel.com/products/graphics/intel_graphics_guide.pdf.
  8. 3D News. [В Интернете] http://www.3dnews.ru/news/617064.
  9. NVidia Inc. Графические процессоры Quadro. NVidia. [В Интернете] http://www.nvidia.ru/object/quadro-workstation-graphics-ru.html.
  10. Технология NVidia SLI. NVidia. [В Интернете] 2011 г. http://www.nvidia.ru/object/sli-technology-overview-ru.html.
  11. Tyng-Yeu Liang и Yu-Wei Chang. GridCuda: A Grid-Enabled CUDA Programming Toolkit. Biopolis : Institute of Electrical and Electronics Engineers, 2011. ISBN 978-1-61284-829-7.
  12. Home. Top500 Supercomputer Sites. [В Интернете] Ноябрь 2011 г. http://www.top500.org/.
  13. Inc., NVidia. nView Specifications. NVidia. [В Интернете] 2011 г. http://www.nvidia.ru/object/IO_8400.html.
  14. Khronos OpenCL Working Group. The OpenCL Specification. Khronos. [В Интернете] 2010 г.
  15. NVidia Inc. [В Интернете] http://www.nvidia.ru/object/product-geforce-gtx-590-ru.html.
  16. Advanced Micro Devices Inc. Технология VISION отAMD. AMD. [В Интернете] 2011 г. http://www.amd.com/ru/vision/Pages/vision.aspx.
  17. Advanced Micro Devices Inc. Графические платы ATI Radeon. AMD. [В Интернете] 2011 г. http://www.amd.com/ru/products/desktop/graphics/ati-radeon-hd-5000/Pages/ati-radeon-hd-5000.aspx.
  18. GPU-based Password Cracking. Sprengers, Martijn. [ред.] Dr. L. Batina, Ir. S. Hegt и Ir. P. Ceelen. 2009.
  19. Википедия - Свободная Энциплопедия. Comparison of AMD graphics processing units. Википедия - Свободная Энциплопедия. [В Интернете] http://en.wikipedia.org/wiki/Comparison_of_AMD_graphics_processing_units.
  20. Advanced Micro Devices Inc. OpenCL™ and the AMD APP SDK . AMD Developer Central. [В Интернете] http://developer.amd.com/documentation/articles/pages/OpenCL-and-the-AMD-APP-SDK.aspx.
  21. Hybrid OpenCL: Enhancing OpenCL for Distributed Processing. Aoki, R., и др., и др. Busan  : б.н., 14 Июль 2011 . 978-1-4577-0391-1 .
  22. AnandTech Inc. AnandTech. [В Интернете] http://www.anandtech.com/show/5261/amd-radeon-hd-7970-review/12.
  23. NVidia Inc. NVIDIA OpenCL Best Practices Guide. б.м. : NVidia Inc.


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