Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
fc8efdb
fix(vllm): fail fast when Mistral-tokenizer image input has no OpenCV
glamr-agent Sep 4, 2026
3de7a67
fix(vllm): import cv2 in the preflight and honour a zero image limit
glamr-agent Sep 5, 2026
4d51a9e
test(vllm): scope the fake cv2 to tmp_path and clarify the installer'…
glamr-agent Sep 5, 2026
b10c487
fix(vllm): ask the registry which modalities the model supports
glamr-agent Sep 5, 2026
a6919a6
fix(vllm): ship a codec-free OpenCV in the vLLM runtime image
glamr-agent Sep 11, 2026
0012284
fix(vllm): scope codec-free OpenCV guidance
glamr-agent Sep 11, 2026
c10a95f
merge: sync main into dyn-4286 vllm runtime opencv dependency
glamr-agent Sep 14, 2026
22d2373
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent Sep 14, 2026
7d000d6
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent Sep 14, 2026
c3b7bb5
fix(vllm): report backendless OpenCV video failures
glamr-agent Sep 14, 2026
d845460
style(vllm): format decoder backend guard
glamr-agent Sep 14, 2026
5bd0c6b
test(vllm): use package codec errors import
glamr-agent Sep 14, 2026
2cbfb5d
test(vllm): satisfy module import lint
glamr-agent Sep 14, 2026
ad7a975
style(vllm): order multimodal test imports
glamr-agent Sep 14, 2026
860bc83
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent Sep 14, 2026
4818893
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent Sep 14, 2026
dfd0d91
Merge remote-tracking branch 'upstream/main' into dyn-4286-vllm-runti…
glamr-agent Sep 14, 2026
3667279
fix(vllm): detect OpenCV video backends case-insensitively
glamr-agent Sep 14, 2026
ea8d506
docs(vllm): clarify media decoder installer scope
glamr-agent Sep 14, 2026
dda3fcd
merge: update from main
glamr-agent Sep 14, 2026
b231a35
style(multimodal): apply black to video_loader
glamr-agent Sep 14, 2026
9a15ef3
Merge branch 'main' into dyn-4286-vllm-runtime-opencv-dependency-660d…
yunzhoul-nv Sep 14, 2026
d69495b
fix(vllm): preserve source-built OpenCV version hint
glamr-agent Sep 14, 2026
b77c09b
Merge branch 'main' of https://github.com/ai-dynamo/dynamo into dyn-4…
glamr-agent Sep 14, 2026
c862393
Merge branch 'main' of https://github.com/ai-dynamo/dynamo into dyn-4…
glamr-agent Sep 14, 2026
9ec9d77
fix(vllm): satisfy black on the OpenCV version hint
glamr-agent Sep 14, 2026
1950f3d
fix(multimodal): match the OpenCV wheel by version prefix
glamr-agent Sep 14, 2026
623d1d9
fix(multimodal): import the decode carrier before classifying it
glamr-agent Sep 14, 2026
ece20cc
docs(multimodal): fall back when OpenCV has no distribution metadata
glamr-agent Sep 14, 2026
221759c
fix(vllm): replace broken decoder carriers
glamr-agent Sep 14, 2026
d70149e
fix(multimodal): defer fixture import until after collection
glamr-agent Sep 15, 2026
fcd2ee2
fix(vllm): harden codec-free OpenCV build guards
glamr-agent Sep 15, 2026
64014c7
Merge branch 'main' into dyn-4286-vllm-runtime-opencv-dependency-660d…
yunzhoul-nv Sep 15, 2026
e927b6f
fix(vllm): restore software video decoder installation
glamr-agent Sep 15, 2026
d15043a
fix(vllm): align decoder diagnostics and device policy
glamr-agent Sep 15, 2026
da34f7a
docs(multimodal): fall back to the bounded OpenCV spec
glamr-agent Sep 15, 2026
c7e3980
fix(multimodal): keep OpenCV diagnostics usable when cv2 is broken
glamr-agent Sep 15, 2026
be60c13
test(multimodal): assert the wheel-only install hint for a broken cv2
glamr-agent Sep 15, 2026
8345843
fix(multimodal): preserve guidance for broken OpenCV imports
glamr-agent Sep 16, 2026
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
84 changes: 77 additions & 7 deletions components/src/dynamo/common/multimodal/codec_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@

from __future__ import annotations

import importlib
import importlib.metadata

from dynamo.common.multimodal.nvdec_decoder import HW_ROUTED_CODECS, nvdec_available
from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS
from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS, installer_covers

INSTALLER_CMD = "python -m dynamo.common.utils.install_media_decoders"

Expand All @@ -35,14 +38,71 @@ class MissingMediaDecoderError(RuntimeError):
"""


def _install_hint(backend: str, package: str) -> str:
spec = VALIDATED_SPECS[package]
def _carrier_present(module: str) -> bool:
"""Return whether the carrier imports; keep import errors out of diagnostics."""
try:
importlib.import_module(module)
except Exception: # noqa: BLE001 - any import failure means unusable here
return False
return True


def _needs_binary_wheel(backend: str, package: str) -> bool:
"""Whether this package must come from a wheel: vLLM's OpenCV sdist has no codecs."""
return backend == "vllm" and package == "opencv-python-headless"


def _is_vllm_source_built_cv2(backend: str, package: str, module: str) -> bool:
"""Whether this is the codec-free OpenCV build shipped by vLLM."""
return (
f"install the validated decoder with `pip install --no-deps '{spec}'` "
f"(or `{INSTALLER_CMD} {backend}`)"
_needs_binary_wheel(backend, package)
and module == "cv2"
and _carrier_present(module)
)


def _installed_version(package: str) -> str | None:
"""The installed distribution version, or None when it has no metadata."""
try:
return importlib.metadata.version(package)
except importlib.metadata.PackageNotFoundError:
return None


def _install_hint(backend: str, package: str, module: str) -> str:
"""Use the installed OpenCV version for vLLM replacements, validated bounds otherwise."""
if _is_vllm_source_built_cv2(backend, package, module):
installed = _installed_version(package)
if installed:
hint = (
"replace the shipped build with the binary wheel of the same "
f"version: `pip install --no-deps --force-reinstall "
f"--only-binary {package} '{package}=={installed}'`"
)
else:
# cv2 omits the distribution packaging revision.
hint = (
"first determine the shipped cv2 version with "
'`python -c "import cv2; print(cv2.__version__)"`, then replace '
"it with the binary wheel built from that same version -- the "
"distribution adds a packaging revision, so match on the prefix: "
f"`pip install --no-deps --force-reinstall --only-binary {package} "
f"'{package}==<cv2-version>.*'`"
)
else:
only_binary = (
f"--only-binary {package} " if _needs_binary_wheel(backend, package) else ""
)
hint = (
"install the validated decoder with "
f"`pip install --no-deps --force-reinstall "
f"{only_binary}'{VALIDATED_SPECS[package]}'`"
)
if installer_covers(backend, package):
hint += f" (or `{INSTALLER_CMD} {backend}`)"
return hint


def _with_cause(message: str, cause: str | None) -> str:
"""Append the underlying decoder text so diagnostics survive the wrap.

Expand Down Expand Up @@ -79,6 +139,13 @@ def video_decoder_missing(
"but NVDEC is unavailable in this container. Grant the 'video' "
"driver capability (NVIDIA_DRIVER_CAPABILITIES) to enable it, or "
)
elif _is_vllm_source_built_cv2(backend, package, module):
lead = (
f"this video ({codec_desc}) has no decoder in this image: shipped "
"images decode only H.264/H.265 (in hardware, via NVDEC), and the "
f"'{module}' they ship is built without a video backend. "
"Re-encode the input to H.264/H.265, or "
)
else:
lead = (
f"this video ({codec_desc}) has no decoder in this image: shipped "
Expand All @@ -88,7 +155,10 @@ def video_decoder_missing(
)
return MissingMediaDecoderError(
_with_cause(
"Cannot decode video: " + lead + _install_hint(backend, package) + ".",
"Cannot decode video: "
+ lead
+ _install_hint(backend, package, module)
+ ".",
cause,
)
)
Expand All @@ -106,7 +176,7 @@ def audio_decoder_missing(
_with_cause(
"Cannot decode audio: this input needs the PyAV decoder ('av'), which "
"this image deliberately does not ship, and NVDEC does not decode "
"audio. To enable audio input, " + _install_hint(backend, "av") + ".",
"audio. To enable audio input, " + _install_hint(backend, "av", "av") + ".",
cause,
)
)
52 changes: 45 additions & 7 deletions components/src/dynamo/common/multimodal/video_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
# limitations under the License.

import asyncio
import functools
import logging
import os
import re
from typing import Any, Awaitable, Dict, Final, List
from urllib.parse import urlparse

Expand Down Expand Up @@ -45,6 +47,34 @@
logger = logging.getLogger(__name__)


def _attributable_to_cv2(exc: BaseException, media_io: Any | None = None) -> bool:
"""Identify OpenCV errors without relabeling failures from other video backends."""
if getattr(exc, "name", None) == "cv2" or "cv2" in str(exc):
return True
return (
isinstance(exc, ValueError)
and str(exc) == "Could not open video stream"
and (getattr(media_io, "kwargs", None) or {}).get("backend", "opencv")
== "opencv"
)


@functools.lru_cache(maxsize=1)
def _cv2_lacks_video_backend() -> bool:
"""Return whether OpenCV is broken or lacks FFmpeg and GStreamer."""
try:
import cv2
except ImportError:
return False
except Exception: # noqa: BLE001 - a broken import has no usable video backend
return True
build_info = cv2.getBuildInformation()
return not any(
re.search(rf"^\s*{backend}:\s*YES", build_info, re.MULTILINE | re.IGNORECASE)
for backend in ("FFMPEG", "GSTREAMER")
)


URL_VARIANT_KEY: Final = "Url"
DECODED_VARIANT_KEY: Final = "Decoded"

Expand Down Expand Up @@ -165,13 +195,8 @@ async def _decode_video_bytes(
) -> tuple[np.ndarray, Dict[str, Any]]:
"""Decode video bytes: H.264/H.265 on NVDEC, all else software.

The runtime images purge the software decode wheels (opencv/av/decord/
torchcodec) for codec compliance, so the software fallback only
resolves where a decoder was installed separately. When it is absent,
vLLM's lazy import surfaces a bare ``No module named 'cv2'`` with no
codec and no remedy -- convert that into the actionable
unsupported-codec error, which can name the codec because the probe
already ran here.
vLLM ships OpenCV for still images without video backends. Translate
its decode failures into install guidance; preserve other backends.

The NVDEC path honors the two ``media_io_kwargs`` that decide *which*
frames come back -- ``num_frames`` and ``fps`` -- because it samples
Expand All @@ -198,9 +223,22 @@ async def _decode_video_bytes(
try:
return await asyncio.to_thread(media_io.load_bytes, content)
except ImportError as exc:
if not _attributable_to_cv2(exc):
raise
raise video_decoder_missing(
"vllm", "opencv-python-headless", "cv2", codec, cause=str(exc)
) from exc
except (OSError, SystemError, ValueError) as exc:
# Confirm both the error source and missing video support.
if not (_attributable_to_cv2(exc, media_io) and _cv2_lacks_video_backend()):
raise
raise video_decoder_missing(
"vllm",
"opencv-python-headless",
"cv2",
codec,
cause=str(exc),
) from exc
Comment thread
glamr-agent marked this conversation as resolved.

def _extract_nvdec_args(self, media_io: Any) -> dict[str, Any]:
num_frames = getattr(media_io, "num_frames", self._num_frames)
Expand Down
31 changes: 31 additions & 0 deletions components/src/dynamo/common/tests/multimodal/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@

import importlib

import pytest
import pytest_asyncio

from dynamo.common.http import close_http_client

# The decode carriers ``codec_errors`` probes. Only these names are answered by
# the ``carrier_imports`` fixture; every other import runs for real.
_MEDIA_CARRIERS = ("cv2", "av", "decord")

# Cached results of probing optional deps used by multimodal unit tests.
# `None` = not attempted, `True` = importable, `False` = raised.
_torch_importable: bool | None = None
Expand Down Expand Up @@ -70,6 +75,32 @@ def pytest_ignore_collect(collection_path, config) -> bool | None:
return None


@pytest.fixture
def carrier_imports(monkeypatch):
"""Mock media-carrier imports while leaving unrelated imports intact."""
# Import after collection's optional-dependency guard: the multimodal
# package imports torch, which CPU-only runtime images may not ship.
from dynamo.common.multimodal import codec_errors

def _set(
present: tuple[str, ...] = (), error: str | BaseException | None = None
) -> None:
real = importlib.import_module

def _fake(name: str, *args, **kwargs):
if name in _MEDIA_CARRIERS:
if name in present:
return object()
if isinstance(error, BaseException):
raise error
raise ImportError(error or f"No module named '{name}'")
Comment thread
glamr-agent marked this conversation as resolved.
return real(name, *args, **kwargs)

monkeypatch.setattr(codec_errors.importlib, "import_module", _fake)

return _set


@pytest_asyncio.fixture(autouse=True)
async def _close_shared_http_client():
yield
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import numpy as np
import pytest

import dynamo.common.multimodal.audio_loader as audio_loader_module
from dynamo.common.http import HttpStatusError
from dynamo.common.http.url_validator import UrlValidationError, UrlValidationPolicy
from dynamo.common.multimodal import audio_loader as audio_loader_module
from dynamo.common.multimodal.audio_loader import AudioLoader
from dynamo.common.multimodal.codec_errors import MissingMediaDecoderError
from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS
Expand Down Expand Up @@ -187,11 +187,14 @@ async def test_load_audio_batch_reads_decoded_variant(monkeypatch):


@pytest.mark.asyncio
async def test_load_audio_missing_decoder_is_actionable():
async def test_load_audio_missing_decoder_is_actionable(carrier_imports):
"""vLLM's own hint here is `pip install vllm[audio]`, which drags in an
unpinned stack; the wrap must point at the validated bounded install and
say there is no hardware alternative for audio."""
loader = AudioLoader()
# 'av' genuinely absent, independent of what the test machine has
# installed: _install_hint asks the running interpreter.
carrier_imports()
loader._load_audio_with_vllm = AsyncMock( # type: ignore[method-assign]
side_effect=ImportError("Please install vllm[audio] for audio support")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

import dynamo.common.multimodal.codec_errors as codec_errors
from dynamo.common.multimodal import codec_errors
from dynamo.common.multimodal.codec_errors import (
MissingMediaDecoderError,
audio_decoder_missing,
Expand All @@ -16,7 +16,13 @@
pytestmark = [pytest.mark.unit, pytest.mark.pre_merge, pytest.mark.gpu_0]


def test_video_message_names_codec_spec_and_installer(monkeypatch):
@pytest.fixture(autouse=True)
def _carrier_absent(carrier_imports):
"""Make missing-carrier messages independent of installed packages."""
carrier_imports()


def test_video_message_names_codec_and_spec(monkeypatch):
monkeypatch.setattr(codec_errors, "nvdec_available", lambda: True)
err = video_decoder_missing("vllm", "opencv-python-headless", "cv2", "vp9")

Expand All @@ -32,6 +38,61 @@ def test_video_message_names_codec_spec_and_installer(monkeypatch):
assert "H.264/H.265" in msg


def test_present_but_unusable_carrier_pins_the_installed_version(
monkeypatch, carrier_imports
):
"""Replacing codec-free OpenCV must preserve the installed version."""
carrier_imports(present=("cv2",))
monkeypatch.setattr(
codec_errors.importlib.metadata, "version", lambda p: "5.0.0.93"
)
msg = str(video_decoder_missing("vllm", "opencv-python-headless", "cv2", "vp9"))
assert "--force-reinstall" in msg
assert "--only-binary opencv-python-headless" in msg
assert "opencv-python-headless==5.0.0.93" in msg
assert VALIDATED_SPECS["opencv-python-headless"] not in msg


def test_present_carrier_without_metadata_requests_its_version(
monkeypatch, carrier_imports
):
"""Without metadata, match the library version plus its packaging revision."""
carrier_imports(present=("cv2",))

def _missing(_package):
raise codec_errors.importlib.metadata.PackageNotFoundError

monkeypatch.setattr(codec_errors.importlib.metadata, "version", _missing)
msg = str(video_decoder_missing("vllm", "opencv-python-headless", "cv2", "vp9"))
assert "print(cv2.__version__)" in msg
assert "opencv-python-headless==<cv2-version>.*" in msg
assert VALIDATED_SPECS["opencv-python-headless"] not in msg
assert "--force-reinstall" in msg


def test_broken_carrier_is_not_treated_as_the_source_build(carrier_imports):
"""Broken imports need a reinstall, not a missing-video-backend diagnosis."""
carrier_imports(error=OSError("libGL.so.1: cannot open shared object file"))

msg = str(
video_decoder_missing(
"vllm",
"opencv-python-headless",
"cv2",
"vp9",
cause="libGL.so.1: cannot open shared object file",
)
)
assert "built without a video backend" not in msg
assert VALIDATED_SPECS["opencv-python-headless"] in msg
assert "--force-reinstall" in msg
# Without this, pip may rebuild the codec-free sdist the operator is
# trying to replace.
assert "--only-binary opencv-python-headless" in msg
# The carrier's own words are what identify the real fault.
assert "libGL.so.1" in msg


def test_hw_codec_without_nvdec_points_at_driver_capability(monkeypatch):
"""H.264 with NVDEC unavailable is a capability problem first.

Expand Down
Loading
Loading