Использование Python для высоконагруженных математических вычислений в ПО

27/02/23
Технические статьи
Использование Python для высоконагруженных математических вычислений в ПО

Среди различных типов программного обеспечения существует отдельный класс задач, специфика которого не меняется десятилетиями. Изменениям подвержены лишь инструменты решения этих задач. Речь идёт о сложных высоконагруженных вычислениях – решениях задач прикладной математики и численных методах. Стоит сразу обозначить их отличие от систем обработки больших или потоковых данных. Алгоритмически сложные задачи гораздо шире, чем обработка видеопотока или распознавание образов при помощи нейросетей. К таким задачам относятся задачи численного моделирования, методы решения дифференциальных уравнений в области физики, задача шумоподавления в оптических, акустических или радиоканалах и прочие.

В 60-х годах это направление бурно развивалось в связи с массовым (по тем временам) внедрением вычислительной техники в научно-техническую жизнь. Тот период отмечен появлением первого высокоуровневого языка Фортран, специализированных НИИ и факультетов по всему миру. С тех пор происходило медленное, но верное вытеснение вычислительных задач на периферию массового сегмента ПО. Все больше внимание специалистов в области ПО переключалось на офисные приложения, базы данных, потом пришел звездный час веб- и мобильной разработки. Да, там тоже есть место алгоритмам и вычислениям, но лишь как малая и далеко не ключевая часть процесса разработки, которая зачастую решается фреймворками «из коробки». Инструменты для специфических вычислительных работ становились все сложнее, дефицит специалистов на рынке труда возрастал и, следовательно, вся разработка становилась дороже. В этой статье мы рассмотрим возможные решения задач, связанных с высоконагруженными математическими вычислениями при разработке ПО, с учетом бизнес-интересов заказчиков, на примере выполненных командой Ауриги проектов.

Доступные варианты решения

В наши дни если у бизнеса возникает задача решать подобные задачи выбор стека не так уж и велик. Специалистов в фортране вы будете искать долго и это имеет смысл, только если ваша работа планируется на многие-многие годы вперёд, и вы собираетесь держать свою лабораторию с минимальными кадровыми изменениям. Отрадно, что специалисты в этой области и не подвержены резким сменам настроения и прыжкам «с проекта на проект» каждые два года.

Второй вариант до последнего момента был наиболее распространён – поиск и найм математиков-программистов на С/С++. Инженеров на рынке гораздо больше, доступен развитый набор библиотек, в том числе, и от гигантов полупроводниковой индустрии – производителей чипов SoC (System-on-Chip). При должном усердии инженеров и руководителей код получался компактный и быстродействующий, с возможностью использования во встроенных системах (embedded). Но это имело свою цену – разработка была достаточно трудоёмка. Каждая строчка кода стоила дорого. И в первую очередь потому, что прототипирование и поиск оптимальных решений выполнялся на том же «трудоёмком» С/С++.

Третий вариант решал проблему высокой трудоемкости – MatLab и аналоги давали возможность вести исследовательскую работу и искать математическое решение самим учёным, без привлечения программистов. Но обратная сторона медали в том, что полученное решение, как правило весьма объёмное, сложно тиражировать или отдать в эксплуатацию коллегам. MatLab не слишком к этому приспособлен и, к тому же, требует собственную лицензию. А в текущих непростых условиях, так и просто недоступен на территории России и Китая.

Уже долгое время многими в научном мире используется язык Питон (Python), как альтернатива Matlab. Относительная простота и лаконичность языка, обширный набор библиотек, бесплатность – три слона на которых основана его популярность в НИИ и лабораториях. Знание питона (Python) стало таким же обязательным требованием, как когда-то знание LaTeX. Для частного бизнеса Python предлагает тоже одни преимущества. На рынке немало инженеров-разработчиков, которым можно передавать код исследователей для оптимизации и дальнейшей «индустриализации». Сам по себе код уже почти готов к встраиванию в большие системы для ранней интеграции. Однако, распространено мнение, что Python непригоден для продуктовых систем и не может использоваться за рамками прототипов или демоверсий – все нужно обязательно портировать на С/С++. Мы считаем, что это утверждение ложно. И Питон сам по себе, и стандартные протоколы более, чем способны жить большой жизнью, решая задачи сложных математических вычислений «в продакшене». Это позволяет бизнесу минимизировать код, разработанный «в корзину», привлекать недорогих разработчиков широкого профиля к совместной разработке с исследователями, не зависеть от «звезд» в команде (если угораздило такую звезду вырастить) и решать бизнес-задачи своего продукта.

Постановка задачи

Несколько лет назад Аурига по заданию европейского медицинского стартапа разрабатывала решение, связанное с параллельной обработкой нескольких потоков оптических данных. Данные эти имели критическое значение для успеха малоинвазивной хирургической операции и являлись единственным источником информации для хирурга. Требовалось добиться следующих характеристик передачи данных: синхронная обработка параллельных потоков данных с общей частотой 30 кадров в секунду, 400 мс на прохождение кадра от драйвера устройства до дисплея врача.

Концепт решения – TCP сокеты

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

Вся вычислительно-сложная математика (Гильбертово преобразование, преобразования Фурье, адаптивная фильтрация и прочие преобразования каскадом до 20 шагов) проводились в модулях на языке Python с помощью библиотек numpy и scipy. Из-за наличия GIL, истинная многопоточность не поддерживалась, поэтому разработчикам приходилось запускать отдельные экземпляры обработчика на Python в виде отдельного процесса ОС для каждого потока данных. Итоговые, сильно похудевшие данные передавались в UI на PyQt. Первая итерация решения – TCP сокеты.

Математика на тот момент обсчитывала кадр примерно за 150 мс, а в оставшееся время (250мс) требовалось уложить все остальные этапы от драйвера до вывода изображения на экран. Тесты производительности нескольких разных очередей сообщений (ActiveMQ, Mosquitto, RabbitMQ, ZeroMQ) показали недостаточную стабильность и скорость передачи данных – происходила рассинхронизация независимых потоков, что приводило к заметным провалам в визуализации и не могло обеспечить достаточный уровень качества для медицинского изделия.

После первоначальной оценки передаваемых данных, инженерам требовалось обеспечить приём и передачу суммарного потока в 25 Мбит/с и размером кадра в 100 килобайт. С таким потоком вполне могло справиться соединение через TCP-сокеты. Однако, дополнительные исследования оптической части устройства продемонстрировали необходимость в передаче и обработке большего объёма данных. Значительное возрастание детализации считываемых данных потребовало укрупнения размера одного кадра со 100 килобайт до 1.1 мегабайта, что, в свою очередь, увеличило поток данных до 26 Гбит/с., а время обработки одного кадра – до 250–300 мс. В то же время на стенде заказчика TCP сокет показывал максимальную скорость 1.7 Гбит/с на синтетических тестах.

Несоответствие на порядок располагаемых и требуемых скоростей передачи данных приводило к мгновенному переполнению буферов TCP и последующей каскадной потере пакетов. Требовалось найти новое решение.

Вторая итерация – Именованные/анонимные каналы

Следующим кандидатом на среду для передачи были именованные или анонимные каналы. Синтетический тест на стенде показал скорость порядка 21.6 Гбит/с, что вплотную приближалось к требованиям. Однако уже при старте реализации возникли технические сложности:

  • Скорость получения данных превышала скорость передачи, что приводило к неконтролируемому росту буферов и непредсказуемости времени передачи – от 50 до 150 мс.
  • Процессы, обсчитывавшие математику, неожиданно сильно и бессистемно грузили топовый 40-ядерный процессор. Через 10 -15 минут после запуска на всех занятых ядрах нагрузка резко взлетала с ~80% до 100%, а время обработки одного кадра увеличивалось с приемлемых 300 мс до 600–700 мс.

Анализ ситуации помог найти причину – она заключалась в сочетании интенсивного выделения и освобождения памяти, что, само по себе, не было проблемой без перезаписи данных, и системного процесса очистки (обнуления) освобождаемой памяти. Совокупность этих факторов приводила к тому, что системный процесс зануления страниц не справлялся с работой в фоновом режиме и перекидывал её на родные ядра процессов, на которых происходили вычисления, ещё сильнее увеличивая нагрузку на них из-за частых переключений контекста на ядре.

Рефакторинг работы с NumPy заметно снизил количество производимой «грязной» памяти, но полностью проблему не решил. Требовалась быстрая и экономная передача данных на стыке драйвера устройства и Python, где объём передаваемых данных был максимальным. Альтернатива в виде портирования математики из Python в C++ не укладывалась ни во временные ограничения, ни в бюджет проекта.

Финальное решение – IPC протокол

Решение было найдено в реализации собственного IPC протокола на кольцевом буфере в разделяемой памяти – задача, хоть и не часто встречающаяся, но вполне известная инженерам. Немного экзотики добавляли только разные языки программирования — коннектор к драйверу на С++ и вычислитель на Python. В реализации собственного IPC использовался пакет PyWin32 для работы с семафорами через win32api, что позволило получить доступ к одному и тому же семафору из независимых приложений.

Примитивность, как протокола, так и передаваемых данных позволила достичь средней скорости передачи одного кадра за 5 мс. Синтетический тест показал нам максимальную пропускную способность порядка 84.7 Гбит/с, с большим запасом перекрывая требования по объёму передаваемых данных и времени доставки.

Вычисления

Полученные через IPC данные необходимо было подвергать сложным многоступенчатым этапам преобразований и фильтрации шумов. Математическая база уже была во многом подготовлена и протестирована в Matlab. Однако GIL внушал опасения, что параллельную синхронную обработку реализовать не удастся. Тем не менее, запрет на многопоточность «из коробки» для библиотек и механизм разделения вычислений собственной разработки позволили так спроектировать весь процесс, что ресурсы конкретной модели процессора утилизировались максимально доступным образом, т. е. с резервом производительности на нештатные случайные всплески. А речь, напомню, идет о специфическом медицинском устройстве, от которого напрямую зависела жизнь пациента.

Второй проблемой было, как уже указывалось выше, постоянное стремление библиотек генерировать большое количество временных структур в памяти с копированием данных и последующим их освобождением. Доступность исходных кодов open source библиотек и вполне очевидная оптимизация через правильную оценку размеров массивов на старте функций, позволило оперативно снизить влияние и этой проблемы.

И финальной полировкой можно считать уменьшение вычислительной сложности самого кода алгоритмов, уменьшение числа циклов и их итераций, переиспользование структур данных и прочие приемы, известные разработчикам, но далекие от ученых. Это позволило добиться производительности сопоставимой и даже превосходящей Matlab, с учётом более полной утилизации ресурсов процессора. При этом оставался резерв идей для дальнейшего увеличения скорости вычислений, но он не потребовался владельцу продукта.

Последующий перенос решения на Linux-подобную ОС реального времени прошёл уже без сюрпризов. Работы по портированию хоть и потребовали дополнительного бюджета, но были точно оценены и контролируемы с точки зрения расходов. В чем немалая заслуга кроссплатформенности Python.

Выводы

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

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

Свяжитесь с нами напрямую

Офисы

Москва

117587, Варшавское ш., д. 125, стр. 16А

Ростов-на-Дону

344002, пр. Буденновский, д. 9, офис 305

Нижний Новгород

603104, ул. Нартова, д. 6, корп. 6, офис 829