快速结论:该问题出现在 vLLM 调度器启用 disable_chunked_mm_input=True(即禁止多模态输入分块)时,单个多模态 item 仍可能被拆到多个 prefill chunk 中执行;优先排查 self.scheduler_config.disable_chunked_mm_input 守卫中的边界判断(是否存在 off-by-one)。
适用环境:Issue 中确认在 vLLM 生产 Docker 镜像 vllm/vllm-openai:v0.26.0-cu129-ubuntu2404、CUDA 12.9.1、构建 commit ffd46bfab2128bb84146050e98b51a617c6575ab 下复现;复现讨论基于 commit 83d4c6196ad90f98515c6be113cb2e957b60a832 的调度器代码(该守卫在当时的 main 上仍相同)。测试模型为 llava-1.5-7b。原文未给出 Python 与 PyTorch 精确版本。
最快修复方案:暂无确认的一步修复方案。Issue 讨论中更倾向的候选修复是把守卫中 (A) 处的严格小于 < 改为 <=,并保留 (A) 检查;同时因为该改动会让编码器-解码器(encoder-decoder)模型进入该守卫并回滚为 0 token,讨论建议在守卫中显式加上 and not self.is_encoder_decoder。上述修复在 Issue 关闭时尚未合并验证。
注意事项:单改 <= 会导致 tests/v1/core/test_scheduler.py 中三个交叉注意力测试失败(test_cross_attn_blocks_not_over_allocated、test_cross_attn_blocks_not_under_allocated、test_variable_length_cross_attn_block_allocation,报 assert 0 == 3)。此外,compute_mm_encoder_budget() 会校验 max_num_batched_tokens,但没有任何逻辑校验 long_prefill_token_threshold:当该阈值低于单个 mm item 长度时,仅加 <= 会让该请求每步都拿 0 token、永久饥饿(engine 持续 idle);long_prefill_token_threshold 影响属于讨论中修正的“不会饥饿”说法,使用前需注意。
问题场景
在 vLLM 上运行多模态模型(讨论中为 llava-1.5-7b)推理,并通过 disable_chunked_mm_input=True 期望禁止多模态输入被拆分到多个 prefill chunk 时,实际调度仍会把一个 mm item 分片。典型可触发配置为 max_num_batched_tokens=800、long_prefill_token_threshold=770,同时存在一个 3000 token 的纯文本请求和一个 mm item 位于 [3, 769) 的请求。
报错原文
[Bug]: A multimodal item can still be split across prefill chunks with disable_chunked_mm_input=True
step 1: scheduled={'text': 770, 'mm': 3} mm_req: computed 0 -> 3
step 2: scheduled={'text': 770, 'mm': 30} mm_req: computed 3 -> 33 # item chunked
step 3: scheduled={'text': 770, 'mm': 30} mm_req: computed 33 -> 63 # item chunked
step 4: scheduled={'text': 690, 'mm': 110} mm_req: computed 63 -> 173 # item chunked
assert 0 == 3
原因分析
可能原因是调度器中 self.scheduler_config.disable_chunked_mm_input 相关守卫存在 off-by-one:守卫包含 (A) 与 (B) 两个判断,(A) 使用严格 <。当 num_computed_tokens=3、start_pos=3 时,(A) 计算 3 < 3 为 False,整个守卫在 (B) 被求值前短路,而这恰恰是 item 尚未开始计算的边界状态,(B) 本可捕获重叠。由于回滚会把 num_computed_tokens 停在 start_pos 上,下一步必然再次命中该 off-by-one,于是 item 被逐步分片。另一个相关点:compute_mm_encoder_budget() 会校验 max_num_batched_tokens,但未校验 long_prefill_token_threshold;当每步 token 配额低于单个 mm item 长度时,会出现结构性触发。编码器-解码器模型下 start_pos 恒为 0,mm_position.length 为编码器长度、不属于解码器 token 序列,因此该范围算术在它们身上无意义,靠 x < 0 静默排除。
环境排查
- 确认 vLLM 版本与镜像:Issue 复现环境为
vllm/vllm-openai:v0.26.0-cu129-ubuntu2404,构建 commitffd46bfab2128bb84146050e98b51a617c6575ab。 - 确认 CUDA 版本:环境变量显示
CUDA_VERSION=12.9.1;NVIDIA_REQUIRE_CUDA=cuda>=12.9。 - 确认算子架构:
TORCH_CUDA_ARCH_LIST=7.5 8.0 8.6 8.9 9.0 10.0 12.0。 - 确认调度参数:
max_num_batched_tokens=800、long_prefill_token_threshold=770(检查阈值是否低于单个 mm item 长度)。 - 确认模型类型:是否运行多模态模型(讨论中为 llava-1.5-7b),以及是否为 encoder-decoder 架构。
- 确认复现 commit 上该守卫代码是否仍与
83d4c6196ad90f98515c6be113cb2e957b60a832一致。
解决步骤
- 定位调度器中读取
self.scheduler_config.disable_chunked_mm_input的守卫逻辑,确认 (A) 判断使用严格<,并在num_computed_tokens=3、start_pos=3时验证 (A) 返回 False 导致 (B) 被短路。 - 按讨论中更倾向的方案,把 (A) 的严格小于
<改为<=(保留 (A),不整体删除),以只改变边界而不移除检查。 - 在该守卫中显式补充
and not self.is_encoder_decoder,避免编码器-解码器模型x <= 0进入守卫后被每步回滚为 0 token。 - 对低于单个 mm item 长度的
long_prefill_token_threshold场景另作评估,补充校验或限制,避免仅加<=后请求永久饥饿。 - 对使用相同场景的调度器级 stub/单测复跑,确认不再出现 item 被分片的 step 序列。
验证方法
使用相同的 llava-1.5-7b 调度场景(max_num_batched_tokens=800、long_prefill_token_threshold=770、文本 3000 token + mm item [3, 769))复跑,观察在 computed 3 -> 33/63/173 一类步骤中该请求是否获得 0 token 并在后续步骤一次性调度整个 item,而不是被拆成多个 chunk;同时运行 tests/v1/core/test_scheduler.py 中三个交叉注意力测试,确认通过。注意 long_prefill_token_threshold 低于 item 长度的场景需单独验证不会出现每步 0 token 的饥饿。
参考来源
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


![ValueError: The number of images in the text [2] and images [1] should be the same.](https://www.chat-gpts.plus/wp-content/uploads/2026/10/47217-a358ec78-768x403.jpg)