快速结论:当你在 vLLM 中开启 return_assistant_tokens_mask=True,并且 prompt 里带有多模态占位符(audio / image / video)时,占位符展开后 assistant_tokens_mask 的条目数少于 token 数,导致第 9 位之后的 mask 全部错位。优先排查多模态 token 替换流程是否对 mask 做了同步扩展。
适用环境:Issue 中确认的复现环境为 Ubuntu 22.04.2 LTS(WSL2)、Python 3.12.13、PyTorch 2.11.0+cu129、CUDA 12.9、vLLM 0.23.1rc1、transformers 5.8.1、NVIDIA GeForce RTX 3080(驱动 576.88)。复现脚本本身不需要模型权重和 GPU,属于 CPU-only 复现。
最快修复方案:暂无确认的一步修复方案。Issue 最终以“该字段计划移除”方向收尾,未在本文讨论链中给出已验证的代码补丁。
注意事项:据讨论,该 mask 的已知下游消费者(speculators 库)已迁移到通过两次 render 的边界自行推导 loss mask,不再请求服务端的 assistant_tokens_mask,因此维护方倾向于移除该功能而非修复。若你自研训练流程直接依赖该字段,不要只做“尾部零填充”——填充只能修正长度,无法修正位置。
问题场景
在 vLLM 中通过 renderer_from_config + render_chat 渲染带多模态内容的对话,并传入 ChatParams(return_assistant_tokens_mask=True) 时触发。典型输入是:user 消息中包含 audio / image / video 占位符,且 assistant 回复位于该占位符之后。
Issue 复现用例使用 Qwen/Qwen3-ASR-0.6B-hf(revision 7f1569a48a89f3e3f4dc3a5c9d28bddd903bc76c),把同一段 4 秒静音以 transformers 的 {"type": "audio", "audio": ...} 与 vLLM 的 {"type": "input_audio", "input_audio": {"data": wav, "format": "wav"}} 两种方式各传一次,对比两边产出的 mask。复现于 main 分支 dffbb714e,后续在 887b4fbb3 上复现结果一致。
报错原文
[Bug]: assistant_tokens_mask misaligned after multimodal placeholder expansion
transformers: 71 ids, 71 mask entries
vllm : 71 ids, 20 mask entries, PlaceholderRange(offset=9, length=52, is_embed=None)
Expected: the mask has one entry per token, as transformers produces.
原因分析
该问题不是 truncate_prompt_tokens 引起的,也不是占位符展开本身出错,而是 mask 没有跟着展开一起走:
assistant_tokens_mask是在“占位符展开之前”的模板 token 流上构建的,此时音频只占 1 个 marker token,总长 20 个条目。- 随后音频在 offset=9 处展开为 52 个占位 token,prompt 从 20 变成 71 个 token,但没有任何环节把 mask 同步扩展。
- 结果是条目 i 不再描述第 i 个 token,位置 9 之后的每个 mask 条目都错位;服务层看到的尾部零填充只补了长度,没有修正位置。
- 该错位机制对 audio 之外同样成立:image 和 video 占位符走的是同一套 token 替换流程,因此同样受影响。
- 难点在于
PromptInsertion:插入本身不消耗目标 token,下游无法反推出某个 span 究竟替换了几个原始 token,所以“事后填充”无法做到正确,修复必须发生在存在映射关系的多模态 processor 的替换环节。
可能原因(讨论中提出的修复方向,均未在本文链条中落地验证):在 vllm/multimodal/processing/processor.py 的 _apply_matches / apply_token_matches(对应 _apply_token_matches_with_placeholders)中使用同一组 splice 扩展 per-token mask,为插入的 feature token(占位符永远不属于 assistant 内容)补零;同时把 PlaceholderFeaturesInfo(processor.py:577-581)目前丢弃的旧 span(match.start_idx / match.end_idx)保留下来并经 PlaceholderRange 暴露,让消费方能一次性拉伸并行数组。较保守的替代方案是:一旦有占位符发生展开就直接返回 null——chat_completion/protocol.py:456-465 已有“模板不支持该 mask 时返回 null”的约定,客户端可处理这种返回。
环境排查
- 确认 vLLM 版本(复现为
0.23.1rc1)以及是否构建自 main 的较新提交。 - 确认
transformers版本(复现为5.8.1),用于和 vLLM 输出做对照。 - 确认 Python 3.12.13、PyTorch 2.11.0+cu129、CUDA 12.9、NVIDIA 驱动 576.88。
- 确认调用侧是否真的传入了
ChatParams(return_assistant_tokens_mask=True);未开启该参数时不会走到这条路径。 - 确认 prompt 中是否含
input_audio/ image / video 等会触发展开的占位符,以及 assistant 回复是否在占位符之后。 - 对比
len(prompt_token_ids)与len(assistant_tokens_mask)是否一致,并检查mm_placeholders中的offset与length。 - 该复现不需要模型权重和 GPU,可在 CPU 环境下用上述脚本完成对照。
解决步骤
- 先用 Issue 中的脚本确认错位确实存在:
prompt_token_ids为 71,assistant_tokens_mask为 20,且mm_placeholders["audio"][0]为PlaceholderRange(offset=9, length=52, is_embed=None)。 - 用 transformers 的
apply_chat_template(..., tokenize=True, return_assistant_tokens_mask=True)得到 71 ids / 71 mask entries 作为参照基线,确认差异来自 vLLM 侧。 - 定位到多模态 processor 的 token 替换环节:
vllm/multimodal/processing/processor.py中的_apply_matches/apply_token_matches(即_apply_token_matches_with_placeholders)。 - 可优先尝试:在同一替换循环里用相同 splice 扩展
assistant_tokens_mask,被替换掉的原始 token 沿用原 mask 值,新插入的 feature token 位置补 0。 - 同时覆盖
PromptReplacement和PromptInsertion两条路径——后者不消耗目标 token,是“事后填充”无法修正的关键所在。 - 若改动过大,可优先尝试退路方案:检测到占位符发生展开时直接返回 null mask,符合
chat_completion/protocol.py:456-465已有的 null 约定。 - 确认该修复对 audio、image、video 三类占位符均生效,因为它们共用同一次替换流程。
验证方法
重跑 Issue 中的对照脚本,期望 vLLM 输出的 len(assistant_tokens_mask) 与 len(prompt_token_ids) 相等(均为 71),且 mask 中的 1-run 精确落在展开后流中 assistant 回复所对应的 token 上,而不是像现在这样落在 offset=9 之后的错误位置。建议同时用 image / video 占位符各跑一次,并在 CPU-only 环境下回归,确认不依赖 GPU 与模型权重。
参考来源
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。

![[Bug] MCP provider detail endpoint returns 500 "invalid input syntax for type uuid" — server_identifier passed where a provider UUID is expe](https://www.chat-gpts.plus/wp-content/uploads/2026/09/41512-4d065714-768x403.jpg)
