Repository navigation
fix(vllm): ship codec-free OpenCV for image inputs #14361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yunzhoul-nv
merged 39 commits into
ai-dynamo:main
from
glamr-agent:dyn-4286-vllm-runtime-opencv-dependency-660d4b58128b
Sep 16, 2026
Merged
Changes from 1 commit
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 3de7a67
fix(vllm): import cv2 in the preflight and honour a zero image limit
glamr-agent 4d51a9e
test(vllm): scope the fake cv2 to tmp_path and clarify the installer'…
glamr-agent b10c487
fix(vllm): ask the registry which modalities the model supports
glamr-agent a6919a6
fix(vllm): ship a codec-free OpenCV in the vLLM runtime image
glamr-agent 0012284
fix(vllm): scope codec-free OpenCV guidance
glamr-agent c10a95f
merge: sync main into dyn-4286 vllm runtime opencv dependency
glamr-agent 22d2373
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent 7d000d6
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent c3b7bb5
fix(vllm): report backendless OpenCV video failures
glamr-agent d845460
style(vllm): format decoder backend guard
glamr-agent 5bd0c6b
test(vllm): use package codec errors import
glamr-agent 2cbfb5d
test(vllm): satisfy module import lint
glamr-agent ad7a975
style(vllm): order multimodal test imports
glamr-agent 860bc83
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent 4818893
merge: sync main into dyn-4286-vllm-runtime-opencv-dependency-660d4b5…
glamr-agent dfd0d91
Merge remote-tracking branch 'upstream/main' into dyn-4286-vllm-runti…
glamr-agent 3667279
fix(vllm): detect OpenCV video backends case-insensitively
glamr-agent ea8d506
docs(vllm): clarify media decoder installer scope
glamr-agent dda3fcd
merge: update from main
glamr-agent b231a35
style(multimodal): apply black to video_loader
glamr-agent 9a15ef3
Merge branch 'main' into dyn-4286-vllm-runtime-opencv-dependency-660d…
yunzhoul-nv d69495b
fix(vllm): preserve source-built OpenCV version hint
glamr-agent b77c09b
Merge branch 'main' of https://github.com/ai-dynamo/dynamo into dyn-4…
glamr-agent c862393
Merge branch 'main' of https://github.com/ai-dynamo/dynamo into dyn-4…
glamr-agent 9ec9d77
fix(vllm): satisfy black on the OpenCV version hint
glamr-agent 1950f3d
fix(multimodal): match the OpenCV wheel by version prefix
glamr-agent 623d1d9
fix(multimodal): import the decode carrier before classifying it
glamr-agent ece20cc
docs(multimodal): fall back when OpenCV has no distribution metadata
glamr-agent 221759c
fix(vllm): replace broken decoder carriers
glamr-agent d70149e
fix(multimodal): defer fixture import until after collection
glamr-agent fcd2ee2
fix(vllm): harden codec-free OpenCV build guards
glamr-agent 64014c7
Merge branch 'main' into dyn-4286-vllm-runtime-opencv-dependency-660d…
yunzhoul-nv e927b6f
fix(vllm): restore software video decoder installation
glamr-agent d15043a
fix(vllm): align decoder diagnostics and device policy
glamr-agent da34f7a
docs(multimodal): fall back to the bounded OpenCV spec
glamr-agent c7e3980
fix(multimodal): keep OpenCV diagnostics usable when cv2 is broken
glamr-agent be60c13
test(multimodal): assert the wheel-only install hint for a broken cv2
glamr-agent 8345843
fix(multimodal): preserve guidance for broken OpenCV imports
glamr-agent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
components/src/dynamo/vllm/tests/test_vllm_mistral_image_preflight.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Startup preflight for image input on the Mistral tokenizer path. | ||
|
|
||
| The runtime images deliberately ship no OpenCV, but ``mistral_common`` resizes | ||
| every still image with ``cv2``. Left alone, that gap surfaces from inside | ||
| vLLM's multimodal profiling run, after the weights have loaded, as an upstream | ||
| ``ImportError`` pointing at an install these images do not support. | ||
| """ | ||
|
|
||
| import dataclasses | ||
| import importlib | ||
| import importlib.machinery | ||
| import importlib.util | ||
| import sys | ||
| from types import ModuleType, SimpleNamespace | ||
|
|
||
| import pytest | ||
|
|
||
| from dynamo.common.multimodal.codec_errors import MissingMediaDecoderError | ||
| from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS | ||
|
|
||
| pytestmark = [ | ||
| pytest.mark.unit, | ||
| pytest.mark.vllm, | ||
| pytest.mark.core, | ||
| pytest.mark.gpu_0, | ||
| pytest.mark.xpu_1, | ||
| pytest.mark.profiled_vram_gib(0), | ||
| pytest.mark.timeout(180), | ||
| pytest.mark.pre_merge, | ||
| ] | ||
|
|
||
|
|
||
| def _load_vllm_main() -> ModuleType: | ||
| """Load the entrypoint lazily, as the other vLLM unit tests do. | ||
|
|
||
| ``dynamo.vllm.main`` imports uvloop at module scope, which the lightweight | ||
| pre-commit collection environment omits. | ||
| """ | ||
| return importlib.import_module("dynamo.vllm.main") | ||
|
|
||
|
|
||
| def _vllm_config(*, multimodal: bool, tokenizer_mode: str) -> SimpleNamespace: | ||
| return SimpleNamespace( | ||
| model_config=SimpleNamespace( | ||
| is_multimodal_model=multimodal, tokenizer_mode=tokenizer_mode | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def _pin_cv2(monkeypatch, *, present: bool) -> None: | ||
| """Pin what ``find_spec("cv2")`` reports, in both directions. | ||
|
|
||
| ``importlib.util.find_spec`` consults ``sys.modules`` first: a ``None`` | ||
| entry makes it return ``None``, and a module carrying a spec makes it | ||
| return that spec. Pinning both directions keeps the test meaningful on a | ||
| host that has opted into the documented install -- otherwise the failing | ||
| branch would never run there. | ||
| """ | ||
| if present: | ||
| stub = ModuleType("cv2") | ||
| stub.__spec__ = importlib.machinery.ModuleSpec("cv2", loader=None) | ||
| monkeypatch.setitem(sys.modules, "cv2", stub) | ||
| else: | ||
| monkeypatch.setitem(sys.modules, "cv2", None) | ||
|
|
||
|
|
||
| def test_mistral_multimodal_without_cv2_fails_before_the_engine_starts(monkeypatch): | ||
| vllm_main = _load_vllm_main() | ||
| _pin_cv2(monkeypatch, present=False) | ||
|
|
||
| with pytest.raises(MissingMediaDecoderError) as excinfo: | ||
| vllm_main.check_mistral_image_decoder( | ||
| _vllm_config(multimodal=True, tokenizer_mode="mistral") | ||
| ) | ||
|
|
||
| msg = str(excinfo.value) | ||
| assert "cv2" in msg | ||
| # The bounded spec and the installer command are what make the failure | ||
|
glamr-agent marked this conversation as resolved.
Resolved
|
||
| # actionable; upstream's own suggestion is the wrong one for these images. | ||
| assert VALIDATED_SPECS["opencv-python-headless"] in msg | ||
| assert "install_media_decoders vllm" in msg | ||
| assert "mistral-common[opencv]" not in msg | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "multimodal, tokenizer_mode", | ||
| [ | ||
| # Text-only Mistral models tokenize no images. | ||
| (False, "mistral"), | ||
| # The Hugging Face processor path imports no OpenCV, so a multimodal | ||
| # model on it must still start without the install. | ||
| (True, "auto"), | ||
| (False, "auto"), | ||
|
glamr-agent marked this conversation as resolved.
Resolved
|
||
| ], | ||
| ) | ||
| def test_preflight_stays_silent_off_the_mistral_image_path( | ||
| monkeypatch, multimodal, tokenizer_mode | ||
| ): | ||
| vllm_main = _load_vllm_main() | ||
| _pin_cv2(monkeypatch, present=False) | ||
|
|
||
| assert ( | ||
| vllm_main.check_mistral_image_decoder( | ||
| _vllm_config(multimodal=multimodal, tokenizer_mode=tokenizer_mode) | ||
| ) | ||
| is None | ||
| ) | ||
|
|
||
|
|
||
| def test_preflight_passes_once_the_decoder_is_installed(monkeypatch): | ||
| """The documented remedy has to clear the check, or a worker could never | ||
|
glamr-agent marked this conversation as resolved.
Resolved
|
||
| start after running it.""" | ||
| vllm_main = _load_vllm_main() | ||
| _pin_cv2(monkeypatch, present=True) | ||
|
|
||
| assert ( | ||
| vllm_main.check_mistral_image_decoder( | ||
| _vllm_config(multimodal=True, tokenizer_mode="mistral") | ||
| ) | ||
| is None | ||
| ) | ||
|
|
||
|
|
||
| def test_preflight_tolerates_a_config_without_the_attributes(monkeypatch): | ||
| """A guard on the startup path must not itself break startup. | ||
|
|
||
| The preflight reads both attributes defensively, so a config object that | ||
| predates them -- or a vLLM release that moves them -- degrades to "do not | ||
| check" rather than raising AttributeError on every worker. | ||
| """ | ||
| vllm_main = _load_vllm_main() | ||
| _pin_cv2(monkeypatch, present=False) | ||
|
|
||
| assert ( | ||
| vllm_main.check_mistral_image_decoder( | ||
| SimpleNamespace(model_config=SimpleNamespace()) | ||
| ) | ||
| is None | ||
| ) | ||
|
|
||
|
|
||
| def test_vllm_model_config_still_exposes_the_attributes_the_preflight_reads(): | ||
| """The loud half of that defensiveness. | ||
|
|
||
| Reading through ``getattr`` means a rename upstream would silently disable | ||
| the check. Pin the two names here so the rename fails CI instead. | ||
| """ | ||
| model_config_cls = pytest.importorskip("vllm.config").ModelConfig | ||
|
|
||
| assert hasattr(model_config_cls, "is_multimodal_model") | ||
| assert "tokenizer_mode" in { | ||
| field.name for field in dataclasses.fields(model_config_cls) | ||
| } | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| importlib.util.find_spec("cv2") is not None, | ||
| reason="cv2 is installed here, so mistral_common's own guard cannot fire", | ||
| ) | ||
| def test_upstream_still_image_encode_needs_cv2(): | ||
| """Ground the preflight against upstream rather than against a mock. | ||
|
|
||
| ``mistral_common.tokens.tokenizers.image.transform_image`` is the single | ||
| site that asserts OpenCV is present, and it is reached for a plain still | ||
| image with no video anywhere. If upstream ever stops needing OpenCV here, | ||
| this test fails and the preflight can go. | ||
| """ | ||
| pil_image = pytest.importorskip("PIL.Image") | ||
| image_tokenizer = pytest.importorskip("mistral_common.tokens.tokenizers.image") | ||
|
|
||
| with pytest.raises(ImportError) as excinfo: | ||
| image_tokenizer.transform_image(pil_image.new("RGB", (64, 64), "red"), (64, 64)) | ||
|
|
||
| assert "opencv" in str(excinfo.value).lower() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.