Prhub

#49524 [Perf] Isolate MM preprocessing on its own executor

原始 PR 作者 guan404ming 合并时间 2026-07-26 16:04 文件变更 3 提交数 8 评论 12 代码增减 +31 / -20

执行摘要

分离 MM 预处理线程池,消除图像预处理阻塞 tokenization

根据 PR 描述,当有图像/视频预处理正在进行时(对于大图像可能超过 1 秒),由于 tokenization 和 MM 预处理共享同一个线程池,所有并发请求的 tokenization 都会被阻塞。为消除这一瓶颈,需将 MM 预处理隔离到独立的执行器中。

值得精读的设计变更,性能提升数据扎实。推荐关注 _mm_executor 的拆分思路以及如何利用独立线程池消除资源竞争。此外,配置校验的精细化处理也值得借鉴。

讨论亮点

核心讨论集中在是否应等待更彻底的 MM 预处理重构,还是先以单 worker 隔离快速解决问题。@noooop 提出质疑,@guan404ming 和 @DarkLight1337 认为单 worker 已足够,因为 MM 预处理多为 CPU 密集型且受 GIL 限制,多 worker 无法加速,而卸载出事件循环是主要收益。最终达成一致,当前 PR 仅将 MM 执行器设为单 worker,并放宽对 generate 模型的限制。

实现拆解

  1. 独立 MM 执行器:在 vllm/renderers/base.pyBaseRenderer.__init__ 中,将 self._mm_executor 从指向共享的 self._executor 改为创建一个独立的单 worker 线程池 ThreadPoolExecutor(max_workers=1)。同时将 _clear_mm_cache_async 也绑定到 _mm_executor,保证缓存清理与 MM 预处理在同一线程中序列化,避免竞态。
  2. 收紧配置校验:在 vllm/config/model.pyModelConfig.__post_init__ 中,给现有的 renderer_num_workers > 1 与 MM processor cache 冲突检查添加 self.runner_type == "pooling" 条件。这使得 generate 模型的用户不再受此限制,可以自由选择多 worker,而 pooling 模型因为预处理仍在 renderer worker 上运行,保留限制。
  3. 更新测试:在 tests/test_config.py 中,更新 test_renderer_num_workers_with_mm_cache 函数,明确区分 pooling 和 generate 场景:pooing 模型多 worker + cache 应触发错误,generate 模型多 worker + cache 应正常通过。新增测试用例验证 generate 模型在多 worker 下可以安全使用 MM cache。
文件 模块 状态 重要度
vllm/renderers/base.py 渲染器 modified 6.2
vllm/config/model.py 配置 modified 5.28
tests/test_config.py 测试 modified 4.76

关键符号

BaseRenderer.__init__ ModelConfig.__post_init__ test_renderer_num_workers_with_mm_cache

关键源码片段

vllm/renderers/base.py core-logic

核心变更:将 MM 预处理执行器从共享线程池分离为独立单 worker 线程池,消除对 tokenization 的阻塞。

# 独立的 tokenizer 线程池,worker 数量由配置决定
pool_workers = config.model_config.renderer_num_workers
self._executor = ThreadPoolExecutor(max_workers=pool_workers)# 单独的 MM 预处理线程池,固定单 worker,确保不会与 tokenization 争抢线程
# 参考 issue #38418:必须单 worker 以保证 P0/P1 顺序
self._mm_executor: Executor = ThreadPoolExecutor(max_workers=1)# tokenization 使用 _executor
self._tokenize_prompt_async = make_async(
    self._tokenize_prompt, executor=self._executor
)
# MM 预处理和缓存清理都使用 _mm_executor
self._clear_mm_cache_async = make_async(
    self.clear_mm_cache, executor=self._mm_executor
)
self._process_multimodal_async = make_async(
    self._process_multimodal, executor=self._mm_executor
)

评论区精华

是否应等待更彻底的 MM 预处理重构,还是先以单 worker 隔离快速解决问题 设计

noooop 提出是否需要进一步重构才修改;guan404ming 认为移除耦合即可,阻塞可测;DarkLight1337 同意单 worker 足够,并提议同时放宽 ValueErro 限制。

结论:当前方案被接受:MM 执行器设为单 worker,并对 generate 模型放宽 renderer_num_workers 限制。 · 已解决

风险与影响

  1. 线程安全: pooling 模型的 MM 预处理仍然在 renderer worker 线程中运行(共享多 worker 线程池),若未来启用多 worker 且 cache 未同步,可能引入竞态。当前保留了对 pooling 模型的多 worker + cache 禁止,风险可控。
  2. 单 worker 瓶颈: 若单请求需同时处理多张超大图像,单 worker 可能成为新瓶颈。但此类场景极少,且预处理本身是 CPU 密集型,单 worker 已能保持事件循环响应。
  3. 依赖: PR 依赖 #44786 的特性(未合并前不可用),但当前实现独立,不阻塞。
  4. 测试覆盖: 新增了 pooling 场景的测试,但未覆盖 MM 预处理与 tokenization 并发时的压力测试,可考虑后续补充。

对用户:所有使用多模态模型(尤其是高分辨率图像)的生成请求将显著受益,tokenization 延迟趋于平坦。对系统:无需额外配置,默认行为自动改善。对团队:代码变更集中在三个文件,逻辑清晰,维护成本低。

线程安全边界收缩 pooling 路径未隔离 单 worker 可能瓶颈

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论