Система плагинов: как завезти нейронные сети в dotnet
Эта статья о том как запихать нейронную сеть в каждый компьютер или смартфон используя dotnet, С++, плагины и немного магии!
Вот уже несколько лет я занимаюсь тем что в свободное время веду Open Source проект Lacmus цель которого помогать добровольным поисково-спасательным отрядом и мчс находить пропавших в лесу людей с помощью компьютерного зрения и нейронных сетей. Подробнее об этом можно почитать в нашей статье на Хабре или посмотреть документальны ролик.
Предыстория
Одним из наших продуктов является кросс платформенное приложение написанное на dotnet core и AvaloniaUI работающее на Windows / Linux / MacOS. Наша главная цель обеспечить возможность запуска и работы приложения на максимальном количестве устройств с максимально возможной производительностью - ведь от скорости обработки данных может зависть жизнь пропавшего бедолаги. Во многом по этому в качестве фреймверка отрисовки интерфейса был выбран Avalonia UI - ведь он быстрый, требует мало ресурсов и способен запускаться на любых устройствах: от Windows компьютеров до RaspberyPI с Arch Linux на борту. А еще на нем можно рисовать красивый UI.
Приложение состоит из 2 частей:
- GUI фронтенда
- Бекенда запускающего нейронные сети
О GUI я немного рассказывал ранее, но как быть с нейрончками?
API v1: как это работало раньше?
Решение было найдено довольно быстро - docker!
Docker - ПО, позволяющее запускать процессы в изолированном от основной ОС окружении на базе специально созданных образов. Таким образом мы избавляем пользователя от необходимости установки дополнительных драйверов, сборке пакетов, настройки окружения необходимого для запуска и работы нейронной сети. Пользователю достаточно иметь только Docker все остальное программа сделает за пользователя. Подробнее о docker можно почитать в моей предыдущей статье в разделе “Погружение в контейнер”.
Итак…
- Для запуска нейронной сети создавался контейнер, внутри которого был маленький http web server (написаный на python) умеющий обрабатывать картинку с помощью нейронной сети и предоставляющий REST API для взаимодействия. Там же, в контейнере находились все необходимые для работы драйвера и библиотеки.
- Контейнер загружался на публичный реестр Docker Hub и становился доступным для скачивания.
- GUI приложение содержало в себе клиент к docker который мог манипулировать контейнерами: запускать, скачивать, удалять…
- После запуска контейнера с нейронной сетью приложение узнавало его порт на localhost-е и начинала отправлять на локальный сервер http запросы с картинками, а в ответ получала результаты работы нейронной сети.
У такого подхода есть очевидные достоинства:
- Конечно это простота разработки и внедрения. Закинул все что надо в контейнер, вбил заветные
docker build
иdocker pull
в терминале и вот твоя новая модель уже доступна всем пользователям. - Отвязанность от языка программирования и ML фреймверков. В докер можно закинуть все что угодно. Ты кодишь на С++ и нависал инференс своего алгоритма на нем - не вопрос. Закодил сервер на python - пожалуйста. NodeJs + TensorflowJS на худой конец? даже эта штука заведется)
- Также docker regestry - удобный способ хранения и версионирования контейнеров.
Но за эти удобства приходится платить:
- Производительность в Windows/OSX. Докер хорошо и нативно работает только на Linux. Тут все процессы запускаются без виртуализации, а значит нет просадок по производительности. Но к сожалению на других ОС это не так: там докер это обычная виртуальная машина с linux, на которой уже запущен докер со всеми вытекающими. Даже на близких к linux - OSX и BSD системах нативной поддержки docker нет и это тоже виртуалка.
- Второй пункт вытекает из первого. На всех системах кроме linux невозможно (или очень сложно) пробросить в docker периферийные устройства - будь то USB с тензорным сопроцессором (intel mividius, google edge tpu) или PCI-E устройство.
- Относительная сложность установки docker на не Linux системы. Это в linux есть пакетные менеджеры и можно прописать docker в зависимости своего приложения. При установке пакетный менеджер сам скачает и установит нужные зависимости. А вот в windows и MacOS по умолчанию этого не предусмотрено => задача установки docker ложится на плечи пользователя, что может вызвать трудности у некоторых людей.
- Размер образов. Помимо операционной системы, docker образ содержит в себе все драйвера и библиотеки и веса модели. Поэтому вес докер образа мог быть и 2 и 4 гб. Это конечно меньшее зло но все-же.
- ARM. ARM шагает по планете, а у докер пока очень слабая поддержка данной архитектуры.
По началу большинство наших пользователей использовали linux и проблем не было (золотое время). Но постепенно, с ростом популярности ПО к нам стали приходить люди с Windows или OSX. И если на машине на Linux обработка 1 изображения занимает 1-2 с, но такой же машине на Windows это же время составляет уже долгие 10 c (!). Да да, виртуализация на Windows и MacOs - полный отстой… До KVM им как до луны… Все это подтолкнуло меня на мысль что дальше так продолжаться не может и миру нужны новые герои…
API v2: Главное - это плагины!
Очевидно что необходимо отказываться от докера - запускать весь код нативно. Но как? Как обеспечить кросс-платформенное выполнение разного кода на разных платформах? Как дать возможность разработчикам использовать разные фреймверки и технологии? Как избавить простого пользователя от установки дополнительных драйверов? Сейчас разберемся!
Будем двигаться снизу вверх. Начнем с низкоуровневых, платформозависимых аспектов и закончим верхнеуровневым кросс-платформенным api.
0. Железо и операционные системы
Несмотря на то что мир компьютерной техники стал более-менее стандартизованным по сравнению с тем что было 15-20 лет назад - количество платформ все еще остается довольно большим. Давайте рассмотрим современные актуальные платформы под которые и будем писать свой нативный C++ код.
- Архитектуры процессора:
- x86_64
- Arm
- Операционные системы:
- Linux
- Widows
- MacOS
- Android
- IOS
- Вычислительные устройства:
- CPU
- GPU
- Nvidia Cuda GPU
- AMD GPU
- Intel GPU
- Сопроцессоры
- Google Edge TPU
- Intel Movidius NPU
С помощью нехитрых математических расчетов и здравого смысла можно легко актуализировать количество поддерживаемых нужных нам платформ:
Как видно из схемы различных конфигураций платформ довольно много. По этому для уменьшения количества информации в этой статье мы рассмотрим только 64-bit системы и только CPU и GPU. Ну что же, давайте карабкаться выше.
1. Драйвера и бекенды фреймверки нейронных сетей.
И так у нас есть конечные железяки на которых запущена та или иная ОС. Чтобы заставить железку производить какие-либо вычисления нужно научиться общаться с железкой на понятном ей языке. Этим и занимаются драйверы и ml-бекенды.
- Драйвер - это программа которая обеспечивает низкоуровневый доступ к железяке. Если драйвер не установлен - то железяка не будет доступна в системе.
- ML-бекенд - это набор низкоуровневых библиотек предоставляющий набор базовых математических операций на железяке или группе железок.
Бекендов может быть много и они могут быть совместимы с разным оборудованием. Так например, выполнять расчеты на процессорах можно с помощью MKL-DNN и oneDNN, на GPU от зеленых - CUDA+cuDNN и directML, на красных GPU - ngraph, ROCm и directML. Очевидно что выбор бекенда прямым образом влияет на производительность и скорость вычислений. Бекенды могут быть как открытыми так и проприетарными. Вторым фактором производительности является операционная система.
Самым быстрым на данный момент набор CUDA+cuDNN+Linux. Именно по этому для обучения нейронных сетей используются GPU от Nvidia и Linux сервера.
Протестировав производительность и выбрав оптимальные я пришел вот к таким результатам:
2. Высокоуровневые ML фреймверки и библиотеки
За сравнительно малое время существования ml как отрасли возникло и умерло много фреймверков. На данный момент актуальными и самыми популярными фреймверками глубокого обучения являются PyTorch и Tensorflow. У каждого из них есть свои фанаты и противники.
Помимо фреймверков для разработки нейронных сетей (PyTorch и Tensorflow) есть библиотеки для инференса (запуска) уже готовых, обученыx моделей. Одни из них является OnnxRuntime. Его разрабатывает и поддерживает Microsoft.
Каждый фреймверк или библиотека имеет свое C++ API и его биндинги в Python.
Для того чтобы иметь возможность использовать фреймверк в шаровом коде мне необходимо было пробросить C++ API в шарпы. …И собрать нативные библиотеки этих фреймверков для всех платформ и бекендов.
Так как я являюсь поклонником Tensorflow то я реализовал поддержку этого фреймверка. И OnnxRuntime - но тут за меня постарался Microsoft.
Сборка библиотек
Итого у меня получился вот такой вот список нативных библиотек:
- Linux-x64:
- tensorflow
- cpu (oneDnn)
- gpu (cuDnn)
- gpu (rocm)
- onnxRuntime
- cpu (oneDnn + openMP) - спасибо microsoft
- gpu (cuDnn) - спасибо microsoft
- gpu (rocm)
- tensorflow
- Windows-x64
- tensorflow
- cpu (oneDnn)
- gpu (cuDnn)
- gpu (directML)
- onnxRuntime
- cpu (oneDnn + openMP) - спасибо microsoft
- gpu (cuDnn) - спасибо microsoft
- gpu (directML) - спасибо microsoft
- tensorflow
- OSX-x64
- tensorflow
- cpu (oneDnn)
- onnxRuntime
- cpu (oneDnn + openMP) - спасибо microsoft
- tensorflow
Примечание - сборка tensorflow это довольно трудоемкий процесс и по сложности он сопоставим наверно со сборкой linux из source кода. Только компиляция занимает 5-8 часов. Самым сложным была сборка под windows с ее отвратительным microsoft C++ компилятором… OnnxRuntime собирается легче и быстрее. В общей сложности на сборку и фикс ошибок всего у меня ушли почти все новогодние праздники)))
Интеграция в C#
OnnxRuntime и так имеет официальное C# API по этому тут не было никаких проблем. Что касается Tensorflow то тут есть 2 проекта - умерший TensorflowSharp
и активно развивающийся Tensorflow.NET
. Ни с тем ни с другим из коробки у меня модель не заработала. Но Tensorflow.NET
оказался более хорошим, разработчики быстро ответили на мое issue я завез им Pull Request и у меня все взлетело! (ура).
3. Система плагинов
Для того чтобы пользователи могли устанавливать различные версии ML моделей а также чтобы другие разработчики могли создавать и подключать свои модели я запилил систему плагинов. Тут будет немного кода и описание того как это работает.
Интерфейс
Для того чтобы было возможно запускать и управлять плагинами у них должен быть единый интерфейс - IObjectDetectionPlugin
:
Он предоставляет программе информацию о плагине и позволяет создать ml модель.
Давайте пробежимся по полям.
Version
- это структура которая хранит версию плагина:
InferenceType
- тип устройства на котором будет производиться инференс:
OperatingSystems
- набор операционных систем для которых доступен плагин:
Сама модель представлена интерфейсом IObjectDetectionModel
:
Метод Infer
реализует инференс нейронной сети. На вход он принимает imagePath
- путь до картинки и ее размеры в пикселях width
и height
. В качестве результата он отдает коллекцию распознанных объектов IObject
:
Пишем свой плагин!
В данном случае будем рассматривать пример с использованием Tensoeflow cpu (oneDnn).
Первым делом создаем проект с нашим плагином LacmusRetinanetPlugin.csproj
и добавим туда ссылку на проект с базовыми интерфейсами:
Важно добавить свойства <Private>false</Private>
и <ExcludeAssets>runtime</ExcludeAssets>
чтобы плагин был доступен для загрузки и вместо с ним поставлялись все нативные библиотеки для различный ОС.
Далее нам нужно установить C# биндинги для работы с tensorflow:
А также нативную библиотеку с нужной нам платформой
Для CUDA+cuDNN надо использовать Install-Package SciSharp.TensorFlow.Redist-Windows-GPU
и Install-Package SciSharp.TensorFlow.Redist-Windows-GPU
. Для DirectML - Install-Package SciSharp.TensorFlow.Redist-Windows-DirectML
для rocm - Install-Package SciSharp.TensorFlow.Redist-Linux-Rocm
и т д. С OnnxRuntime история повторяется. Список доступных библиотек можно будет посмотреть в нашем репозитории.
Важно понимать что одновременно можно использовать только один набор нативных библиотек. То есть установить в один плагин и tf c oneDnn и tf с Cuda не получится (я надеюсь понятно почему). Для каждого типа инференса нужно будет собрать отдельный плагин.
И так для нормальной работы плагина нам необходимо имплементировать 3 интерфейса IObjectDetectionPlugin
, IObjectDetectionModel
и IObject
.
Код класса Plugin.cs
:
Код класса Model.cs
:
И наконец DetectedObject.cs
:
После сборки плагина мы получим вот такую структуру:
Как видно все наши нативные библиотеки и их зависимости включены в сборку и пользователю не придется ничего устанавливать
Веса модели frozen_inference_graph.pb
тут были добавлены в проект как EmbeddedResource. Таким образом мы зашиваем их в готовую библиотеку и скрываем от пользователя. Чтобы получить их придется заняться реверс инженерингом и изрядно попотеть. Б - безопасность наше все.
После успешной сборки плагин можно протестировать с помощью консольной утилиты которая находится в том же репозитории.
Что дальше?
Плагины надо как то доставлять конечным пользователям. Тут ничего не поделать придется писать код реализующий реестр плагинов и предоставляющий API для доступа к ним. Скорее всего это будет что-то по типу docker реестра - open source сервер на dotnet. Мы сможем поднять его на наших серверах и будет что то вроде “официального” реестра. Открытый код же позволит другим разработчикам поднимать свои реплики у себя или к нам в рестр, а пользователь используя функционал программы сможет добавить себе тот или иной реестр и получить плагины из желаемого источника. Как говориться свобода и демократия.
Также есть мысль подобным образом сделать биндинг python и C# кода. Тогда разработчик сможет писать плагины на родном ему python. По задумке разработчик должен будет написать python скрипт реализующий необходимый интерфейс, дать команду сборщику и запустить процесс сборки. Сборщик генерирует C# код, установит необходимые библиотеки и запустит процесс компиляции. После компиляции появится готовый плагин, в папке native
окажутся интерпретатор питона виртуальное окружение со всеми необходимыми пакетами, чтобы конечному пользователю не пришлось ничего устанавливать. А C# код вызывал бы интерпретатор python и получал бы от него результат как то так:
Вот она дружба народов!
З.Ы.
Ыыыы. Што то сложна вышло. Надеюсь, ваш мозг взорвался и вы дожили до конца) Надеюсь, было интересно. Спасибо за внимание.
Полезные ссылки
- Исходный код проекта с плагинами - да пока тут нет внятного описания. Скоро появися)
- Исходный код GUI приложения
- Исходный код нейронной сети
- Разница в декодировании изображений между opencv и tf
- Ссылка на наш чат в телеге - заходите у нас лампово