执行摘要
- 一句话:预热 readonly MM processor 并修正缓存路由
- 推荐动作:本 PR 值得一线工程师和架构师精读,特别是
BaseRenderer.warmup 的提取以及参数化 skip_mm_cache 的决策,展示了如何在不破坏现有接口的前提下修正路由逻辑。新建的测试文件可作为模拟多模态环境的参考。
功能与动机
在首次 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 同时修复这两个问题。
实现拆解
- 提取预热与清理方法:在
vllm/renderers/base.py 中将多模态处理器的预热逻辑提取为 _warmup_mm_processor() 方法,并新增 _clear_processor_cache() 静态方法,便于对任意 processor(包括 _readonly_mm_processor)进行预热和缓存清理。
- 启动时同时预热 readonly 处理器:在
BaseRenderer.warmup() 中,除原有 mm_processor 预热外,新增对 _readonly_mm_processor 的预热和缓存清理,日志输出 "Readonly multi-modal warmup completed in Xs"。
- 调整参数路由:在
OpenAIServingRender.render_chat() 和 render_completion() 中添加 skip_mm_cache 参数,默认 False。引擎执行路径(如 /v1/chat/completions)使用默认值,renderer-only 路径(如 /render)和令牌化路径传递 True。
- 调整各端点调用:
OpenAIServingChat、OpenAIServingCompletion 的引擎执行调用保持 skip_mm_cache=False;OpenAIServingTokenization 的令牌化调用传递 True;ServingTokens.serve_tokens() 因远程引擎独立运行,恢复为 skip_mm_cache=True。
- 添加回归测试:新增
tests/entrypoints/serve/tokenize/test_serving_tokenization.py,并在 test_completion_error.py、test_chat_error.py、test_generate_stream.py、test_warmup.py 中扩展测试用例,覆盖引擎路径(断言 skip_mm_cache=False)、renderer-only 路径(断言 skip_mm_cache=True)以及 readonly 预热回调验证。
关键文件:
vllm/renderers/base.py(模块 渲染器核心;类别 source;类型 core-logic;符号 _clear_processor_cache, _warmup_mm_processor): 核心变更:提取 _warmup_mm_processor 和 _clear_processor_cache,并在 warmup() 中新增 readonly processor 预热逻辑。
vllm/entrypoints/serve/render/serving.py(模块 渲染服务;类别 source;类型 core-logic;符号 render_chat, render_completion): 修改 render_chat 和 render_completion 添加 skip_mm_cache 参数,控制是否使用 readonly MM processor。
vllm/entrypoints/serve/disagg/serving.py(模块 分离服务;类别 source;类型 core-logic): 修改 serve_tokens 的 skip_mm_cache 参数从 False 回退到 True,因为远程引擎独立运行,需保持使用 readonly 处理器。
tests/entrypoints/serve/tokenize/test_serving_tokenization.py(模块 令牌化测试;类别 test;类型 test-coverage;符号 test_tokenize_chat_skips_mm_cache_for_renderer_only_path, test_tokenize_completion_skips_mm_cache_for_renderer_only_path): 新增测试文件,验证 tokenize 路径(chat/completion)在 renderer-only 时传 skip_mm_cache=True。
tests/entrypoints/openai/completion/test_completion_error.py(模块 完成测试;类别 test;类型 test-coverage;符号 test_openai_completion_keeps_mm_cache_for_engine_execution, test_renderer_only_completion_request_skips_mm_cache): 新增两个测试函数,验证引擎执行路径 skip_mm_cache=False,renderer-only 路径 skip_mm_cache=True。
tests/entrypoints/openai/chat_completion/test_chat_error.py(模块 聊天测试;类别 test;类型 test-coverage;符号 test_openai_chat_keeps_mm_cache_for_engine_execution, test_renderer_only_chat_request_skips_mm_cache): 新增两个测试函数,验证 chat 引擎执行路径 skip_mm_cache=False,renderer-only 路径 skip_mm_cache=True。
tests/renderers/test_warmup.py(模块 预热测试;类别 test;类型 test-coverage;符号 TestReadonlyMmWarmup, test_readonly_processor_apply_called_and_cache_cleared): 新增 TestReadonlyMmWarmup 测试类,验证 readonly MM processor 的 warmup 和 cache 清理被正确调用。
tests/entrypoints/serve/disagg/test_generate_stream.py(模块 生成流测试;类别 test;类型 test-coverage;符号 test_serve_tokens_skips_mm_cache_for_remote_engine_execution): 新增 test_serve_tokens_skips_mm_cache_for_remote_engine_execution 测试,验证 disagg 路径下 serve_tokens 使用 skip_mm_cache=True。
关键符号:_warmup_mm_processor, _clear_processor_cache, warmup, render_chat, render_completion, serve_tokens
关键源码片段
vllm/renderers/base.py
核心变更:提取 _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
新增两个测试函数,验证引擎执行路径 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
新增 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()
评论区精华
核心讨论围绕 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 按要求修改并添加了测试。
- Readonly MM processor 只应用于 renderer-only 路径 (correctness): fake0fan 按要求修改并添加了测试,最终代码中引擎路径使用默认 False,renderer-only 路径传 True。
- Disagg 远程引擎应保持 skip_mm_cache=True (design): 该路径最终保持 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 和缓存清理,增加短暂启动时间。团队:模块间责任更清晰,引入的参数沿袭易于扩展。测试覆盖增加,降低回归风险。
- 风险标记:缓存路由修正, 冷启动改善, 启动时间增加, 测试覆盖全面
关联脉络
参与讨论