Grammar queue、ready 检查与 decode 前置等待#
这章解决什么问题#
前面的结构化生成章节已经讲了 grammar backend 自身怎样维护状态、GrammarManager 怎样做预处理缓存与超时控制,但还差最后一层非常关键的 scheduler 结合点:当 grammar 对象还没 ready 时,请求到底停在哪里?ready 之后又怎样重新回到 waiting queue?换句话说,约束生成不是“编译完就算了”,它还会真实地改变请求在调度层里的等待方式。
这一章就是把这层 decode 前置等待讲清楚。
为什么这层应该放在结构化生成章节#
因为这里讨论的核心不是一般排队,而是“结构化约束会给请求增加一个额外的前置等待阶段”。这和普通 waiting queue 不是同一回事,它是 grammar 特有的进入门槛。
也正因为这样,它更适合写在结构化生成章节末端,而不是只在调度章节里一带而过。
一张图:带 grammar 的请求在真正进入普通等待队列前,还会多经过一层 queue#
这张图解决的理解障碍是:很多读者会以为 grammar 请求和普通请求的区别只发生在 decode 时,其实更早就已经分叉了。
flowchart LR
Req["request with json_schema / regex / ebnf / structural_tag"] --> GM["GrammarManager.process_req_with_grammar()"]
GM --> GQ["grammar_queue"]
GQ --> Ready["get_ready_grammar_requests()"]
Ready --> WQ["waiting_queue"]
WQ --> Decode["normal scheduler path"]这张图比纯文字多解释的一点是:grammar 请求在成为普通 waiting request 之前,先要通过一层“约束就绪”检查。
process_req_with_grammar(...) 真正改变了请求进入调度的顺序#
scheduler.py 在处理生成请求时会调用:
added_to_grammar_queue = self.grammar_manager.process_req_with_grammar(req)
if not added_to_grammar_queue:
self._add_request_to_queue(req)这说明 grammar 请求和普通请求在最开始就分岔了:
- 没有 grammar:直接进普通队列
- 有 grammar 且未命中 ready cache:先进 grammar queue
这是一条非常重要的结构化生成事实,因为它说明约束不是只在 decode 时加一层检查,而是会改变请求进入普通调度主线的时机。
grammar_queue 在等待什么#
它等待的不是模型资源,也不是 network,而是 grammar object 本身从 future 变成 ready object。也就是说,这是一个“前置约束准备队列”,而不是普通等待执行队列。
这类区分特别值得技术书写出来,因为它帮助读者在排障时问对问题:
- 这条请求慢,是因为 GPU 忙?
- 还是因为约束对象还没准备好?
get_ready_grammar_requests() 为什么是关键函数#
这个函数做了几件特别值得强调的事:
- 轮询
grammar_queue看哪些 request 的 future 已 done。 - 如果等待过久,则累计
grammar_wait_ct并可能进入 timeout 路径。 - 多 rank 场景下,还会通过
all_gather_object同步各 rank 的 ready / failed 结果。 - ready 请求会把 future result 落成真正 grammar object,并写回 cache。
- failed 请求则会被置成
InvalidGrammarObject(\"Grammar preprocessing timed out\")并 abort。
这说明 grammar queue 并不是本地小队列,而是会受到多 rank 一致性语义约束的正式调度前置层。
为什么多 rank 要做 ready / failed 同步#
因为如果不同 rank 对“这个 grammar 已经 ready 了吗”产生分歧,后续 decode 就会在结构化约束上失去一致性。也就是说,grammar queue 的同步价值和 execution 里的 token id 同步属于同一类问题:都在维护跨 rank 的一致性边界。
cache 命中和 grammar queue 的关系#
如果 process_req_with_grammar(...) 命中了可立即复用的 grammar object,就不需要进 grammar queue;只有未命中或仍在 future 阶段时,才需要额外等待。
这说明 cache 的意义不仅是减少编译时间,还会直接改变请求是否需要多经历一个调度前置阶段。
timeout 为什么要落成 abort,而不是无穷等待#
get_ready_grammar_requests() 明确在超时后:
- cancel future
- 把 cache 写成
InvalidGrammarObject(\"Grammar preprocessing timed out\") - 对请求设置 abort
这说明系统宁可显式失败,也不愿让 grammar queue 无穷拖住请求。这是一个非常值得写进书里的运行时取舍:结构化生成必须是可失败、可诊断的,而不是无限等待的黑箱。
scheduler 为什么会在取新 prefill batch 前先处理 grammar queue#
_get_new_batch_prefill_raw(...) 的开头就会检查:
- grammar queue 里有没有 ready request
如果有,就先把这些 request 放回 waiting queue。也就是说,grammar ready 检查本身已经成为“是否能形成下一批 prefill”的一部分。
这说明 grammar queue 不是与调度主循环平行的后台线程,而是会在关键 batch 形成点被正式消费。
这一层最容易出现的误判#
1. 以为 grammar 准备只会影响第一次请求#
实际上 cache 命中与否、future 是否 ready,会持续影响同类请求进入 waiting queue 的时机。
2. 以为 grammar queue 只是本地编译等待#
多 rank 同步逻辑说明它还承担一致性职责。
3. 以为“结构化结果慢”只可能是 decode 慢#
实际上 grammar preprocessing 与 queue ready 也可能是瓶颈。
如果你怀疑问题在 grammar queue,先怎么查#
建议按这个顺序:
- 看请求是否真的带了 grammar 约束。
- 看
process_req_with_grammar(...)是否把它送进了 grammar queue。 - 看
grammar_wait_ct是否持续增长。 - 看 ready/failed 同步后它是进入 waiting queue,还是被 abort。
- 最后再判断问题是 grammar backend 编译慢、cache 没命中,还是多 rank 一致性导致的等待。
小结#
这一章真正想补齐的,是结构化生成里经常被漏掉的“调度前置等待层”:
- grammar 请求不总是直接进 waiting queue
- 它们可能先进入 grammar queue 等待约束 ready
- 这个前置阶段会真实改变请求生命周期和调度行为
到这里,结构化生成章节才真正从“约束对象如何创建”扩成“约束对象准备好之前,请求如何被调度地等待”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。