vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation
Summary
Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through media_io_kwargs.video.video_backend, but engine GPU memory reservation is computed only from static startup configuration and VLLM_VIDEO_LOADER_BACKEND. If the server starts with the default OpenCV/software backend and no --mm-ipc-gpu-memory-gb budget, a client can still route a video request into the PyNvVideoCodec path after startup, causing frontend CUDA-context, decoder-surface, and decoded-frame GPU allocations that were not carved out of the engine KV-cache budget.
Technical Details
The vulnerable boundary is the split between request-time media decoding choices in the API server and startup-time memory budgeting in the engine worker. Request bodies for Chat Completions and Responses expose media_io_kwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetch_video() copies self.media_io_kwargs["video"] into video_io_kwargs, only setting a model-derived backend when video_backend is absent. VideoMediaIO.__init__() then consumes video_backend from those kwargs and loads that backend from VIDEO_LOADER_REGISTRY.
The relevant request-side source path is:
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
if "video_backend" not in video_io_kwargs and (
video_backend := get_video_loader_backend_for_processor(video_processor)
):
video_io_kwargs["video_backend"] = video_backend
video_io = VideoMediaIO(image_io, **video_io_kwargs)
video_loader_backend = (
kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND
)
self.video_loader = VIDEO_LOADER_REGISTRY.load(video_loader_backend)
VideoBackend.load_bytes() then dispatches backend == "pynvvideocodec" into decode_frames_pynvvideocodec(), which constructs a PyNvVideoCodec decoder, creates or uses a CUDA stream, reads stream metadata, decodes selected frames on the GPU, and copies those frames into pinned host memory. The new frontend GPU memory pool accounts only for raw decoded frame bytes when a pool exists; it does not make request-time backend selection safe when no startup reservation was made.
The engine-side reservation code makes its decision from static model config and environment only:
def _uses_pynvvideocodec_video_backend(mm_config) -> bool:
video_kwargs = mm_config.media_io_kwargs.get("video", {})
video_loader_backend = (
video_kwargs.get("video_backend") or envs.VLLM_VIDEO_LOADER_BACKEND
)
codec_backend = video_kwargs.get("backend")
return (
video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
)
decoder_reserved_bytes = (
num_api_servers * per_server_decoder_bytes
if self._uses_pynvvideocodec_video_backend(mm_config)
else 0
)
reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes
if reserved_bytes str:
return (repo / rel).read_text(encoding="utf-8")
def const_int(source: str, name: str) -> int:
expr = re.search(rf"^{name}\s*=\s*(.+)$", source, flags=re.MULTILINE).group(1).strip()
if expr == "128 * MiB_bytes":
return 128 * MIB
if expr == "int(1.8 * 1024 * MiB_bytes)":
return int(1.8 * 1024 * MIB)
if expr == "1":
return 1
raise AssertionError(expr)
def uses_pynv_static(static_media_io_kwargs: dict[str, dict[str, str]], env_backend: str) -> bool:
video_kwargs = static_media_io_kwargs.get("video", {})
video_loader_backend = video_kwargs.get("video_backend") or env_backend
codec_backend = video_kwargs.get("backend")
return video_loader_backend == "pynvvideocodec" or codec_backend == "pynvvideocodec"
def reserve_bytes(static_media_io_kwargs, env_backend, mm_ipc_gpu_memory_gb, decoder_bytes, cuda_context_bytes, retained_decoders):
raw_frame_reserved_bytes = int(mm_ipc_gpu_memory_gb * GIB)
per_server_decoder_bytes = decoder_bytes * retained_decoders + cuda_context_bytes
decoder_reserved_bytes = per_server_decoder_bytes if uses_pynv_static(static_media_io_kwargs, env_backend) else 0
return raw_frame_reserved_bytes + decoder_reserved_bytes
parser = argparse.ArgumentParser()
parser.add_argument("--repo", required=True, type=Path)
repo = parser.parse_args().repo.resolve()
media_video = read(repo, "vllm/multimodal/media/video.py")
connector = read(repo, "vllm/multimodal/media/connector.py")
chat_protocol = read(repo, "vllm/entrypoints/openai/chat_completion/protocol.py")
responses_protocol = read(repo, "vllm/entrypoints/openai/responses/protocol.py")
gpu_worker = read(repo, "vllm/v1/worker/gpu_worker.py")
video_core = read(repo, "vllm/multimodal/video.py")
assert "media_io_kwargs: dict[str, dict[str, Any]] | None = Field(" in chat_protocol
assert "media_io_kwargs: dict[str, dict[str, Any]] | None = Field(" in responses_protocol
assert 'video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))' in connector
assert 'if "video_backend" not in video_io_kwargs and (' in connector
assert 'kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND' in media_video
assert "elif backend == PYNVVIDEOCODEC_VIDEO_BACKEND:" in video_core
assert 'video_kwargs = mm_config.media_io_kwargs.get("video", {})' in gpu_worker
decoder_bytes = const_int(video_core, "PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES")
retained_decoders = const_int(video_core, "PYNVVIDEOCODEC_MAX_RETAINED_DECODERS")
cuda_context_bytes = const_int(video_core, "PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES")
per_server_decoder_bytes = decoder_bytes * retained_decoders + cuda_context_bytes
vulnerable_static_reserved = reserve_bytes({}, "opencv", 0.0, decoder_bytes, cuda_context_bytes, retained_decoders)
negative_control_reserved = reserve_bytes({"video": {"video_backend": "pynvvideocodec"}}, "opencv", 0.0, decoder_bytes, cuda_context_bytes, retained_decoders)
raw_frame_only_control = reserve_bytes({}, "opencv", 0.25, decoder_bytes, cuda_context_bytes, retained_decoders)
head = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip()
print(json.dumps({
"head": head,
"vulnerable": vulnerable_static_reserved == 0 and negative_control_reserved == per_server_decoder_bytes,
"reservation_simulation": {
"env_video_loader_backend": "opencv",
"request_selects_pynv_after_startup": True,
"vulnerable_static_reserved_bytes": vulnerable_static_reserved,
"negative_control_static_pynv_reserved_bytes": negative_control_reserved,
"raw_frame_only_control_reserved_bytes": raw_frame_only_control,
"unreserved_decoder_bytes_when_only_request_selects_pynv": per_server_decoder_bytes,
},
}, indent=2, sort_keys=True))