High performance
19 October 2009

OpenCL. Подробности технологии



Здравствуй, уважаемое хабрасообщество.

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


OpenCL задумывался как технология для создания приложений, которые могли бы исполняться в гетерогенной среде. Более того, он разработан так, чтобы обеспечивать комфортную работу с такими устройствами, которые сейчас находятся только в планах и даже с теми, которые еще никто не придумал. Для координации работы всех этих устройств гетерогенной системе всегда есть одно «главное» устройство, который взаимодействует со всеми остальным посредствами OpenCL API. Такое устройство называется «хост», он определяется вне OpenCL.

Поэтому OpenCL исходит из наиболее общих предпосылок, дающих представление об устройстве с поддержкой OpenCL: так как это устройство предполагается использовать для вычислений – в нем есть некий «процессор» в общем смысле этого слова. Нечто, что может исполнять команды. Так как OpenCL создан для параллельных вычислений, то такой процессор может, иметь средства параллелизма внутри себя (например, несколько ядер одного CPU, несколько SPE процессоров в Cell). Также элементарным способом наращивания производительности параллельных вычислений является установка нескольких таких процессоров на устройстве (к примеру, многопроцессорные материнские платы PC итд.). И естественно в гетерогенной системе может быть несколько таких OpenCL-устройств (вообще говоря, с различной архитектурой).

Кроме вычислительных ресурсов устройство имеет какой-то объем памяти. Причем никаких требований к этой памяти не предъявляется, она может быть как на устройстве, так и вообще быть размечена на ОЗУ хоста (как например, это сделано у встроенных видеокарт).

Собственно все. Больше об устройстве никаких предположений не делается.

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

OpenCL предоставляет программисту низкоуровневый API, через который он взаимодействует с ресурсами устройства. OpenCL API может либо напрямую поддерживаться устройством, либо работать через промежуточный API (как в случае NVidia: OpenCL работает поверх CUDA Driver API, поддерживаемый устройствами), это зависит от конкретной реализации не описывается стандартом.

Рассмотрим как же OpenCL обеспечивает такую универсальность, сохраняя при этом низкоуровневую природу.

Далее я приведу вольный перевод части спецификации OpenCL 1.0 с некоторыми комментариями и дополнениями.

Для описания основных идей OpenCL воспользуемся иерархией из 4х моделей:
  • Модель платформы (Platform Model);
  • Модель памяти (Memory Model);
  • Модель исполнения (Execution Model);
  • Программная модель (Programming Model);


Модель платформы (Platform Model).


Платформа OpenCL состоит из хоста соединенного с устройствами, поддерживающими OpenCL. Каждое OpenCL-устройство состоит из вычислительных блоков (Compute Unit), которые далее разделяются на один или более элементы-обработчики (Processing Elements, далее PE).

OpenCL-приложение исполняется на хосте в соответствии с нативными моделями его платформы. OpenCL-приложение отправляет с хоста команды устройствам на выполнение вычислений на PE. PE в рамках вычислительного блока выполняют один поток команд как SIMD блоки (одна инструкция выполняется всеми одновременно, обработка следующей инструкции не начнется, пока все PE не завершат исполнение текущей инструкции), либо как SPMD блоки (у каждого PE собственный счетчик инструкций (program counter)).
То есть OpenCL обрабатывает некие команды, поступающие от хоста. Таким образом приложение не связано жестко с OpenCL, а значит всегда можно подменить реализацию OpenCL, не нарушив работоспособность программы. Даже если будет создано такое устройство, которое не укладывается в модель «OpenCL-устройства», для него можно будет создать реализацию OpenCL, транслирующую команды хоста в более удобный для устройства вид.

Модель исполнения (Execution Model).


Выполение OpenCL-программы состоит из двух частей: хостовая часть программы и kernels (ядра; с Вашего позволения я далее буду употреблять английский термин, как более привычный большинству из нас) исполняющиеся на OpenCL-устройстве. Хостовая часть программы определяет контекст, в котором исполняются kernel'ы, и управляет их исполнением.

Основная часть модели исполнения OpenCL описывает исполнение kernel’ов. Когда kernel ставится в очередь на исполнение, определяется пространство индексов (NDRange, определение будет дано ниже). Копия (instanse) kernel'а выполнятся для каждого индекса из этого пространства. Копия kernel'а выполняющаяся для конкретного индекса называется «Work-Item» (рабочей единицей) и определяется точкой в пространстве индексов, то есть каждой «единице» предоставляется глобальный ID. Каждый Work-Item выполняет один и тот же код, но конкретный путь исполнения (ветвления итп.) и данные, с которыми он работает, могут быть различными.

Work-Item'ы организуются в группы (Work-Groups). Группы предоставляют более крупное разбиение в пространстве индексов. Каждой группе приписывается групповой ID с такой же размерностью, которая использовалась для адресации отдельных элементов. Каждому элементу сопоставляется уникальный, в рамках группы, локальный ID. Таким образом, Work-Item'ы могут быть адресованы как по глобальному ID, так и по комбинации группового и локального ID.

Work-Item'ы в группе исполняются конкурентно (параллельно) на PE одного вычислительного блока.
Здесь хорошо видна унифицированная модель устройства: несколько PE -> CU, несколько CU -> устройство, несколько устройств -> гетерогенная система.

Пространство индексов в OpenCL 1.0 называется NDRange и может быть 1-, 2- и 3-мерным. NDRange – массив целых чисел (integer) длины N, указывающий размерность в каждом из направлений.
Выбор размерности NDRange определяется удобством для конкретного алгоритма: в случае работы с трехмерными моделями удобно индексировать по трехмерным координатам, в случае работы с изображениями или двумерными сетками – удобнее, когда размерность индексов – 2. 4х-мерные объекты в нашем мире большая редкость, поэтому размерность ограничена 3. Кроме того, как бы там ни было, но в данный момент основная цель OpenCL – это GPU. GPU Nvidia сейчас нативно поддерживают размерность индексов до 3, соответственно, чтобы реализовать большую размерность, пришлось бы прибегать к хитростям и усложнению либо CUDA Driver API, либо реализации OpenCL.

Контекст исполнения и очереди команд в модели исполнения OpenCL.


Хост определяет контекст исполнения kernel'ов. Контекст включает в себя следующие ресурсы:
  • Устройства: набор OpenCL-устройств, которые использует хост.
  • Kernel'ы: OpenCL функции, которые исполняются на устройствах.
  • Объекты программ (Program Objects): исходные коды и исполняемые файлы kernel’ов.
  • Объекты памяти (Memory Objects): набор объектов в памяти, видимых как хосту, так и OpenCL устройству. Объекты памяти содержат значения, с которыми могут работать kernel'ы.

Контекст создается и управляется по средствам функций из API OpenCL. Хост создает структуру данных, называемую «очередь команд» (command-queue), чтобы управлять исполнением kernel’ов на устройствах. Хост отправляет команды в очередь, после чего они устанавливаются планировщиком для исполнения на устройствах в нужном контексте.

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


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

С одним контекстом можно связать несколько очередей команд. Эти очереди исполняются, конкурируя между собой, и независимо без каких-либо явных способов синхронизации между ними.
Использование очереди команд, позволяет добиться большой универсальности и гибкости при использовании OpenCL. Современные GPU имеют собственный планировщик, который решает, что и когда и на каких вычислительных блоках исполнять. Использование очереди не стесняет работу планировщика, который имеет собственную очередь команд.

Модель исполнения: категории kernel.


В OpenCL kernel может быть двух категорий:
  • OpenCL kernel: написаны на OpenCL C и компилируется компилятором OpenCL. Все реализации OpenCL должны поддерживать OpenCl-kernel. Реализации могут предоставлять другие механизмы создания OpenCL-kernel.
  • Naitive kernel: доступ к ним осуществляется через хостовые указатели на функцию. Нативные kernel ставится в очередь на исполнение, так же как и OpenCL-kernel и использует те же объекты памяти, что и OpenCL-kernel. К примеру, такие kernel'ы могут быть функциями, определенными в коде приложения или экспортированными из библиотеки. Отметим, что возможность исполнять нативные kernel'ы является опциональной и их семантика не определяется стандартом. API OpenCL включает функции для опроса возможностей устройства на предмет поддержки таких kernel'ов.

Модель памяти (Memory Model).


Work-Item, исполняющий kernel может использовать четыре различных типа памяти:
  • Глобальная память. Эта память предоставляет доступ на чтение и запись элементам всех групп. Каждый Work-Item может писать и читать из любой части объекта памяти. Запись и чтение глобальной памяти может кэшироваться в зависимости от возможностей устройства.
  • Константная память. Область глобальной памяти, которая остается постоянной во время исполнения kernel'а. Хост аллоцирует и инициализирует объекты памяти, расположенные в константной памяти.
  • Локальная память. Область памяти, локальная для группы. Эта область памяти может использоваться, чтобы создавать переменные, разделяемые всей группой. Она может быть реализована как отдельная память на OpenCL-устройстве. Альтернативно эта память может быть размечена как область в глобальной памяти.
  • Частная (private) память. Область памяти, принадлежащая Work-Item. Переменные, определенные в частной памяти одного Work-Item’а, не видны другим.

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

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

Программная модель. (Programming Model)


Модель исполнения OpenCL поддерживает две программные модели: параллелизм данных (Data Parallel) и параллелизм заданий (Task Parallel), так же поддерживаются гибридные модели. Основная модель, определяющая дизайн OpenCL, – параллелизм данных.

Программная модель с параллелизмом данных.


Эта модель определяет вычисления как последовательность инструкций, применяемых к множеству элементов объекта памяти. Пространство индексов, ассоциированное с моделью исполнения OpenCL, определяет Work-Item'ы и как данные распределяются между ними. В строгой модели параллелизма данных существует строгое соответствие один к одному между Work-Item и элементом в объекте памяти, с которым kernel может работать параллельно. OpenCL реализует более мягкую модель параллелизма данных, где строгое соответствие один к одному не требуется.

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

Программная модель с параллелизмом заданий.


В этой модели каждая копия kernel'а исполняется независимо от какого-либо пространства индексов. Логически это эквивалентно исполнению kernel'а на вычислительном блоке (CU) с группой, состоящей из одного элемента. В такой модели пользователи выражают параллелизм следующими способами:
  • используют векторные типы данных, реализованные в устройстве;
  • устанавливают в очередь множество заданий;
  • устанавливают в очередь нативные kernel'ы, использующие программную модель, ортогональную к OpenCL;


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

Из чего состоит платформа OpenCL


Платформа OpenCL позволяет приложениям использовать хост и одно или несколько OpenCL-устройств как одну гетерогенную параллельную компьютерную систему. Платформа состоит из следующих компонент:
  • OpenCL Platform Layer: позволяет хосту обнаруживать OpenCL-устройства, опрашивать их свойства и создавать контекст.
  • OpenCL Runtime: среда исполнения позволяет программе на хосте управлять контекстами после того как они были созданы.
  • Компилятор OpenCL: компилятор OpenCL создает исполняемые файлы, содержащие OpenCL–kernel. Язык программирования OpenCL-C реализуется компилятором, который поддерживает подмножество стандарта языка ISO C99 с расширениями для параллелизма.

Как это все работает?


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

Сейчас же я схематически опишу из каких шагов состоит процесс создания такого приложения:
  1. Создаем контекст для исполнения нашей программы на устройстве.
  2. Выбираем необходимое устройство (можно сразу выбрать устройство с наибольшим количеством Flops).
  3. Инициализируем выбранное устройство созданным нами контекстом.
  4. Создаем очередь команд на основе ID устройства и контекста.
  5. Создаем программу на основе исходных кодов и контекста,
    либо на основе бинарных файлов и контекста.
  6. Собираем программу (build).
  7. Создаем kernel.
  8. Создаем объекты памяти для входных и выходных данных.
  9. Ставим в очередь команду записи данных из области памяти с данными на хосте в память устройства.
  10. Ставим в очередь команду исполнения созданного нами kernel.
  11. Ставим в очередь команду считывания данных из устройства.
  12. Ждем завершения операций.

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

Заключение.


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

Ссылки:




+41
29.9k 55
Comments 10
Top of the day