Вычисления на RISC-V: исследуем производительность OpenCL на CPU и совместимых GPU
с помощью нейросети
Михаил Козлов является инженером-стажером в группе разработки математических библиотек в YADRO. Эта сфера активно развивается на RISC-V: известные математические библиотеки, такие как OpenBLAS, Eigen и многие другие, портируют и оптимизируют под открытую архитектуру. Большой интерес представляет OpenCL — открытый стандарт разработки программного обеспечения для гетерогенных вычислений. Он используется во многих областях: HPC, AI/ML, AR/VR, линейной алгебре, где он наиболее широко представлен с помощью библиотек clBLAS и CLBlast.
ClBLAS — более старая, а CLBlast — более современная библиотека, со встроенным тюнером для оптимизации под конкретное железо. В этой статье Михаил расскажет, как они с командой исследовали производительность этих библиотек на GPU от Imagination и ARM Mali ARM. Кроме того, в статье будет показано, как запустить эти библиотеки на RISC-V CPU при помощи OpenCL — точнее, ее модификации PoCL, созданной разработчиками GPU Vortex.
- как запустить библиотеку CLBlast на GPU
- как быстро работает CLBlast на разных GPU, совместимых с RISC-V
- как запустить реализацию OpenCL на процессорах RISC-V
- как быстро работает OpenCL на разных процессорах RISC-V
Запуск СLBlast на GPU
CLBlast — это open source-библиотека на C++ с лицензией Apache License 2.0, использующая ядра на OpenCL. CLBlast реализует функции, соответствующие стандарту BLAS, и 9 дополнительных BLAS-подобных функций. Каждая реализация поддерживает пять типов чисел с плавающей запятой: три типа вещественных (FP16, FP32, FP64) и два типа комплексных чисел (2xFP32, 2xFP64).
У библиотеки есть несколько API
- CLBlast API С — написан на С, не поддерживает шаблонные функции, создан для совместимости с BLAS API.
- СBLast API — написан на C++, поддерживает шаблонные функции, функциональность аналогична CLBlast API С.
- Netlib BLAS API — написан на С, поддерживает все вычислительные операции, нужен для поддержки приложений, который основывается на этот стандартный API.
- CLCudaAPI — написан на C++, реализует обертку над OpenCL API и/или CUDA API. Обеспечивает высокую портативность между CUDA и non-CUDA девайсами при небольшом оверхэде.
CLBlast включает 18 ядер, что значительно меньше числа поддерживаемых функций. Причины этому две. Во-первых, каждое ядро может работать с любым из перечисленных типов данных, которые определяются на этапе компиляции. Во-вторых, многие ядра могут быть переиспользованы в разных функциях: например, реализация функции GBMV использует OpenCL-ядро GEMV с изменением препроцессинга.
У ядер CLBlast много параметров, что позволяет использовать CLTune для их оптимизации. Параметры делятся на два набора. Первый — это наиболее частые комбинации, чье влияние на производительность ядра на различных устройствах лучше всего изучено. Второй — все возможные комбинации: здесь количество наборов так велико, что пользователи сами проводят эксперименты с различными наборами и предлагают новые оптимальные параметры.
Пользователь также может с помощью CLTune самостоятельно оптимизировать ядра для задач с особыми размерностями. CLTune запускает ядра с различными наборами параметров и выбирает лучший набор по всем запускам. Вот для примера некоторые из этих параметров:
- WGS (Work Group Size). Этот параметр есть во многих ядрах, он определяет количество потоков, размерность группы потоков, необходимый размер локальной памяти. В ядрах параметр можно использовать по-разному. В одномерных задачах выделяется группа тредов размерностью WGS x 1, а в двухмерных — WGS x WGS. При работе с большими размерностями данные перекидываются на девайс и обрабатываются на нем порционно. В таких случаях WGS определяет размер локального массива, который будет использоваться в вычислениях.
- WPT (Work Per Thread). Встречается в ядрах AXPY и GEMV. Этот параметр определяет количество последовательно расположенных в локальной памяти элементов, которые будут обрабатываться одним потоком. При оптимально подобранном параметре можно уменьшить число запросов к памяти в несколько раз.
- VW (Vector Width). Встречается явно в ядрах AXPY и GEMV, используется в векторной реализации умножения векторов. Параметр задает количество элементов, которое помещаются в регистр.
Ниже представлена архитектура библиотеки:
Тестирование библиотеки CLBlast
Перед использованием новой библиотеки нужно убедиться в ее функциональной корректности, особенно на новой архитектуре. Для этого запустим функциональные тесты на исследуемых платформах.
Система функционального тестирования в CLBlast устроена следующим образом. Сначала из референс-библиотеки запускаем реализацию тестируемой функции: ею может выступать clBLAS или какая-нибудь CPU-BLAS-библиотека. Я остановил выбор на OpenBLAS — это наиболее популярная библиотека своего рода, хорошо оптимизированная под конкретное железо. Мы убедились в ее корректности, собственноручно исправив множество проблем.
Затем запускаем ту же функцию из CLBlast, сравниваем полученные ответы или, при сравнении с clBLAS, возвращаемый код. По итогам запуска я получил довольно большой список падений и некорректных ответов. Все упавшие тесты можно разделить на пять групп.
Несовпадение возвращаемых кодов. Система тестирования ожидает одинаковые коды ошибок от CLBlast и clBLAS. Но функции в библиотеках реализованы по-разному и требуют разного количества ресурсов. Например, в CLBlast есть усовершенствованная версия GEMM, использующая дополнительный буфер, в clBLAS такой реализации нет. Так что тест на недостаточный размер дополнительного буфера не проходит: мы получаем от CLBlast код ошибки, а от clBLAS — код успешного завершения.
Работа с числами половинной точности. Система тестирования CLBlast универсальна при работе со всеми типами данных. Исключение представляют лишь некоторые функции над числами половинной точности. В C++ нет поддержки чисел типа fp16, поэтому в коде они представлены как последовательность бит, записанных в тип unsigned short — это стандартный способ их представления в OpenCL. Для таких чисел реализованы методы для перевода в тип fp32 и обратно.
Сначала сложности возникли с функцией, возвращающей индекс максимального/минимального элемента: она отличается тем, что возвращает целое число, а не дробное. Остальные функции интерпретируют возвращаемую ядром последовательность бит как число с плавающей запятой (массив чисел с плавающей запятой), поэтому вызывают функцию перевода из fp16 в fp32. Из-за универсальности тестовой системы последовательность бит, возвращаемая iAMAX, будет также восприниматься как fp16, являясь в то же время целым числом. В tester.cpp:
Проблема немного другого характера — в тестировании функции HGEMMBATCHED и HAXPYBATHED. BATCHED-версией алгоритма называется объединение n вызовов одного ядра в один вызов. При этом выделяется память под входные и выходные данные сразу всех вызовов, вычисления происходят одновременно. У функций умножения матриц и сложения векторов есть также скалярные параметры.
В BATCHED-версиях тест реализован таким образом, что каждый из нескольких сгруппированных вызовов получает различные значения этих параметров. На каждом следующем вызове значение увеличивается на 1. При работе с числами половинной точности происходит сложение не по правилам чисел с плавающей точкой, а по правилам целых чисел. Результат сложения будет интерпретироваться как fp16, что переводит скалярные параметры в значения типа nan. В xgemmbatched.hpp:
Некорректная работа референса. В clBLAS функция TRSM получает иной ответ, нежели при использовании этой же функции в CLBlast и OpenBLAS.
Проблемы в OpenCL-ядрах. Ядра функций GEMMBATCHED и AXPYBATHED при работе с числами половинной точности пытались прочитать массив 16-битных чисел как массив 32-битных чисел. Связано это с некорректной работой с макросами. В xgemmbatched.opencl:
В common.opencl:
Проблемы с отдельными устройствами. Все описанные проблемы воспроизводились на всех конфигурациях, участвовавших в тесте. Но были и случаи, когда проблемы возникали только на определенных платах. Например, функция ASUM некорректно работала на ARM Mali GPU.
Мы исправили проблемы, касающиеся неправильной работы тестовой системы и ядер с числами половинной точности. CLBlas уже не особо актуален, и до него, возможно, руки дойдут позже, а стороннее «железо» нам неподвластно, так что мы сможем изучить лишь причины проблемы. Поэтому ряд таких проблемных функций в тестах производительности мы решили не рассматривать.
Проблема с несовпадением возвращаемых кодов фундаментальна для тестовой системы. Мы пришли к выводу, что ее исправление потребует глубокой переработки всей системы. Убедившись, что возвращаемые статусы СLBlast корректны и соответствуют ожиданиям, мы решили отложить этот вопрос.
Результаты экспериментов
В качестве тестовых мы выбрали три популярные функции из разных уровней BLAS API:
- Первый уровень — AXPY, операция по сложению векторов, относящаяся к memory-bound задачам.
- Второй уровень — GEMV, матрично-векторное умножение.
- Третий уровень — GEMM, матричное умножение, compute-bound задача.
Все вычисления проводились над числами одинарной точности. Использование встроенного в CLBlast тюнера в нашем случае не дало прироста производительности — чаще всего лучшими для запуска оставались параметры по умолчанию. На GPU на плате VIM4 с новыми параметрами функции начинали работать медленнее. Это может быть связано с тем, что тюнинг производится только для конкретной размерности. Тестовые конфигурации были следующими:
OpenBLAS для LicheePI собрали с поддержкой векторного расширения V0p7 для запуска в многопоточном режиме — 4 потока на RISC-V, 8 потоков на ARM. Показатели производительности GPU получили с помощью утилиты clpeak, в которой реализованы синтетические бенчмарки на OpenCL.
AXPY
CLBlast превосходит clBLAS на 25−50% на ARM-платах, отстает почти в 2,5 раза на BXM-4−64 и выступает наравне на BXE-2−32. На Mali G610 OpenBLAS быстрее clBLAS на 5% и медленнее CLBlast на 25%. На остальных картах OpenBLAS опережает конкурентов на 5−17%.
GEMV
Производительность clBLAS довольно сильно колеблется исходя из размерности задачи. По сравнению с ним CLBlast работает в среднем на 70% медленнее, но только на BXM-4−64 и на больших размерностях. На остальных же ускорителях CLBlast, наоборот, на 75−100% более производительна, чем clBLAS.
Что касается OpenBLAS, то на Imagination GPU он в среднем на 55−70% медленнее clBLAS. На Mali G610 его отставание от CLBlast и clBLAS составляет 80 и 8% соответственно. Но на Mali G52 производительность OpenBLAS в 2−3,5 раза выше конкурентов.
GEMM
На больших размерностях CLBlast превосходит clBLAS на 45−65%. На BXM-4−64 CLBlast работает быстрее в 2,5 раза. На BXM-4−64 OpenBLAS опережает CLBlast на 10%, на Mali G52 — на 56%. На BXE-2−32 и Mali G610 производительность OpenBLAS ниже, чем у OpenBLAS, на 30 и 44% соответственно.
Из результатов запусков на GPU можно сделать вывод, что в большинстве случаев CLBlast существенно превосходит по производительности clBLAS, исключением являются только запуски memory-bound бенчмарков SAXPY и SGEMV на LicheePi. Также у CLBlast на Mali G610 и BXE-2−32 производительность выше, чем OpenBLAS, во всех задачах.
Сравнение производительности разных GPU
Полученные результаты хорошо соотносятся с характеристиками рассматриваемых GPU: более производительные решения на ARM показывают лучшие результаты. Наиболее высокая производительность — у Mali G610: на AXPY и GEMM ускоритель опережает ближайший по мощности Mali G52 примерно в 2,2 раза, а GEMV в среднем работает вообще в 4 раза быстрее.
Также G610 демонстрирует интересную динамику на матрично-векторном умножении: на больших размерностях происходит снижение производительности. Это может быть связано с тем, что при увеличении количества элементов растет и доля операций по перемещению данных с CPU на GPU в итоговом времени.
Весьма любопытно себя ведут GPU от Imagination. На AXPY и GEMV ускоритель BXM-4−64 работает медленнее, чем BXE-2−32, хотя на бенчмарках clpeak производительность у первого GPU выше. Причиной этому может быть медленный перенос данных с CPU на GPU на плате LicheePi. На GEMM производительность этих GPU соотносится так же, как в бенчмарках clpeak: BXM-4−64 работает примерно в два раза быстрее.
Запуск OpenCL на RISC-V СPU
Для запуска OpenCL на RISC-V мы будем использовать PoCL — переносимую реализацию OpenCL с открытым исходным кодом. Она запускается на большом количестве устройств, включая CPU и GPU общего назначения, а также другие пользовательские ускорители.
Архитектура PoCL включает в себя runtime-библиотеку, реализующую OpenCL API, и компилятор на базе LLVM для компиляции ядер. Специально для поддержки RISC-V в PoCL были произведены следующие модификации:
- добавлены новые поддерживаемые устройства для конфигурации сборки,
- обеспечена компиляция среды выполнения в библиотеку RISC-V посредством кросс-компиляции,
- добавлен новый режим исполнения для автономной компиляции ядра.
Более подробно работа PoCL на RISC-V описана в статье разработчиков Vortex GPU.
Сборка PoCL на плате не вызвала особых затруднений, зато кросс-компиляция обернулась настоящим испытанием. Делюсь порядком действий, к которому я в итоге пришел:
- Установить локально clang и LLVM для локальной машины и для платы (можно просто скопировать с платы /usr/lib/llvm-x, где x — версия LLVM на плате). Версии обеих сборок должны совпадать.
- Если нужно, установить на плате ocl-icd и libhwloc и скопировать их файлы с платы в папку lib, расположенную в установочной директории LLVM, собранной для платы.
- Установить pkg-config на машину, на которой проходит сборка, и настроить переменную окружения.
- Скопировать с платы папку с основными библиотеками (/usr/lib/riscv64-linux-gnu) в папку lib, расположенную в установочной директории LLVM, собранной для платы.
- Cкопировать /usr/lib/gcc/riscv64-linux-gnu/13/libstdc++.so в папку lib, расположенную в установочной директории LLVM, собранной для платы.
- Скачать toolchain для использования заголовочных файлов из папки ris cv-gcc/sysroot/usr/include и скопировать содержимое в папку include из установочной директории LLVM, собранной для платы.
- Добавить флаги компиляции и прописать пути к папкам в файле /<pocl_source_dir>/ToolchainExample.cmake.
- Скопировать с заменой исполняемый файл llvm-config из папки bin в установочной директории LLVM, собранной для локальной машины, — в папку bin в установочной директории LLVM, собранной для платы.
- Так же, как и при локальной сборке, изменить файл <pocl_source_dir>/lib/CL/pocl_llvm_wg.cc.
- Если на этапе сборки PoCL возникнут проблемы с переопределением типов, нужно изменить исходные файлы в директории LLVM, собранной для платы, и исходные файлы PoCL.
$LLVM_BUILD_PREFIX/lib/clang/x/include/stdint.h:
<pocl_source_dir>/lib/kernel/printf.c:
- Осуществить сборку.
Если PoCL был собран с ключом -DENABLE_ICD=ON, нужно исправить путь до библиотеки, который прописан в файле <pocl_install_dir>/etc/OpenCL/vendors/pocl.icd.
Результаты экспериментов
Увы, нам не удалось собрать PoCL специально под плату LicheePI, так что на ней запуски производились с использованием PoCL, собранного под VisionFive.
AXPY
В большинстве случаев разница в производительности между OpenBLAS и CLBlast небольшая, не более 15−20%. Существенный разрыв появляется только на плате Radaxa ROCK5 Model B: ускорение CLBlast относительно OpenBLAS может достигать 85%.
GEMV
С GEMV видно, что хоть на меньших размерностях разница в производительности между CLBlast и OpenBLAS может быть не такой большой (10−15%), но при увеличении числа элементов OpenBLAS оказывается производительнее, чем CLBlast, на 85−230%.
GEMM
В случае больших размерностей производительность OpenBLAS выше CLBlast PoCL: на 25% — на VisionFive, на 55% — на VIM4, на 110% — на Radaxa ROCK5 Model B, на 257% — на LicheePI. Также стоит отметить, что при увеличении числа элементов матрицы производительность OpenBLAS возрастает, а производительность CLBlast, наоборот, падает.
В целом мы получили ожидаемые результаты. Более популярная библиотека OpenBLAS работает лучше, чем экспериментальный и сырой вариант запуска OpenCL BLAS-функций на CPU. Но местами отставание не такое большое даже на compute-bound задаче (GEMM VisionFive 2), а на AXPY CLBlast и вовсе оказывался быстрее. Это аргумент в пользу развития OpenCL на архитектуре RISC-V.
Заключение
Мы убедились, что CLBlast показывает высокую производительность по сравнению с clBLAS и OpenBLAS. Нам удалось запустить CLBlast на RISC-V CPU. Чаще всего CLBlast довольно сильно проигрывает в производительности OpenBLAS, но на memory-bound задачах CLBlast держится на том же уровне или существенно обгоняет OpenBLAS. При замере скорости работы GEMM на плате VisionFive 2 мы получили отставание всего на 25%.
Подобные результаты обнадеживают. Если появится реализация OpenCL, оптимизированная под архитектуру RISC-V, то библиотеки на ее основе смогут приблизиться к более популярным решениям и даже обогнать их.
This guy has a great future.