OpenCL для RaspberryPI
Изначально у меня не было планов писать код под GPU. Обработка сигналов спутников на Java вполне возможна. И я продолжаю развивать это направление. Код на Java позволяет достаточно быстро написать обработчик сигнала и внедрить в работающую систему. Также с помощью Java гораздо удобнее обрабатывать неструктурированные или слабо структурированные данные. К таким данным обычно относятся различные высокоуровневые протоколы. Однако, есть и недостатки - скорость. Java не очень подходит для быстрой обработки однотипных данных.
В проекте r2cloud скорость обработки данных не так важна. Именно поэтому в основном используется Java. Тем не менее мне захотелось ускорить некоторые наиболее нагруженные участки. И причин этому несколько:
- быстрее обработка сигналов - меньше потребление энергии. Меньше потребление энергии - возможность использования солнечной энергии для питания станции. А значит возможность устанавливать станции в труднодоступных местах и большая независимость работы.
- крайне медленная работа в Raspberrypi 1. Да, эта модель морально устарела, но, представьте себе, сколько устройств уже выпущено. И их всех можно использовать, а не выбрасывать.
- возможность запускать несколько проектов на одном Raspberrypi. Если r2cloud будет более экономичный в плане ресурсов, то больше пользователей смогут его поставить рядом с уже работающими проектами. Например, flightaware или SondeHub.
- разработка и продвижение инфраструктуры по доставке оптимизированного кода. На текущий момент в Opensource сообществе нет единого подхода, который бы позволил доставить CPU-специфичный или GPU-специфичный код на конечные компьютеры. Возможно, делая такой обширный проект как r2cloud мне удастся разобраться с множеством слабосвязанных проблем доставки такого кода и предложить работающее решение.
Raspberrypi
Итак, как же ускорить обработку сигналов на Raspberrypi? Первое, что приходит в голову - это использовать SIMD инструкции процессора. Они позволяют ускорить приложение примерно в 4 раза. Вместо четырёх операций умножения, SIMD инструкции позволяют выполнять одну одновременно над четырьмя float.
Однако, в Raspberrypi 1 нет поддержки SIMD инструкций. Поэтому скорость работы там крайне низкая. perf_xlating тест на Raspberrypi 3 выполняется 0.024855 секунд, а на Raspberrypi 1 - 0.332934. То есть на порядок медленнее.
Все Raspberrypi построены на основе SoC от Broadcom. Это значит, что в одном чипе есть и CPU и GPU. Причём GPU, зачастую, почти не задействован! Я попробовал разобраться увеличится ли скорость обработки данных с использованием GPU. Программы под GPU обычно пишутся на OpenCL. Либо с помощью проприетарного SDK, например, Nvidia cuda toolkit. Под Raspberrypi есть проект VC4CL, который реализует ABI OpenCL для GPU VideoCore IV. Здесь и далее я буду давать примеры именно из этого проекта.
OpenCL
Для начала немного об OpenCL. OpenCL - это фреймворк для написания программ, связанных с параллельными вычислениями на различных графических и центральных процессорах, а также FPGA. Программы, написанные с помощью этого фреймворка существенно отличаются от традиционных программ на Си или Java. Во-первых, у программы нет как таковой точки входа. Она запускается из основной программы (host-программы). Во-вторых, вся работа построена на параллелизации вычислений. Именно поэтому программы на OpenCL представляют собой описание некоего worker’а, который будет обрабатывать параллельно некоторый кусок данных или задачи. В терминах OpenCL такой worker называется kernel. В-третьих, совершенно обычным делом считается компиляция kernel’а во время запуска основной host-программы.
Запуск и работа с OpenCL выглядит следующим образом:
- инициализация устройства
- загрузка кода kernel из текстового файла или из бинарного блоба
- компиляция kernel
- инициализация буферов обмена
- в главном цикле программы:
- копирование данных в буферы GPU
- подсчёт необходимого количества вычислений kernel’а. Здесь нужно учитывать поддерживаемый параллелизм устройства, алгоритм программы и количество доступных данных
- вызов kernel
- ожидание результатов
- (опционально) вызов другого kernel, которому на вход передаётся результат вычислений первого, но с другим уровнем параллелизма.
Из всего этого наиболее важным является распараллеливание вычислений. Для того чтобы это сделать, нужно понимать модель выполнения программы на OpenCL.
Модель выполнения OpenCL
В OpenCL существует несколько концепций:
- work-item. Это минимальный и неделимый кусочек работы. По факту, код kernel обрабатывает именно этот один элемент работы. OpenCL подразумевает, что программист сам разделит обработку данных на такие небольшие кусочки, которые будут выполняться параллельно.
- work-dim или work dimension. Размерность данных для обработки. Для значения 1 данные представляют собой вектор, 2 - 2D, 3 - 3D.
- work group. Несколько work-item могут быть сгруппированы в один work-group. Вся работа разбивается на несколько work group и каждый work-item внутри такой группы запускается независимо.
Всё это достаточно сложно представить, поэтому я попытался визуализировать.
Задача разбивается на work-item. Это такие прозрачные квадратики сверху. После этого идёт группировка в work group. А затем work group запускаются на параллельных ядрах на устройстве.
Пока всё достаточно просто, но какое программирование на GPU без 2D? При work-dim=2 work-item могут быть частью 2D массива.
Задача разбивается на двумерные элементы и дальше по аналогии с одномерными векторами. Примером такой задачи может быть перемножение матриц. Дальше - больше. 3D.
Мне сложно представить класс задач, которые нужно решать в трёхмерном пространстве. Расчёт движения атмосферы? Предсказание погоды?
Итак, как задача запускается на параллельных потоках, в принципе, понятно. Однако, пока не ясно, а как же разбить её на одинаковые work-item.
Распараллеливание алгоритма
Универсального ответа на этот вопрос, конечно же, нет. Не каждый алгоритм можно эффективно распараллелить. В таком случае ждать от OpenCL и GPU чуда совсем не стоит. И даже если удастся придумать такой алгоритм, нужно учитывать затраты на передачу данных в GPU и обратно. Зачастую, это может стать решающий фактором. Например, CPU может быстрее обработать данные за счёт кэша и SIMD операций, чем передача данных по медленной шине в GPU, быстрый расчёт на GPU и медленная передача результатов обратно в CPU.
В случае с Raspberrypi - это не очень большая проблема, так как CPU и GPU находятся на одном чипе и, по идее, имеют доступ к одной и той же оперативной памяти. Правда, как именно это реализовано в VC4CL непонятно. Есть ли поддержка ядра Linux тоже непонятно.
Для тестирования вычислений на GPU я выбрал алгоритм Frequency Xlating FIR filter. Я о нём уже писал, когда оптимизировал код под CPU. Работа такого фильтра заключается в перемножении массива входящих комплексных чисел на другой массив комплексных чисел (характеристика фильтра). Если размер входящего массива N, а характеристика фильтра - это М, то нужно выполнить N * M умножений комплексных чисел. А это, на секундочку, 4 умножения и 2 сложения. В общем виде алгоритм выглядит так:
for (unsigned int i = 0; i < input_len; i++) {
for (unsigned int j = 0; j < taps_len; j++) {
result_real += (input[2 * i + 2 * j] * taps[2 * j]) - (input[2 * i + 2 * j + 1] * taps[2 * j + 1]);
result_imag += (input[2 * i + 2 * j] * taps[2 * j + 1]) + (input[2 * i + 2 * j + 1] * taps[2 * j]);
}
}
Для каждого входящего комплексного числа выполнить перемножение всех комплексных чисел на taps_len
вперёд с коэффициентами фильтра. В результате получится другой массив комплексных чисел.
Этот алгоритм достаточно хорошо распараллеливается. Так как каждый следующий результат не зависит от предыдущего, то можно разбить весь входящий массив на Х одинаковых частей, перемножить и результаты положить в результирующий массив со смещением Х.
Для более хитрых алгоритмов, например, перемножения матриц, существуют различные мощные оптимизации. Например, в этой статье Cedric Nugteren пытается с помощью OpenCL получить ту же производительность в перемножении матриц, что и библиотека cuBLAS. С помощью довольно хитрых оптимизаций он смог улучшить производительность в ~11 раз. Кстати, в конце статьи он пришёл к интересным выводам: чтобы улучшить производительность ещё больше и подобраться вплотную к cuBLAS, нужно спустится на уровень ниже и написать kernel на ассемблере.
Далее
Информации по запуску OpenCL под Raspberrypi получилось так много, что я вынужден её разбить на несколько частей. В следующей части я постараюсь описать практические шаги необходимые для компиляции и запуска программы.