Prhub

#43016 [ROCm][CI] Stabilize 400 error return code for invalid schema inputs

原始 PR 作者 AndreasKaratzas 合并时间 2026-05-24 18:06 文件变更 11 提交数 16 评论 13 代码增减 +74 / -20

执行摘要

修复无效输入逃逸为 500,统一返回 400 错误

PR body 指出 Schemathesis 生成的畸形但可解析请求会穿透 Pydantic 模型,在后续 conversation 渲染、采样处理、token 验证路径中引发未捕获异常,返回 500 或超时。这是客户端输入错误,应返回 HTTP 400。该问题在 ROCm CI 中因服务器启动更慢而更频繁暴露。

值得精读。该 PR 展示了如何在 post-Pydantic 阶段捕获验证错误并映射到合适 HTTP 状态码,涉及 catch-all vs local exception handling 的设计权衡,对入口层开发者有参考价值。

讨论亮点

· 设计权衡:DarkLight1337 对在 serving 方法中本地 try-except 捕获 ValueError 提出质疑,认为应依赖全局 exception_handler 避免重复代码。AndreasKaratzas 解释初衷是显式声明预期错误,最终接受建议移除本地 catch,改用全局处理器。
· 性能考量:DarkLight1337 指出在 kv_transfer 清理包装器中每请求尝试 msgpack 编码会引入开销,AndreasKaratzas 确认并移除该 eager check。

实现拆解

  1. chat_utils.py_postprocess_messages 中增加 tool_calls 结构和类型校验,不符合则抛 VLLMValidationError。
  2. batch_serving.pychat_completion_full_generator_batch 中将 role 计算移入每个 prompt 循环,正确处理 batch 内不同角色。
  3. disagg/serving.pyserve_tokens 中添加 max_num_seqs 上限检查和 msgspec 序列化检查,失败返回 400。
  4. v1/utils.py 中修复 shutdown 超时逻辑,timeout=None 时不再设为 0 再取 max,直接使用 5 秒保底。
  5. 在协议模型(protocol.py)中给 batch messages 增加 min_length=1 约束。
    测试配套:更新 weight_transfer 测试 mock 适配新签名,添加 MockSchedulerConfig,补充 shutdown 超时测试。
文件 模块 状态 重要度
vllm/entrypoints/chat_utils.py 消息处理 modified 6.48
vllm/entrypoints/serve/disagg/serving.py 分片服务 modified 6.27
vllm/entrypoints/openai/chat_completion/batch_serving.py 批处理 modified 5.72
vllm/v1/utils.py 工具函数 modified 5.39
vllm/entrypoints/openai/chat_completion/protocol.py 协议模型 modified 5.11
tests/entrypoints/weight_transfer/test_weight_transfer_llm.py 权重传输测试 modified 5.05
tests/entrypoints/openai/completion/test_shutdown.py 关闭测试 modified 4.91

关键符号

_postprocess_messages serve_tokens chat_completion_full_generator_batch shutdown MockWeightTransferEngine.__init__ mock_create_engine

关键源码片段

vllm/entrypoints/chat_utils.py core-logic

核心验证增强:新增 tool_calls 结构和类型校验,涵盖绝大部分助手消息的非法输入场景。

def _postprocess_messages(messages: list[ConversationMessage]) -> None:
    '''后处理消息:将 tool_call 参数从 JSON 字符串解析为 dict,并校验结构'''
    for message in messages:
        if message['role'] == 'assistant' and 'tool_calls' in message:
            tool_calls = message.get('tool_calls')
            if not isinstance(tool_calls, list):
                continue
​
            if len(tool_calls) == 0:
                # 空 tool_calls 应移除,使模板走普通助手路径
                message.pop('tool_calls', None)
                continue
​
            for item in tool_calls:
                # 校验每个 tool_call 条目必须是 dict 类型
                if not isinstance(item, dict):
                    raise VLLMValidationError(
                        'assistant tool_calls 条目必须是对象。',
                        parameter='tool_calls',
                    )
​
                function = item.get('function')
                # 仅支持 function 类型的 tool_calls
                if item.get('type', 'function') != 'function' or not isinstance(
                    function, dict
                ):
                    raise VLLMValidationError(
                        'chat completions 仅支持 function 类型的 assistant tool_calls。',
                        parameter='tool_calls',
                    )
​
                # 将 arguments 从 JSON 字符串解析为字典(如果还不是)
                if content := function.get('arguments'):
                    if not isinstance(content, (dict, list)):
                        function['arguments'] = json.loads(content)
                else:
                    function['arguments'] = {}
vllm/entrypoints/serve/disagg/serving.py dependency-wiring

分片服务入口增加采样参数前置检查,防止无效参数到达引擎。

async def serve_tokens(self, request, raw_request=None):
    # ... 前置检查 ...
    sampling_params = request.sampling_params
    max_num_seqs = self.engine_client.vllm_config.scheduler_config.max_num_seqs
    if sampling_params.n > max_num_seqs:
        return self.create_error_response(
            f'sampling_params.n must be at most the server max_num_seqs '
            f'({max_num_seqs}), got {sampling_params.n}.'
        )
    try:
        msgspec.msgpack.encode(sampling_params)
    except (OverflowError, TypeError, ValueError) as e:
        return self.create_error_response(e)
    # ... 继续构建 engine_input ...
vllm/entrypoints/openai/chat_completion/batch_serving.py core-logic

修复 batch 对话中 role 计算时机,确保每个 choice 的角色正确。

# 在 chat_completion_full_generator_batch 方法中,原代码在开头一次计算 role
# 修改后,在每个 prompt 输出循环内逐步计算
for prompt_idx in range(len(generators)):
    final_res = final_results.get(prompt_idx)
    # ... 前置处理 ...
​
    for output in final_res.outputs:
        # ... logprobs, reasoning 等 ...
        # 动态获取当前 prompt 的 role
        role = (
            self.response_role
            if request.add_generation_prompt
            else request.messages[prompt_idx][-1]['role']
        )
        message = ChatMessage(role=role, reasoning=reasoning, content=content)
        # ... 构建 choice ...

评论区精华

Post-Pydantic 错误处理方式 设计

DarkLight1337 质疑在 serving 方法中使用本地 try-except 捕获 ValueError/ValidationError,认为应依赖全局 exception_handler 减少重复代码。AndreasKaratzas 解释本地捕获能显式声明这些是预期错误,最终接受 reviewer 建议移除本地 catch。

结论:采用全局 catch-all 异常处理器 · 已解决

KV transfer 参数验证开销 性能

DarkLight1337 指出在 _with_kv_transfer_rejection_cleanup 中每次请求都尝试 msgpack 编码 kv_transfer_params 会引入额外开销,即使请求成功。AndreasKaratzas 确认并移除该 eager check。

结论:移除,仅在需要时验证 · 已解决

风险与影响

· API 响应码变更:部分之前返回 500 的请求现在返回 400,可能被客户端或监控视为行为变化,但符合 HTTP 规范。
· shutdown 超时行为变化:timeout=None 时从 0→5 秒,所有调用方至少等待 5 秒,减少意外强制杀进程。
· 测试 mock 签名变更:MockWeightTransferEngine 和 mock_create_engine 增加 model 参数,外部直接调用需同步更新,已在 PR 中修复。
· 额外序列化开销:serve_tokens 中新增 msgpack.encode 对成功请求也有一次调用,但采样参数小,影响可忽略。

用户:无效请求将获得更准确的 400 错误,而不是 500 或超时,有利于调试和熔断。系统:API 边界更加健壮,减少因畸形请求导致的服务器不稳定。团队:ROCm CI Entrypoints 测试组 flakiness 预期降低,但部分 mock 签名变更需其他开发者留意。

API 响应码变更 关闭超时行为变化 测试 mock 依赖接口变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论