Prhub

#46609 Add TorchCodec as a video decoding backend

原始 PR 作者 NicolasHug 合并时间 2026-07-07 10:58 文件变更 9 提交数 7 评论 19 代码增减 +300 / -12

执行摘要

新增 TorchCodec 视频解码后端,提升稀疏采样性能

现有的 opencv 和 pyav 后端在解码视频时,即使只需要少量帧(稀疏采样),也会解码整个视频的所有帧,浪费大量计算资源。另一方面,两者内部都使用多线程解码,但线程数不可配置,在多进程服务模式下容易导致 CPU oversubscription,反而降低整体吞吐量。TorchCodec 是 PyTorch 团队开发的 FFmpeg 封装库,其设计目标正是解决上述痛点:只解码请求的帧、支持 num_ffmpeg_threads 自定义线程数、提供 seek_mode 精细控制搜索精度,并且与 PyTorch 张量原生集成。

该 PR 值得所有使用多模态视频输入的团队精读。设计上通过 Mixin 模式优雅地扩展后端,值得借鉴。建议重点关注基准测试方法和 num_ffmpeg_threads 参数对解码性能的影响,以及未来可扩展性(如 CUDA 解码、音频解码等)。

讨论亮点

Review 中核心讨论点如下:

  • 依赖更新:Isotr0py 建议更新 requirements/cuda.txtrequirements/cpu.txt 等锁文件,PR 作者随后进行了更新。
  • FFmpeg 缺失报错:Isotr0py 询问若系统未安装 FFmpeg,torchcodec 是否会给出明确错误。NicolasHug 展示 torchcodec 内部的错误提示链接,确认会给出指导。
  • 版本兼容性:Isotr0py 担心 torchcodec 是否需要与特定 torch 版本绑定。NicolasHug 解释从 0.13 起已实现 ABI 稳定,仅需 torchcodec>=0.14 即可。
  • 像素限制检查遗漏:depthfirst-app[bot] 自动审查指出 TorchCodec 分支缺少 _check_frame_pixel_limit 调用,且 width/height 默认为 0 可能绕过限制。PR 作者在后续提交中添加了该检查。
  • 多线程 oversubscription 问题:NicolasHug 在评论中详细阐述了现有 opencv/pyav 后端多线程不可控导致 CPU 过订阅的问题,强调 TorchCodec 通过暴露 num_ffmpeg_threads 参数允许用户规避这一问题。

实现拆解

步骤:

  1. 导入与占位处理:在 vllm/multimodal/video.py 的模块级别添加对 torchcodec.decoders.VideoDecoder 的尝试导入,若未安装则使用 PlaceholderModule 占位,与现有 opencv、pyav 的模式保持一致。

  2. 新增 TorchCodecVideoBackendMixin:定义三个静态方法:make_torchcodec_decoderget_torchcodec_metadatadecode_torchcodec_frames。其中 decode_torchcodec_frames 调用 decoder.get_frames_at(frame_indices) 一次批处理解码。

  3. 集成到现有后端类:将 TorchCodecVideoBackendMixin 添加到 VideoBackendVideoBackendDynamic 的多继承列表。在 load_bytes 方法中增加 'torchcodec' 分支,先断言 frame_recovery=False,然后调用 mixin 方法,并在解码前执行 _check_frame_pixel_limit 安全检查。

  4. 安全修复:自动审查工具指出初始实现缺少像素限制检查,在后续提交中补充了 _check_frame_pixel_limit 调用。

  5. 测试覆盖:在 tests/multimodal/test_video.py 中新增 4 个测试函数:基本加载、动态采样、拒绝 frame_recovery、以及回归测试确保返回目标帧而非关键帧。

  6. 文档与依赖:在 docs/features/multimodal_inputs.md 中新增视频解码后端说明。在 requirements/test/cuda.in 中添加 torchcodec>=0.14,并同步更新各平台的 .txt 锁文件。

文件 模块 状态 重要度
vllm/multimodal/video.py 视频解码 modified 8.38
tests/multimodal/test_video.py 测试 modified 7.35
docs/features/multimodal_inputs.md 文档 modified 3.18
requirements/test/cuda.in 构建依赖 modified 2.71

关键符号

make_torchcodec_decoder get_torchcodec_metadata decode_torchcodec_frames

关键源码片段

vllm/multimodal/video.py core-logic

核心实现,新增 TorchCodecVideoBackendMixin 及集成逻辑。

class TorchCodecVideoBackendMixin:
    '''TorchCodec (FFmpeg 后端,PyTorch 原生) 解码工具类。    将内存字节构建为 VideoDecoder,并通过一次批处理 get_frames_at 调用
    获取所有请求帧,解码期间释放 GIL。
    '''
​
    @staticmethod
    def make_torchcodec_decoder(
        data: bytes,
        *,
        num_ffmpeg_threads: int = 0,
        seek_mode: Literal['exact', 'approximate'] = 'exact',
    ) -> 'VideoDecoder':
        # NHWC 排列匹配下游网络预期的 (num_frames, H, W, 3) RGB 布局,
        # 避免额外的张量转置操作。
        return VideoDecoder(
            data,
            dimension_order='NHWC',
            num_ffmpeg_threads=num_ffmpeg_threads,
            seek_mode=seek_mode,
        )
​
    @staticmethod
    def get_torchcodec_metadata(decoder: 'VideoDecoder') -> VideoSourceMetadata:
        md = decoder.metadata
        total_frames = md.num_frames or 0
        fps = float(md.average_fps) if md.average_fps else 0.0
        duration = float(md.duration_seconds) if md.duration_seconds else 0.0
        # 当总帧数未知但存在时长和帧率时,推算总帧数
        if total_frames == 0 and duration > 0 and fps > 0:
            total_frames = int(duration * fps)
        return VideoSourceMetadata(total_frames, fps, duration)
​
    @staticmethod
    def decode_torchcodec_frames(
        decoder: 'VideoDecoder',
        frame_indices: list[int],
    ) -> tuple[npt.NDArray, list[int]]:
        '''一次批处理调用精确解码请求的帧索引。'''
        if not frame_indices:
            return np.empty((0,), dtype=np.uint8), []
        # 注意:torchcodec 在整个调用期间释放 GIL
        batch = decoder.get_frames_at(frame_indices)
        # 返回 NumPy 数组视图(零拷贝转换)
        return batch.data.numpy(), list(frame_indices)
tests/multimodal/test_video.py test-coverage

新增 4 个测试函数覆盖 TorchCodec 后端的各种场景。

def test_torchcodec_backend_loads_frames(
    dummy_video_path, monkeypatch: pytest.MonkeyPatch
):
    '''验证 torchcodec 后端可以正常加载指定数量的帧。'''
    pytest.importorskip('torchcodec')
    with monkeypatch.context() as m:
        # 设置加载器为 opencv(实际只使用注册的 loader,后端通过 backend 参数指定)
        m.setenv('VLLM_VIDEO_LOADER_BACKEND', 'opencv')
​
        with open(dummy_video_path, 'rb') as f:
            video_data = f.read()
​
        loader = VIDEO_LOADER_REGISTRY.load('opencv')
        frames, metadata = loader.load_bytes(
            video_data, num_frames=8, backend='torchcodec'
        )
​
        # 验证输出张量形状为 (8, H, W, 3)
        assert frames.ndim == 4
        assert frames.shape[3] == 3 # RGB
        assert frames.shape[0] == 8
        # metadata 包含帧索引、后端名称、总帧数、帧率、时长
        assert frames.shape[0] == len(metadata['frames_indices'])
        assert metadata['video_backend'] == 'torchcodec'
        assert 'total_num_frames' in metadata
        assert 'fps' in metadata
        assert 'duration' in metadata

评论区精华

是否需要更新 requirements 锁文件 测试

Isotr0py 在 review 中建议更新 requirements/cuda.txt 和 requirements/cpu.txt 等锁文件。

结论:PR 作者随后在 requirements/test/cuda.in 中添加了 torchcodec 依赖,并同步更新了 cuda.txt、cpu.txt 等锁文件,解决了该问题。 · 已解决

系统缺失 FFmpeg 时的错误提示 question

Isotr0py 询问如果系统未安装 FFmpeg,torchcodec 是否会给出明确错误提示。

结论:NicolasHug 提供 torchcodec 内部错误抛出位置的链接,指出 torchcodec 会给出清晰的安装指导。 · 已解决

TorchCodec 与 torch 版本的兼容性 question

Isotr0py 提出 torchcodec 是否需要像 torchvision 一样与 torch 版本严格对应。

结论:NicolasHug 解释从 0.13 起 TorchCodec 已 ABI 稳定,仅需 `torchcodec>=0.14`,无需严格版本匹配。 · 已解决

缺少像素限制检查 正确性

depthfirst-app[bot] 自动审查指出 torchcodec 分支在解码前未调用 _check_frame_pixel_limit,可能导致大像素视频消耗过多内存。

结论:PR 作者后续提交中添加了 _check_frame_pixel_limit 调用,但自动 bot 另指出 width/height 默认 0 仍可能绕过,需要进一步加固。 · 已解决

风险与影响

主要风险包括:

  • 新增外部依赖:torchcodec 是一个相对较新的库,可能存在未发现的 bug 或 API 变动风险。但依赖版本使用 >=0.14,向下兼容性较好。
  • 性能退化:若用户在密集采样或特定视频格式下使用 TorchCodec,性能可能不如 opencv/pyav(但基准测试表明 TorchCodec 至少在均匀采样下也不差)。不过默认为 opencv,不会影响现有用户。
  • 像素限制绕过:自动审查曾指出宽高默认值为 0 时可能绕过 _check_frame_pixel_limit,PR 作者已通过硬性检查修复。但类似的安全遗漏仍需警惕。
  • FFmpeg 系统依赖:torchcodec 依赖系统 FFmpeg,如果用户系统没有 FFmpeg 或版本过旧,解码会失败。但 torchcodec 会抛出清晰的安装指南,且 Python 包索引中已包含相关提示。

对用户:新增了一个可选的、性能更优的视频解码后端,特别是适合稀疏采样和大规模视频处理场景。用户可以通过 --media-io-kwargs 轻松切换。对系统:需要安装 torchcodec 库,但向后兼容,默认行为不变。对团队:新增的 mixin 设计减少了与现有代码的耦合,维护成本较低;但需要关注 torchcodec 的版本更新和 bug 修复。

新增外部依赖 像素限制检查遗漏(已修复) 多线程配置不当

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论