Уже пару лет я успешно сливаю карму в спорах со свидетелями "никогда нейросети не достигнут/не смогут/не заменят и т.п." В целом, мне нет нужды что-то доказывать прячущим в песок голову скептикам, однако, вчера я столкнулся со сложной проблемой и попросил сделать Opus 4.6 ее траблшутинг. Около 40 минут заняло расследование проблемы, по мотивам которого, я попросил его написать статью, которую и предлагаю вашему вниманию как пример того, на что я мог потратить целый день, а большинство middle-grade инженеров бы не справилось вообще. Весь траблшутинг и статья сделаны в полностью автоматическом режиме без участия человека.

Бонусом вы узнаете что-то о кривых руках инженеров NVIDIA. Впрочем, их руки далеко не самые кривые в индустрии...

Итак, погнали.

Почему nvv4l2h264enc не работает внутри Docker на Jetson Orin NX (JetPack 6.2.2)

После свежей установки JetPack 6.2.2 (L4T R36.5.0) на Jetson Orin NX я столкнулся с
озадачивающей проблемой: аппаратное кодирование H.264 прекрасно работало на хосте, но стабильно падало внутри Docker-контейнеров. Последовало глубокое погружение в логи strace, DRM ioctl'ы и внутренности закрытых библиотек NVIDIA — и всё это только для того, чтобы обнаружить, что причиной был отсутствующий двухмегабайтный пакет.

Конфигурация

  • Плата: Jetson Orin NX (Engineering Reference Developer Kit)

  • JetPack: 6.2.2 (L4T R36.5.0, ядро 5.15.185-tegra, OOT-модули ядра)

  • Контейнер: nvcr.io/nvidia/deepstream-l4t:7.1-samples-multiarch

  • Docker runtime: --runtime=nvidia --privileged

Симптом

На хосте каноничный GStreamer-пайплайн аппаратного кодирования запускается без проблем:

gst-launch-1.0 videotestsrc ! nvvidconv ! 'video/x-raw(memory:NVMM), framerate=5/1' \
  ! nvv4l2h264enc ! fakesink
Setting pipeline to PAUSED ...
Opening in BLOCKING MODE
NvMMLiteOpen : Block : BlockType = 4
===== NvVideo: NVENC =====
NvMMLiteBlockCreate : Block : BlockType = 4
H264: Profile = 66 Level = 0
NVMEDIA: Need to set EMC bandwidth : 21000
Pipeline is PREROLLED ...

Внутри Docker — тот же образ, то же железо, --privileged, --runtime=nvidia — падает:

Opening in BLOCKING MODE
ENC_CTX(0xffff88008460) Error in initializing nvenc context
ERROR: from element /GstPipeline:pipeline0/nvv4l2h264enc:nvv4l2h264enc0:
  Could not get/set settings from/on resource.
  Device is in streaming mode

Ни NvMMLiteOpen. Ни ===== NvVideo: NVENC =====. Библиотека NVMM сдаётся ещё до
попытки обратиться к аппаратуре.

Что я исключил

Узлы устройств — все на месте

Контейнер видит все необходимые устройства: /dev/nvmap, /dev/host1x-fence,
/dev/dri/renderD128 (tegra host1x DRM), /dev/nvgpu/igpu0/*, /dev/v4l2-nvenc.
Мажорные и минорные номера полностью совпадают с хостом.

Библиотеки — идентичны

Все библиотеки NVIDIA монтируются с хоста через nvidia-container-runtime. Я сверил контрольные суммы критичных библиотек (libnvtvmr.so, libnvrm_host1x.so, libtegrav4l2.so, libv4l2_nvvideocodec.so, libnvmmlite_video.so) — все идентичны между хостом и контейнером.

DRM-доступ — работает нормально

Я написал быстрый тест на Python, который открывает /dev/dri/renderD128 и вызывает DRM_IOCTL_TEGRA_CHANNEL_OPEN для host1x-класса NVENC (0x21). Он успешно выполняется в обоих окружениях, возвращая context=1, version=35, capabilities=1.

Права и cgroups — не причина

С --privileged Docker отключает seccomp, AppArmor и cgroup-фильтрацию устройств. NVIDIA_VISIBLE_DEVICES=all ничего не изменил.

sysfs — доступна

/sys/bus/nvmem/devices/fuse/nvmem, /sys/devices/soc0/*, /sys/firmware/devicetree/base/* — всё читается внутри контейнера. Чип корректно идентифицируется как Tegra234 (Orin), soc_id=35.

Расследование

Сравнение DRM ioctl-трассировок

Я снял полные трассировки strace -f -e trace=ioctl на хосте и в контейнере. Разница была разительной:

Метрика

Хост

Контейнер

Всего вызовов ioctl

2016

1644

Вызовов DRM_IOCTL

236

36

DRM_IOCTL_TEGRA_CHANNEL_OPEN

Да

Ни разу

На хосте поток энкодера открывает DRM tegra-канал к NVENC-движку. В контейнере библиотека запрашивает DRM_IOCTL_VERSION на render-нодах, но дальше к открытию канала не переходит. Инициализация прерывается на более высоком уровне.

Трассировка потока энкодера

Трассируя все системные вызовы через strace -f, я определил точный поток, отвечающий за инициализацию энкодера. На хосте этот поток выводит NvMMLiteOpen и затем открывает DRM-каналы. В контейнере поток порождает два дочерних процесса через clone() + execve() и тут же сообщает об ошибке.

Дочерние процессы были:

  1. lsmod — завершается с кодом 127 («команда не найдена»)

  2. grep nvgpu — читает из пайпа, ничего не получает, завершается с ненулевым кодом

Библиотека NVIDIA NVMM вызывает нечто эквивалентное popen("lsmod | grep nvgpu"), чтобы убедиться, что модуль ядра nvgpu загружен, прежде чем инициализировать контекст энкодера.

Цепочка отказа

libnvv4l2.so
  └─ libv4l2_nvvideocodec.so
       └─ libnvtvmr.so (Tegra Video Resource Manager)
            └─ system("lsmod | grep nvgpu")
                 └─ sh: lsmod: not found
                      └─ grep читает пустой пайп → exit 1
                           └─ libnvtvmr делает вывод: нет GPU → отмена инициализации NVENC

Библиотека не проверяет /proc/modules напрямую. Не запрашивает ядро через sysfs. Она вызывает lsmod — утилиту из пакета kmod — и когда этого бинарника нет, молча заключает, что аппаратура отсутствует.

Решение

apt-get install -y kmod

Вот и всё. После установки kmod (который предоставляет lsmod, modprobe, modinfo и т.д.) энкодер успешно инициализируется внутри контейнера:

Opening in BLOCKING MODE
NvMMLiteOpen : Block : BlockType = 4
===== NvVideo: NVENC =====
NvMMLiteBlockCreate : Block : BlockType = 4
H264: Profile = 66 Level = 0
NVMEDIA: Need to set EMC bandwidth : 21000
NvVideo: bBlitMode is set to TRUE
Pipeline is PREROLLED ...
Got EOS from element "pipeline0".

Для Dockerfile добавьте kmod в список пакетов:

RUN apt-get update && apt-get install -y kmod && rm -rf /var/lib/apt/lists/*

Почему это проявилось именно сейчас

На JetPack 5.x (эпоха Xavier) базовые образы контейнеров, как правило, включали kmod как зависимость других пакетов. Контейнер DeepStream L4T 7.1 для JetPack 6.x поставляется с более компактной базой, которая не подтягивает его. На хосте kmod всегда установлен как часть базовой системы Ubuntu — поэтому проблема проявляется только в контейнерах.

Сообщение об ошибке Error in initializing nvenc context ни на что не указывает в отношении отсутствующей утилиты. Предупреждение sh: 1: lsmod: not found действительно появляется в выводе GStreamer plugin scanner, но оно погребено среди десятков других безобидных предупреждений и легко отбрасывается как нерелевантное, поскольку возникает при сканировании плагинов, а не во время фактической попытки кодирования. Ситуацию усугубляет то, что библиотека NVMM выполняет собственную проверку lsmod молча — stderr от неудавшегося вызова перенаправляется в /dev/null.

Выводы

  1. --privileged не поможет, если проблема — отсутствующий бинарник, а не отсутствующие права. Все узлы устройств, записи в sysfs и DRM ioctl были доступны. Библиотека просто отказалась их использовать.

  2. Закрытые библиотеки с зависимостями от shell-вызовов — кошмар для отладки. Без доступа к исходному коду единственный путь — трассировка на уровне бинарников. Сравнение поведения потоков через strace на хосте и в контейнере в конечном счёте выявило зависимость от lsmod.

  3. Всегда устанавливайте kmod в Docker-контейнерах для Jetson. Если ваш контейнер выполняет любую мультимедийную нагрузку NVIDIA (кодирование, декодирование, VIC), стек библиотек NVMM может зависеть от lsmod для обнаружения аппаратуры. Это нигде не задокументировано в руководствах NVIDIA по контейнерам.

  4. Предупреждение lsmod: not found при работе gst-plugin-scanner — тревожный сигнал. Если вы его видите, ваши пайплайны кодирования/декодирования, скорее всего, упадут при запуске, даже если элементы GStreamer успешно загружаются.