Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
99 changes: 98 additions & 1 deletion tests/multimodal/media/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
video_to_pil_images_list,
)
from vllm.multimodal.media import ImageMediaIO, VideoMediaIO
from vllm.multimodal.video import VIDEO_LOADER_REGISTRY, VideoLoader
from vllm.multimodal.video import (
GPU_VIDEO_BACKENDS,
PYNVVIDEOCODEC_VIDEO_BACKEND,
VIDEO_LOADER_REGISTRY,
VideoLoader,
)

from ..utils import cosine_similarity, create_video_from_image, normalize_image

Expand Down Expand Up @@ -357,3 +362,95 @@ def test_load_base64_jpeg_raises_on_zero_num_frames():

with pytest.raises(ValueError, match="num_frames must be greater than 0 or -1"):
videoio.load_base64("video/jpeg", data)


# ---------------------------------------------------------------------------
# GPU video backend policy tests
# ---------------------------------------------------------------------------


class TestMergeKwargsGpuBackendPolicy:
"""Verify that merge_kwargs blocks request-level GPU backend selection
when the static (engine-level) config did not configure that backend."""

def test_pynvvideocodec_in_gpu_set(self):
assert PYNVVIDEOCODEC_VIDEO_BACKEND in GPU_VIDEO_BACKENDS

def test_strips_video_backend_pynv_when_not_static(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs=None,
runtime_kwargs={"video_backend": "pynvvideocodec"},
)
assert "video_backend" not in result

def test_strips_backend_pynv_when_not_static(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs={"num_frames": 16},
runtime_kwargs={"backend": "pynvvideocodec"},
)
assert result.get("backend") != "pynvvideocodec"

def test_preserves_video_backend_pynv_when_static(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs={"video_backend": "pynvvideocodec"},
runtime_kwargs={"video_backend": "pynvvideocodec", "num_frames": 8},
)
assert result["video_backend"] == "pynvvideocodec"
assert result["num_frames"] == 8

def test_preserves_backend_pynv_when_static(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs={"backend": "pynvvideocodec"},
runtime_kwargs={"backend": "pynvvideocodec"},
)
assert result["backend"] == "pynvvideocodec"

@pytest.mark.parametrize("backend", ["opencv", "pyav", "torchcodec"])
def test_software_video_backend_passes_through(self, backend: str):
result = VideoMediaIO.merge_kwargs(
default_kwargs=None,
runtime_kwargs={"video_backend": backend},
)
assert result["video_backend"] == backend

@pytest.mark.parametrize("backend", ["opencv", "pyav"])
def test_software_codec_backend_passes_through(self, backend: str):
result = VideoMediaIO.merge_kwargs(
default_kwargs=None,
runtime_kwargs={"backend": backend},
)
assert result["backend"] == backend

def test_strips_both_keys_independently(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs=None,
runtime_kwargs={
"video_backend": "pynvvideocodec",
"backend": "pynvvideocodec",
"num_frames": 4,
},
)
assert "video_backend" not in result
assert result.get("backend") != "pynvvideocodec"
assert result["num_frames"] == 4

def test_other_kwargs_preserved_when_gpu_backend_stripped(self):
result = VideoMediaIO.merge_kwargs(
default_kwargs={"fps": 2},
runtime_kwargs={
"video_backend": "pynvvideocodec",
"num_frames": 16,
},
)
assert "video_backend" not in result
assert result["num_frames"] == 16

def test_static_pynv_with_different_runtime_gpu_backend(self):
"""If static sets pynv via video_backend but runtime tries to set it
via the codec-level 'backend' key (without a static match), strip it."""
result = VideoMediaIO.merge_kwargs(
default_kwargs={"video_backend": "pynvvideocodec"},
runtime_kwargs={"backend": "pynvvideocodec"},
)
assert result.get("backend") != "pynvvideocodec"
assert result["video_backend"] == "pynvvideocodec"
23 changes: 22 additions & 1 deletion vllm/multimodal/media/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
from PIL import Image

from vllm import envs
from vllm.logger import init_logger

from ..video import VIDEO_LOADER_REGISTRY
from ..video import GPU_VIDEO_BACKENDS, VIDEO_LOADER_REGISTRY
from .base import MediaIO
from .image import ImageMediaIO

logger = init_logger(__name__)


class VideoMediaIO(MediaIO[tuple[npt.NDArray, dict[str, Any]]]):
"""Configuration values can be user-provided either by --media-io-kwargs or
Expand All @@ -28,6 +31,24 @@ def merge_kwargs(
default_kwargs: dict[str, Any] | None,
runtime_kwargs: dict[str, Any] | None,
) -> dict[str, Any]:
if runtime_kwargs:
# Block request-level selection of GPU video backends that
# were not configured (and VRAM-reserved) at startup.
for key in ("video_backend", "backend"):
requested = runtime_kwargs.get(key)
if requested and requested in GPU_VIDEO_BACKENDS:
static_val = (default_kwargs or {}).get(key)
if static_val != requested:
logger.warning(
Comment thread
jperezdealgaba marked this conversation as resolved.
Outdated
"Stripping request-level %s=%r: GPU video "
"backend not configured at startup.",
key,
requested,
)
runtime_kwargs = {
k: v for k, v in runtime_kwargs.items() if k != key
}

merged = super().merge_kwargs(default_kwargs, runtime_kwargs)
# fps and num_frames interact with each other, so if either is
# overridden at request time, wipe the other from defaults to
Expand Down
7 changes: 7 additions & 0 deletions vllm/multimodal/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ def create_hf_metadata(
# PyNvVideoCodec 2.0.4 on H100.
PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES = int(1.8 * 1024 * MiB_bytes)

# Video backends that allocate GPU VRAM (decoder surfaces, CUDA context)
# and therefore require startup-time memory reservation via
# _reserve_mm_ipc_gpu_memory. Request-level media_io_kwargs must not be
# allowed to select these backends unless the static config already
# reserved the corresponding budget.
GPU_VIDEO_BACKENDS: frozenset[str] = frozenset({PYNVVIDEOCODEC_VIDEO_BACKEND})
Comment thread
DarkLight1337 marked this conversation as resolved.
Outdated


class PyNvVideoCodecDecoderSlot:
"""A retained PyNv decoder slot and its CUDA stream.
Expand Down
Loading