Skip to content

7aitsev/ocl_labs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

About

The project is an essential part of my bachelor's thesis in computer science and enginierinng.


Содержание

Тезаурус(1)

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

Устройство — это набор вычислительных блоков. Используется очередь команд для добавления в порядке FIFO команд для их выполнения устройством. Примерами служат команды для выполнения ядер, чтение/запись над объектами памяти.

Ядром является функция, определенная в программе и исполняемая на устройстве. Ядро отличается от других функций спецификатором __kernel.

Программа — программа, написанная на языке OpenCL C (специальном расширении языка C99), включающая множество ядер. Программа может содержать вспомогательные функции, вызываемые __kernel функциями, а также неизменяемые данные.

Фреймворк — это система программного обеспечения, содержащая набор компонентов для поддержки разработки ПО и его выполнения. Фреймворк обычно включает библиотеки, программные интерфейсы приложения (APIs), системы времени исполнения, компиляторы и т.д.

Хост — это та часть гетерогенной системы, которая взаимодействует с контекстом через OpenCL API.

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

Объект памяти — это описатель глобальной области памяти с подсчетом ссылок (после обнуления счетчика память разрешено освободить).

(1) Определения взяты из [1], раздел «2. Glossary».

Подготовка

В каталоге D:\Student\ocl_labs расположены предкомпилированные OpenCL ядра и все необходимые исходные коды. Структура директории:

  • CmakeLists.txt — файл для автоматизации сборки;
  • toolchain.cmake — файл с указанием сведений о целевой платформе и используемых компиляторах;
  • common/ — исходные коды, используемые в разных работах;
  • kernels/ — содержит предкомпилированные ядра;
  • lab*/ — каталоги одноименных работ.

Потребуется трансляция программ работ, поэтому запустите bash (Win+R -> bash) и выполните команды ниже для инициализации CMake:

$ cd /d/Student/ocl_labs/
$ mkdir build && cd build # здесь создаем *каталог сборки*
$ cmake .. -G "MSYS Makefiles" -DCMAKE_TOOLCHAIN_FILE="../toolchain.cmake"

В директории build сгенерируется Makefile, с помощью которого будут собираться программы работ.

Убедитесь, что на обратной стороне отладочной платы DE1-SOC выводы MSEL[4:0] выставлены в положении «01010» — это означает, что ПЛИС будет конфигурироваться аппаратной процессорной системой на базе ARM-процессора.

Настройка выводов MSEL платы DE1-SOC

Для переноса исполняемых файлов на отладочный комплекс понадобится USB флеш накопитель с разделом, отформатированном в одну из файловых систем: FAT, NTFS (запись не поддерживается) или EXT*. Рекомендуется создать директорию ocl_labs в корне раздела, где будут размещаться исполняемые файлы.

Если в ходе выполнения заданий возникнут затруднения, то можно посмотреть вариант решения. Для примера возьмем «Задание 1», второй вариант, работа lab1. Согласно условию, нужно изменить код файла lab1/host.c. Введите git status, находясь в каталоге сборки. Найдите имя подходящего patch-файла, записанного в формате: lab<номер_работы>/<имя_файла>_t<номер_задачи>[o<номер_варианта>].patch. В данном случае нас интересует lab1/host_t1o2. Введите git checkout -- lab1/host.c lab1/host_t1o2, чтобы восстановить редактируемый файл (здесь — host.c) и сам patch-файл, а затем выполните patch lab1/host.c < lab1/host_t1o2, чтобы применить изменения.

lab0: получение сведений о платформе и устройствах

Программа в каталоге lab0 выводит сведения о платформе и всех ее устройствах, доступных для OpenCL. Платформа похожа на драйвер тем, что предоставляет поддерживаемые устройства для использования посредством OpenCL Runtime API. Так, например, в одном ПК может быть платформа от AMD для одной видеокарты и одного процессора, а также платформа от nVIDIA для второй видеокарты.

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

Для запуска lab0 выполните следующие шаги:

  1. Подключите плату к компьютеру через UART-to-USB разъем.

  2. Запустите «Диспетчер устройств» (например, с помощью Win+R, введя имя devmgmt.msc). В категории «Порты (COM и LPT) найдите «USB Serial Port (COMn)», где n – номер устройства.

    Выбор USB Serial устройства для подключения к плате

  3. Откройте PuTTY, выберете последовательный тип подключения, укажите название устройства (в данном случае — COM7) и скорость — 115200. Откройте соединение с помощью кнопки «Open» (в появившейся консоли пока ничего нет).

    Настройка putty

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

  5. Когда появится приглашение, выполните вход под пользователем root (пароль не установлен).

  6. Выполните source ./init_opencl.sh, чтобы загрузить драйвер OpenCL и установить переменные среды окружения для предустановленной библиотеки времени исполнения OpenCL.

  7. Загрузите на USB накопитель исполняемый файл lab0. Подключите его к плате и смонтируйте, например, так:

    $ mkdir /mnt/usb
    $ lsblk # узнайте название устройства, пусть оно будет sda1
    $ mount /dev/sda1 /mnt/usb
    $ #umount /mnt/usb #для размонтирования, при этом нельзя находиться на носителе

    Замечание: рекомендуется прописать следующие псевдонимы:

    $ alias mnt='mount /dev/sda1 /mnt/usb/'
    $ alias umnt='umount /mnt/usb'
    $ alias usb='cd /mnt/usb/ocl_labs' #предполагается, что ocl_labs создана на USB
  8. В директории с расположением исполняемого файла запустите программу, введя ./lab0.

Программа использует всего четыре вызова OpenCL API:

  • clGetPlatformIDs(…) — извлечение списка платформ;
  • clGetPlatfromInfo(…) — получение свойства платформы, соответствующего передаваемой константе из перечисления cl_platform_info;
  • clGetDeviceIDs(…) — извлечение списка устройств платформы, описатель которой передается в качестве аргумента функции;
  • clGetDeviceInfo(…) — получение свойства устройства, соответствующего передаваемой константе из перечисления cl_device_info.

Потребуется обратиться к спецификации [1] для того, чтобы ответить на контрольные вопросы и выполнить задания. В разделе «4.1. Querying Platform Info» на стр. 29 приводятся сведения о вызове clGetPlatformInfo(…), а в Табл. 4.1 перечислены возможные константы, тип возвращаемого значения и описание. В разделе «4.2 Querying Devices» дано описание вызова clGetDeviceInfo(…), а все возможные константы для получения информации об устройстве представлены в Табл. 4.3.

Контрольные вопросы

Изучите вывод программы. Сколько платформ и устройств имеется на отладочной плате? Что такое профили OpenCL, и к какому из них принадлежит DE1-SOC? Как формируется список поддерживаемых расширений платформы: что он в себя включает, когда устройства платформы имеют разные списки расширений?

Обратите внимание, что устройство содержит только один вычислительный блок — согласно свойству CL_DEVICE_MAX_WORK_GROUP_SIZE. Однако вычислительный блок не реализован в виде фиксированного аппаратного устройства, которым разработчик желает воспользоваться в полном объеме для достижения максимальной производительности. Напротив, вычислительный блок является гибким и формируется с учетом ядра [2].

Задание

Измените исходный код программы lab0 (файл host.c, где оставлено место, обозначенное словами «type here…»), чтобы выяснить свойства устройства:

  • вариант 1 — поддерживается ли работа с изображениями?
  • вариант 2 — какой порядок байт используется устройством?
  • вариант 3 — какое разрешение у таймера профилирования?

Посмотрите, как выполняются вызовы clGetDeviceInfo(…) в программе. Внимательнее изучите прототип функции в спецификации и найдите(2) нужные константы в Табл. 4.3. После исправлений в коде программы достаточно выполнить make lab0 повторно. Проверьте вывод программы после запуска ее на плате.

(2) Список свойств довольно большой, поэтому подсказка: для первых двух вариантов требуемые свойства запросят информацию с типом cl_bool, а свойство для третьего варианта соответствует типу size_t.

lab1: выполнение ядра на устройстве

Демонстрационная программа lab1 состоит уже из двух частей: одна будет выполняться на хосте, а другая — работать на устройстве. Такая программа обычно включает в себя 13 шагов (не строго в указанном порядке, причем некоторые могут отсутствовать за ненадобностью):

  1. включение заголовочного файла «CL/opencl.h»;
  2. выделение памяти для данных хоста;
  3. создание контекста — clCreateContext(…);
  4. создание очереди команд — clCreateCommandQueue(…);
  5. выделение памяти устройства (создание объектов памяти) — clCreateBuffer(…);
  6. передача обрабатываемых данных с хоста на устройства — clEnqueueWriteBuffer(…);
  7. чтение предкомилированного файла ядра во временный буфер хоста;
  8. сборка программы — clCreateProgramWithBinary(…) и clBuildProgram(…);
  9. создание объекта ядра из программы — clCreateKernel(…);
  10. задание аргументов, передаваемых ядру — clCreateKernelArg(…);
  11. добавление ядра в очередь для выполнения — clEnqueueTask(…) или clEnqueueNDRangeKernel(…);
  12. чтение результата из устройства (объектов памяти) — clEnqueueReadBuffer(…);
  13. освобождение ресурсов — семейство функций с именем clRelease*(…).

Просмотрите обзорно код хостовой части работы lab1, чтобы пронаблюдать перечисленные выше шаги. В командном интерпретаторе, находясь в build, выполните make lab1. Скопируйте соответствующий исполняемый файл на USB носитель, а также предкомпилированное ядро lab1.aocx, размещенное в каталоге kernels. Запустите эту программу на плате.

Код ядра представлен в lab1/core.cl и продублирован ниже:

__kernel void
inout(__global int* out, int in)
{
    *out = in; // out[0] = in;
}

Функция ядра называется inout и имеет спецификатор __kernel. Тип возвращаемого значения должен быть void. Первый принимаемый аргумент — указатель на глобальную область памяти, второй — простое целочисленное значение, неявно заданное со спецификатором __private. Все, что делает это ядро — получает целочисленное значение in и записывает его в глобальную область памяти через указатель, так что хост может считать значение обратно из объекта памяти buf_out, который передавался первым аргументом и создан вызовом clCreateBuffer(…).

При создании очереди команд было включено профилирование (свойство CL_QUEUE_PROFILING_ENABLE). Обратитесь к разделу «5.9 Profiling Operations on Memory Objects and Kernels», чтобы узнать, какую информацию позволяет получить профилирование и выполнить следующее задание.

Задание 1

Определите время, затраченное на завершение выполнения ядра, начиная с момента:

  • вариант 1 — постановки ядра в очередь команд;
  • вариант 2 — отправки ядра на устройство;
  • вариант 3 — начала выполнения ядра.

Для этого найдите в тексте lab1/host.c участки, помеченные как «task1». Описатель события kernel_event типа cl_event позволит получить сведения о работе ядра. kernel_event нужно добавить в последний аргумент вызова clEnqueueTask(…). Используйте функцию clGetEventProfilingInfo(…), чтобы получить профилировочную информацию о команде, связанной с событием. Выводите результат в микросекундах. Повторите сборку получившейся программы и запустите ее на плате.

Задание 2

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

Профилирование можно убрать (вернуть код программы в исходное состояние). Достаточно немного модифицировать код для того, чтобы установить второй аргумент для первого запуска ядра 0xF00D, а для второго раза — 0xBEEF. Очередь команд схематично будет выглядеть примерно так:

Предполагаемая схема очереди команд

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


Ядра NDRange позволяют воспользоваться параллелизмом данных — когда весь набор входных значений разбивается на более мелкие части, обрабатываемые одновременно путем выполнения одинаковой операции над каждой частью в отдельном потоке. Такой поток называют рабочим элементом (work-item), выполняющийся независимо друг от друга, а вся задача целиком называется NDRange (N-dimensional range). Размерность NDRange может быть в диапазоне от 1 до 3. Т.е. можно представить NDRange как одномерный массив рабочих элементов, если данные удобно представить в таком виде (сложение векторов), или как двумерный (умножение матриц). У рабочих элементов есть порядковые номера (id), которые уникальны для всех элементов устройства. Получить такой идентификатор элемента можно с помощью get_global_id(N), где N - размерность (число от 0 до 2). Рабочие элементы могут объединяться в рабочую группу (work-group), что позволяет им производить синхронизацию между собой и делить локальную память, которая для каждой рабочей группы своя. В целом, рабочие группы дают возможность декомпозировать задачу. Идентификаторы рабочих элементов внутри рабочих групп можно выяснить с помощью get_local_id(N).

Идентификаторы рабочих элементов и групп

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

Задание 3

Передайте в ядро два числа 0xF00D и 0xBEEF и выведите их с помощью одного запуска ядра путем вызова clEnqueueNDRangeKernel(…). Сделайте так, чтобы всего было два рабочих элемента, и каждый бы записал в выходной буфер только одно целочисленное значение, прочитанное из входного буфера.

В этом задании мы имеем дело с двумя одномерными массивами (объектами памяти) типа cl_mem — например, buf_out и buf_in. Первый объект памяти служит для записи результата выполнения ядра. Второй объект — для чтения значения внутри тела функции-ядра. Поскольку всего требуется «обработать» два независимых числа, а результат записать в две непересекающиеся ячейки памяти, то можно воспользоваться параллелизмом по данным. Функция get_global_id(0) вернет индекс 0 для ядра, выполняемого на первом рабочем элементе, и индекс 1 — для второго. Поэтому индексы можно использовать для получения данных из buf_in и записи в buf_out.

Напишите сперва код ядра. Чтобы его скомпилировать, откройте новое окно терминала, перейдите в lab1, и выполните команду aoc core.cl --board de1soc_sharedonly -v -o ../build/lab1_task3.aocx, где

  • core.cl — файл с исходным кодом ядра;
  • --board — выбор платы (посмотреть список плат — aoc --board-list);
  • -v — показывать прогресс выполнения компиляции;
  • -o — позволяет указать путь, где будет находиться ядро и другие артефакты. Например, если ядро называется lab1.aocx, то будет создан каталог lab1, внутри которого есть файлы журналирования процесса сборки (с расширением .log).

Пока идет компиляция, исправьте код хостовой части программы. В памяти хоста следует создать два массива: под входные данные и под результат. Необходимо воспользоваться вызовом clEnqueueWriteBuffer(…) над объектом памяти buf_in для записи передаваемых из хоста в ядро значений. Чтобы проверить работу программы на плате, передайте в качестве входного аргумента для lab1 путь к новому ядру.

Список источников

  1. Спецификация OpenCL 1.0
  2. Accelerating Workloads on FPGAs via OpenCL: A Case Study with OpenDwarfs
  3. DE1-SOC User Manual
  4. DE1-SoC OpenCL User Manual
  5. OpenCL (1.2) high level overview
  6. OpenCL (1.2) C
  7. The OpenCL Programming Book