Берем типовой Jetson Orin Nano и YOLOv8 среднего размера. Голый PyTorch выдает около 83 миллисекунд на кадр. Скармливаем это железу как есть — и получаем 12 FPS, раскаленный радиатор и забитую память. Если вы дошли до стадии деплоя нейросети на заводской конвейер или автономный дрон, тема «TensorRT и ONNX на edge: как ускорить инференс в 3-5 раз на Jetson» перестает быть кликбейтом из туториалов и становится условием физического выживания продукта. Правильно скомпилированный движок сжимает latency до 18 мс, а потребление RAM падает с двух гигабайт до четырехсот мегабайт. Но между запуском скрипта и этим результатом лежит минное поле из несовместимых операторов, поломанной квантизации и температурного троттлинга.
От PyTorch к ONNX без иллюзий
Экспорт графа вычислений через стандартный интерфейс фреймворка — это всегда лотерея. Многие оставляют динамические оси для размера батча и разрешения картинки, думая, что делают модель универсальной. На edge-устройствах эта универсальность бьет по производительности. TensorRT, получая на вход граф с плавающими размерностями, вынужден планировать память по наихудшему сценарию или встраивать дополнительные проверки на лету. Наш подход в Morana Labs здесь прямолинеен: статические шейпы везде, где это технически возможно. Проще и надежнее держать в памяти два разных оптимизированных движка под разные разрешения камер, чем отдавать планировщику видеопамяти право решать, сколько ресурса отъесть под внезапно изменившийся входной тензор.
Вторая классическая проблема — операторы, которых нет в стандарте ONNX или которые не умеет переваривать сам аппаратный ускоритель. Специфичный слой выборки из архитектуры трекера или кастомный решейп гарантированно вызовет ошибку сегментации при сборке движка. В таких случаях приходится спускаться на уровень C++ и писать собственные CUDA-плагины, пробрасывая их через внутренний реестр, что автоматически умножает сложность деплоя модели на два.
FP16, INT8 и цена калибровки
Переход с 32-битных флоатов на FP16 — это практически бесплатный бонус. Веса худеют в два раза, пропускная способность памяти визуально удваивается, а метрики точности проседают на десятые доли процента. Настоящая инженерия начинается там, где вы упираетесь в потолок шины на Jetson и вынуждены спускаться в INT8.
Квантизация до восьми бит означает, что вам нужно впихнуть непрерывное распределение чисел с плавающей точкой в 256 жестких дискретных ячеек. Если сделать это линейно по крайним значениям весов, выбросы в активациях сплющат весь полезный сигнал, и объектный детектор превратится в генератор случайных чисел. Чтобы этого избежать, пайплайн требует прогнать через модель калибровочный датасет перед компиляцией. Калибровочная выборка не может быть набором случайных картинок из интернета. Она обязана математически точно отражать то распределение данных, которое камера увидит в реальной эксплуатации. Если вы детектируете дефекты сварного шва под тусклым светом цеха, а калибруете сеть на контрастных стоковых фотографиях, активации будут обрезаны неправильно. Нейросеть ослепнет именно на тех паттернах, ради которых создавалась.
import tensorrt as trt
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
config = builder.create_builder_config()
# Задаем жесткий лимит рабочего пространства памяти
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)
if builder.platform_has_fast_int8:
config.set_flag(trt.BuilderFlag.INT8)
# Энтропийная калибровка для сохранения распределения
config.int8_calibrator = CustomEntropyCalibrator("calib_data.cache")
profile = builder.create_optimization_profile()
profile.set_shape("input", (1, 3, 640, 640), (1, 3, 640, 640), (1, 3, 640, 640))
config.add_optimization_profile(profile)Этот фрагмент конфигуратора выглядит тривиально, но именно в реализации калибратора зашит весь успех упаковки моделей под конкретное edge-железо. Выбор алгоритма калибровки зависит от архитектуры сети. Для классических сверточных слоев энтропийная калибровка обычно работает лучше, сохраняя плотность информации, в то время как трансформерные блоки требуют более тонкого подхода с явным указанием шкал для отдельных тензоров.
Аппаратная привязка и почему десктоп врет
Критическая ошибка деплоя — замерять производительность полученных артефактов на мощной десктопной видеокарте, а затем экстраполировать цифры на edge. Jetson — это система с жесткими лимитами по теплопакету. На столе разработчика модель может выдавать сотни кадров, а внутри защитного IP67-корпуса на станке чип моментально разогреется до 85 градусов и уйдет в глубокий троттлинг. Измерения имеют смысл только на целевой железке после часа непрерывной нагрузки, когда системный демон принудительно переведен в режим максимальной производительности, а частоты зафиксированы.
Здесь же всплывает главный трейд-офф индустриального деплоя. Скомпилированный бинарник намертво привязан к конкретной микроархитектуре GPU, версии драйвера и системных библиотек. Вы не можете собрать файл на десктопной RTX 4090 и скопировать его на Jetson. Наш подход в Morana Labs идет вразрез с концепцией «собрал в офисе и забыл»: мы выстраиваем архитектуру приложения так, чтобы пересборка графа из универсального ONNX происходила прямо на устройстве клиента при первом запуске. Да, это требует нескольких минут на инициализацию, зато страхует от фатальной несовместимости при обновлении прошивок по воздуху.
Альтернативный путь — использовать ONNX Runtime с соответствующим execution provider. В этом случае рантайм сам парсит граф при загрузке, скармливает поддерживаемые узлы в ускоритель, а остаток исполняет на обычных CUDA-ядрах или центральном процессоре. Это невероятно гибко и решает проблему переноса моделей между разными ревизиями плат. Но за абстракцию придется платить: накладные расходы на диспетчеризацию вызовов съедают до трети чистой производительности, которую мог бы выдать нативный C++ API. Когда каждый милливатт и каждый такт определяют жизнеспособность продукта, выбор между удобством разработки и предельной скоростью железа становится абсолютно однозначным.