Prhub

#49753 [multimodal] Make PyNvVideoCodec decoder concurrency configurable

原始 PR 作者 brandonpelfrey 合并时间 2026-07-25 14:31 文件变更 7 提交数 2 评论 1 代码增减 +190 / -24

执行摘要

使 PyNvVideoCodec 硬件解码器并发数可配置,默认提升至 2

在 front-end heavy workloads(低 OSL、大视频)中,固定单解码器串行化了 GPU 视频解码,即使 GPU 有额外解码能力。吞吐量测试显示增加解码器数量可提升高达 94%。该变更遵循 RFC #30839 和 #44465,为视频解码提供前端可控的并发度。

值得阅读,展示了如何在多模态前端中引入可控并发配置,以及如何保证内存预留与配置一致。对关注视频处理性能和 GPU 资源管理的工程师有参考价值。

讨论亮点

PR 获得维护者 DarkLight1337 批准,无实质性讨论。Claude bot 自动评论由于 PR 来自 fork,review 被禁用。

实现拆解

  1. 新增验证与配置入口:在 vllm/multimodal/video.py 中添加 validate_pynvvideocodec_hw_decoders 函数和 _configure_decoder_slots 类方法。前者校验 hw_decoders 为正整数,后者在首次配置时设置 _max_decoder_slots,后续配置若不一致则抛出异常。

  2. 调整解码器槽位池逻辑:在 PyNvVideoCodecVideoBackendMixin 中增加 _max_decoder_slots 类变量,将 _borrow_decoder_slot 中的硬编码上限 PYNVVIDEOCODEC_MAX_RETAINED_DECODERS 替换为 max_decoder_slots。当配置未设置时,借出操作会抛出异常。

  3. 同步 GPU 内存预留:在 vllm/multimodal/gpu_ipc_memory.pyreserve_mm_ipc_gpu_memory 函数中,读取 media_io_kwargs 中的 hw_decoders(默认 PYNVVIDEOCODEC_DEFAULT_HW_DECODERS=2),按实际解码器数量计算每进程预留的表面内存和 CUDA 上下文,并从 KV cache 预算中扣除。错误信息也提示用户可调整 hw_decoders

  4. 阻止请求级覆盖:在 vllm/multimodal/media/video.pyVideoMediaIO.merge_kwargs 中,在运行时合并参数时弹出 hw_decoders 键,防止请求级别动态修改该配置,因为解码器内存已在启动阶段预留。

  5. 更新测试与文档:参数化现有边界测试以覆盖多 hw_decoders 值,新增测试验证配置一次性、拒绝无效值、请求级覆盖被阻止以及内存预留按配置缩放。文档添加 hw_decoders 的说明、推荐值(默认2)和内存权衡。

文件 模块 状态 重要度
vllm/multimodal/video.py 视频解码 modified 7.24
vllm/multimodal/gpu_ipc_memory.py GPU 内存管理 modified 6.95
tests/multimodal/test_video.py 测试 modified 6.36
tests/multimodal/test_gpu_ipc_memory.py 内存测试 modified 6.11
tests/multimodal/media/test_video.py 媒体测试 modified 5.47
vllm/multimodal/media/video.py 输入合并 modified 5.07
docs/features/multimodal_inputs.md 文档 modified 2.47

关键符号

validate_pynvvideocodec_hw_decoders _configure_decoder_slots reserve_mm_ipc_gpu_memory merge_kwargs _borrow_decoder_slot _pynvvideocodec_decoder_budget

关键源码片段

vllm/multimodal/video.py core-logic

核心变更:添加 hw_decoders 验证与配置函数,修改解码器池上限逻辑,引入默认常量。

def validate_pynvvideocodec_hw_decoders(hw_decoders: object) -> int:
    """校验 hw_decoders 为正整数,拒绝 bool 、浮点、负数或零。"""
    if (
        isinstance(hw_decoders, bool)
        or not isinstance(hw_decoders, int)
        or hw_decoders < 1
    ):
        raise ValueError("hw_decoders must be a positive integer")
    return hw_decoders
​
​
class PyNvVideoCodecVideoBackendMixin:
    # ... 其他属性 ...
    _max_decoder_slots: ClassVar[int | None] = None # 新增:配置上限,首次配置后锁定
​
    @classmethod
    def _configure_decoder_slots(cls, hw_decoders: object) -> None:
        """首次配置解码器槽位数量,后续调用必须一致。"""
        hw_decoders = validate_pynvvideocodec_hw_decoders(hw_decoders)
        with cls._decoder_slot_cond:
            if cls._max_decoder_slots is None:
                cls._max_decoder_slots = hw_decoders
            elif cls._max_decoder_slots != hw_decoders:
                raise RuntimeError(
                    f"PyNvVideoCodec decoder count is already configured as "
                    f"{cls._max_decoder_slots}, got {hw_decoders}"
                )
​
    @classmethod
    def _borrow_decoder_slot(cls):
        """借出一个解码器槽位,若所有槽位已用完则阻塞。"""
        with cls._decoder_slot_cond:
            max_decoder_slots = cls._max_decoder_slots
            if max_decoder_slots is None:
                raise RuntimeError("PyNvVideoCodec decoder slots are not configured")
            while True:
                if cls._decoder_slots:
                    slot = cls._decoder_slots.pop()
                    break
                if cls._active_decoder_slots < max_decoder_slots:
                    cls._active_decoder_slots += 1
                    create_slot = True
                    break
                cls._decoder_slot_cond.wait()
vllm/multimodal/gpu_ipc_memory.py dependency-wiring

修改 GPU IPC 内存预留函数以读取 hw_decoders 并缩放预留,新增导入和配置检测逻辑。

def reserve_mm_ipc_gpu_memory(
    available_kv_cache_memory_bytes: int,
    mm_config: "MultiModalConfig | None",
    api_process_count: int = 1,
) -> int:
    # ... 省略前半部分 ...
    from vllm import envs
    from vllm.multimodal.video import (
        PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES,
        PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES,
        PYNVVIDEOCODEC_DEFAULT_HW_DECODERS,
        PYNVVIDEOCODEC_VIDEO_BACKEND,
        validate_pynvvideocodec_hw_decoders,
    )
​
    raw_frame_reserved_bytes = int(mm_config.mm_ipc_gpu_memory_gb * GiB_bytes)
    num_api_servers = max(1, api_process_count)
    # 读取 video 子配置中的 video_backend 和 backend,判断是否使用 PyNvVideoCodec
    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")
    uses_pynvvideocodec = (
        video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
        or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND
    )
    # 获取 hw_decoders,默认使用 PYNVVIDEOCODEC_DEFAULT_HW_DECODERS (2)
    hw_decoders = (
        validate_pynvvideocodec_hw_decoders(
            video_kwargs.get("hw_decoders", PYNVVIDEOCODEC_DEFAULT_HW_DECODERS)
        )
        if uses_pynvvideocodec
        else 1
    )
    per_server_decoder_bytes = (
        PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES * hw_decoders
        + PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES
    )
    decoder_reserved_bytes = (
        num_api_servers * per_server_decoder_bytes
        if mm_config.use_gpu_video_backend()
        else 0
    )
    reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes
    # ... 检查是否超出可用内存 ...
tests/multimodal/test_video.py test-coverage

测试覆盖:参数化解码器槽位边界测试,新增配置一次性和无效值拒绝测试。

@pytest.mark.parametrize("hw_decoders", [1, 3])
def test_pynvvideocodec_decoder_slots_are_bounded(
    monkeypatch: pytest.MonkeyPatch,
    hw_decoders: int,
):
    """验证槽位池大小限制为配置的 hw_decoders 数,超出时线程阻塞。"""
    # 保存原始状态并设置配置
    old_slots = PyNvVideoCodecVideoBackend._decoder_slots
    old_active = PyNvVideoCodecVideoBackend._active_decoder_slots
    old_cond = PyNvVideoCodecVideoBackend._decoder_slot_cond
    old_max = PyNvVideoCodecVideoBackend._max_decoder_slots
    try:
        PyNvVideoCodecVideoBackend._decoder_slots = []
        PyNvVideoCodecVideoBackend._active_decoder_slots = 0
        PyNvVideoCodecVideoBackend._decoder_slot_cond = threading.Condition()
        PyNvVideoCodecVideoBackend._max_decoder_slots = None
        PyNvVideoCodecVideoBackend._configure_decoder_slots(hw_decoders)
​
        # 借出所有 hw_decoders 个槽位
        with ExitStack() as stack:
            retained_slots = [
                stack.enter_context(
                    PyNvVideoCodecVideoBackend._borrow_decoder_slot()
                )
                for _ in range(hw_decoders)
            ]
            # 试图再借一个应该阻塞
            borrowed = threading.Event()
            seen_slots = []
            def borrow_extra():
                with PyNvVideoCodecVideoBackend._borrow_decoder_slot() as s:
                    seen_slots.append(s)
                    borrowed.set()
            thread = threading.Thread(target=borrow_extra)
            thread.start()
            assert not borrowed.wait(timeout=0.2)
        # 释放后线程应该获得槽位
        assert borrowed.wait(timeout=2.0)
        thread.join()
        assert seen_slots[0] in retained_slots
        # 验证创建的 decoder slot 数量等于 hw_decoders
        assert create_count == hw_decoders
    finally:
        # 恢复原始状态
        PyNvVideoCodecVideoBackend._decoder_slots = old_slots
        PyNvVideoCodecVideoBackend._active_decoder_slots = old_active
        PyNvVideoCodecVideoBackend._decoder_slot_cond = old_cond
        PyNvVideoCodecVideoBackend._max_decoder_slots = old_max
​
​
@pytest.mark.parametrize("hw_decoders", [0, -1, 1.5, True, "2"])
def test_pynvvideocodec_rejects_invalid_hw_decoders(hw_decoders: object):
    """验证无效的 hw_decoders 值被拒绝。"""
    with pytest.raises(ValueError, match="hw_decoders must be a positive integer"):
        VideoBackend.load_bytes(
            b"fake video",
            backend=PYNVVIDEOCODEC_VIDEO_BACKEND,
            hw_decoders=hw_decoders,
        )

评论区精华

PR Approval other

维护者 DarkLight1337 批准了此 PR,无进一步讨论。

结论:PR 已合并。 · RESOLVED

风险与影响

如果用户设置 hw_decoders 过高,可能导致 GPU 内存耗尽,但错误提示已更新以包含该选项。默认值从 1 变为 2 可能增加默认内存消耗(每个解码器约 128 MB 表面内存),但 PR 认为这是合理的折中。另外,启动后不能更改解码器数量,对动态调整场景不够灵活,但设计上这是有意为之。测试覆盖了边界和异常情况。

对使用 PyNvVideoCodec 的用户:新增 --hw_decoders 选项,默认提升并发度,可提高视频解码吞吐量,但可能增加 GPU 内存占用。对系统:内存预留公式按解码器数量缩放,多个 API server 进程时预留相应倍数。对团队:配置入口单一,易于维护。

默认内存消耗增加 启动一次性配置 请求级配置被禁止

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论