Prhub

#47312 [Bugfix] handle grammar compilation failures to avoid engine crash

原始 PR 作者 izhuhaoran 合并时间 2026-07-23 18:13 文件变更 7 提交数 9 评论 21 代码增减 +123 / -41

执行摘要

修复 grammar 编译失败导致引擎崩溃

PR 解决了 StructuredOutputManager._create_grammar 中长期存在的 TODO:# TODO: we still need to handle xgrammar compilation failures, though it should be unlikely as we test that up front as well.。上游验证(schema→BNF)无法捕获编译阶段(BNF→FSM/token-mask)的异常,导致 compile_grammar 失败直接崩溃引擎。该问题已在生产环境引发多起事故。

本 PR 值得精读,尤其是:

  1. 如何在 ThreadPoolExecutor+Future 模型中安全处理生产者线程的异常,避免全局崩溃。
  2. 将同步路径的异常统一包装为 Future 的设计模式,简化了调用方的处理逻辑。
  3. Review 讨论中关于错误消息暴露的权衡(安全性 vs 可调试性)是很好的安全边界案例。
讨论亮点
  • njhill 建议:“Rather than having a separate grammar_error field, it might be nicer to just use the fact that _grammar can be a Future, just let it be a failed future (even in the sync case).” → 作者同意并移除单独字段。
  • njhill 建议在 _try_promote_blocked_waiting_request 中检查 grammar 是否为 Exception。→ 作者采纳。
  • njhill 建议将 grammar 编译错误和 KV 加载错误合并处理,共享 finish_requests 调用。→ 作者合并。
  • njhill 建议“not include an arbitrary error message in the output here since it can reveal details about internals”,因此不应设置 stop_reason 为编译器消息。→ 作者移除 stop_reason 设置。

实现拆解

  1. 统一异常载体:修改 vllm/v1/structured_output/request.pyStructuredOutputRequest_grammar 类型,允许存入 Exception;修改 _check_grammar_completionFuture.result() 超时以外的异常时将 _grammar 直接赋值为该异常,使 grammar 属性可返回 Exception 类型。
  2. 同步路径异常包装:修改 vllm/v1/structured_output/__init__.py 中的 grammar_init,在非异步编译路径(_use_async_grammar_compilation=False)上使用 try/except 捕获异常,构造一个 Future 并通过 set_exception 放入异常,从而与异步路径保持一致的接口——request.structured_output_request.grammar 要么是有效 grammar,要么是异常。
  3. 调度器错误收集与处理:在 vllm/v1/core/sched/scheduler.pyScheduler 中添加 grammar_compile_error_reqs: set[str] 集合。在 update_from_output 中将 grammar_compile_error_reqs 与已有的 failed_kv_load_req_ids 合并到一个 error_req_ids 集合中,统一调用 finish_requests 并以 FINISHED_ERROR 状态结束这些请求,构造 EngineCoreOutput 通知客户端。在 _try_promote_blocked_waiting_request 中检测 StructuredOutputRequest.grammar 是否为 Exception,若是则将请求加入 grammar_compile_error_reqs 并跳过调度。
  4. 接口适配vllm/v1/core/sched/interface.pyfinish_requests 的返回类型从 list[tuple[str, int]] 改为 list[Request]vllm/v1/engine/core.py_send_abort_outputs 相应调整参数类型和内部实现。
  5. 测试覆盖:在 tests/v1/core/test_scheduler.py 中添加 test_grammar_compile_error_finishes_only_request,参数化 async_grammar,验证无论同步/异步编译,错误请求被正确终止且其他健康请求可继续调度。
文件 模块 状态 重要度
vllm/v1/core/sched/scheduler.py 调度器 modified 7.05
vllm/v1/structured_output/__init__.py 结构化输出 modified 6.78
vllm/v1/structured_output/request.py 数据结构 modified 6.65
tests/v1/core/test_scheduler.py 测试 modified 6.38
vllm/v1/engine/core.py 引擎核心 modified 6.04
vllm/v1/core/sched/interface.py 调度器接口 modified 5.38
tests/v1/core/test_async_scheduler.py 异步调度测试 modified 4.62

关键符号

StructuredOutputManager.grammar_init StructuredOutputManager._create_grammar StructuredOutputRequest._check_grammar_completion StructuredOutputRequest.grammar Scheduler.update_from_output Scheduler._try_promote_blocked_waiting_request EngineCore._send_abort_outputs Scheduler.finish_requests

分析完成后,这里会展示 LLM 生成的相对完整源码片段和详细注释。

评论区精华

使用 failed Future 统一异常传递 设计

njhill 建议:'Rather than having a separate grammar_error field, it might be nicer to just use the fact that _grammar can be a Future, just let it be a failed future (even in the sync case).' 作者同意并实现。

结论:采纳,移除独立的 grammar_error 字段,同步路径异常也包装为 Future。 · 已解决

检查 grammar 是否为 Exception 正确性

njhill 建议在 _try_promote_blocked_waiting_request 中检查 grammar 是否为 Exception 类型。

结论:采纳,在 _try_promote_blocked_waiting_request 中检测 Exception 并加入 grammar_compile_error_reqs。 · 已解决

合并 grammar 错误与 KV 加载错误处理 设计

njhill 建议将 grammar 编译错误和 KV 加载失败合并到同一个 error_req_ids 集合,统一调用 finish_requests。

结论:采纳,合并成一个 error_req_ids 集合并统一处理。 · 已解决

不暴露编译器错误消息给客户端 安全

njhill 建议不要在 stop_reason 中包含编译器错误消息,避免泄露内部信息。

结论:采纳,移除 stop_reason 设置,异常详情仅记录日志。 · 已解决

风险与影响

  • 接口兼容风险finish_requests 返回类型从 list[tuple[str, int]] 改为 list[Request],影响所有 SchedulerInterface 的实现(包括 AsyncScheduler)。需要确保下游实现同步更新。
  • 异常传播路径:异步编译路径下的异常通过 Future 传递,调度器通过 _check_grammar_completion 转换为 Exception 后仍可能被误用(如开发者直接访问 _grammar 未检查类型)。
  • 日志安全性:当前异常详情已记录到日志,但 stop_reason 不暴露错误消息,符合安全要求;但若后续其他需要区分错误类型时,可能不够。
  • 测试边界:测试覆盖了同步和异步编译路径,但未覆盖其他 grammar 后端(如 guidance、outlines)的异常情况,它们可能产生不同的异常类型。
  • 用户影响:单个 grammar 编译失败不再导致整个引擎重启,大幅提升含有大型 JSON schema 请求的生产环境稳定性(例如 xgrammar int16 索引限制触发时)。受影响请求收到 finish_reason=error,客户端应重试或修正输入。
  • 系统影响:引入 grammar_compile_error_reqs 集合和异常类型判断,对调度器每次迭代仅有微小额外开销。finish_requests 返回 Request 对象而非 tuple 使得调用方可以访问更多请求信息。
  • 团队影响:其他 scheduler 开发者需同步更新 finish_requests 实现以匹配新的返回类型。
接口返回类型变更 同步 / 异步路径统一

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论