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 получилось так много, что я вынужден её разбить на несколько частей. В следующей части я постараюсь описать практические шаги необходимые для компиляции и запуска программы.