Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions components/src/dynamo/common/multimodal/nvdec_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,24 @@ def _gpu_id() -> int:


def _frame_to_rgb_hwc(frame) -> np.ndarray:
"""Copy a decoded (device) RGB frame to a host ``(H, W, 3)`` uint8 array.

A 2.x ``DecodedFrame`` (``output_color_type=RGB``) holds a CUDA buffer and
supports the DLPack protocol, so torch wraps it zero-copy on the GPU and
``.cpu()`` copies to host. Validated on PyNvVideoCodec 2.1.1 for H.264/H.265.
"""Copy a decoded RGB frame into an owned host ``(H, W, 3)`` uint8 array.

A 2.x ``DecodedFrame`` (``output_color_type=RGB``) supports the DLPack
protocol, so torch wraps its buffer zero-copy. The decoder is constructed
with ``use_device_memory=False``, so that buffer is already host memory:
``.cpu()`` is a no-op and ``.numpy()`` would keep aliasing decoder-owned
memory, which the decoder is free to recycle for the next indexed read.
The explicit ``np.array(..., copy=True)`` is what guarantees every
collected frame owns its pixels (and normalizes dtype in the same step).
Validated on PyNvVideoCodec 2.1.1 for H.264/H.265.
"""
import torch

try:
tensor = torch.from_dlpack(frame)
except Exception: # noqa: BLE001 - fall back to the CUDA-array-interface path
tensor = torch.as_tensor(frame, device="cuda")
arr = tensor.cpu().numpy()
if arr.dtype != np.uint8:
arr = arr.astype(np.uint8)
return arr
return np.array(tensor.cpu().numpy(), dtype=np.uint8, copy=True)


def _source_fps(decoder) -> float:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ def test_should_use_nvdec_false_when_unavailable(monkeypatch):
assert nd.should_use_nvdec("h264") is False


def test_frame_to_rgb_hwc_copies_out_of_decoder_memory():
"""The decoder hands over host frames (``use_device_memory=False``) and is
free to recycle the underlying buffer between indexed reads, so the
conversion must copy. numpy arrays export DLPack exactly like a
``DecodedFrame`` does: without the copy, torch wraps the buffer zero-copy
and mutating the source would corrupt the already-collected frame.
"""
source = np.full((4, 6, 3), 7, dtype=np.uint8)

converted = nd._frame_to_rgb_hwc(source)
source[:] = 99 # the decoder reuses its buffer for the next frame

assert converted.dtype == np.uint8
assert converted.shape == (4, 6, 3)
np.testing.assert_array_equal(converted, np.full((4, 6, 3), 7, dtype=np.uint8))


def test_decode_matches_frame_contract(monkeypatch):
monkeypatch.setitem(
sys.modules, "PyNvVideoCodec", _fake_pynv(num_frames=10, h=4, w=6)
Expand Down
18 changes: 18 additions & 0 deletions container/templates/sglang_runtime.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,29 @@ RUN --mount=type=bind,source=./container/deps/requirements.common.txt,target=/tm
# Install SGLang-specific runtime dependencies without changing the upstream
# dependency solution. imageio-ffmpeg is installed from source (no bundled
# binary) for the VP9 video-encode path; see requirements.sglang.txt.
{% if device == "cuda" %}
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 && \
pip install --break-system-packages --force-reinstall --no-deps \
--requirement /tmp/requirements.sglang.txt
{% else %}
# PyNvVideoCodec decodes on NVDEC through libnvcuvid, so it is inert on a
# non-NVIDIA device. Drop it from the shared requirements rather than ship an
# unusable NVIDIA codec wheel in the Intel XPU image. The pattern is anchored
# to the line start so it cannot match inside another requirement, and the
# 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

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)".

Comment on lines +153 to +163

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.

{% endif %}

# Remove the codec-bearing video-DECODE components from the upstream SGLang image
# (PyAV, decord, OpenCV, torchcodec + any base ffmpeg/libav*), then copy the
Expand Down
Loading