Когда у вас несколько десятков моделей компьютерного зрения, тысячи камер на заводах по всей стране и только несколько секунд, чтобы успеть оповестить оператора — важна каждая миллисекунда.
Но что делать, если вы работаете не в IT-гиганте с дата-центрами и парком GPU, а в промышленной компании с изолированными сетями, ограниченными ресурсами и жёсткими требованиями к отказоустойчивости? Расскажу:
почему разработка видеоаналитики в промышленности отличается от БигТеха;
какие ограничения приходится учитывать: отсутствие GPU, изолированные сети и жёсткие требования к отказоустойчивости;
как удалось оптимизировать пайплайн и сохранить стабильность его работы;
какие локальные оптимизации реально работают (а какие дают минимальный прирост);
как архитектурные изменения увеличили производительность в 28 раз;
с какими вызовами команда сталкивается сегодня и что предстоит решать дальше.
Идеи из этой статьи будут полезны при разработке как продуктов видеоаналитики, так и других систем со множеством источников данных и обработчиков.
Привет, Хабр! Меня зовут Даниил Блинов, я работаю в СИБУРе Цифровом. В IT в общей сложности семь лет, из них три года проработал с децентрализованными системами и почти два года — непосредственно в Видеоаналитике СИБУРа.
Видеоаналитика в СИБУРе — это решение задач компьютерного зрения на изображениях с камер, расположенных на предприятиях, и наблюдающих непосредственно за технологическим процессом. Нужно это для облегчения контроля за производством продукции, состояния оборудования и поддержания безопасной работы на заводах.
Заводы расположены по всей стране. Вот, например, ЗапСибНефтехим в Тобольске.

Проект Видеоаналитика
Внутри завода находится много информационных систем, данные из которых выводятся операторам на мониторы.

Информации много, и на операторов накладывается большая нагрузка, которую мы, команда Видеоаналитики, стараемся снизить.
В зоне ответственности одного оператора может быть до сотни камер. Например, одно из реализованных решений — «чёрный экран», его работа заключается в том, что изображения выводятся операторам на мониторы только тогда, когда действительно требуется внимание оператора.
Когда я только пришёл в команду, основной задачей было сократить среднее время обработки кадра, которое, по сути, сводилось к минимизации потребления процессорного времени.
Зачем?
Первая и самая прямая и очевидная причина — масштабируемость и экономия денег.
Пример:
Скрытый текст
Представьте, что ваш алгоритм обрабатывает 1 кадр за 100 мс. На одном ядре процессора вы можете обработать 10 кадров в секунду.
Запрос бизнеса: "Нам нужно обрабатывать видеопоток с 50 камер в реальном времени".
До оптимизации: 50 камер * 10 кадр/с = 500 кадров в секунду. Вам потребуется 500 / 10 = 50 ядер процессора только для вашего алгоритма. Это очень дорогие серверы.
После оптимизации: Вы ускорили алгоритм в 5 раз. Теперь 1 кадр обрабатывается за 20 мс → 50 кадров в секунду на одном ядре. Для 50 камер потребуется всего 10 ядер.
Результат: в 5 раз сократили требуемую вычислительную мощность и, следовательно, в 5 раз снизили затраты на инфраструктуру (либо аренда, либо закупка, электричество и обслуживание). Система стала легко масштабируемой.
Вторая причина — работа в реальном времени.
Для наших задач критически важна минимальная задержка между событием и реакцией человека. Сократив время обработки кадра, мы уменьшаем общую задержку системы, делая её пригодной для работы в реальном времени.
И третья причина — повышение пропускной способности.
Даже если реальное время не критично, часто важно обработать как можно больший объем данных за фиксированное время.
Пример:
Скрытый текст
Обработка архива видео за прошлый месяц для поиска событий. У вас есть неделя и парк из 10 серверов.
Медленный алгоритм (100 мс/кадр): успеет проанализировать X терабайт данных.
Быстрый алгоритм (20 мс/кадр): успеет проанализировать 5X терабайт данных за то же время и на том же железе.
Результат: Вы либо получаете результат в 5 раз быстрее, либо обрабатываете в 5 раз больше данных, что повышает качество анализа.
И что с этим делать?
Первые мысли, которые пришли в голову — вынести вычисления в облако, а раскатку инференса модели проводить на GPU.
Но это было в мечтах, а реальность оказалась другой.
Требования и ограничения
Географическая распределённость камер
В реальности мы имеем кейсы видеоаналитики, развернутые в серверных, находящихся непосредственно на предприятиях и географически распределенных от Твери до Амура.Максимум пять секунд на оповещение в большинстве кейсов — в течение этого времени нужно уведомить о том, что произошло.
У нас в арсенале 40+ computer vision-моделей, и разрабатываются новые. Поэтому продукт должен быть универсальным и подходить под любые комбинации, которые мы из них составляем.
Система должна быть отказоустойчивой.
Отсутствие GPU и доступа к облакам — пожалуй, самое ощутимое ограничение.
В этой статье по мотивам моего доклада для Saint Highload++ 2025 расскажу, как мне удалось оптимизировать пайплайн видеоаналитики и какие из оптимизаций дали наибольший прирост.
В первом приближении
Первые кейсы видеоаналитики были реализованы в 2019 году. Как часто бывает у новых продуктов, сначала был пайплайн, решающий узкоспециализированную задачу. Глобально пайплайн выглядел так:

Есть камеры, с которых по RTSP считывается видеопоток. Изображения видеопотока поступают в ИВН (Интеллектуальное ВидеоНаблюдение), где обрабатываются computer-vision-моделями. Дальше результаты работы CV-моделей интерпретируются согласно бизнес-логике, например: выводится текст операторам на мониторы, срабатывает звуковое оповещение, отправляются уведомлений на почту, сохраняются данные для аналитических дашбордов и так далее.
Архитектура ИВН
По ходу развития системы, мы пришли к следующей комплектации сервисов:

Я разделил их на три части, по функциональному признаку:
Зелёный цвет — сервисы, которые считывают видеопоток с камер и получают информацию о видеопотоке с систем видеонаблюдения. Некоторые из них также выполняют обработку изображений CV-моделями.
Оранжевый цвет — сервисы, которые обрабатывают результаты работы CV-моделей.
Синий цвет — шина данных. Это написанный нами сервис, который нужен для межсервисной коммуникации внутри продукта.
Hybrid
Самое важное и высоконагруженное звено в нашей системе — сервис, который мы называем гибрид.

Его основная задача — считать видеопоток, обработать изображение CV-моделями и передать результаты на шину данных для обработки следующими сервисами.
Логически гибрид состоит из трех компонентов:

Камера. Это компонент, задача которого по указанному источнику считать видеопоток. Это может быть локальный файл или RTSP-поток.
Детектор. Кадры с камеры поступают на детектор — это обёртка над CV-моделью. Задача — обработать кадр и выдать результаты детекции. Результаты поступают на третий компонент.
Клиент шины. Задача каждого клиента — представить результаты детекции в нужном формате и обеспечить их гарантированную доставку на шину.
У гибрида есть свой конфиг, в котором задаётся описание параметров камер, детекторов и роутов. Роут определяет маршрут кадра: с какой камеры и по каким детекторам в процессе обработки должен пройти кадр. Напомню задачу — минимизировать потребление процессорного времени, имея в качестве вычислительных юнитов только центральный процессор.
cameras:
camera_1337:
cls: opencv
options:
source: rttp://source
fps: 10.0
regions:
region_1:
corners:
- x: 0.0
y: 0.0
- x: 0.026041666666666668
y: 0.046296296296296294
name: region_1
type: point
storage:
duration: 2.0
size:
x: 1920
y: 1080
detectors:
object_detector:
cls: object_detector
config:
object_threshold: 0.4
required_classes:
- person
storage:
cls: ram
object_crop_detector:
cls: object_crop_detector
config:
crop_object_threshold: 0.3
crop_required_classes: [blue, white, yellow, red, none]
storage:
cls: ram
routes:
- camera: camera_1337
chain_name: chain_object_detector
detector:
- condition:
confidence: 0.5
event: person
name: object_detector
- name: object_crop_detector
Профилирование
Перед тем, как проводить оптимизации, выполнили профилирование. Сервис — многопроцессный, и помимо основной вычислительной логики, в нём заключено большое число служебных задач (например, сохранение артефактов в разных форматах, дамп результатов детекции). По ходу профилирования нужно было определить среднее время обработки кадра. Профилирование проводилось следующим образом:
Пайплайн разбит на стадии.
На каждой стадии фиксируется время обработки кадра.
Итоговое время суммируется по всем стадиям и усредняется по количеству кадров.
В результате получилась наглядная картина того, сколько времени мы тратим как на каждую стадию в отдельности, так и целиком на весь пайплайн.
Результат профилирования показал:
Наибольшая часть процессорного времени тратится на детекцию.
Утилизация ресурсов сильно зависит от кейса, но это всегда больше 75%.
В текущем, рассматриваемом кейсе 95% процессорного времени уходит на детекцию.
Помимо этого, определялось, сколько ресурсов затрачивается на основные и служебные задачи. На основе этого были проведены локальные и глобальные оптимизации, о которых будет рассказано далее.
Исходная архитектура
До оптимизации гибрид представлял собой следующее:

Для эффективной работы с процессами и потоками мы разработали такую функциональность, как экзекьютор. Это обёртка, позволяющая запускать код в отдельном потоке или процессе. Коммуникация между экзекьюторами реализуется с помощью паттерна Pub/Sub. На схеме каждый блок — это отдельный экзекьютор. Весь жизненный цикл сводится к тому, что экзекьютор дожидается данных в свою очередь, обрабатывает их и результат кладёт в единственного получателя.
Изначально, когда гибрид попал ко мне в руки, он представлял собой набор экзекьюторов, которые при запуске сервиса жестко фиксировали указатель на своего получателя. Кроме того, изменение конфигурации, задержки и сбой в любом из компонентов вызывали перезапуск всего сервиса. То есть все компоненты прекращали свою работу, применялась новая конфигурация, и компоненты запускались заново. Это обусловлено тем, что изначально гибрид разрабатывался для работы с небольшим количеством камер, и задержка означала неполадку, которую проще и быстрее решить за счет рестарта, равно как обработка сбоя или изменения конфигурации.
Также подчеркну, что цепочка на схеме отражает лишь один роут. Если роутов в конфиге несколько, то и цепочек будет столько же.
Здесь приходим к тому, чтобы подсчитать, какое количество экзекьюторов у нас было.

На одну камеру приходилось два процесса из пяти потоков. В этих потоках выполнялись задачи ввода-вывода, например, чтение кадров, буферизация, сохранение на диск и другие.
На один детектор приходилось по одному процессу из шести потоков, пять из которых выполняли разнородные I/O-bound задачи, а шестой выполнял CPU-bound задачу — детекцию.
Несколько потоков в главном процессе выполняли роль клиентов шины.
Достоинства такой архитектуры:
Решение реализуется достаточно быстро.
Простая логика маршрутизации.
Потенциально меньше проблем с гонками данных и очередями.
Это объясняется линейностью передачи данных: данные ожидаются из одного источника и помещаются в одного получателя.
Недостатки и ограничения:
Высокий темп роста количества потоков и процессов.
Такое решение подходит, если у нас есть одна камера и пара детекторов, но с увеличением их количества сильно проседает производительность, поскольку растет количество потоков и процессов.
Сильная связность компонентов.
При добавлении новой фичи или тестировании гипотезы, может потребоваться переписать весь пайплайн. Также это мешает применять решение на нескольких камерах и детекторах, поскольку при изменении конфигурации или сбое в одном из компонентов перезапускается весь сервис. Для решения второй проблемы требуется реализовывать асинхронное обновление конфигурации и обеспечивать меньшую связность компонентов.
Сложно масштабировать.
Локальная оптимизация
Перед тем, как проводить какие-то радикальные изменения, начали с локальных оптимизаций. Все числа, которые приведу в рамках каждой оптимизации, независимы друг от друга. Основной язык, на котором написан гибрид, это Python v3.11. Имплементацию используем классическую — CPython.
Использование альтернативных имплементаций Python
Учитывая огромную кодовую базу, при использовании альтернативных имплементаций Python мы опирались не только на возможный прирост в производительности, но и оценивали объем кода, который придется переписать.
PyPy
Потребление 0.5x CPU, 2.5x RAM
Пробовали использовать интерпретатор PyPy, который в других сервисах снизил нагрузку на процессор в два раза, но столкнулись с ограничениями:
Далеко не все библиотеки поддерживают PyPy. Например, uvloop может отрабатывать в четыре раза быстрее, чем asyncio, или orjson, который сериализует данные в десятки раз быстрее, чем стандартный пакет.
Ограничение по версии Python.
Cython
Потребление 0.85x CPU, 1x RAM
Нужно переписывать всю кодовую базу.
Преимуществ, предлагаемых Cython, достигли пока что не полностью, поскольку для этого требуется модификация всей кодовой базы. Эти изменения мы отложили на будущее.
Внедрение самописных библиотек на компилируемых языках
Помимо Python, в наш код интегрированы самописные библиотеки на Rust и сторонние библиотеки на C и C++. Библиотеки на Rust появились после профилирования, когда мы увидели, что некоторые функции из стандартных пакетов Python представляют собой системные вызовы, который заключены в тонну питоновских оберток.
Например, заменив вызов функции fromtimestamp из пакета datetime парой вызовов на Rust:
ts = Utc.timestamp_opt(timestamp, 0).unwrap();
dt = DateTime::<Local>::from(ts).to_string(),
мы смогли сократить утилизацию ЦПУ на вызов этой функции с 2% до 0,5%, таким образом, обеспечили глобальный прирост в 1,5%.
Переписывание малоэффективных функций — глобальный прирост <1%
При локальной оптимизации зачастую имеет место переписывание малоэффективных функций, выявленных при профилировании. В нашем случае это обеспечило глобальный прирост меньше 1%.
Разделение I/O-bound и CPU-bound задач — глобальный прирост 1%
При анализе процесса детекции и его оптимизации, возникла интересная идея, связанная с планировщиком операционной системы.
Напомню, что в процессе детектора было шесть потоков, пять из которых — I/O-bound, а шестой — CPU-bound. Выделив CPU-bound поток в отдельный процесс, появилась возможность, во-первых, этим процессам распределяться на разные ядра (а значит, снижение конкуренции за ресурсы и меньше затрат на переключение контекста), а во-вторых, использовать приоритизацию процессов планировщика ОС.
Numba — глобальный прирост <1%
Ещё один важный инструмент — это JIT-компилятор Numba.
Подготовка изображений для детекции, а также код детекторов содержат много математических операций, которые могут хорошо оптимизироваться при помощи Numba. В нашем случае, произошло ускорение работы некоторых функций в пять раз, но мы столкнулись с двумя ограничениями:
Numba поддерживает ограниченное множество математических операций, и многие используемые в наших детекторах функции в это множество не входят. Например, все функции из библиотеки см2 и часть функций из библиотеки numpy.
Для достижения всех преимуществ, предлагаемых Numba, необходимо изменять кодовую базу (путем задания аннотаций или более упрощенного представления кода), поскольку Numba плохо работает с кастомными типами.
Использование libjpeg-turbo — ускорение сжатия в jpeg в 5 раз
Финальная из локальных оптимизаций — это инструменты для сжатия изображений. Профилирование показало, что 90% времени пайплайна, которое не тратится на детекцию, уходило на сжатие изображений в jpeg. Мы изначально сжимали с помощью opencv, но заменив это на инструмент libjpeg-turbo, достигли ускорения сжатия в пять раз.
Глобально все локальные оптимизации обеспечили прирост производительности на 5%, и мы уперлись в потолок. Стало понятно, что нужно менять архитектуру.
Нужно менять архитектуру
Перед изменениями я определил следующие вводные.
Ориентиры
Сократить количество объектов ОС. Нужно снизить темп выделяемых объектов операционной системы, по максимуму сжать процессы и потоки, и, где возможно, использовать асинхронные операции.
Бесперебойно поставлять кадры на детекторы. Поскольку детектор представляет собой самый нагруженный компонент, не должно быть перебоев в поставке данных до него.
Эффективно параллелизировать детекцию.
Асинхронно обрабатывать сбои и переконфигурацию компонентов сервиса. Изменение конфигурации или сбои в любом из компонентов не должны вызывать перезапуск всего сервиса.
С чего начать?
Оставляем камеры и детекторы в рамках одного сервиса. Камеры и детекторы были оставлены в рамках одного сервиса, чтобы избежать издержек при передаче кадров по сети.
Организуем микросервисы внутри микросервиса через внутреннюю шину данных. Для минимизации связности компонентов сервиса и достижения преимуществ микросервисной архитектуры, я выделил логические функциональные компоненты и связал их внутренней шиной данных.
Сохраняем идею с экзекьюторами, но меняем логику их жизненного цикла, обеспечивая автономность от других экзекьюторов. Дальше расскажу в деталях про каждый компонент, а затем посмотрим, как они вместе образуют единую картину.
Камера
Изображение
В основном мы работаем с IP-камерами, изображение чаще всего RGB, Full HD, кодек H264. В 99% случаев нужен ресайз перед обработкой CV-моделью.
Работа с видеопотоком начинается с чтения и декодирования кадров. Я попробовал разные инструменты для этих операций, в том числе:
gstreamer;
kornia-rs;
opencv с разными бэкендами:
- ffmpeg + opencv;
- gstreamer default + opencv;
- gstreamer custom + opencv,
Кадр (фрейм) — это numpy-массив, готовый для обработки детекторами.
Таким образом, все бенчмарки сводились к тому, чтобы определить, что эффективнее всего преобразует видеопоток в numpy-массив. В нашем случае наиболее эффективным оказалось использование opencv с кастомным пайплайном gstreamer, но прирост в производительности составил всего 0,001%.
Пайплайн gstreamer представлял собой классическое считывание RTP пакетов, декодирование в RTSP пакет и затем ресайзинг. В какой-то момент выяснилось: независимо от того, насколько качественное изображение приходит с камер, оно практически всегда «шакалится» до матриц малой размерности, например, 128×128, в редких случаях это качество в 720p.

Зачастую cv-модели не нужно высокое разрешение, поэтому изображение всегда ресайзится.
Пришла идея выполнять ресайзинг средствами gstreamer на этапе получения изображения вместо того, чтобы делать это вручную в коде. В результате это сильно разгрузило оперативную память, но не подошло практически ни для одного кейса, потому что одной из задач гибрида является выгрузка по запросу изображений в том виде, в каком они приходят с камер.
При чтении и декодировании кадров мы применяем подход grab-retrieve. Поскольку мы работаем с живым потоком, и при этом ограничены в скорости детекции, для каждого кейса определяется, какое оптимальное количество кадров нам нужно, чтобы выполнять требования по бизнес-логике. Поэтому читаем каждый кадр, а декодируем выборочные, когда наступает нужный момент. За счет этого экономим большое количество ресурсов.
Детекторы
Изначально под каждый кейс видеоаналитики разрабатывался один большой, монолитный детектор — многокаскадная модель, в которой все этапы обработки выполнялись последовательно. Для нас, разработчиков, детектор являлся черным ящиком — инструментом, который встраивается в пайплайн, и на исходный код которого мы не можем влиять.
Отсюда вытекало две проблемы:
Много копипасты для смежных задач.
Такое решение невозможно эффективно распараллеливать.
Приведу пример.

Задача — классифицировать сотрудников по наличию каски и её цвету:
На первом каскаде определяем человека.
На втором — определяем, есть ли на нём каска и какого она цвета.
Допустим, на изображении 100 человек. В случае монолитного детектора на первом каскаде мы в одно действие определяем 100 кропов изображения, а затем на втором каскаде каждый кроп последовательно начинает обрабатываться. Вопрос, который возникает при попытке распараллелить — как делить задачи? Если мы сделаем два инстанса этого детектора, то так же на первом каскаде в одно действие будем детектить 100 кропов, а затем неразрывно будет выполняться второй каскад.
Так мы пришли к идее с цепочками детекторов, или детектор-чейнам.

Идея следующая. Каждый каскад монолитного детектора выделяется в обособленный, маленький детектор, кото��ый выполняет типовую задачу (детекция людей, касок, брикетов и так далее). Таким образом, получается набор инструментов, которые нужно как-то организовать. Для этого применяем паттерн Manager-Worker — каждому уникальному типу детектора назначается свой менеджер.
Менеджер детектора:
Автоматически регулирует количество воркеров, опираясь на загрузку сервера и другие показатели.
Организует распределение задач по воркерам и сбор результатов.
Эффективно реализует распараллеливание за счёт выделения каскада в отдельный детектор.
Выделив функциональность каскада в отдельный детектор, мы убрали привязку к другим каскадам, что позволило в рамках одного сервиса реализовывать самые разные цепочки. Например, если по одной камере нужно подсчитать число людей, а по второй камере определить людей с касками, то у этих двух задач есть общее звено — детекция человека. Соответственно, если мы объединим усилия в один компонент, сможем реализовывать распараллеливание красиво и эффективно.
Клиенты шины
Клиенты шины в рамках гибрида представляют собой пост-процессинг результатов детекции:
Выполняют I/O-задачи: отправка событий, метрик и артефактов.
Сетевой клиент взаимодействует с шиной по WebSocket.
Сериализация в json с помощью библиотеки orjson.
Собираем всё вместе
Итоговая картина выглядит так:

На этапе запуска гибрида парсится конфиг и определяется, из каких источников будет считываться видеопоток, какие cv-модели будут задействованы, с каких камер на какие детекторы будут поступать кадры, и другие параметры.
Каждая камера захватывает кадры из своего источника и формирует специальный пакет, который поступает во внутреннюю шину данных. В пакет закладываются кадры, имя отправителя и получателя, имя детектор-чейна и ttl.
Из внутренней шины пакет перетекает в роутер детекций. Роутер распределяет пакеты в буферы детекторов-получателей, который может быть несколько. Внутри буферов реализована логика с разделением очередей по продюсерам,
Детектор-менеджер извлекает из своего буфера очередной пакет и распределяет его на выбранный воркер согласно заданной стратегии распределения. После обработки воркером пакета, результаты поступают во внутреннюю шину данных, из которой ивенты и метрики с помощью клиентов шины отправляются во внешние сервисы, а производные пакеты, в которых находятся кропы от предыдущих детекторов, идут на следующие детекторы, и так по цепочке.
Если оценивать эффективность такой архитектурной оптимизации, то в рамках одного роута удалось достичь ускорения в пять раз, а для 16 роутов — в 28 раз.

Подчеркну, что цифры сильно зависят от выбранного кейса. Наиболее высокими показатели будут там, где есть смежные задачи между роутами.
При разработке продукта с такой архитектурой нужно ответить на два вопроса:
Возможны ли гонки данных?
Может быть сценарий, когда несколько продюсеров пишут в одного получателя. Скорее всего, они будут поставлять кадры с разной скоростью. Если у нас единая очередь для получателя, может возникнуть ситуация, когда более быстрый перебивает своими задачами более медленного. Получается, что мы что-то будем обрабатывать регулярно, а что-то — как придётся. Для решения этой проблемы была разработана очередь с квотами, где в рамках очереди каждого буфера детектора выделяется фиксированное число ячеек, в которые может писать только какой-то определенный продюсер.Просчитаны ли размеры очередей?
Слишком большой размер: задержка детекции, рост RAM.
Слишком маленький размер: разрыв между соседними кадрами.
У нас есть большое число очередей, размеры которых нужно обязательно просчитывать. Если этого не сделать и не ограничить очередь, у нас быстро закончится оперативка. А если размер оставить слишком высоким, возникает риск большой задержки при детекции. Объясняется это тем, продюсеры с большой вероятностью могут поставлять кадры быстрее, чем они будут успевать обрабатываться, из-за чего очередь в буфере будет расти. Но если размер слишком маленький, можно выстрелить себе в ногу, когда, например, какая-нибудь stateful-модель опирается на предыдущие кадры, которые были ей получены, из-за чего она может делать неверные выводы.
Приведу пример — кейс с подсчетом готовой продукции.
Есть конвейерная лента, в которую поступают брикеты из верхнего положения в нижнее. Задача модели — определить брикет и его переход из одного положения в другое. Лента при этом движется с высокой скоростью, а сама модель чувствительна к частоте кадров.

Если мы поставим слишком низкую скорость чтения видеопотока, получится так, что модель брикет определила на первом кадре, а когда дошла до следующего кадра, брикет уже уехал в другой конец ленты. Модель брикет потеряла и не смогла сделать верные выводы. При этом, если ставить слишком высокий fps, может возникнуть ситуация, когда модель обрабатывает текущий кадр, а следующий, который ей нужен, уже был удален из очереди, потому что пришли новые кадры, которые заменили старые. То есть нужно и просчитывать оптимальный fps, и размер очереди выставить правильно. Как именно — зависит от кейса.
Резюмируем
Были проведены локальные и глобальные, архитектурные оптимизации.
Локальные оптимизации:
Сжатие изображений с libjpeg-turbo
локально 5х, глобально <3%Numba
локально 5х, глобально <1%От интерпретируемого к компилируемому
Разделение CPU-bound и I/O-bound
Смена имплементации Python
2х снижение нагрузки на CPU при использовании PyPyПереписывание малоэффективных функций
глобально <1%
Из локальных оптимизаций наибольший прирост дало внедрение инструмента для сжатия изображений libjpeg-turbo и замена интерпретируемого кода на компилируемый (применение JIT-компилятора Numba, самописных и сторонних библиотек на Rust и C/C++). Потенциально перспективно выглядит разделение процессов по CPU-bound и I/O-bound задачам и использование планировщика операционной системы.
Когда мы уперлись в потолок, достигнув прироста производительности в 5%, перешли уже к глобальным изменениям.
Глобальная оптимизация через изменение архитектуры:
Мультипроцессная архитектура с IPC через очереди сообщений и внутреннюю шину данных.
Изолированность компонентов
Выделили функциональные компоненты, которые были в сервисе, и связали их через внутреннюю шину данных, вместо того, чтобы они были жестко связаны друг с другом напрямую.
Детекторы реализуются по паттерну Manager-Worker
Динамическое регулирование нагрузки
Разделив многокаскадные детекторы на множество однокаскадных и организовав детекцию с помощью паттерна Manager-Worker, удалось достичь глобального прироста производительности в пять раз для одного роута (одна камера, в которой два детектора) и в 28 раз для 16 роутов.
Видеоаналитика не стоит на месте. Мы продолжаем развиваться, и перед нами стоит ряд вопросов:
Как автоматизировать тестирование продуктов видеоаналитики?
Как наиболее эффективно декодировать кадры в numpy-array?
Что еще можно оптимизировать в пайплайне?
Если перед вами стоят аналогичные вопросы, приходите на следующий сезон конференции Highload++ 2025 — будем разбираться вместе.
Комментарии (2)
Zekori
03.10.2025 14:42если избавиться от кучи копирования памяти (в том числе скрытого, при передаче из одной библиотеки в другую) , то будет куда интереснее.
камера -> еpoll -> депакетайзер -> nalu -> openh264 -> кадр -> yuvtorgb -> avx2, 512-> r, g, b каналы. все это можно выполнить с минимальным количеством memcpy, а дальше как написали выше openvino и вполне себе неплохой результат на cpu можно получить
xaoc80
Главная тема статьи: "Что делать, если нет GPU"? как-бы осталась не раскрыта полностью. Ваши оптимизации понятны, они больше архитектурного характера. Но, что вы делали с инференсом модели? Если это cpu Intel, то логично, что использовалось openvino для оптимизации модели и инференса. Если это что-то иное, то tflight или что-то похожее. Может быть batch оптимизация работала хорошо на cpu. Опять таки, в серверных процессорах Intel тоже GPU встречается.