build(container): media-codec cleanup + build gate + NVDEC video decode (OPS-7665) - #11836
Conversation
This comment has been minimized.
This comment has been minimized.
WalkthroughChangesThe 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
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
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 winAdd TRT-LLM images to the FFmpeg and libvpx attribution lists.
trtllm_runtime.Dockerfilelines 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-efaApply this to both
ffmpegandlibvpx.🤖 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
⛔ Files ignored due to path filters (5)
lib/llm/tests/data/media/2160p_10.mp4is excluded by!**/*.mp4lib/llm/tests/data/media/240p_1.mp4is excluded by!**/*.mp4lib/llm/tests/data/media/240p_10.mp4is excluded by!**/*.mp4lib/llm/tests/data/media/240p_100.mp4is excluded by!**/*.mp4lib/llm/tests/data/media/2p_10.mp4is excluded by!**/*.mp4
📒 Files selected for processing (14)
container/compliance/native_packages.yamlcontainer/compliance/policy/codec_policy.yamlcontainer/compliance/scan_codecs.pycontainer/compliance/tests/test_scan_codecs.pycontainer/context.yamlcontainer/deps/requirements.sglang.txtcontainer/templates/compliance.Dockerfilecontainer/templates/sglang_runtime.Dockerfilecontainer/templates/trtllm_runtime.Dockerfilecontainer/templates/vllm_runtime.Dockerfilecontainer/templates/wheel_builder.Dockerfilelib/llm/src/preprocessor/media/README.mdlib/llm/src/preprocessor/media/decoders/video.rstests/dependencies/test_no_opencv.py
|
/ok to test 9faaa3d |
|
/ok to test 57e5490 |
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>
|
Thanks @dagil-nvidia - both points were worth raising, and one of them was a real hole in the gate itself.
|
| 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_NVDECnow reads throughenv_boolin the SGLang encode worker (0f4e712). Measured in the shipped image:0andfalsereadTruethrough the old raw truthiness andFalsethroughenv_bool, so that one setting meant opposite things depending on the backend.- The stale doc lines are corrected (e8ed9f2):
diffusion/overview.mdno longer promisesh264_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:131pointing atDYN_ENABLE_MEDIA_DECODERSis 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_serviceis missing from that image and fails identically on an untouched one, andgoogle_crc32carrived with the ModelExpress 0.4.0 → 0.5.0 bump (build: bump ModelExpress to 0.5.0(OPS-7978) #12455); it is imported bymodelexpress/metadata/artifact_manifest.pyand 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>
…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>
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>
VideoMediaIO.load_bytes' OpenCV backend was removed from the vLLM runtime image in ai-dynamo#11836 Signed-off-by: Jan Kwiecinski <jkwiecinski@nvidia.com>
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
PyNvVideoCodecacross all three backends, so the cleanup is not a capabilityregression. This is the larger half of the diff and brought its own requirements: the
container must be granted the
videodriver 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_nvencand linked
nv-codec-headers. A hardware H.264 encoder is still a distributable H.264codec surface, so it is now omitted entirely:
--enable-nvencand 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
wheel_builder): narrow decoder/demuxer/parser allowlist(VP8/VP9 + rawvideo); encoders reduced to
libvpx_vp9. Bumped 8.1 → 8.1.2.(
av,opencv,decord,torchcodec). Keeps the in-tree ffmpeg +imageio-ffmpegfor the video-generation encode path, which now encodes VP9.
a build-failing guard.
opencv-python-headless.licensesstage): filesystem + SBOM scan that fails the build ifa 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:
common/multimodal/nvdec_decoder.py: codec probe → route → decode to(T, H, W, 3)uint8 RGB, matching the existingVideoLoadercontract.PyNvVideoCodec>=2.2.0. 2.0.x bundles a full FFmpeg includinglibavcodec,which the gate rejects; 2.2.0 bundles only
libavutil+libavformat(containerdemux, no codec) and links
libnvcuvidat runtime.http(s),file://anddata:all hardware-decode.file://stays behindthe existing
DYN_MM_LOCAL_PATHpolicy.videodrivercapability. 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:
mm_agg_video_nvdec(h264, h265)video_e_pd_qwen_nvdec(h264, h265)aggregated_multimodal_video_nvdec(h264, h265)test_nvdec_decoder_gpu(h264, hevc)test_media_sourcefile://sandbox: prefix, traversal, symlink escape, data URIstest_video_encode_realNone 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 thatVP8/VP9 works in an unmodified image (
-m "not installs_extra_dependencies"selectsonly unmodified-image behaviour).
Validation
CI, expanded suite — run 30513728452
on
116728e016ran the backend jobs with(pre_merge or post_merge)markers rather thanpre_mergealone, so the post_merge cases were exercised before merge: green acrossvLLM, SGLang and TensorRT-LLM (Test, Multi-GPU Test and Compliance audit, amd64 + arm64).
CI, merge-ready config — run 30562129491
on
6efdd3b2a6green after the temporary markers and raised timeouts were reverted, confirming the
configuration that actually merges is clean at the original budgets.
CI, latest — run 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 multimodaldocumentation into
pages/use-cases/multimodal-serving/and added therequest-trace-s3build feature. Both conflicts were merged rather than resolved in favour of one side, so
request-trace-s3is now built into every wheel while SGLang still builds withoutmedia-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 thanpre_mergealone,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], wedgedfor ~4245s against its own declared
timeout(320)— while reporting 602 tests passedand zero test-level failures. That timeout cannot fire: the test is documented in-tree
(DYN-2784) as blocking forever in
KvRouteronmin_initial_workers=2when a worker diesduring launch, with the
pytest.mark.timeoutsignal swallowed at the C-level syscall.GPU0: 5.4/22 GiBagainst a 12.0 GiB profile shows only one of the two workers came up.That flake reproduces on
mainindependently 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 defectbeing 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
videodriver capability thatlibnvcuvidneeds, so every one of them skipped and the paths rested on a dev box.
ai-dynamo/velonix#550 added
NVIDIA_DRIVER_CAPABILITIES=video,compute,utilityto the Dynamo GPU testers, and they nowexecute on the runners' L4s — which have four NVDEC engines. In
run 30685708717, with
zero capability-based skips anywhere:
mm_agg_video_nvdec_qwen3-vl-2b_h264serve e2emm_agg_video_nvdec_qwen3-vl-2b_h265serve e2eaggregated_multimodal_video_nvdecserve e2evideo_e_pd_qwen_nvdecserve e2e (embedding cache disabled)test_nvdec_decodes_real_clip[h264]/[hevc]libnvcuviddecodeThe 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:
executed=5 skipped=0video_e_pd_qwen_nvdecserve e2eexecuted=1 skipped=0, no decoder fallbackaggregated_multimodal_video_nvdecserve e2eexecuted=1 skipped=0video_metadatashim vs realMMEncoder._encode(sglang 0.5.16)(648, 2048)VideoDatavs vendorasync_load_video(1.3.0rc22)file://anddata:decode + sandbox refusalsGetting those tests to actually execute surfaced four defects CI could not have caught,
all fixed here: a
filterwarnings=errorinteraction that cached the NVDEC capabilityprobe 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 videorequest 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:
9f0650b1ad). The conversion wasreachable 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.
9f0650b1ad, completed by10b56197e2). Pre-sampled frames tookpreprocess_video'sreturn vr, Nonebranch, sothe model's
vision_config.videopolicy never applied and metadata was synthesized. AVideoDecoderWrappersubclass over PyNvVideoCodec now lets SGLang choose frames fromtrue source values: measured
nframes=4 -> 4 frames,fps=10.0rather than afabricated
24.0, indices[0, 3, 6, 9], with an ndarray control reproducing the oldbehaviour.
c60d651be7).UrlValidationErrorwas caught alongside decode failures, so a rejected URL was handedto 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 recordszero 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 — andclosing it would start rejecting input
mainaccepts today, so it belongs in its ownchange rather than folded into a compliance PR.
Video content assertions rebuilt (
bb5e1b230d). The serve tests asserted words againstffmpeg testsrc2colour bars — first["red", "static", "still"], describing content thoseclips 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_EXPECTEDconstant, so theexpectation 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, andreturns 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 preservedrather 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_qwenandmultimodal_e_pd_qwen— and the job failed onthe unrelated SGLang flake described earlier:
test_sglang_indexers_syncwedged for 4235sagainst its own declared
timeout=320s, blocking three queued workers until the runnerreported
Executing the custom container implementation failed. The triangle fixtures and theVP9 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.dalioutright. 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 outrightwould 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_runtimerebases on upstream and overlaysruntime_full, and DALI'slibraries 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.pyrepeats the check on the shipped image. Thattest was verified to fail on 2.1.0 before being trusted to pass afterwards.
The DALI waiver in
codec_policy.yamlstays, with a corrected rationale: the denyglobs 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_diffusionand the E/PD multimodal cases.One test is skipped, deliberately.
test_router_e2e_with_sglang.py::test_sglang_indexers_synchangs rather thanfails — 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
9c154b62cdis green: 61 jobs succeeded, 17 skipped, 0 failed, with thetemporary 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 assertedthe 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 sixtest_vllm_worker_factorycases fail onimport. They fail identically on the untouched image. That lane runs only when the
xpulabel 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 VP9stream 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.loadshape-checked
deny_globsandexceptionsbut notallow_paths— the one key that failsopen. Written as a scalar it iterates per character,
"/"is among them, and everyabsolute path starts with
"/", so a single missing-classified every violation asallowedwhile still printing a full report. Reproduced on GPU:/opt/evil/libx264.so.164→('allowed', None)as a scalar,('violation', None)as alist. 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 asFalse,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, theserve 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). Theencode 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_videotakesUnion[str, bytes, VideoData],so no shim is needed. Measured against a counting HTTP server: two origin fetches per
request before, one after.
DYN_DISABLE_NVDECmeant opposite things per backend (0f4e71274d, @dagil-nvidia).One call site tested raw truthiness where the rest use
env_bool, so=0and=falsedisabled 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:
a4ba547211). The purge sat outside thedevice 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.
e8ed9f2ba2), listing video assupported in aggregated deployments where the paragraph nine lines below says it is not,
and stating the disaggregated case unconditionally though
_NVDEC_UNSAFE_MODEL_TYPESexcludes the Qwen3-VL family.
Coverage added along the way. Video was only ever addressed over http; the local branch
reads through
read_local_media_bytesbehind aDYN_MM_LOCAL_PATHgate before joining thesame NVDEC routing, and had no serve coverage (
ea3743517b). The note above those fixturesalso claimed
file://is not hardware-decoded, which is wrong for vLLM and SGLang andargued 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
Summary by CodeRabbit
New Features
Changes
Documentation