class PyNvVideoCodecVideoBackendMixin:
"""PyNvVideoCodec utilities for GPU-backed frame decode."""
_DEVICE_INDEX: ClassVar[int] = 0
@classmethod
def _create_decoder_slot(cls) -> PyNvVideoCodecDecoderSlot:
import torch
return PyNvVideoCodecDecoderSlot(torch.cuda.Stream(device=cls._DEVICE_INDEX))
@classmethod
def _configure_decoder_slots(cls, hw_decoders: object) -> None:
hw_decoders = validate_pynvvideocodec_hw_decoders(hw_decoders)
_pynv_decoder_pool.configure(hw_decoders)
@staticmethod
@contextmanager
def _torch_stream_context(stream):
import torch
torch.accelerator.set_device_index(stream.device.index)
previous_stream = torch.accelerator.current_stream()
torch.accelerator.set_stream(stream)
try:
yield
finally:
torch.accelerator.set_stream(previous_stream)
@classmethod
@contextmanager
def _borrow_decoder_slot(cls):
pool = _pynv_decoder_pool
create_slot = False
with pool.cond:
if pool.max_slots is None:
raise RuntimeError("PyNvVideoCodec decoder slots are not configured")
while True:
if pool.slots:
slot = pool.slots.pop()
break
if pool.active < pool.max_slots:
pool.active += 1
create_slot = True
break
pool.cond.wait()
if create_slot:
try:
slot = cls._create_decoder_slot()
except Exception:
with pool.cond:
pool.active -= 1
pool.cond.notify()
raise
borrow_succeeded = False
try:
yield slot
borrow_succeeded = True
finally:
if not borrow_succeeded:
slot.recover()
with pool.cond:
if borrow_succeeded:
pool.slots.append(slot)
else:
pool.active -= 1
pool.cond.notify()
@staticmethod
def _metadata_value(metadata, *names: str, default=None):
for name in names:
value = getattr(metadata, name, None)
if value is not None:
return value
return default
@classmethod
def _validate_decoder_caps(
cls,
file_path: str,
nvc,
max_width: int | None = None,
max_height: int | None = None,
) -> tuple[int, int]:
demuxer = nvc.CreateDemuxer(file_path)
width = demuxer.Width()
height = demuxer.Height()
caps = nvc.GetDecoderCaps(
cls._DEVICE_INDEX,
demuxer.GetNvCodecId(),
demuxer.ChromaFormat(),
demuxer.BitDepth(),
)
width_min = int(caps["width_min"])
width_max = int(caps["width_max"])
height_min = int(caps["height_min"])
height_max = int(caps["height_max"])
mb_num_max = int(caps["mb_num_max"])
macroblock_count = ((width + 15) // 16) * ((height + 15) // 16)
if (
not caps["supported"]
or width < width_min
or width > width_max
or height < height_min
or height > height_max
or macroblock_count > mb_num_max
):
raise ValueError("Invalid or unsupported video file.")
if max_width is None or max_height is None:
max_dimension = max(width, height)
orientation_mb_count = ((max_dimension + 15) // 16) ** 2
if (
max_dimension <= width_max
and max_dimension <= height_max
and orientation_mb_count <= mb_num_max
and _decoder_surface_bytes(max_dimension, max_dimension)
<= PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES
):
return max_dimension, max_dimension
return width, height
decoder_max_width = min(max_width, width_max)
decoder_max_height = min(max_height, height_max)
decoder_mb_count = ((decoder_max_width + 15) // 16) * (
(decoder_max_height + 15) // 16
)
if decoder_mb_count > mb_num_max:
decoder_max_width = width
decoder_max_height = height
if width > decoder_max_width or height > decoder_max_height:
raise ValueError("Invalid or unsupported video file.")
return decoder_max_width, decoder_max_height
@classmethod
def _read_source_metadata(
cls,
file_path: str,
nvc,
max_width: int,
max_height: int,
) -> PyNvVideoCodecSourceMetadata:
with cls._borrow_decoder_slot() as decoder_slot:
with cls._torch_stream_context(decoder_slot.stream):
decoder = decoder_slot.get_decoder(
file_path,
nvc,
device_index=cls._DEVICE_INDEX,
max_width=max_width,
max_height=max_height,
)
try:
metadata = decoder.get_stream_metadata()
total_frames_num = len(decoder)
finally:
del decoder
width = int(cls._metadata_value(metadata, "width", default=0))
height = int(cls._metadata_value(metadata, "height", default=0))
original_fps = float(
cls._metadata_value(
metadata,
"average_fps",
"avg_frame_rate",
"frame_rate",
"frameRate",
default=0.0,
)
)
duration = float(
cls._metadata_value(metadata, "duration", default=0.0)
or (total_frames_num / original_fps if original_fps > 0 else 0.0)
)
if total_frames_num <= 0:
raise ValueError("Could not determine video frame count")
if width <= 0 or height <= 0:
raise ValueError("Could not determine video dimensions")
return PyNvVideoCodecSourceMetadata(
source=VideoSourceMetadata(total_frames_num, original_fps, duration),
width=width,
height=height,
)
@classmethod
def _decode_to_pinned_host(
cls,
file_path: str,
frame_idx: list[int],
nvc,
max_width: int,
max_height: int,
) -> npt.NDArray:
import torch
if not frame_idx:
return np.empty((0,), dtype=np.uint8)
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,
max_width=max_width,
max_height=max_height,
)
host_frames = None
decoded_count = 0
for start in range(0, len(frame_idx), PYNVVIDEOCODEC_DECODE_BATCH_SIZE):
chunk_indices = frame_idx[
start : start + PYNVVIDEOCODEC_DECODE_BATCH_SIZE
]
invalid_video = False
try:
decoded_frames = decoder.get_batch_frames_by_index(
chunk_indices
)
except Exception as exc:
decoder = None
if not isinstance(
exc,
_pynvvideocodec_exception_types(nvc) + (IndexError,),
):
raise
invalid_video = True
if invalid_video:
raise ValueError("Invalid or unsupported video file.")
torch_frames = [
torch.from_dlpack(frame) for frame in decoded_frames
]
if not torch_frames:
stream.synchronize()
break
device_frames = _pynvvc_frames_to_nhwc(torch.stack(torch_frames))
if device_frames.ndim != 4:
raise ValueError(
"PyNvVideoCodec returned frames with unexpected shape "
f"{tuple(device_frames.shape)}"
)
if host_frames is None:
host_frames = torch.empty(
(len(frame_idx), *device_frames.shape[1:]),
dtype=device_frames.dtype,
device="cpu",
pin_memory=True,
)
elif tuple(device_frames.shape[1:]) != tuple(host_frames.shape[1:]):
raise ValueError(
"PyNvVideoCodec returned frames with inconsistent shapes"
)
next_count = decoded_count + len(device_frames)
host_frames[decoded_count:next_count].copy_(
device_frames, non_blocking=True
)
stream.synchronize()
decoded_count = next_count
del decoded_frames, torch_frames, device_frames
if decoded_count < start + len(chunk_indices):
break
if decoded_count < len(frame_idx):
logger.warning(
"pynvvideocodec video loading: expected %d frames but got %d.",
len(frame_idx),
decoded_count,
)
if host_frames is None:
return np.empty((0,), dtype=np.uint8)
return host_frames[:decoded_count].numpy()
@classmethod
def decode_frames_pynvvideocodec(
cls,
loader_cls,
data: bytes,
target: VideoTargetMetadata,
max_width: int | None = None,
max_height: int | None = None,
**kwargs,
) -> tuple[npt.NDArray, VideoSourceMetadata, list[int], list[int]]:
import PyNvVideoCodec as nvc
from vllm.multimodal.gpu_ipc_memory import get_mm_gpu_ipc_pool
temp_fd, temp_path = tempfile.mkstemp(suffix=".mp4")
try:
with os.fdopen(temp_fd, "wb") as temp_file:
temp_file.write(data)
invalid_video = False
try:
decoder_max_width, decoder_max_height = cls._validate_decoder_caps(
temp_path,
nvc,
max_width=max_width,
max_height=max_height,
)
except Exception as exc:
if not isinstance(exc, _pynvvideocodec_exception_types(nvc)):
raise
invalid_video = True
if invalid_video:
raise ValueError("Invalid or unsupported video file.")
try:
gpu_source = cls._read_source_metadata(
temp_path, nvc, decoder_max_width, decoder_max_height
)
except Exception as exc:
if not isinstance(exc, _pynvvideocodec_exception_types(nvc)):
raise
invalid_video = True
if invalid_video:
raise ValueError("Invalid or unsupported video file.")
check_frame_pixel_limit(gpu_source.width, gpu_source.height)
source = loader_cls._prepare_source(gpu_source.source)
frame_idx = loader_cls.compute_frames_index_to_sample(
source=source, target=target, **kwargs
)
raw_frame_bytes = len(frame_idx) * gpu_source.height * gpu_source.width * 3
pool = get_mm_gpu_ipc_pool()
if pool is None or raw_frame_bytes == 0:
frames = cls._decode_to_pinned_host(
temp_path,
frame_idx,
nvc,
decoder_max_width,
decoder_max_height,
)
else:
with pool.acquire(raw_frame_bytes):
frames = cls._decode_to_pinned_host(
temp_path,
frame_idx,
nvc,
decoder_max_width,
decoder_max_height,
)
finally:
with suppress(FileNotFoundError):
os.unlink(temp_path)
valid_frame_indices = frame_idx[: int(frames.shape[0])]
return frames, source, frame_idx, valid_frame_indices