Prhub

#51931 [Misc] Use VLLMValidationError in pooling input validation

原始 PR 作者 frank-suwen 合并时间 2026-08-13 14:13 文件变更 3 提交数 2 评论 3 代码增减 +83 / -5

执行摘要

池化输入校验改用 VLLMValidationError,语义更精确

RFC #48227 指出 vLLM 入口错误处理碎片化:全库有 2k+ 处 raise ValueError,而语义化 VLLMValidationError 只有 80+ 处;AsyncLLM.generate() 把一切 ValueError 当作请求校验错误映射为 HTTP 400,导致模型执行等内部错误被误报为客户端错误,Prometheus 指标也可能把 4xx 记成 5xx。本 PR 是 Step 5「将入口层调用方错误迁移为 VLLMValidationError」的文件级落地,与 PR body 中提到的 #51753 同系列,聚焦 vllm/entrypoints/pooling/base/io_processor.py 中 4 处可由调用方触发的错误。

值得快速阅读,特别是作为 RFC #48227 迁移的样板 PR:关注点包括「哪些错误应该迁移、哪些应该保留」的判断标准、object.__new__(PoolingIOProcessor) 的轻量测试手法,以及刻意保持 parameter/valueNone 的设计取舍。该 PR 逻辑简单、无争议,但理解其边界划分有助于评审后续同系列迁移。

讨论亮点

Review 过程没有实质性技术交锋:claude[bot] 因 PR 来自 fork 而自动禁用代码审查,维护者 noooop 直接 APPROVED 并回复「thanks!」。值得提取的设计说明来自 PR body:作者明确划定了迁移边界——只迁移「调用方可触发的 4 个错误」,而 invalid request type、unsupported render params、priority 长度不匹配 3 处保留为 ValueError,理由是它们属于内部 guard 或当前公开 pooling 路径不可达;同时解释了 parameter/value 字段保持 None 的原因(在线/离线入口公开参数名不同)。

实现拆解

  1. 变更入口与依赖:在 vllm/entrypoints/pooling/base/io_processor.py 顶部新增 from vllm.exceptions import VLLMValidationError,这是语义异常迁移的依赖基础。
  2. 迁移 4 处 raise ValueErrorraise VLLMValidationError,集中在 PoolingIOProcessor 的校验路径:get_request_factory_offlineparam.taskpooling_task 冲突;_validate_chat_template 中未设置 --trust-request-chat-template 时拒绝请求级 chat template;_params_to_seq 中 prompts 与 params 数量不匹配;_lora_request_to_seq 中 prompts 与 lora_request 数量不匹配。错误消息文本保持不变。同时刻意保留 3 处 ValueErrorrender() 中的 unsupported render_params 类型(内部 guard)、_priority_to_seq 的 priority 长度不匹配(当前公开 pooling 路径不可达)、以及 invalid request type 分支。
  3. 新增单测文件 tests/entrypoints/pooling/test_io_processor.py(+76/-0):通过 object.__new__(PoolingIOProcessor) 构造实例绕过 __init__,直接调用 4 个私有/受保护校验方法,断言异常类型为 VLLMValidationError、消息文本不变、parametervalue 结构化字段保持 None
  4. 更新集成断言 tests/entrypoints/pooling/basic/test_encode.pytest_multiple_pooling_paramspytest.raises(ValueError) 改为 pytest.raises(VLLMValidationError),使 LLM.encode() 离线路径的异常语义与源码一致。
  5. 配套说明:未修改任何在线 API 处理逻辑,因为 VLLMValidationError 仍继承 ValueError,在线入口捕获 ValueError 的路径继续返回 HTTP 400;parameter/value 字段不设置是因为这些共享 helper 同时服务于在线与离线入口,公开参数名不同,结构化字段在此场景无意义。
文件 模块 状态 重要度
vllm/entrypoints/pooling/base/io_processor.py 入口校验 modified 6.05
tests/entrypoints/pooling/test_io_processor.py 池化测试 added 7.0
tests/entrypoints/pooling/basic/test_encode.py 编码集成 modified 3.68

关键符号

get_request_factory_offline _validate_chat_template _params_to_seq _lora_request_to_seq

关键源码片段

vllm/entrypoints/pooling/base/io_processor.py core-logic

核心变更文件:4 处面向调用方的 `raise ValueError` 迁移为 `raise VLLMValidationError`,并新增异常导入;同时明确保留 3 处内部 guard 不变的边界。

# vllm/entrypoints/pooling/base/io_processor.py(异常语义迁移后的核心校验逻辑)
from vllm.exceptions import VLLMValidationErrorclass PoolingIOProcessor:
    def get_request_factory_offline(self, ctx):
        # ...
        for param in params_seq:
            if param.task is None:
                param.task = pooling_task
            elif pooling_task == "plugin":
                # plugin 任务由 io_processor.parse_request 校验输入,
                # 因此允许 plugin 覆盖 pooling_task
                pass
            elif param.task != pooling_task:
                # 客户端同时指定互斥的 pooling task,属用户输入错误,
                # 迁移为 VLLMValidationError 以携带 400 语义
                msg = f"You cannot overwrite {param.task=!r} with {pooling_task=!r}!"
                raise VLLMValidationError(msg)
​
    def _validate_chat_template(
        self,
        request_chat_template: str | None,
        chat_template_kwargs: dict[str, Any] | None,
        trust_request_chat_template: bool,
    ):
        # 请求级 chat template 必须显式信任,否则拒绝;典型客户端 400 场景
        if not trust_request_chat_template and (
            request_chat_template is not None
            or (chat_template_kwargs
                and chat_template_kwargs.get("chat_template") is not None)
        ):
            raise VLLMValidationError(
                "Chat template is passed with request, but "
                "--trust-request-chat-template is not set. "
                "Refused request with untrusted chat template."
            )
        return None
​
    def _params_to_seq(self, params, num_requests):
        if isinstance(params, Sequence):
            if len(params) != num_requests:
                raise VLLMValidationError(
                    f"The lengths of prompts ({num_requests}) "
                    f"and params ({len(params)}) must be the same."
                )
            return params
        return [params] * num_requests
​
    def _lora_request_to_seq(self, lora_request, num_requests):
        if isinstance(lora_request, Sequence):
            if len(lora_request) != num_requests:
                raise VLLMValidationError(
                    f"The lengths of prompts ({num_requests}) "
                    f"and lora_request ({len(lora_request)}) must be the same."
                )
            return lora_request
        return [lora_request] * num_requests
tests/entrypoints/pooling/test_io_processor.py test-coverage

新增 4 个单测覆盖 4 个迁移点,验证异常类型、消息文本与结构化字段,是本次变更的测试主体。

# tests/entrypoints/pooling/test_io_processor.py(新增单测,验证异常类型与字段)
@pytest.fixture
def processor() -> PoolingIOProcessor:
    # 用 object.__new__ 绕过 __init__,只测校验逻辑本身,避免构造完整依赖
    return object.__new__(PoolingIOProcessor)def test_rejects_untrusted_request_chat_template(processor: PoolingIOProcessor):
    with pytest.raises(VLLMValidationError) as exc_info:
        processor._validate_chat_template("template", None, False)
​
    assert str(exc_info.value) == (
        "Chat template is passed with request, but "
        "--trust-request-chat-template is not set. "
        "Refused request with untrusted chat template."
    )
    # parameter/value 保持 None:共享 helper 的公开参数名在在线与离线入口
    # 间不同,结构化字段在此场景无意义
    assert exc_info.value.parameter is None
    assert exc_info.value.value is Nonedef test_rejects_mismatched_pooling_params(processor: PoolingIOProcessor):
    with pytest.raises(VLLMValidationError) as exc_info:
        processor._params_to_seq([PoolingParams()], num_requests=2)
​
    assert str(exc_info.value) == (
        "The lengths of prompts (2) and params (1) must be the same."
    )
    assert exc_info.value.parameter is None
    assert exc_info.value.value is None

评论区精华

保留部分 ValueError 的边界划分 设计

PR body 说明:invalid request type、unsupported internal render parameters、priority 长度不匹配 3 处保留为 `ValueError`,因为属于内部 guard 或当前公开 pooling 路径不可达;`parameter`/`value` 字段不设置是因为共享 helper 服务于在线与离线入口,公开参数名不同。

结论:维持 4 处迁移、3 处保留的边界,错误消息与字段行为不变。 · acknowledged

fork 仓库自动化 review 被禁用 other

claude[bot] 指出本 PR 来自 fork,自动 review 被禁用,可由维护者触发一次性 review。

结论:维护者 noooop 人工 APPROVED 并回复「thanks!」。 · 已解决

维护者审批 other

noooop 对 PR 进行 APPROVED,仅回复「thanks!」,无额外修改意见。

结论:PR 合入 main。 · 已解决

风险与影响

  1. 异常类型变更影响离线调用方LLM.encode() 现在抛 VLLMValidationError,依赖 ValueError 精确匹配的调用方仍兼容(继承关系),但依赖异常子类区分的调用方需更新。
  2. 部分迁移导致类型不一致_priority_to_seq 的 priority 长度不匹配仍抛 ValueError,而相邻的 _params_to_seq_lora_request_to_seq 已迁移,同一文件内离线调用方会看到两种异常类型,可能造成困惑。
  3. 未来 reparent 风险:RFC #48227 Step 1 计划将 VLLMValidationError 的父类从 ValueError 改为 VLLMClientError,届时任何仍靠 except ValueError 捕获校验错误的旧调用方会静默失效,本 PR 的迁移是前置条件但并非终点。
  4. 测试覆盖:4 个新单测为纯逻辑测试,不启动模型;test_encode.py 的模型集成断言在本地未执行,依赖 CI 覆盖,存在 CI 环境差异风险。整体影响面小,主要集中于 pooling 入口层。

对用户/调用方:离线 pooling 调用(LLM.encode())的校验异常从裸 ValueError 变为带语义的 VLLMValidationError,便于程序化处理;在线 OpenAI/Pooling 服务 HTTP 状态码不变。对系统:这是 RFC #48227 逐步落地的一环,后续引擎层 catch 切换为 VLLMClientError 后,本 PR 覆盖的路径能正确以 4xx 语义通过;同时 test_io_processor.pyobject.__new__ 直测私有方法的模式为后续同类迁移提供了低成本测试样板。对团队:迁移边界清晰(调用方错误 vs 内部 guard),可作为后续文件迁移的评审参照。

部分迁移导致异常类型不一致 离线调用方异常类型变更 依赖 ValueError 的旧 catch 在 RFC reparent 后失效 模型级集成测试依赖 CI 覆盖

关联 Issue

#48227 [RFC]: Standardize vLLM Entrypoint Error Handling

完整报告

参与讨论