Prhub

#35342 [VLM] Route every multimodal processor through the worker pool's call site

原始 PR 作者 mickqian 合并时间 2026-08-27 10:27 文件变更 34 提交数 2 评论 2 代码增减 +130 / -33

执行摘要

多模态处理器统一改走 worker 池异步调用点

PR body 指出 process_and_combine_mm_data 是 multimodal processor worker pool 实际运行的函数,但 37 个调用点中有 33 个直接调用它,导致这些模型即使配置 --mm-processor-worker-num > 1 也会在启动时构建线程池和 deepcopy 处理器克隆,却把每个请求直接绕过池子——这是静默失败,模型始终以单 worker 速度服务。PaddleOCR-VL 从第一天起就是一个例子:接入并发(#35318)后,H200 上 32 路并发吞吐从 6.7 req/s 提升到 10.9 req/s,纯粹靠 87 ms/page 的预处理与其他请求重叠。

值得精读,重点看三处:新增测试文件的 AST 扫描设计(用静态分析锁定调用点不变量)、PR body 对身份变换的论证(await 不挂起则行为不变)、以及开启并发前的性能权衡方法论(GIL 释放、prefill 批碎裂)。它是典型的大范围机械重构 + 防回退测试的样板,适合作为后续批量迁移的参考。

讨论亮点

PR 的 review 评论为空,核心讨论集中在 PR body 和 issue 评论中:

  • 作者在 Notes for reviewers 中明确把“开启并发”排除在本 PR 之外,并给出权衡:ThreadPoolExecutor 只在预处理释放 GIL 时才有效(PIL、numpy、torch 的 C 代码可以,纯 Python 预处理不行);更多 worker 会分散请求到达,导致 GPU prefill 批处理碎片化,qwen_vl 选 2 个 worker、PaddleOCR-VL 上 8 个 worker 反而比 4 个慢。
  • rebase 说明中提到 transformers_auto.py 在 main 上新增的调用点正是被本 PR 自己的审计测试捕获,说明审计测试在实际开发中起了作用;pixtral.py 和 qwen_audio.py 的冲突以 main 版本为准。
  • issue 评论中作者解释两次 CI 失败均为 runner 环境问题:XPU 镜像缺 libsycl.so.8,NPU 脚本无测试输出,与本 PR 无关,且这两个 job 已在 #35238 中排除出 fast-fail 级联。

实现拆解

实现分为 4 步:

  1. 盘点调用点:用 AST 扫描 python/sglang/srt/multimodal/processors 目录,确认 37 个调用点,其中 33 个直连同步版,只有 qwen_vl、kimi_k25、kimi_k3、muse_glimmer 已在正确的异步入口。
  2. 机械迁移:对 33 个调用点逐一执行 await + _async 改名,覆盖 clip.py、cohere2_vision.py、deepseek_ocr.py、deepseek_vl_v2.py、dots_vlm.py、ernie45_vl.py、gemma3.py、gemma3n.py、gemma4.py、glm4v.py、glm_image.py 等文件;所有调用点均位于 async def 内,因此 await 合法。Rebase 时处理了 transformers_auto.py 新增调用点和 pixtral.py、qwen_audio.py 冲突。
  3. 新增审计测试:test/registered/unit/multimodal/test_processor_async_call_sites.py 用 AST 扫描整个 multimodal 目录,断言所有 process_and_combine_mm_data* 调用必须以 _async 结尾;同时验证每个调用点都位于 async def 内,并断言调用点数量大于 20 防止扫描静默失效。测试注册为 CPU CI(est_time=3)。
  4. 验证:作者用脚本机械核对 33 个旧调用点清零、每个改动行仅差 await 和 _async 后缀,且 isort、black、ruff 全部通过。由于是身份变换,本 PR 未做 benchmark。
文件 模块 状态 重要度
test/registered/unit/multimodal/test_processor_async_call_sites.py 审计测试 added 7.73
python/sglang/srt/multimodal/processors/clip.py 多模态处理 modified 4.89
python/sglang/srt/multimodal/processors/deepseek_ocr.py 多模态处理 modified 4.89

关键符号

process_and_combine_mm_data process_and_combine_mm_data_async process_mm_data_async _enclosing_function _call_sites test_no_processor_bypasses_the_worker_pool test_every_call_site_can_await test_the_scan_actually_finds_call_sites

关键源码片段

python/sglang/srt/multimodal/processors/clip.py core-logic

CLIP 处理器是 33 个迁移文件之一,展示 sync 调用改为 await async 变体的标准形态,适合作为代表性样例。

# 文件:python/sglang/srt/multimodal/processors/clip.py
# ClipImageProcessor.process_mm_data_async 是这次迁移的典型形态。async def process_mm_data_async(
    self, image_data: List[Union[str, bytes]], input_text, *args, **kwargs
):
    # 先加载并解析图像与文本数据,得到统一的 base_output
    base_output = await self.load_mm_data(
        prompt=input_text,
        multimodal_tokens=self.mm_tokens,
        image_data=image_data,
    )
​
    # 关键改动:原来直接调用同步函数 process_and_combine_mm_data,
    # 现在改为 await process_and_combine_mm_data_async(...)。
    # 无 executor 时该协程直接委托给同步实现,行为完全不变;
    # 有 executor 时才经 run_in_executor 进入线程池,实现并发。
    mm_items, input_ids, _ = await self.process_and_combine_mm_data_async(
        base_output, self.mm_tokens
    )
​
    return MultimodalProcessorOutput(
        mm_items=mm_items,
        input_ids=input_ids.tolist(),
    )

评论区精华

CI 失败是否由本 PR 引入 other

作者合并 main 后重新触发 CI,指出两个 root-cause 失败都是 runner 环境:XPU 任务报 OSError: libsycl.so.8 无法打开共享库,NPU 任务脚本退出 1 且无任何测试输出;其余失败均来自 PR check health 的 fast-fail 级联。

结论:这些 job 已在 #35238 中排除出 fast-fail 级联,失败与本 PR 无关;最终 nightly CI 未发现由改动引入的失败。 · 已解决

是否在本 PR 内直接开启并发 设计

作者在 Notes for reviewers 中明确说明开启并发是 per-model 决策:线程池只在预处理释放 GIL 时有效(PIL、numpy、torch C 代码可行),并且更多 worker 会分散请求到达、打碎 GPU prefill 批次;qwen_vl 选 2 个 worker,PaddleOCR-VL 上 8 个 worker 反而比 4 个慢。muse_glimmer 已在异步调用点但缺能力开关。

结论:本 PR 只统一调用点,不开启任何模型的并发;后续每个模型需单独测量后再接入。 · 已解决

Rebase 冲突与审计测试的自证 other

Rebase 到 CI 所需 base 后发现 main 上 transformers_auto.py 新增了一个 process_and_combine_mm_data 调用,正是被本 PR 新加的审计测试捕获;pixtral.py 和 qwen_audio.py 的冲突均以 main 版本为准并转换调用。

结论:审计测试在 rebase 中实际发现了漏网调用点,证明 AST 审计的必要性;冲突解决后仍是身份变换。 · 已解决

风险与影响

  • 行为不变性的前提:identity transform 依赖 process_and_combine_mm_data_async 在无 executor 时直接委托同步函数且不挂起;若未来该函数增加挂起点(如引入 await 其他协程),请求交错行为就会改变。
  • AST 扫描维护成本:审计测试依赖 _MULTIMODAL_ROOT 路径和 _EXEMPT 豁免名单(base_processor.py);新增处理器目录或改名后需要同步维护,否则可能漏检或误报。
  • 并发配置风险:本 PR 不开启并发,但为后续开启铺路;开启时若不逐个模型测量,可能出现吞吐反而下降(PaddleOCR-VL 8 workers 慢于 4 workers 的先例)。
  • 覆盖缺口:审计只锁定了调用点命名,不验证并发路径的正确性,也没有端到端回归测试;未来 executor 逻辑出错时可能不会被本测试发现。
  • 用户侧:行为逐位不变,现有部署零风险;未来模型只需三行改动(加能力开关 + 确认调用点在异步入口)即可接入并发预处理。
  • 系统侧:37 个调用点全部统一到异步入口,消除了“启动时构建线程池但请求全部绕过”的静默单 worker 模式;PaddleOCR-VL 等重预处理模型可直接受益。
  • 团队侧:新增的 AST 审计测试成为 permanent guard,main 上新增的 transformers_auto.py 调用点正是被它发现的,验证了测试价值。
依赖 async 委托语义 AST 扫描路径维护 并发开关需逐个验证 rebase 后新增调用点易遗漏 无端到端并发回归测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论