Skip to content

fix(multimodal): own NVDEC frame memory and CUDA-gate PyNvVideoCodec in the SGLang image (OPS-8040) - #12733

Open
dmitry-tokarev-nv wants to merge 2 commits into
mainfrom
dtokarev/ops-8040-nvdec-xpu-filter-frame-copy
Open

fix(multimodal): own NVDEC frame memory and CUDA-gate PyNvVideoCodec in the SGLang image (OPS-8040)#12733
dmitry-tokarev-nv wants to merge 2 commits into
mainfrom
dtokarev/ops-8040-nvdec-xpu-filter-frame-copy

Conversation

@dmitry-tokarev-nv

@dmitry-tokarev-nv dmitry-tokarev-nv commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Two hardening items surfaced by automated review on the release cherry-pick #12703. Both target code that merged in #11836, so they land on main first.

  • components/src/dynamo/common/multimodal/nvdec_decoder.py: _frame_to_rgb_hwc now copies each decoded frame into memory it owns. The decoder is constructed with use_device_memory=False, so the DLPack-wrapped frame is already host memory: .cpu() was a no-op and .numpy() aliased decoder-owned buffers — correctness relied on PyNvVideoCodec (undocumented) handing each DecodedFrame its own buffer. The copy removes that dependency and covers both call sites (decode_video_nvdec and the SGLang NvdecVideoDecoder.get_frames_as_tensor convert through this helper). The docstring still described the device-memory path, so it is rewritten. Raised in this review thread.
  • container/templates/sglang_runtime.Dockerfile: drop PyNvVideoCodec from the non-CUDA (Intel XPU) SGLang image, mirroring the existing device branch in vllm_runtime.Dockerfile: line-anchored grep -v '^PyNvVideoCodec' over the shared requirements plus a negative import check so a silently broken filter fails the build. The wheel is inert without libnvcuvid; this stops shipping it where it can never run. Raised in this review thread.

Validation

  • New CPU regression test test_frame_to_rgb_hwc_copies_out_of_decoder_memory drives the helper with a numpy source (numpy exports DLPack exactly like a DecodedFrame): it fails on the previous code (mutating the source corrupts the collected frame) and passes with the copy. Full test_nvdec_decoder.py: 33 passed.
  • Rendered the sglang template for both devices with container/render.py: the CUDA output is functionally unchanged (same unfiltered install; blank-line-only diff from the template branch markers), the XPU output gains exactly the filter block and import check.
  • pre-commit run --all-files: all hooks pass.

Linear: OPS-8040 (sub-issue of OPS-7665)

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes

    • Improved video frame handling to ensure decoded RGB data remains stable when source buffers are reused or changed.
    • Added regression coverage for frame data preservation.
  • Chores

    • Updated runtime image dependency installation based on device type.
    • Non-CUDA images now exclude unsupported video decoding components and validate the resulting installation.

dmitry-tokarev-nv and others added 2 commits August 5, 2026 20:26
_frame_to_rgb_hwc converted frames with tensor.cpu().numpy(). The decoder
is constructed with use_device_memory=False, so the DLPack-wrapped frame
is already host memory: .cpu() is a no-op and .numpy() aliases memory the
decoder owns and may recycle between indexed reads. Correct behaviour
relied on PyNvVideoCodec handing each DecodedFrame its own buffer, which
is undocumented. Copy each frame into owned memory instead; the docstring
also still described the device-memory path, so rewrite it.

Covers both call sites: decode_video_nvdec and the SGLang
NvdecVideoDecoder.get_frames_as_tensor convert through this helper.

The new regression test drives the helper with a numpy source (numpy
exports DLPack exactly like a DecodedFrame): it fails on the previous
code and passes with the copy. Raised by automated review on the release
cherry-pick (#12703).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
The shared requirements.sglang.txt install was device-unguarded, so the
NVIDIA-only NVDEC wheel landed in the Intel XPU image, where it is inert
(no libnvcuvid). Mirror the existing branch in vllm_runtime.Dockerfile:
filter it out with a line-anchored grep for non-CUDA devices and fail the
build if the package still imports, so a silently broken filter cannot
look like success. Rendered CUDA output is unchanged.

Raised by automated review on the release cherry-pick (#12703).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

grep -v '^PyNvVideoCodec' /tmp/requirements.sglang.txt > /tmp/requirements.sglang.nonvidia.txt && \
pip install --break-system-packages --force-reinstall --no-deps \
--requirement /tmp/requirements.sglang.nonvidia.txt && \
! python3 -c "import PyNvVideoCodec" 2>/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The non-CUDA guard passes when PyNvVideoCodec is installed but cannot import because libnvcuvid is missing, so the XPU image can still ship the unusable wheel. Fix: check module/package presence without importing PyNvVideoCodec.

🤖 AI Fix

In container/templates/sglang_runtime.Dockerfile, in the non-CUDA SGLang requirements RUN block, replace ! python3 -c "import PyNvVideoCodec" 2>/dev/null with python3 -c "import importlib.util, sys; sys.exit(importlib.util.find_spec('PyNvVideoCodec') is not None)".

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The decoder now returns an owned uint8 NumPy array for host-memory frames. Tests verify independent storage. The SGLang runtime Dockerfile installs PyNvVideoCodec only for CUDA images.

Changes

NVDEC runtime behavior

Layer / File(s) Summary
Device-specific SGLang dependency installation
container/templates/sglang_runtime.Dockerfile
CUDA images install the complete requirements file. Non-CUDA images exclude PyNvVideoCodec and verify that it cannot be imported.
Owned NVDEC frame conversion
components/src/dynamo/common/multimodal/nvdec_decoder.py, components/src/dynamo/common/tests/multimodal/test_nvdec_decoder.py
_frame_to_rgb_hwc uses host-memory decoding and returns an owned uint8 HWC array. The regression test verifies the converted shape and independent storage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: NVDEC frame ownership and CUDA-gated PyNvVideoCodec installation.
Description check ✅ Passed The description explains the changes, validation, affected files, and related issue, but uses Summary and Validation headings instead of the template headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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: 1

🧹 Nitpick comments (2)
components/src/dynamo/common/multimodal/nvdec_decoder.py (2)

170-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move torch to module scope.

This local import hides a third-party dependency and delays import errors until _frame_to_rgb_hwc runs. Move it to the module-level third-party import block and run isort.

As per coding guidelines, Python imports must stay at the top of the file. As per path instructions, .ai/python-guidelines.md requires NVDEC imports at module scope and ordered via isort. Verify that torch is available in every environment that imports this module.

🤖 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 `@components/src/dynamo/common/multimodal/nvdec_decoder.py` at line 170, Move
the torch import from the local scope of _frame_to_rgb_hwc to the module-level
third-party import block, then run isort to maintain the required ordering.
Verify torch is available in every environment that imports this module.

Sources: Coding guidelines, Path instructions


172-175: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the DLPack fallback exception.

except Exception treats every torch.from_dlpack(frame) failure as an unsupported protocol. A malformed frame or conversion failure then falls through to torch.as_tensor(frame, device="cuda"), which can hide the original error and produce a misleading CUDA error. Catch only the documented unsupported-protocol or type exceptions. Re-raise other failures.

As per coding guidelines, catch specific exceptions and fail fast instead of continuing after a broad Exception catch. Verify the exact exception classes for the supported Torch and PyNvVideoCodec versions.

🤖 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 `@components/src/dynamo/common/multimodal/nvdec_decoder.py` around lines 172 -
175, In the DLPack conversion block around torch.from_dlpack, replace the broad
Exception handler with only the documented unsupported-protocol or type-related
exception classes supported by the project’s Torch and PyNvVideoCodec versions.
Keep the torch.as_tensor CUDA fallback only for those exceptions, allowing all
other conversion failures to propagate unchanged.

Sources: Coding guidelines, Path instructions

🤖 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/templates/sglang_runtime.Dockerfile`:
- Around line 153-163: The non-CUDA guard in the sglang dependency installation
RUN block must verify package presence rather than importability. Replace the
PyNvVideoCodec import check with python3 -m pip show PyNvVideoCodec, and make
the guard fail with an explicit error message when the package is installed.

---

Nitpick comments:
In `@components/src/dynamo/common/multimodal/nvdec_decoder.py`:
- Line 170: Move the torch import from the local scope of _frame_to_rgb_hwc to
the module-level third-party import block, then run isort to maintain the
required ordering. Verify torch is available in every environment that imports
this module.
- Around line 172-175: In the DLPack conversion block around torch.from_dlpack,
replace the broad Exception handler with only the documented
unsupported-protocol or type-related exception classes supported by the
project’s Torch and PyNvVideoCodec versions. Keep the torch.as_tensor CUDA
fallback only for those exceptions, allowing all other conversion failures to
propagate unchanged.
🪄 Autofix

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: a2acee82-3ca6-48fd-8aca-4e4c709e9002

📥 Commits

Reviewing files that changed from the base of the PR and between b6d6480 and 3e0dfdb.

📒 Files selected for processing (3)
  • components/src/dynamo/common/multimodal/nvdec_decoder.py
  • components/src/dynamo/common/tests/multimodal/test_nvdec_decoder.py
  • container/templates/sglang_runtime.Dockerfile

Comment on lines +153 to +163
# import check fails the build if the package arrives by another route -- a
# filter that silently stopped matching would otherwise look like success.
# Whole-RUN branches rather than a conditional inside one RUN, for the reasons
# documented at the equivalent block in vllm_runtime.Dockerfile.
RUN --mount=type=bind,source=./container/deps/requirements.sglang.txt,target=/tmp/requirements.sglang.txt \
--mount=type=cache,target=/root/.cache/pip,sharing=locked \
export PIP_CACHE_DIR=/root/.cache/pip && \
grep -v '^PyNvVideoCodec' /tmp/requirements.sglang.txt > /tmp/requirements.sglang.nonvidia.txt && \
pip install --break-system-packages --force-reinstall --no-deps \
--requirement /tmp/requirements.sglang.nonvidia.txt && \
! python3 -c "import PyNvVideoCodec" 2>/dev/null

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target Dockerfile ---'
sed -n '135,175p' container/templates/sglang_runtime.Dockerfile
printf '%s\n' '--- equivalent vLLM block ---'
rg -n -A24 -B8 'PyNvVideoCodec|nonvidia|non-CUDA|non-CUDA' container/templates/vllm_runtime.Dockerfile
printf '%s\n' '--- all relevant references ---'
rg -n 'PyNvVideoCodec|pip show|requirements\.sglang' container container/deps
printf '%s\n' '--- file status ---'
git status --short

Repository: ai-dynamo/dynamo

Length of output: 15572


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import os
import pathlib
import subprocess
import sys
import tempfile

with tempfile.TemporaryDirectory() as td:
    root = pathlib.Path(td)
    site = root / "site"
    site.mkdir()
    (site / "PyNvVideoCodec").mkdir()
    (site / "PyNvVideoCodec" / "__init__.py").write_text(
        "raise ImportError('libnvcuvid.so.1: cannot open shared object file')\n"
    )
    (site / "PyNvVideoCodec-2.2.0.dist-info").mkdir()
    (site / "PyNvVideoCodec-2.2.0.dist-info" / "METADATA").write_text(
        "Metadata-Version: 2.1\nName: PyNvVideoCodec\nVersion: 2.2.0\n"
    )
    env = os.environ.copy()
    env["PYTHONPATH"] = str(site)
    import_result = subprocess.run(
        [sys.executable, "-c", "import PyNvVideoCodec"],
        env=env, capture_output=True, text=True,
    )
    show_result = subprocess.run(
        [sys.executable, "-m", "pip", "show", "PyNvVideoCodec"],
        env=env, capture_output=True, text=True,
    )
    print("import return code:", import_result.returncode)
    print("import stderr contains libnvcuvid:", "libnvcuvid" in import_result.stderr)
    print("pip show return code:", show_result.returncode)
    print("pip show reports package:", "Name: PyNvVideoCodec" in show_result.stdout)

    absent = subprocess.run(
        [sys.executable, "-m", "pip", "show", "PyNvVideoCodec"],
        env={**os.environ, "PYTHONPATH": str(root / "empty")},
        capture_output=True, text=True,
    )
    print("absent pip show return code:", absent.returncode)
PY

printf '%s\n' '--- SGLang post-install guards ---'
sed -n '365,390p' container/templates/sglang_runtime.Dockerfile
printf '%s\n' '--- SGLang image and device conditions ---'
rg -n -A8 -B8 'sglang|device ==|device !=|base.*image|FROM ' container/templates/sglang_runtime.Dockerfile | head -160

Repository: ai-dynamo/dynamo

Length of output: 9866


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import pathlib
import subprocess
import sys
import tempfile
import venv

with tempfile.TemporaryDirectory() as td:
    env_dir = pathlib.Path(td) / "venv"
    venv.EnvBuilder(with_pip=True).create(env_dir)
    py = env_dir / "bin" / "python"
    site = pathlib.Path(
        subprocess.check_output(
            [py, "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"],
            text=True,
        ).strip()
    )
    (site / "PyNvVideoCodec").mkdir()
    (site / "PyNvVideoCodec" / "__init__.py").write_text(
        "raise ImportError('libnvcuvid.so.1: cannot open shared object file')\n"
    )
    dist = site / "PyNvVideoCodec-2.2.0.dist-info"
    dist.mkdir()
    (dist / "METADATA").write_text(
        "Metadata-Version: 2.1\nName: PyNvVideoCodec\nVersion: 2.2.0\n"
    )

    import_result = subprocess.run(
        [py, "-c", "import PyNvVideoCodec"],
        capture_output=True, text=True,
    )
    show_result = subprocess.run(
        [py, "-m", "pip", "show", "PyNvVideoCodec"],
        capture_output=True, text=True,
    )
    negated_import = subprocess.run(
        ["/bin/sh", "-c", f"! '{py}' -c 'import PyNvVideoCodec' 2>/dev/null"],
        capture_output=True, text=True,
    )
    presence_guard = subprocess.run(
        ["/bin/sh", "-c",
         f"if '{py}' -m pip show PyNvVideoCodec >/dev/null 2>&1; then "
         "echo 'ERROR: PyNvVideoCodec is installed in a non-CUDA image' >&2; "
         "exit 1; fi"],
        capture_output=True, text=True,
    )

    print("import return code:", import_result.returncode)
    print("pip show return code:", show_result.returncode)
    print("pip show reports package:", "Name: PyNvVideoCodec" in show_result.stdout)
    print("negated import guard return code:", negated_import.returncode)
    print("presence guard return code:", presence_guard.returncode)
    print("presence guard message:", presence_guard.stderr.strip())
PY

Repository: ai-dynamo/dynamo

Length of output: 870


🏁 Script executed:

#!/bin/bash
set -eu
python3 -m pip show pip >/tmp/pip-show-present.txt
if python3 -m pip show __definitely_missing_pynvvideocode__ >/dev/null 2>&1; then
    echo "unexpected package found" >&2
    exit 1
fi
printf 'pip show detects an installed distribution: '
grep -q '^Name: pip$' /tmp/pip-show-present.txt && echo yes

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/python3" <<'EOF'
#!/bin/sh
exit 1
EOF
chmod +x "$tmpdir/python3"

if PATH="$tmpdir:$PATH" sh -c '! python3 -c "import PyNvVideoCodec" 2>/dev/null'; then
    echo "negated import guard passes when import fails"
else
    echo "negated import guard failed unexpectedly" >&2
    exit 1
fi

Repository: ai-dynamo/dynamo

Length of output: 192


Make the non-CUDA guard test package presence. ! python3 -c "import PyNvVideoCodec" passes when the installed package fails to import because libnvcuvid is unavailable. Use python3 -m pip show PyNvVideoCodec and fail with an explicit error instead.

🤖 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/templates/sglang_runtime.Dockerfile` around lines 153 - 163, The
non-CUDA guard in the sglang dependency installation RUN block must verify
package presence rather than importability. Replace the PyNvVideoCodec import
check with python3 -m pip show PyNvVideoCodec, and make the guard fail with an
explicit error message when the package is installed.

@datadog-official

datadog-official Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tests

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 14.29%
Overall Coverage: 46.13% (-5.91%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3e0dfdb | Docs | Datadog PR Page | Give us feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant