← Новости

165 кадров в секунду на пределе шины PCIe обходят через C++/CUDA

165 кадров в секунду на пределе шины PCIe обходят через C++/CUDA
Иллюстрация: The Value Engineering, ИИ

Частота в 165 кадров в секунду при инференсе нейросетей смещает аппаратное узкое место с чистой производительности кремния на шину передачи данных. Когда модели с INT8-квантованием отрабатывают за единицы миллисекунд, передача кадров высокого разрешения и ограничивающих рамок для задач детекции объектов между центральным процессором и дискретным графическим ускорителем по шине PCIe начинает срезать пропускную способность всей системы. Возникает вопрос: сколько серверных стоек удастся сэкономить при отказе от стандартных интерфейсов и переходе на специализированный конвейер? Ответ упирается в расчет задержек ввода-вывода, где выигрыш обеспечивается низкоуровневой дисциплиной работы с памятью.

Архитектура Zero-Host-Copy удерживает видеокадры целиком в памяти графического процессора на протяжении всего цикла — от чтения с диска перед аппаратным декодированием до записи после финального кодирования. Копирование между хостом и ускорителем здесь сведено к минимуму, а передача блоков данных между стадиями опирается на принципы Zero-Copy, где модули обмениваются указателями без дублирования массивов.

Синхронизация и скрытые паузы GPU

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

«Динамическое выделение памяти в процессе работы системы столь же пагубно сказывается на быстродействии в реальном времени. Это происходит в связи с тем, что вызов cudaMalloc влечет за собой неявную синхронизацию (implicit synchronization), заставляя CPU ждать GPU, что нарушает асинхронность конвейера».

Чтобы устранить простой вычислительных блоков, жизненный цикл системы разделяется на три фазы: инициализацию, фазу подготовки и активную работу. На первом этапе приложение заранее резервирует пулы памяти, очереди и буферы с заданной глубиной. В ходе фазы подготовки конвейер прогоняет тестовую нагрузку и при необходимости добирает буферы под фактический профиль задач. Как только система переходит к активной фазе, вызовы cudaMalloc или оператора new полностью блокируются, и пайплайн циклически переиспользует уже выделенные участки памяти.

Архитектура конвейера и изоляция потоков

Реализация этой схемы в классе InferencePipeline разбивает нагрузку на три независимых этапа по шаблону Chained Producer-Consumer: Source, Detector и Sink. Поток Source работает в основном потоке, читает файлы и отдает их аппаратному декодеру. Модуль Detector выполняет инференс и постобработку методом подавления немаксимумов, поддерживая переключение между бэкендами TensorRT, универсальным OnnxDetector и заглушкой StubDetector, нужной для точной оценки накладных расходов ввода-вывода. Завершающий этап Sink принимает координаты рамок, запускает алгоритм трекинга, наносит аннотации на кадры, кодирует видео и сохраняет результат на твердотельный накопитель.

Потоки связывает очередь SafeQueue с механизмом обратного давления на базе стандартных мьютексов и условных переменных. Если аппаратный декодер обрабатывает поток со скоростью 300 кадров в секунду, а детектор успевает разобрать лишь 165 кадров в секунду, неограниченный буфер быстро переполнит доступную оперативную память и приведет к системному сбою. Механизм обратного давления жестко ограничивает объем хранимых в очереди данных, принудительно останавливая работу потока Source на уровне операционной системы до освобождения места в очереди.

«DeepStream — отличный инструмент, но создание собственных плагинов GStreamer на C++ — процесс, известный своей громоздкостью и недостаточной гибкостью. Я хотел реализовать конвейер обработки, работающий непосредственно на железе, дающий полный контроль над многопоточностью и лишенный зависимостей от GStreamer».

Отказ от готового фреймворка NVIDIA DeepStream и плагинов GStreamer в пользу низкоуровневого кода на C++ и CUDA сопряжен с существенным усложнением поддержки кастомной кодовой базы, открытый репозиторий которой опубликован под названием ZeroHostCopyInference. Однако прямое управление потоками и исключение динамических аллокаций позволяют гарантированно согласовать пропускную способность аппаратного декодера в 300 кадров в секунду и детектора на 165 кадров в секунду без утечек памяти и задержек синхронизации.

Что за этим стоит

Снижение задержек и экономия серверных стоек упираются теперь не в скорость вычислений кремния, а в дисциплину работы с памятью при передаче кадров через PCIe. Выигрыш в 165 кадров в секунду на квантованных моделях заставляет переносить весь пайплайн в память ускорителя, но за это приходится платить отказом от готовых экосистем. Написание собственного конвейера на C++ и CUDA взамен плагинов DeepStream и GStreamer создает тяжелую кастомную базу, которую команде придется поддерживать в одиночку. Аппаратная оптимизация шины передачи данных неизбежно перекладывает стоимость серверных мощностей на фонд оплаты труда узкопрофильных C++ инженеров.

Источник: Хабр ML