约束 mask、grammar 同步与 overlap 限制#

执行模型前面已经解释了 sampling、finish 语义、output processing 和结构化生成会在这附近相遇,但如果没有一章把“grammar 到底怎样真正影响采样和执行模式”单独讲透,这条桥仍然会显得断裂。很多读者会自然把 grammar 想成“sample 之后再检查输出是否合法”;源码真正做的事情远不止如此。

这章的价值,正在于把 grammar 从结构化生成章节里重新拉回 execution loop:它不仅会改写 vocab 空间,还会改变 token 同步策略,甚至在 overlap + spec 场景下直接收紧执行路径。也就是说,这不是“功能层的约束”,而是 execution model 自己的一部分。

先把 grammar 放回采样边界#

如果只读结构化生成章节,你会知道:

  • grammar backend 能生成 vocab mask
  • GrammarManager 会准备 grammar 对象

但你仍然不知道这些对象怎样真正进入 execution loop。下面这张图的职责,就是把这条桥明确压成一条 execution-side 链:

flowchart LR
    Grammar["req.grammar / vocab mask"] --> SInfo["sampling_info.grammars"]
    SInfo --> Samp["Sampler / token selection"]
    Samp --> Sync["TP sync when grammars present"]
    Sync --> Out["output processing / accept_token"]

图里最重要的一点是:grammar 并不是“采样之外的附属层”,而是直接卡在 sampling 边界上,并继续影响后面的同步和输出处理。

grammar 真正进入执行层的入口其实在 ScheduleBatch#

这也是很容易被忽略的一点。schedule_batch.py 在构造 ModelWorkerBatch 前,会显式检查:

  • 如果 self.sampling_info 存在
  • self.has_grammar
  • 就把 self.sampling_info.grammars = [req.grammar for req in self.reqs]

这说明 grammar 对象并不是停在 scheduler 外面的旁路字段,而是在准备 worker batch 时,就已经被正式塞进 sampling metadata。换句话说,从这一刻开始,结构化约束已经是 execution 输入,而不是结构化生成章节独有的上游概念。

这类桥接点特别值钱,因为它能让读者真正看清:为什么 execution model 和 structured generation 在书里必须互相回扣。

grammar 存在时,sampler 为什么必须更保守地同步 token#

sampler.py::_sync_token_ids_across_tp(...) 的条件特别值得读。正常情况下,SGLang 为了性能,并不默认同步最终 token ids across TP;但当:

  • sampling_info.grammars 存在

时,系统会显式触发一次 all_reduce 来同步最终 token ids。源码注释已经把原因写得很直白:平时不做默认同步是为了性能,但使用 grammar,尤其是 xgrammar 时,不同 TP rank 之间出现稀有非确定性的风险更高,因此必须更保守地同步,避免 rank desync 之后挂死。

这是一条特别像“好系统书的桥章节”该讲的内容,因为它把结构化生成从功能层一下拉到了分布式一致性层。只要这一点读稳了,读者就不再会把 grammar 只看成“输出格式更规整”,而会开始看到它对执行路径也有真实代价。

vocab mask 的本质,是直接改写采样支持集#

xgrammar_backend.pyllguidance_backend.pyoutlines_backend.py 这几条后端路径看,grammar 对象都会实现:

  • allocate_vocab_mask(...)
  • fill_vocab_mask(...)
  • apply_vocab_mask(...)

这意味着 grammar 对 execution model 的直接影响并不是“抽象上的约束”,而是把当前可选 vocab 空间直接改写成一个受当前 grammar 状态限制的支持集。execution model 到这里,不是“生成之后再过滤”,而是在 sampling 前就直接收窄了候选空间。

这也是为什么第 5 节不能只讲 sampler 算法,而必须把 grammar mask 一起讲进去。否则采样边界会显得不完整。

overlap + spec + grammar 的组合为什么会触发收紧#

这一点是 execution model 最像工程折中的地方之一。scheduler.py 里会判断:

  • 当前 batch 是 spec_v2
  • batch.has_grammar
  • batch.forward_mode.is_decode()
  • 且结果队列不为空

一旦满足这些条件,need_grammar_sync 为真,本轮就会关闭 overlap。这个判断特别值钱,因为它说明 grammar 的代价不是“稍微让 sampling 慢一点”,而是会进一步反向约束 execution model 允许使用的优化组合。

这可以非常明确地写成一本系统书里的 tradeoff:

  • overlap + spec 的收益是更高并行度
  • grammar 的代价是更严格的一致性和同步要求
  • 当前实现选择在这个组合下保守退回,优先保证正确性

这类 tradeoff 一旦讲透,读者就更容易理解为什么系统并不总是“功能和性能一起要到最大”。

这章属于 execution model,而不是结构化生成的尾注#

因为到这里,讨论的重点已经不再是 grammar 对象怎样创建,而是它一旦存在,会怎样改写:

  • sampling metadata
  • TP 同步策略
  • overlap 允许性
  • output processing 之后的 accept_token(...)

这些都已经是 execution loop 自己的行为边界,而不是单纯的上游 grammar 生命周期。因此把这章放在第 5 节,比把它塞回第 6 节更能帮助读者形成稳定心智模型。

最容易出现的三种误判#

第一,误以为 grammar 只是输出后验证。
实际上它会在采样前直接改写可选 token 空间。

第二,误以为使用 grammar 只会影响功能正确性,不会影响执行模式。
源码已经明确显示它会改写 overlap 和 TP 同步策略。

第三,误以为 sampler 的 token 同步逻辑和结构化生成无关。
恰恰相反,grammar 是触发更保守同步策略的重要条件之一。

真正怀疑结构化约束导致执行层行为变化时,更稳的顺序#

建议按这个顺序:

  1. 先看 ScheduleBatch.has_grammar 是否为真。
  2. 再看 sampling_info.grammars 是否真的被填进 worker batch。
  3. 再看 sampler 是否因为 grammar 走了额外 TP token 同步。
  4. 再看当前 batch 是否因此关闭了 overlap。
  5. 最后才判断问题主要在 grammar object 自身,还是 execution path 被迫收紧。

这条顺序最重要的意义,是它先验证“grammar 有没有真正进入 execution model”,再去判断它在 execution model 里造成了哪类代价。

小结#

这章真正补齐的,是 execution model 和 structured generation 之间最关键的一座桥:grammar 对象一旦进入 sampling_info,就会直接改写采样支持集、TP 同步策略和执行模式组合。

读懂这一层之后,execution model 才算真正把“约束怎样进入 loop”讲透。