[Bugfix][Frontend] Return 400 for invalid PyNvVideoCodec video input (#51120)
Signed-off-by: Dat Mai <dmai@nvidia.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
@@ -19,6 +21,7 @@ from vllm.multimodal.media import ImageMediaIO, VideoMediaIO
|
||||
from vllm.multimodal.video import (
|
||||
PYNVVIDEOCODEC_VIDEO_BACKEND,
|
||||
VIDEO_LOADER_REGISTRY,
|
||||
PyNvVideoCodecVideoBackend,
|
||||
VideoLoader,
|
||||
_pynvvc_frames_to_nhwc,
|
||||
)
|
||||
@@ -364,6 +367,31 @@ def test_load_base64_jpeg_raises_on_zero_num_frames():
|
||||
videoio.load_base64("video/jpeg", data)
|
||||
|
||||
|
||||
def test_pynvvideocodec_unrelated_error_propagates(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
class FakePyNvVCException(Exception):
|
||||
pass
|
||||
|
||||
fake_nvc = SimpleNamespace(PyNvVCException=FakePyNvVCException)
|
||||
monkeypatch.setitem(sys.modules, "PyNvVideoCodec", fake_nvc)
|
||||
original_error = RuntimeError("GPU decoder unavailable")
|
||||
|
||||
def raise_unrelated_error(cls, file_path, nvc):
|
||||
raise original_error
|
||||
|
||||
monkeypatch.setattr(
|
||||
PyNvVideoCodecVideoBackend,
|
||||
"_read_source_metadata",
|
||||
classmethod(raise_unrelated_error),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
PyNvVideoCodecVideoBackend.decode_frames_pynvvideocodec(b"video", None)
|
||||
|
||||
assert exc_info.value is original_error
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GPU video backend policy tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -200,6 +200,60 @@ def test_pynvvideocodec_codec_uses_dynamic_sampling_strategy(
|
||||
assert metadata["frames_indices"] == [0, 9]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="Requires CUDA")
|
||||
def test_pynvvideocodec_corrupted_videos_raise_value_error():
|
||||
valid_video = create_long_gop_video(num_frames=2, width=64, height=64)
|
||||
corrupted_video = (ASSETS_DIR / "corrupted.mp4").read_bytes()
|
||||
malformed_video = corrupted_video[:128]
|
||||
|
||||
old_slots = PyNvVideoCodecVideoBackend._decoder_slots
|
||||
old_active_slots = PyNvVideoCodecVideoBackend._active_decoder_slots
|
||||
old_cond = PyNvVideoCodecVideoBackend._decoder_slot_cond
|
||||
old_max_slots = PyNvVideoCodecVideoBackend._max_decoder_slots
|
||||
try:
|
||||
PyNvVideoCodecVideoBackend._decoder_slots = []
|
||||
PyNvVideoCodecVideoBackend._active_decoder_slots = 0
|
||||
PyNvVideoCodecVideoBackend._decoder_slot_cond = threading.Condition()
|
||||
PyNvVideoCodecVideoBackend._max_decoder_slots = None
|
||||
|
||||
loader = VIDEO_LOADER_REGISTRY.load(PYNVVIDEOCODEC_VIDEO_BACKEND)
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"^Invalid or unsupported video file\.$",
|
||||
) as malformed_exc:
|
||||
loader.load_bytes(
|
||||
malformed_video,
|
||||
num_frames=1,
|
||||
hw_decoders=1,
|
||||
)
|
||||
|
||||
assert malformed_exc.value.__cause__ is not None
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"^Invalid or unsupported video file\.$",
|
||||
) as exc_info:
|
||||
loader.load_bytes(
|
||||
corrupted_video,
|
||||
num_frames=-1,
|
||||
hw_decoders=1,
|
||||
)
|
||||
|
||||
assert exc_info.value.__cause__ is not None
|
||||
|
||||
frames, _ = loader.load_bytes(
|
||||
valid_video,
|
||||
num_frames=1,
|
||||
hw_decoders=1,
|
||||
)
|
||||
assert frames.shape[0] == 1
|
||||
finally:
|
||||
PyNvVideoCodecVideoBackend._decoder_slots = old_slots
|
||||
PyNvVideoCodecVideoBackend._active_decoder_slots = old_active_slots
|
||||
PyNvVideoCodecVideoBackend._decoder_slot_cond = old_cond
|
||||
PyNvVideoCodecVideoBackend._max_decoder_slots = old_max_slots
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hw_decoders", [1, 3])
|
||||
def test_pynvvideocodec_decoder_slots_are_bounded(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -234,6 +234,16 @@ def validate_pynvvideocodec_hw_decoders(hw_decoders: object) -> int:
|
||||
return hw_decoders
|
||||
|
||||
|
||||
def _pynvvideocodec_exception_types(nvc) -> tuple[type[Exception], ...]:
|
||||
return tuple(
|
||||
exception_type
|
||||
for name in dir(nvc)
|
||||
if name.startswith("PyNvVCException")
|
||||
and isinstance((exception_type := getattr(nvc, name)), type)
|
||||
and issubclass(exception_type, Exception)
|
||||
)
|
||||
|
||||
|
||||
class PyNvVideoCodecDecoderSlot:
|
||||
"""A retained PyNv decoder slot and its CUDA stream.
|
||||
|
||||
@@ -806,10 +816,18 @@ class PyNvVideoCodecVideoBackendMixin:
|
||||
with cls._borrow_decoder_slot() as decoder_slot:
|
||||
stream = decoder_slot.stream
|
||||
with cls._torch_stream_context(stream):
|
||||
decoder = decoder_slot.get_decoder(
|
||||
file_path, nvc, device_index=cls._DEVICE_INDEX
|
||||
)
|
||||
decoded_frames = decoder.get_batch_frames_by_index(frame_idx)
|
||||
try:
|
||||
decoder = decoder_slot.get_decoder(
|
||||
file_path, nvc, device_index=cls._DEVICE_INDEX
|
||||
)
|
||||
decoded_frames = decoder.get_batch_frames_by_index(frame_idx)
|
||||
except Exception as exc:
|
||||
if not isinstance(
|
||||
exc,
|
||||
_pynvvideocodec_exception_types(nvc) + (IndexError,),
|
||||
):
|
||||
raise
|
||||
raise ValueError("Invalid or unsupported video file.") from exc
|
||||
if len(decoded_frames) < len(frame_idx):
|
||||
logger.warning(
|
||||
"pynvvideocodec video loading: expected %d frames but got %d.",
|
||||
@@ -854,7 +872,12 @@ class PyNvVideoCodecVideoBackendMixin:
|
||||
with os.fdopen(temp_fd, "wb") as temp_file:
|
||||
temp_file.write(data)
|
||||
|
||||
gpu_source = cls._read_source_metadata(temp_path, nvc)
|
||||
try:
|
||||
gpu_source = cls._read_source_metadata(temp_path, nvc)
|
||||
except Exception as exc:
|
||||
if not isinstance(exc, _pynvvideocodec_exception_types(nvc)):
|
||||
raise
|
||||
raise ValueError("Invalid or unsupported video file.") from exc
|
||||
_check_frame_pixel_limit(gpu_source.width, gpu_source.height)
|
||||
source = cls._prepare_source(gpu_source.source)
|
||||
frame_idx = cls.compute_frames_index_to_sample(
|
||||
|
||||
Reference in New Issue
Block a user