Prhub

#51795 [Bugfix] Reject negative token ids as out-of-vocabulary

原始 PR 作者 ECMGit 合并时间 2026-08-14 09:07 文件变更 2 提交数 3 评论 18 代码增减 +21 / -0

执行摘要

在校验路径上拒绝负 token id,返回 HTTP 400

调用方传入 token id 后,该 id 会在下游作为 embedding 索引使用,负索引永远无效;但现有校验只检查上界,导致负 id 通过校验并最终触发 CUDA 设备端断言使服务崩溃(ECMGit 在评论中确认此前 e2e 场景下服务器会崩溃且 PyTorch 无法捕获该输入)。PR 目的即补上对称的下界检查,在共享输入校验路径上统一拦截非法 token id。

值得精读的输入校验鲁棒性修复。重点关注两点:一是共享校验路径上如何以最小成本补齐边界检查;二是测试位置的调整(从 completion 到 pooling)体现了对 OpenAI schema 与引擎级校验分层拦截的理解,对类似校验场景有借鉴意义。

讨论亮点
  • DarkLight1337 在 issue 评论中询问 e2e 场景下传负 token id 会发生什么,ECMGit 回答会导致 CUDA 设备端断言崩溃,且 PyTorch 无法提前捕获,这直接论证了本次修复的必要性。
  • njhill 在 review 中对 min()max() 两次遍历长 prompt 提出性能疑问,随后自我否定("built-in functions are faster"),最终未要求改成单循环,确认该实现性能无损。

实现拆解

  1. 修改校验入口:在 vllm/v1/engine/input_processor.py_validate_model_input 方法中,新增 min_input_id = min(prompt_ids, default=0),并在计算 model_vocab_size 后增加 if min_input_id < 0: raise VLLMValidationError(...)。该错误经 VLLMValidationError 映射为 HTTP 400。
  2. 保持上界逻辑不变:原有的 max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1) 检查继续保留,新增检查与之对称,不改变已有合法上限的判定。
  3. 补充回归测试:在 tests/entrypoints/pooling/embed/test_online.py 中新增 test_negative_token_ids,通过 /v1/embeddings 接口发送 input=[-1],断言返回 openai.BadRequestError 且消息匹配 "out of vocabulary"。测试放在 pooling 端点是因为 OpenAI completion schema 已用 Field(ge=0) 在 pydantic 层拦截负 id,无法到达引擎层校验,而 pooling schema 没有该约束(见 commit 8e6a6d0)。
文件 模块 状态 重要度
vllm/v1/engine/input_processor.py 输入校验 modified 6.29
tests/entrypoints/pooling/embed/test_online.py 嵌入端点 modified 5.1

关键符号

_validate_model_input

关键源码片段

vllm/v1/engine/input_processor.py core-logic

核心修复文件:在共享输入校验路径 `_validate_model_input` 中新增负 token id 下界检查,是该 PR 的行为变更主体。

# vllm/v1/engine/input_processor.py(节选:_validate_model_input 中的 vocab 校验部分)
if prompt_ids and tokenizer is not None:
    max_input_id = max(prompt_ids, default=0)
    min_input_id = min(prompt_ids, default=0) # 新增:对称的下界计算
​
    # 这里取 tokenizer 与 model 两者 vocab 的较大者判断上界,
    # 因为 Qwen3 等模型的 tokenizer 与 model vocab 大小可能不一致。
    model_vocab_size = model_config.get_vocab_size()
​
    # 负 token id 同样属于 out-of-vocabulary,但原上界检查无法捕获;
    # 若放任其进入下游,会以负数索引 embedding,触发 CUDA 设备端断言。
    # 该路径为 generate / embedding / pooling 共享,因此一处修复覆盖三类请求。
    if min_input_id < 0:
        raise VLLMValidationError(
            f"Token id {min_input_id} is out of vocabulary"
        )
    if max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1):
        raise VLLMValidationError(
            f"Token id {max_input_id} is out of vocabulary"
        )
tests/entrypoints/pooling/embed/test_online.py test-coverage

回归测试:验证负 token id 在引擎级共享校验路径上被拒绝,并说明为何选择 pooling 端点而非 completion 端点。

# tests/entrypoints/pooling/embed/test_online.py(新增测试)
@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME])
async def test_negative_token_ids(client: openai.AsyncOpenAI, model_name: str):
    # 完成端点(/v1/completions)的 schema 已用 Field(ge=0) 拒绝负 token id,
    # 请求到不了引擎层;而 pooling 的 embeddings schema 没有该约束,
    # 因此这里通过 /v1/embeddings 验证共享的引擎级校验路径是否兜底。
    with pytest.raises(openai.BadRequestError, match=".*out of vocabulary.*"):
        await client.embeddings.create(model=model_name, input=[-1])

评论区精华

e2e 场景下负 token id 是否会导致崩溃 question

DarkLight1337 提问:本 PR 之前,e2e 传负 token id 会发生什么?引擎会崩溃吗?ECMGit 回答:会,服务器会通过 CUDA 设备端断言崩溃,且 PyTorch 无法捕获这个输入。

结论:确认负 token id 是真实崩溃源,验证了修复必要性。 · 已解决

长 prompt 下 min/max 两次遍历的性能 性能

njhill review 评论:考虑长 prompt 性能,是否应当用单循环同时取 min 和 max?随后自我否定:内置函数实际上更快。

结论:接受内置 min/max 实现,无需优化。 · 已解决

风险与影响

  • 共享路径影响面:校验位于 generate / embedding / pooling 三类请求共用的 _validate_model_input,行为收紧会同时影响所有入口,需回归这三类请求的正常输入路径。
  • 隐式行为变更:此前负 token id 可能以 Python 负索引语义流转到深层(尽管最终会崩溃或产生错误结果),现在直接返回 400,属于破坏性但正确的行为变化,需确认没有用户依赖该异常行为。
  • 测试覆盖盲区:新增测试只覆盖了 pooling 端点,completion 端点因 pydantic 拦截(错误消息为字段约束而非 "out of vocabulary")未在本 PR 内验证,存在两类端点错误消息不一致的轻微文档/行为差异。
  • 用户侧:传入负 token id 的请求从服务崩溃变为干净的 HTTP 400 错误,显著提升错误可诊断性。
  • 系统侧:避免 CUDA 设备端断言导致的进程级崩溃,提高服务稳定性;新增一个 min() 遍历,对长 prompt 的性能影响可忽略(njhill 已确认内置函数更快)。
  • 团队侧:改动仅 21 行(源码 10 行、测试 11 行),风险低,维护成本小;为后续更多输入边界校验提供了可参考的对称检查模式。
共享校验路径变更 行为收紧 测试仅覆盖 pooling 端点

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论