Skip to content

build(container): media-codec cleanup + build gate + NVDEC video decode (OPS-7665) - #11836

Merged
saturley-hall merged 68 commits into
mainfrom
dtokarev/ops-7665-codec-compliance
Aug 5, 2026
Merged

build(container): media-codec cleanup + build gate + NVDEC video decode (OPS-7665)#11836
saturley-hall merged 68 commits into
mainfrom
dtokarev/ops-7665-codec-compliance

Conversation

@dmitry-tokarev-nv

@dmitry-tokarev-nv dmitry-tokarev-nv commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidates the media-codec compliance work for all three runtime images and adds a
build gate that enforces it, then restores H.264/H.265 video input on the GPU so the
cleanup is not a capability regression for those codecs.

The removals and the gate have to land together: the gate only passes once every image
is clean, which made the original split PRs interdependent and impossible to green
individually.

Scope, and how it grew

This began as a compliance cleanup: remove the software H.264/H.265/AAC codecs from the
three runtime images and gate the result so they cannot come back. It expanded twice,
both times because removal on its own would have shipped either a regression or an
incomplete result. Both expansions concern the GPU's dedicated video engines, and they
went in opposite directions.

NVDEC — hardware decode, added. Removing the software decoders would have dropped
H.264/H.265 video input outright. Those codecs now route to the GPU's NVDEC engine
through PyNvVideoCodec across all three backends, so the cleanup is not a capability
regression. This is the larger half of the diff and brought its own requirements: the
container must be granted the video driver capability
(ai-dynamo/velonix#550 does this for the
Dynamo GPU testers), and three product defects surfaced only once the tests genuinely
executed against it.

NVENC — hardware encode, removed. The in-tree ffmpeg previously built h264_nvenc
and linked nv-codec-headers. A hardware H.264 encoder is still a distributable H.264
codec surface, so it is now omitted entirely: --enable-nvenc and the encoder are gone,
the headers are no longer cloned, and video generation encodes VP9 (libvpx) instead. A
post-build guard fails the build if any H.264/H.265/AAC/NVENC surface reappears.

DALI — upgraded past its own cleanup. The TensorRT-LLM base image ships a DALI whose
vendored ffmpeg carries the same decoders this PR removes everywhere else, and they
cannot be deleted independently of the package. Detailed below.

Together these mean the PR is no longer only a removal: it changes which engine serves
H.264/H.265 (GPU rather than CPU), and it drops one GPU path while adding another.

Per image

  • In-tree ffmpeg (wheel_builder): narrow decoder/demuxer/parser allowlist
    (VP8/VP9 + rawvideo); encoders reduced to libvpx_vp9. Bumped 8.1 → 8.1.2.
  • vLLM: removes the codec-bearing video-decode wheels inherited from the base
    (av, opencv, decord, torchcodec). Keeps the in-tree ffmpeg + imageio-ffmpeg
    for the video-generation encode path, which now encodes VP9.
  • SGLang: builds without the in-tree ffmpeg stack and purges the same wheels, with
    a build-failing guard.
  • TensorRT-LLM: removes preinstalled opencv-python-headless.
  • Compliance gate (licenses stage): filesystem + SBOM scan that fails the build if
    a media-codec library ships outside the allowlist or a reasoned exception.

Hardware video decode (NVDEC)

Removing the software decoders would have dropped H.264/H.265 video input entirely, so
this PR routes those codecs to the GPU's dedicated NVDEC engine via PyNvVideoCodec,
across all three backends:

  • Shared core common/multimodal/nvdec_decoder.py: codec probe → route → decode to
    (T, H, W, 3) uint8 RGB, matching the existing VideoLoader contract.
  • Pinned PyNvVideoCodec>=2.2.0. 2.0.x bundles a full FFmpeg including libavcodec,
    which the gate rejects; 2.2.0 bundles only libavutil + libavformat (container
    demux, no codec) and links libnvcuvid at runtime.
  • Sources: http(s), file:// and data: all hardware-decode. file:// stays behind
    the existing DYN_MM_LOCAL_PATH policy.
  • Requires a GPU with a decode engine and a container granted the video driver
    capability. NVDEC is present across the datacenter line (A100 5 engines, Hopper and
    Blackwell 7) — it is NVENC, the encoder, that those parts omit, which is also why
    the generation path encodes VP9 on CPU. Documented in
    features/multimodal/video-decode-gpu-requirements.md.

Known limitation

VP8/VP9/AV1 video input has no decoder in these images. NVDEC covers H.264/H.265;
everything else relied on the removed software wheels. #12051 restores them behind an
opt-in installer. This is deliberate and documented on all three backend pages.

Tests added

Video input previously had no serve-level cover outside vLLM. This adds it on the
other two backends, so all three now exercise video->text end to end:

Test Backend What it covers
mm_agg_video_nvdec (h264, h265) vLLM NVDEC serve path
video_e_pd_qwen_nvdec (h264, h265) SGLang new — the encode-worker NVDEC path, with the embedding cache disabled so it runs the default configuration
aggregated_multimodal_video_nvdec (h264, h265) TensorRT-LLM new — this backend had no video serve test at all
test_nvdec_decoder_gpu (h264, hevc) shared real NVDEC decode against committed fixtures
test_media_source shared file:// sandbox: prefix, traversal, symlink escape, data URIs
test_video_encode_real all three output really is VP9, via the ffmpeg in each image

None of the new NVDEC cases install a decoder, so they exercise what a deployment
actually receives. The four pre-existing tests that do pip-install one are now marked
installs_extra_dependencies, so a green run cannot be mistaken for evidence that
VP8/VP9 works in an unmodified image (-m "not installs_extra_dependencies" selects
only unmodified-image behaviour).

Validation

CI, expanded suiterun 30513728452
on 116728e016 ran the backend jobs with (pre_merge or post_merge) markers rather than
pre_merge alone, so the post_merge cases were exercised before merge: green across
vLLM, SGLang and TensorRT-LLM (Test, Multi-GPU Test and Compliance audit, amd64 + arm64).

CI, merge-ready configrun 30562129491
on 6efdd3b2a6
green after the temporary markers and raised timeouts were reverted, confirming the
configuration that actually merges is clean at the original budgets.

CI, latestrun 30598827010
green on the final pre-rebase commit, again at the merge-ready budgets: 61 jobs
succeeded, 17 skipped, 0 failed
, covering every backend Test and Multi-GPU Test lane on
amd64 and arm64 plus all four Compliance audits (vLLM, SGLang, TensorRT-LLM, frontend).

The branch has since been rebased onto current main, which had moved the multimodal
documentation into pages/use-cases/multimodal-serving/ and added the request-trace-s3
build feature. Both conflicts were merged rather than resolved in favour of one side, so
request-trace-s3 is now built into every wheel while SGLang still builds without
media-ffmpeg.

CI, rebased tree with post_merge markers
run 30601314656 is
green: 61 jobs succeeded, 17 skipped, 0 failed. This is the widest run on this
branch — the backend lanes ran (pre_merge or post_merge) rather than pre_merge alone,
so the post_merge video cases were exercised before merge — with vLLM green at a raised
150m budget, TensorRT-LLM at 90m, and every Compliance audit passing.

The SGLang single-GPU lane needed re-runs to get there, for a reason unrelated to this
PR. On the first two attempts its parallel stage was killed at the job budget because one
router test, test_router_e2e_with_sglang.py::test_sglang_indexers_sync[nats-tcp], wedged
for ~4245s against its own declared timeout(320) — while reporting 602 tests passed
and zero test-level failures
. That timeout cannot fire: the test is documented in-tree
(DYN-2784) as blocking forever in KvRouter on min_initial_workers=2 when a worker dies
during launch, with the pytest.mark.timeout signal swallowed at the C-level syscall.
GPU0: 5.4/22 GiB against a 12.0 GiB profile shows only one of the two workers came up.

That flake reproduces on main independently of this branch —
run 30606035706 wedged on
the same test, same worker, same 5.4/22 GiB, for 6077s — so it is a pre-existing defect
being handled separately. The third attempt passed the lane cleanly.

The temporary markers and raised budgets have since been reverted, so the configuration
that merges is the stock one.

CI now exercises hardware decode directly. Until now the NVDEC cases could not run in
CI at all: the runners were not granted the video driver capability that libnvcuvid
needs, so every one of them skipped and the paths rested on a dev box.
ai-dynamo/velonix#550 added
NVIDIA_DRIVER_CAPABILITIES=video,compute,utility to the Dynamo GPU testers, and they now
execute on the runners' L4s — which have four NVDEC engines. In
run 30685708717, with
zero capability-based skips anywhere:

Backend Test Result
vLLM mm_agg_video_nvdec_qwen3-vl-2b_h264 serve e2e passed, 138s
vLLM mm_agg_video_nvdec_qwen3-vl-2b_h265 serve e2e passed, 104s
TensorRT-LLM aggregated_multimodal_video_nvdec serve e2e passed, 106s
SGLang video_e_pd_qwen_nvdec serve e2e (embedding cache disabled) passed
shared test_nvdec_decodes_real_clip[h264] / [hevc] passed — real libnvcuvid decode

The NVDEC unit suites pass on all three backends in the same run. This closes the one
coverage gap this PR previously carried.

GPU hardware (RTX 6000 Ada dev box) — how the NVDEC paths were validated before the
runners could do it, and still where each fix is reproduced before pushing. Run against
the built artifacts with no patching, and every run asserts something actually
executed rather than trusting a green exit:

Check Result
vLLM NVDEC decode + H.264/H.265 serve e2e executed=5 skipped=0
SGLang video_e_pd_qwen_nvdec serve e2e passed, executed=1 skipped=0, no decoder fallback
TensorRT-LLM aggregated_multimodal_video_nvdec serve e2e passed, executed=1 skipped=0
SGLang shipped video_metadata shim vs real MMEncoder._encode (sglang 0.5.16) before fail -> after pass, embeddings (648, 2048)
TensorRT-LLM VideoData vs vendor async_load_video (1.3.0rc22) structurally identical
file:// and data: decode + sandbox refusals decode ok; local-access-off, traversal and symlink escape all refused
Fixes present in the artifact, unpatched verified on all three flavours

Getting those tests to actually execute surfaced four defects CI could not have caught,
all fixed here: a filterwarnings=error interaction that cached the NVDEC capability
probe as unavailable for an entire session; a GPU test that built its fixture with an
encoder these images intentionally omit, so it could never run in the image it
validates; a source frame rate reported as 0, which made every H.264/H.265 vLLM video
request fail downstream; and file:///data: video reaching no decoder at all.

Review findings on the SGLang encode worker (thanks @Chokoyo) — three, each confirmed
on GPU hardware before any code changed, and each with a regression test:

  • NVDEC never ran in the default configuration (9f0650b1ad). The conversion was
    reachable only from _encode_with_cache, but the embedding cache defaults to disabled,
    so a stock deployment passed raw URLs to SGLang and every H.264/H.265 request failed.
    The serve test had masked it by setting a non-zero cache capacity; it now runs with the
    cache off.
  • SGLang's frame-selection policy was bypassed (9f0650b1ad, completed by
    10b56197e2). Pre-sampled frames took preprocess_video's return vr, None branch, so
    the model's vision_config.video policy never applied and metadata was synthesized. A
    VideoDecoderWrapper subclass over PyNvVideoCodec now lets SGLang choose frames from
    true source values: measured nframes=4 -> 4 frames, fps=10.0 rather than a
    fabricated 24.0, indices [0, 3, 6, 9], with an ndarray control reproducing the old
    behaviour.
  • A URL-policy refusal degraded to an unvalidated fetch (c60d651be7).
    UrlValidationError was caught alongside decode failures, so a rejected URL was handed
    to SGLang — which applies no policy of its own. Reproduced first: a loopback URL the
    policy refused was fetched anyway (38128 bytes from a blocked address), and a refused
    file:// path resolved to a readable local file. After the fix the same probe records
    zero hits on that host. Refusals now propagate; genuine decode failures still fall back.

One related gap is deliberately not closed here: video whose codec is not
hardware-routed still reaches SGLang unvalidated, with no exception involved. That
predates this PR — main's SGLang encode worker performs no URL validation at all — and
closing it would start rejecting input main accepts today, so it belongs in its own
change rather than folded into a compliance PR.

Video content assertions rebuilt (bb5e1b230d). The serve tests asserted words against
ffmpeg testsrc2 colour bars — first ["red", "static", "still"], describing content those
clips never contained, then a wider list chosen from observed model output. Both were weak:
the first passed only when the model happened to say "still", and the second confirms the
model described something colourful, not that it saw the right video.

The fixtures are now a yellow triangle on a near-black background, generated once as raw
frames and encoded three ways (lib/llm/tests/data/media/make_triangle_fixture.py), so VP9,
H.264 and H.265 carry identical content and every codec path asserts the same word. Measured
round-trip against the source: 1.59 / 1.68 / 1.63 mean absolute per-pixel difference,
within 0.1 of each other — the previous fixtures differed by 17.77 between codecs, which
made cross-codec comparison impossible. Confirmed on hardware, where H.264 and H.265 each
returned the same sentence: "The video begins with a black screen. A yellow triangle appears
in the center of the screen." Forcing an impossible expected value fails the test, so a pass
is not vacuous.

One limit is recorded in the fixture comment rather than left implied: the triangle moves,
but that is not coverage of temporal sampling. Asked to describe the clip, Qwen2.5-VL-3B
reports it as "static and does not move" — it names the object reliably and the movement not
at all, so a regression collapsing frame selection to a single frame would not surface in the
response text. Cross-codec frame comparison is the check to build on for that, and the
identical-content fixtures now make it possible.

Every consumer routes through the shared MULTIMODAL_VIDEO_EXPECTED constant, so the
expectation cannot drift from the footage again — the vLLM and XPU suites had been asserting
against a clip they no longer used.

VP9 rejection is now asserted (3d0a860927). These images decode video through cv2,
which they no longer ship, so NVDEC covers H.264/H.265 and VP8/VP9/AV1 have no decode path at
all. Nothing exercised a codec the image cannot decode, so a base-image bump that
reintroduced cv2 — or a routing change that sent VP9 down the hardware path — would both have
gone unnoticed. Asserting the failure rather than leaving the gap untested is the approach
DALI took when it trimmed its own FFmpeg build (NVIDIA/DALI#6352). Verified against a real
image (1.3.0rc22) rather than assumed: VP9 probes as vp9, does not route to NVDEC, and
returns HTTP 400 carrying the upstream reason. The assertions are scoped to what this repo
owns — the status, the Failed to load video (<url>) prefix, and that the cause is preserved
rather than swallowed — so an upstream rewording cannot fail the test for no defect.

CI status. The most recent full run,
30710098691 on a887cfb186,
predates the two commits above and so ran the earlier fixtures. Its video lanes passed —
video_e_pd_qwen_nvdec, video_e_pd_qwen and multimodal_e_pd_qwen — and the job failed on
the unrelated SGLang flake described earlier: test_sglang_indexers_sync wedged for 4235s
against its own declared timeout=320s, blocking three queued workers until the runner
reported Executing the custom container implementation failed. The triangle fixtures and the
VP9 test are validated on GPU hardware against the shipped images, but have not yet been
through CI.

DALI upgraded past its own media-codec cleanup. The TensorRT-LLM base image
ships DALI 2.1.0, whose vendored ffmpeg registers 446 decoders including h264,
hevc, aac, aac_fixed and aac_latm. Those cannot be removed on their own:
libavcodec, libavfilter, libavformat, libavutil and libswscale are each
DT_NEEDED by libdali.so, libdali_kernels.so, libdali_operators.so and every
python_function_plugin, so deleting the codec libraries breaks import nvidia.dali outright. Upstream restricted that build
(DALI_deps#162,
DALI#6352) and released it in 2.1.1,
which registers 440 with all five absent and vp8/vp9/mjpeg/av1 retained.

The image now pins nvidia-dali-cuda130>=2.1.1. Removing the package outright
would also have worked — nothing declares a dependency on it, nothing imports it
across 43,455 files scanned, and the suite passes without it — but it belongs to
TensorRT-LLM, and a version bump clears the codecs without taking a package out
of someone else's image.

A whiteout entry comes with it, and that is the part that makes the upgrade real.
pre_runtime rebases on upstream and overlays runtime_full, and DALI's
libraries are hash-named, so an upgrade renames rather than overwrites. Built
both ways to check: without the whiteout the image keeps two libavcodec
copies — the upgraded one and the 2.1.0 one still registering h264/hevc/aac —
and reports version 2.1.0 while carrying both; with it, one copy and none of
them. Two build guards enumerate every bundled libavcodec, one after the upgrade
and one after the overlay COPY (the only stage where the fault can exist), and
test_no_software_video_codecs.py repeats the check on the shipped image. That
test was verified to fail on 2.1.0 before being trusted to pass afterwards.

The DALI waiver in codec_policy.yaml stays, with a corrected rationale: the deny
globs match on presence, and DALI still vendors a libavcodec on disk even with the
decoders gone. An earlier revision claimed the waiver could be narrowed to the
non-codec companions, which the link graph rules out.

GPU validation (RTX 6000 Ada) — unit suite green, and the TensorRT-LLM
integration lane green: 16 passed including aggregated_multimodal_video_nvdec,
video_diffusion, image_diffusion and the E/PD multimodal cases.

One test is skipped, deliberately.
test_router_e2e_with_sglang.py::test_sglang_indexers_sync hangs rather than
fails — a worker dies during launch, the router blocks forever waiting for two,
and the declared timeout cannot fire because the signal is swallowed at a C-level
syscall. The note previously scoped the skip to nightly on the basis that it
passed elsewhere; measured over five days that is 82% of nightly but also 63% of
post_merge, with observed hangs of 3659s, 4235s and 6077s, each holding a 12 GiB
reservation with three workers queued behind it. The comment records what would
let it come back: fixing the launch race, or giving the parallel orchestrator a
hard-kill for a child that outlives its timeout.

CI, with the DALI work
run 30778874769
on 9c154b62cd is green: 61 jobs succeeded, 17 skipped, 0 failed, with the
temporary post_merge widening in place so the video cases were exercised. This is
the first run to put the DALI change through a real build: both guards executed in
situ, the whiteout survived the full-root COPY, and license validation passed with
the three new SBOM components the upgrade pulls in — none of which any GPU run can
reach.

One job needed a re-run.
test_mocker_disagg_startup_lifecycle[frontend-decode-prefill] on arm64 asserted
the prefill worker id and received the decode worker's, a startup race in a router
mocker test this PR does not touch, which had passed on the three previous runs of
this branch. It passed on re-run.

The XPU lane is red for an unrelated pre-existing reason: the XPU image ships
without gpu_memory_service, so six test_vllm_worker_factory cases fail on
import. They fail identically on the untouched image. That lane runs only when the
xpu label is present, which was applied automatically because this PR touches two
*xpu* test files, so this is the first time it has executed here.

Release note

Behaviour change — video output codec. output_format: "mp4" now returns a VP9
stream in an MP4 container, where it previously returned H.264. The file extension is
unchanged, so a client that hands the bytes straight to a player can fail with no error
from Dynamo. VP9-in-MP4 plays in Chrome, Firefox and ffmpeg; it does not play in Safari
or QuickTime.

Why it cannot stay H.264: NVIDIA omits the hardware encoder (NVENC) from the datacenter
line — A100, H100, H200, B200 and GB200 have none, against 5–7 NVDEC decode engines — so
a hardware H.264 path would work only on L4/L40S/RTX-class parts and fail on the GPUs
Dynamo is deployed on. The alternative is a software H.264 encoder, which is the codec
surface these images deliberately no longer carry. VP9 is the only option that behaves
identically on every supported GPU.

Consumers that need H.264 can transcode on their side:
ffmpeg -i output.mp4 -c:v libx264 output-h264.mp4.

Video input is unaffected: H.264 and H.265 are decoded on the GPU via NVDEC, which is
present across the datacenter lineup.

Fixed during review

Reviewers found four defects that the tests here did not, and each is fixed with a
regression test. Worth listing because three of them share a shape: a control path that
looked healthy while doing nothing.

The gate itself could pass every image (a1919637e2, @dagil-nvidia). CodecPolicy.load
shape-checked deny_globs and exceptions but not allow_paths — the one key that fails
open. Written as a scalar it iterates per character, "/" is among them, and every
absolute path starts with "/", so a single missing - classified every violation as
allowed while still printing a full report. Reproduced on GPU:
/opt/evil/libx264.so.164('allowed', None) as a scalar, ('violation', None) as a
list. Three tests, verified with the check stripped back out.

vLLM's frame-sampling contract was dropped for short clips (e1d8c454be, @Chokoyo).
The NVDEC metadata omitted do_sample_frames; Qwen3-VL reads a missing flag as False,
so clips where every source frame is returned looked pre-sampled and the model's own fps
policy never ran. Their reproduction against vLLM 0.26.0 showed video_grid_thw
[[5,16,20]] instead of [[2,16,20]]. Asserted in a unit test because, as they noted, the
serve tests cannot see it — the answer does not change when the frame count does.

SGLang downloaded every non-NVDEC video twice (1da12f978f, Codex via @rmccorm4). The
encode worker fetched the payload to probe its codec, discarded it, and handed SGLang the
URL to fetch again. That doubles ingress, breaks one-use signed URLs, and — since SGLang
applies no URL policy of its own — meant the bytes it decoded were never the validated
ones. It now passes the fetched bytes; load_video takes Union[str, bytes, VideoData],
so no shim is needed. Measured against a counting HTTP server: two origin fetches per
request before, one after.

DYN_DISABLE_NVDEC meant opposite things per backend (0f4e71274d, @dagil-nvidia).
One call site tested raw truthiness where the rest use env_bool, so =0 and =false
disabled NVDEC on SGLang while leaving it on elsewhere.

Two further problems came from outside the PR review, both in the container/docs surface
where the test suite does not reach:

  • SGLang XPU was purged with no replacement (a4ba547211). The purge sat outside the
    device conditional while the replacement ffmpeg is copied in under device == "cuda",
    leaving that image with no ffmpeg at all. Gating the purge alone would have broken the
    build — the end-of-stage codec guard was not device-scoped either and only passed there
    because the purge had run; both are now gated together.
  • The SGLang support matrix contradicted its own page (e8ed9f2ba2), listing video as
    supported in aggregated deployments where the paragraph nine lines below says it is not,
    and stating the disaggregated case unconditionally though _NVDEC_UNSAFE_MODEL_TYPES
    excludes the Qwen3-VL family.

Coverage added along the way. Video was only ever addressed over http; the local branch
reads through read_local_media_bytes behind a DYN_MM_LOCAL_PATH gate before joining the
same NVDEC routing, and had no serve coverage (ea3743517b). The note above those fixtures
also claimed file:// is not hardware-decoded, which is wrong for vLLM and SGLang and
argued against writing the missing test; TensorRT-LLM is the real exception and is now named
as such.

Supersedes and combines #11607, #11628 and #11632, which stay open for their review
threads. #11872 (actionable decode errors) is stacked on this and rebases once it lands.

Linear: https://linear.app/nvidia/issue/OPS-7665/disable-ffmpeg-decoders-and-add-image-scan

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added automated media-codec compliance checks for container images, including SBOM-based version validation.
    • Added safeguards to prevent unsupported codec libraries and OpenCV from being included in selected runtime images.
  • Changes

    • Updated bundled FFmpeg to version 8.1.2.
    • Reduced SGLang media support and removed its bundled FFmpeg stack.
    • Restricted supported video decoding to VP8/VP9 scenarios; H.264/H.265 software decoding is unavailable.
  • Documentation

    • Clarified supported video codecs and known decoding limitations.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@datadog-official

This comment has been minimized.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread container/compliance/native_packages.yaml
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The change adds a codec compliance policy and scanner, integrates it into image builds, narrows FFmpeg capabilities, removes codec artifacts from runtime images, updates SGLang and attribution configuration, and adds scanner, media, and OpenCV regression tests.

Codec compliance and runtime media handling

Layer / File(s) Summary
Codec policy, scanner, and build integration
container/compliance/policy/codec_policy.yaml, container/compliance/scan_codecs.py, container/templates/compliance.Dockerfile
Adds deny, allow, exception, and SBOM version rules; scans filesystems and SBOMs; and fails the compliance image stage on violations.
Scanner validation
container/compliance/tests/test_scan_codecs.py
Tests allowlisted paths, exceptions, violations, SBOM checks, traversal, missing SBOMs, and CLI exit modes.
Framework-specific FFmpeg build surface
container/context.yaml, container/templates/wheel_builder.Dockerfile, container/deps/requirements.sglang.txt, container/compliance/native_packages.yaml
Updates FFmpeg version and attribution, disables SGLang media FFmpeg, narrows FFmpeg features, and removes the SGLang imageio-ffmpeg dependency.
Runtime codec cleanup
container/templates/sglang_runtime.Dockerfile, container/templates/vllm_runtime.Dockerfile, container/templates/trtllm_runtime.Dockerfile
Removes codec packages and artifacts from runtime images and adds build-time guards for remaining files and imports.
Media documentation and OpenCV regression coverage
lib/llm/src/preprocessor/media/README.md, lib/llm/src/preprocessor/media/decoders/video.rs, tests/dependencies/test_no_opencv.py
Documents supported decoder limitations and verifies that TRT-LLM images lack OpenCV packages and vendored libraries.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the media-codec cleanup, build gate, and NVDEC changes, with an appropriate issue reference.
Description check ✅ Passed The description thoroughly covers scope, implementation, testing, validation, release impact, and related issues, despite not using every template heading.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
container/compliance/native_packages.yaml (1)

80-97: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add TRT-LLM images to the FFmpeg and libvpx attribution lists.

trtllm_runtime.Dockerfile lines 150-161 copies both components into TRT-LLM runtimes, but these entries only attribute them to vLLM. This leaves the TRT-LLM native-package inventory incomplete.

Proposed fix
   images:
     - vllm-runtime
     - vllm-runtime-efa
+    - trtllm-runtime
+    - trtllm-runtime-efa

Apply this to both ffmpeg and libvpx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@container/compliance/native_packages.yaml` around lines 80 - 97, Update the
images lists for both the ffmpeg and libvpx entries in native_packages.yaml to
include the TRT-LLM runtime image names alongside the existing vLLM images,
matching the identifiers used by trtllm_runtime.Dockerfile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@container/compliance/policy/codec_policy.yaml`:
- Around line 26-43: Extend the codec library denylist entries in policy codec
configuration to cover wheel-renamed hashed artifacts by adding patterns for
libx264-*.so*, libx265-*.so*, libfdk-aac-*.so*, and libpostproc-*.so*. Preserve
the existing un-hashed patterns and coverage for the other codec libraries.

In `@container/compliance/scan_codecs.py`:
- Around line 63-72: Update CodecPolicy.load and initialization to validate the
policy schema before scanning: require deny_globs, allow_paths, deny_components,
and exceptions to be present lists, rejecting missing or misspelled fields
instead of defaulting them to empty values. Validate every exception entry has
non-empty glob, reason, and owner fields, and raise an error on invalid input so
scanning fails closed.

In `@container/compliance/tests/test_scan_codecs.py`:
- Around line 25-29: Replace the module-level mutable _POLICY instance with a
pytest fixture that loads CodecPolicy from the same codec_policy.yaml path for
each test. Update tests to request and use the fixture instead of _POLICY,
preserving assertions against the real shipped policy.

In `@container/templates/vllm_runtime.Dockerfile`:
- Around line 286-300: Extend the post-cleanup verification in the codec-removal
RUN block to assert that imports for decord, decord2, torchcodec, and
PyNvVideoCodec all fail, alongside the existing cv2 and av checks; keep the
uninstall and file cleanup behavior unchanged.

---

Outside diff comments:
In `@container/compliance/native_packages.yaml`:
- Around line 80-97: Update the images lists for both the ffmpeg and libvpx
entries in native_packages.yaml to include the TRT-LLM runtime image names
alongside the existing vLLM images, matching the identifiers used by
trtllm_runtime.Dockerfile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9ee6f563-8fa6-4274-b22a-022b32831073

📥 Commits

Reviewing files that changed from the base of the PR and between e30cc7b and 806881e.

⛔ Files ignored due to path filters (5)
  • lib/llm/tests/data/media/2160p_10.mp4 is excluded by !**/*.mp4
  • lib/llm/tests/data/media/240p_1.mp4 is excluded by !**/*.mp4
  • lib/llm/tests/data/media/240p_10.mp4 is excluded by !**/*.mp4
  • lib/llm/tests/data/media/240p_100.mp4 is excluded by !**/*.mp4
  • lib/llm/tests/data/media/2p_10.mp4 is excluded by !**/*.mp4
📒 Files selected for processing (14)
  • container/compliance/native_packages.yaml
  • container/compliance/policy/codec_policy.yaml
  • container/compliance/scan_codecs.py
  • container/compliance/tests/test_scan_codecs.py
  • container/context.yaml
  • container/deps/requirements.sglang.txt
  • container/templates/compliance.Dockerfile
  • container/templates/sglang_runtime.Dockerfile
  • container/templates/trtllm_runtime.Dockerfile
  • container/templates/vllm_runtime.Dockerfile
  • container/templates/wheel_builder.Dockerfile
  • lib/llm/src/preprocessor/media/README.md
  • lib/llm/src/preprocessor/media/decoders/video.rs
  • tests/dependencies/test_no_opencv.py

Comment thread container/compliance/policy/codec_policy.yaml
Comment thread container/compliance/scan_codecs.py Outdated
Comment thread container/compliance/tests/test_scan_codecs.py
Comment thread container/templates/vllm_runtime.Dockerfile Outdated
@copy-pr-bot

copy-pr-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@dmitry-tokarev-nv

Copy link
Copy Markdown
Contributor Author

/ok to test 9faaa3d

@dmitry-tokarev-nv

Copy link
Copy Markdown
Contributor Author

/ok to test 57e5490

dmitry-tokarev-nv and others added 4 commits August 4, 2026 12:25
CodecPolicy.load shape-checks deny_globs and exceptions but not
allow_paths, and allow_paths is the one whose malformed shape passes
everything. classify() tests `abspath.startswith(p) for p in allow_paths`;
a scalar string iterates per character, one of which is "/", and every
absolute path starts with "/". A single missing "- " therefore turns the
gate into a pass while it still prints a full report, because every
finding comes back verdict "allowed".

Reported by @dagil-nvidia, who ran it at head: /opt/evil/libx264.so.164
classifies ('allowed', None) as a scalar against ('violation', None) as a
list. Reproduced on GPU before and after.

The docstring immediately above already said a gate that silently passes
every image is worse than no gate. That was the intent; allow_paths was
the key it did not cover.

Three tests: the scalar is rejected at load, the behaviour it prevents is
pinned so the reason stays visible, and a well-formed list still loads and
still allows. Verified with the check stripped back out, where the suite
fails as it must.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
The purge this branch added to the SGLang image sits outside any device
conditional, while the replacement VP9 ffmpeg is copied in under
`device == "cuda"`. The XPU image therefore had its base media stack
stripped and nothing put back: no ffmpeg binary, no libav*, and an empty
IMAGEIO_FFMPEG_EXE. That is worse than either leaving it alone or
replacing it properly. The equivalent vLLM purge was already CUDA-scoped;
this one was missed when that was done.

Reported over Slack. The XPU image is not published, so there is nothing
to remove from it -- the same reason the codec gate skips it.

The end-of-stage codec guard moves under the same conditional. It asserts
that the purge worked, and on XPU there is now no purge to assert; left
ungated it would fail that build on the base image's own libraries, which
is exactly what the first version of this change did. Gating the purge
alone would have turned a working XPU build into a failing one.

Rendering both ways: the CUDA Dockerfile is identical to before apart from
these comments, so the image CI builds is unaffected, and XPU emits no
purge, no guard and no gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
This one call site tested the raw value for truthiness where everything
else goes through env_bool. Any non-empty string is truthy, so
DYN_DISABLE_NVDEC=0 and =false DISABLED NVDEC on SGLang while leaving it
enabled on vLLM and TensorRT-LLM -- one setting meaning opposite things
depending on the backend.

Reported by @dagil-nvidia. Measured in the shipped image: "0" and "false"
both read True through raw truthiness and False through env_bool; "1" and
"true" agree either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Two pages describe behaviour that changed under this branch.

The diffusion overview promised an ffmpeg build with h264_nvenc in the
TensorRT-LLM image and told readers video output needs an NVENC-capable
GPU. This branch removed that encoder build-wide; output is VP9 in an MP4
container and needs no GPU encoder. Reported by @dagil-nvidia.

The SGLang support matrix listed video as supported in aggregated
deployments, which the paragraph nine lines below it flatly contradicts:
SGLang resolves and decodes the URL itself there, so Dynamo never sees the
bytes and cannot route them to NVDEC. The disaggregated entry was also
unconditional, though _NVDEC_UNSAFE_MODEL_TYPES skips NVDEC for the
Qwen3-VL family and those requests fall back to a URL path with no decoder
in this image. Both constraints now appear in the table, where someone
consulting a support matrix will actually see them. Reported over Slack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Resolves one conflict in sglang_runtime.Dockerfile. Main added a UCX
alias block (#12558) in the same region where this branch scopes the media
purge and the in-tree ffmpeg copy to CUDA, and both sides ended at the same
`{% endif %}` -- mine closing `device == "cuda"`, main's closing
`device != "xpu" and target not in (dev, local-dev)`.

Kept both, each with its own conditional, rather than picking a side:
the two guard unrelated things and their conditions are not equivalent.
Verified by rendering sglang for cuda/runtime, xpu/runtime and cuda/dev --
the purge and codec guard appear only for CUDA, the UCX block for
everything except XPU, exactly as each side intended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmitry-tokarev-nv

dmitry-tokarev-nv commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @dagil-nvidia - both points were worth raising, and one of them was a real hole in the gate itself.

allow_paths fails open

Confirmed and fixed in a191963. You were right, and I reproduced it before changing anything:

scalar allow_paths -> ('allowed', None)      # /opt/evil/libx264.so.164
list   allow_paths -> ('violation', None)

load() now rejects a non-list allow_paths, with three tests: the scalar is refused at load, the behaviour it prevents is pinned so the reason stays visible, and a well-formed list still loads and still allows. Verified on GPU with the check stripped back out, where the suite fails as it must — a guard that passes with and without the thing it guards is worth nothing.

The docstring immediately above already said a gate that silently passes every image is worse than no gate. That was the intent; allow_paths was the one key it didn't cover, and it was the one that fails open.

output_format: "mp4" now returns VP9

Worth separating what changed from what was already true, because "produced H.264 before this PR" holds on some hardware and not on the hardware this runs on.

H.264 output cannot come from NVENC on the parts Dynamo is deployed on. NVIDIA omits the encoder from the datacenter line entirely:

GPU NVDEC (decode) NVENC (encode)
A100 5 engines none
H100, H200 7 engines none
B200, GB200 7 engines none
L4 / L40S / RTX 6000 Ada 3–4 engines 2–3

That asymmetry is why decode and encode are not the same problem here: NVDEC is present everywhere, NVENC is present nowhere that matters.

So the pre-PR behaviour was not "H.264 everywhere":

before on datacenter GPUs
vLLM software H.264 via export_to_video worked — and is the codec surface this PR removes
SGLang h264_nvenc + libx264 fallback the GPL fallback is what made it work
TRT-LLM h264_nvenc, no fallback already failing, as you noted

Restoring H.264 for the datacenter case therefore means restoring a software encoder, which is exactly what this change removes. NVENC would cover only L4/RTX-class parts and would leave the output codec varying by GPU — a worse contract than a uniform change. VP9 is the only option that behaves identically on every supported GPU, needs no hardware encoder, and carries no software H.264. The container stays MP4; the stream inside is VP9.

The real cost is the one you identified: the extension is unchanged and the contents are not. VP9-in-MP4 plays in Chrome, Firefox and ffmpeg, but not in Safari or QuickTime, so a client that hands the bytes straight to a player can fail with no error from us. That goes in the release notes — it is the mitigation that makes this defensible rather than something to be discovered later.

The rest

  • DYN_DISABLE_NVDEC now reads through env_bool in the SGLang encode worker (0f4e712). Measured in the shipped image: 0 and false read True through the old raw truthiness and False through env_bool, so that one setting meant opposite things depending on the backend.
  • The stale doc lines are corrected (e8ed9f2): diffusion/overview.md no longer promises h264_nvenc, and the SGLang support matrix no longer claims aggregated video works — the paragraph nine lines below it said the opposite.
  • video-decode-gpu-requirements.md:131 pointing at DYN_ENABLE_MEDIA_DECODERS is left alone deliberately: that switch is implemented in feat(media): explicit installer for additional media decoders (OPS-7795) #12051, which has not merged. Fixing it here would either document a flag that does not exist yet or pre-empt that PR.
  • On the pre-commit and XPU reds: agreed they are not from this branch. The ruff one cleared with the merge. XPU has two failures, both traced and neither ours — gpu_memory_service is missing from that image and fails identically on an untouched one, and google_crc32c arrived with the ModelExpress 0.4.0 → 0.5.0 bump (build: bump ModelExpress to 0.5.0(OPS-7978) #12455); it is imported by modelexpress/metadata/artifact_manifest.py and the 2-card lane passed on the commit immediately before that merge.

`output_format: "mp4"` returns VP9 where it returned H.264, under an
unchanged file extension, so a client that passes the bytes straight to a
player can fail with no error from Dynamo. VP9-in-MP4 plays in Chrome,
Firefox and ffmpeg but not in Safari or QuickTime.

Raised by @dagil-nvidia, whose point was that the extension did not change
and the contents did. Saying it in the docs is the mitigation that makes
the change defensible rather than something a consumer discovers later, so
it is called out where the output format is configured and repeated as a
release note on the PR.

Records why it cannot stay H.264: NVIDIA omits NVENC from the datacenter
line, so a hardware path would work only on L4/L40S/RTX-class parts, and
the alternative is the software encoder these images exist to remove.
Includes the client-side transcode for anyone who needs H.264.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Comment thread docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md Outdated
Comment thread components/src/dynamo/common/multimodal/nvdec_decoder.py Outdated
dmitry-tokarev-nv and others added 3 commits August 4, 2026 19:40
…nloaded

_maybe_nvdec_decoder downloads the whole payload to probe its codec. When
the codec is not H.264/H.265, or the decoder cannot be built, it discarded
those bytes and returned None, so _build_encode_inputs handed SGLang the
URL and SGLang fetched the same payload again.

Three consequences, all reported by Codex on #11836: doubled ingress and
latency for every non-NVDEC video; a one-use signed URL that fails on the
second request; and -- because SGLang applies no URL policy of its own,
fetching http(s) straight through get_mm_http_session -- bytes decoded that
were never the ones this handler validated, with a redirect free to resolve
differently between the two fetches. That last one is the same class as the
policy-refusal hole fixed earlier in this branch, reached from the other
side, and it was noted then as a gap left open.

Return the fetched bytes instead. SGLang needs no shim: load_video takes
Union[str, bytes, VideoData] and _normalize_video_input returns bytes
untouched (v0.5.16). The URL is now returned only when nothing was fetched
-- an unsupported scheme, or a failed fetch -- which is the one case where
it really is all the caller has.

Measured on GPU against an HTTP server counting GETs, running the real
_build_encode_inputs and feeding its output to SGLang's real load_video:
two origin fetches for one request before, one after.

Two existing tests asserted the old contract and are corrected rather than
deleted; the third outcome is added to the _build_encode_inputs case, which
previously covered only decoder-or-URL. In the shipped image this fallback
still fails for want of a software decoder, so today the saving precedes an
error; it becomes load-bearing once a decoder is present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Both mentions of DYN_ENABLE_MEDIA_DECODERS point at a flag implemented in
#12051, which has not merged. Someone reading this on main would go looking
for it and find nothing, which is worse than not mentioning it.

Raised by @rmccorm4. #12051 adds its own documentation page and should
reintroduce these two mentions when it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
The triangle fixtures were only ever addressed over http. The local branch
takes a different route to the same decoder -- read_local_media_bytes
instead of a fetch, behind the DYN_MM_LOCAL_PATH policy gate -- so that
read, its gate, and its hand-off to NVDEC had no serve coverage at all.
MULTIMODAL_VIDEO_PATH existed in conftest and was never consumed by
anything, which made the gap look covered.

Added as a third payload on the existing NVDEC deployment rather than a new
one, so it costs an extra request instead of an extra engine start. All
three payloads are the same triangle footage over different transports, so
a difference in the answer points at the transport rather than the model or
the encoding.

Also corrects the note above those fixtures. It claimed file:// video is
not hardware-decoded, which is wrong for vLLM and SGLang: both read local
media and then route it to NVDEC exactly as they do http, and
video_loader.py says so in its own comment -- otherwise a local H.264 file
would reach only the software decoder these images do not ship. TensorRT-LLM
is the real exception, and is now named as such. A wrong note of that shape
is worse than no note, because it argues against writing the missing test.

Verified on GPU: video_e_pd_qwen_nvdec passes with all three payloads, and
video_e_pd_qwen passes, which is the case where the bytes returned by the
double-fetch fix travel the full encode chain to the processor rather than
stopping at load_video.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Comment thread docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md Outdated
This page said other formats "continue to use the software decode path"
and framed only H.264/H.265 as lacking one. Both are wrong, and they
contradicted the backend pages added in the same PR, which state that no
other codec has a decoder in these images.

Reported by @tanmayv25. The reality is a little worse than reported:
context.yaml sets enable_media_ffmpeg "false" for every image block, so the
Rust media-ffmpeg decoder is not built either. The in-tree VP8/VP9 FFmpeg
serves the video OUTPUT path and is not wired to video input in any form,
so the mechanism this page implied did not exist even partially. AV1 was
also missing from the list; NVDEC does not decode it on Hopper and there is
no software carrier, so it fails the same way.

This branch's own tests are the evidence: video_e_pd_qwen and
video_agg_qwen carry installs_extra_dependencies precisely because they
must pip install decord before a VP9 clip will decode. On GPU, a VP9 clip
returns ModuleNotFoundError through SGLang and "OpenCV (cv2) is required
for video decoding but is not installed" through TensorRT-LLM.

Also corrects the DYN_DISABLE_NVDEC row, which promised the same
non-existent fallback: it leaves video input with no decoder at all, making
it a debugging switch rather than a way to fall back.

Deliberately does not name DYN_ENABLE_MEDIA_DECODERS as the remedy, though
that was suggested: the switch lives in an unmerged PR, and @rmccorm4 asked
for its references removed from this branch for that reason. The remedy is
described generically until it ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
@saturley-hall
saturley-hall merged commit b310cda into main Aug 5, 2026
119 of 122 checks passed
@saturley-hall
saturley-hall deleted the dtokarev/ops-7665-codec-compliance branch August 5, 2026 15:31
pvijayakrish pushed a commit that referenced this pull request Aug 6, 2026
…C video decode (OPS-7665) (#11836) (#12703)

Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Co-authored-by: Harrison King Saturley-Hall <hsaturleyhal@nvidia.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Jankwi added a commit to Jankwi/dynamo that referenced this pull request Aug 30, 2026
VideoMediaIO.load_bytes' OpenCV backend was removed from the vLLM runtime image in ai-dynamo#11836

Signed-off-by: Jan Kwiecinski <jkwiecinski@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

actions backend::sglang Relates to the sglang backend backend::trtllm Relates to the trtllm backend backend::vllm Relates to the vllm backend build container documentation Improvements or additions to documentation frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` multimodal size/XXL xpu

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants