Prhub

#40797 [Feature] Warm up readonly multimodal processor during renderer startup

原始 PR 作者 fake0fan 合并时间 2026-04-25 11:57 文件变更 7 提交数 7 评论 4 代码增减 +368 / -27

执行摘要

预热 readonly MM processor 并修正缓存路由

在首次 renderer-only 多模态请求(如 /v1/chat/completions/render)时,由于 _readonly_mm_processor 未被预热,存在约 7-8s 的冷启动延迟。同时,reviewer DarkLight1337 指出 readonly MM processor 不应被用于涉及 engine 执行的请求,但原有 render_chat/render_completion 强制 skip_mm_cache=True,导致引擎路径错误地绕过了主缓存。本 PR 同时修复这两个问题。

本 PR 值得一线工程师和架构师精读,特别是 BaseRenderer.warmup 的提取以及参数化 skip_mm_cache 的决策,展示了如何在不破坏现有接口的前提下修正路由逻辑。新建的测试文件可作为模拟多模态环境的参考。

讨论亮点

核心讨论围绕 skip_mm_cache 的默认值路由。DarkLight1337 指出 "Actually the readonly MM processor shouldn't be used for any requests that involve engine execution",要求 render_chat/render_completion 接受 skip_mm_cache 参数默认 False。在与 disagg 相关的评论中,DarkLight1337 说明远程引擎场景可以保留 skip_mm_cache=True。fake0fan 按要求修改并添加了测试。

实现拆解

  1. 提取预热与清理方法:在 vllm/renderers/base.py 中将多模态处理器的预热逻辑提取为 _warmup_mm_processor() 方法,并新增 _clear_processor_cache() 静态方法,便于对任意 processor(包括 _readonly_mm_processor)进行预热和缓存清理。
  2. 启动时同时预热 readonly 处理器:在 BaseRenderer.warmup() 中,除原有 mm_processor 预热外,新增对 _readonly_mm_processor 的预热和缓存清理,日志输出 "Readonly multi-modal warmup completed in Xs"。
  3. 调整参数路由:在 OpenAIServingRender.render_chat()render_completion() 中添加 skip_mm_cache 参数,默认 False。引擎执行路径(如 /v1/chat/completions)使用默认值,renderer-only 路径(如 /render)和令牌化路径传递 True
  4. 调整各端点调用OpenAIServingChatOpenAIServingCompletion 的引擎执行调用保持 skip_mm_cache=FalseOpenAIServingTokenization 的令牌化调用传递 TrueServingTokens.serve_tokens() 因远程引擎独立运行,恢复为 skip_mm_cache=True
  5. 添加回归测试:新增 tests/entrypoints/serve/tokenize/test_serving_tokenization.py,并在 test_completion_error.pytest_chat_error.pytest_generate_stream.pytest_warmup.py 中扩展测试用例,覆盖引擎路径(断言 skip_mm_cache=False)、renderer-only 路径(断言 skip_mm_cache=True)以及 readonly 预热回调验证。
文件 模块 状态 重要度
vllm/renderers/base.py 渲染器核心 modified 7.47
vllm/entrypoints/serve/render/serving.py 渲染服务 modified 5.97
vllm/entrypoints/serve/disagg/serving.py 分离服务 modified 5.0
tests/entrypoints/serve/tokenize/test_serving_tokenization.py 令牌化测试 added 7.28
tests/entrypoints/openai/completion/test_completion_error.py 完成测试 modified 6.3
tests/entrypoints/openai/chat_completion/test_chat_error.py 聊天测试 modified 6.24
tests/renderers/test_warmup.py 预热测试 modified 5.98
tests/entrypoints/serve/disagg/test_generate_stream.py 生成流测试 modified 5.71

关键符号

_warmup_mm_processor _clear_processor_cache warmup render_chat render_completion serve_tokens

关键源码片段

vllm/renderers/base.py core-logic

核心变更:提取 `_warmup_mm_processor` 和 `_clear_processor_cache`,并在 `warmup()` 中新增 readonly processor 预热逻辑。

# vllm/renderers/base.py 中新增的预热和缓存清理方法。
# _warmup_mm_processor 接收任意 processor 进行预热,_clear_processor_cache 清理其缓存。
# warmup() 中现在同时预热主 mm_processor 和 _readonly_mm_processor。@staticmethod
def _clear_processor_cache(
    processor: "BaseMultiModalProcessor | None",
) -> None:
    if processor is None:
        return
    processor_cache = processor.cache
    if processor_cache is not None:
        processor_cache.clear_cache()def _warmup_mm_processor(
    self,
    processor: "BaseMultiModalProcessor",
    *,
    log_prefix: str,
) -> None:
    from vllm.multimodal.processing import TimingContext
    model_config = self.model_config
    mm_config = model_config.get_multimodal_config()
    mm_limits = {k: v for k, v in processor.info.allowed_mm_limits.items() if v > 0}
    start_time = time.perf_counter()
    processor_inputs = processor.dummy_inputs.get_dummy_processor_inputs(
        seq_len=model_config.max_model_len,
        mm_counts=dict.fromkeys(mm_limits, 1),
        mm_options=mm_config.limit_per_prompt,
    )
    _ = processor.apply(processor_inputs, timing_ctx=TimingContext(enabled=False))
    elapsed = time.perf_counter() - start_time
    logger.info("%s warmup completed in %.3fs", log_prefix, elapsed)def warmup(self, chat_params: ChatParams) -> None:
    # ... 聊天模板预热 ...
    if self.mm_processor:
        try:
            logger.debug("Warming up multi-modal processing...")
            self._warmup_mm_processor(self.mm_processor, log_prefix="Multi-modal")
        except Exception:
            logger.warning("Multi-modal warmup failed")
        finally:
            self.clear_mm_cache()
​
    if self._readonly_mm_processor is not None:
        try:
            logger.debug("Warming up readonly multi-modal processing...")
            self._warmup_mm_processor(self._readonly_mm_processor, log_prefix="Readonly multi-modal")
        except Exception:
            logger.warning("Readonly multi-modal warmup failed")
        finally:
            self._clear_processor_cache(self._readonly_mm_processor)
tests/entrypoints/openai/completion/test_completion_error.py test-coverage

新增两个测试函数,验证引擎执行路径 skip_mm_cache=False,renderer-only 路径 skip_mm_cache=True。

# tests/entrypoints/openai/completion/test_completion_error.py
# 两个测试分别断言引擎执行和 renderer-only 路径的 skip_mm_cache 值。@pytest.mark.asyncio
async def test_openai_completion_keeps_mm_cache_for_engine_execution():
    # 引擎执行路径(render_completion_request 内部调用 preprocess_completion)
    # 应保持 skip_mm_cache=False
    result = await serving_completion.render_completion_request(request)
    assert isinstance(result, list)
    assert (
        serving_completion.openai_serving_render.preprocess_completion.call_args.kwargs[
            "skip_mm_cache"
        ]
        is False
    )@pytest.mark.asyncio
async def test_renderer_only_completion_request_skips_mm_cache():
    # 直接调用 renderer-only 的 render_completion_request,
    # 应传递 skip_mm_cache=True
    result = await serving_completion.openai_serving_render.render_completion_request(request)
    assert isinstance(result, list)
    assert (
        serving_completion.openai_serving_render.preprocess_completion.call_args.kwargs[
            "skip_mm_cache"
        ]
        is True
    )
tests/renderers/test_warmup.py test-coverage

新增 TestReadonlyMmWarmup 测试类,验证 readonly MM processor 的 warmup 和 cache 清理被正确调用。

# tests/renderers/test_warmup.py
# 测试 readonly processor 的 warmup 过程中 apply 被调用且缓存被清理。class TestReadonlyMmWarmup:
    """Readonly MM processor warmup must mirror the render path behavior."""
​
    def test_readonly_processor_apply_called_and_cache_cleared(self):
        renderer = _make_renderer_mock({"image": 1})
        readonly_mm_processor = MagicMock()
        readonly_mm_processor.info.allowed_mm_limits = {"image": 1}
        renderer._readonly_mm_processor = readonly_mm_processor
​
        with patch("vllm.multimodal.processing.TimingContext", autospec=True):
            BaseRenderer.warmup(renderer, ChatParams())
​
        readonly_mm_processor.apply.assert_called_once()
        readonly_mm_processor.cache.clear_cache.assert_called_once()

评论区精华

Readonly MM processor 只应用于 renderer-only 路径 正确性

DarkLight1337 指出 readonly MM processor 不应被用于涉及 engine 执行的请求,要求 render_chat/render_completion 接受 skip_mm_cache 参数默认 False。

结论:fake0fan 按要求修改并添加了测试,最终代码中引擎路径使用默认 False,renderer-only 路径传 True。 · 已解决

Disagg 远程引擎应保持 skip_mm_cache=True 设计

DarkLight1337 评论指出 disagg 远程引擎独立运行,可以保留 skip_mm_cache=True。fake0fan 回复 Fixed。

结论:该路径最终保持 skip_mm_cache=True,符合设计。 · 已解决

风险与影响

兼容性风险:新增的 skip_mm_cache 参数默认值为 False,且所有现有调用点均已调整,向后兼容。但如果外部代码直接调用 render_chat/render_completion 且依赖于旧行为(强制 True),可能行为变化;不过此类用法应是内部或测试。性能风险:新增的 readonly warmup 增加启动时间约 8-9s,但这是单次预热,后续请求受益。功能风险:如果某些路径漏传 skip_mm_cache 或传错,可能导致缓存命中率下降或错误;但测试已覆盖主要路径。

用户:renderer-only 多模态请求首次延迟大幅降低(7-8s -> 0.04s);引擎路径缓存行为恢复正常。系统:启动时额外执行一次 readonly MM 的 apply 和缓存清理,增加短暂启动时间。团队:模块间责任更清晰,引入的参数沿袭易于扩展。测试覆盖增加,降低回归风险。

缓存路由修正 冷启动改善 启动时间增加 测试覆盖全面

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论