结构化约束如何真正进入 sampler#
这章解决什么问题#
执行模型前面已经讲了 SamplingBatchInfo、vocab mask、grammar sync,但还差最后一块真正闭环的桥:当 response_format、json_schema、regex、ebnf 或 tool-call constraint 最终落成 sampling metadata 后,它们在 sampler 这一侧到底怎样实际改变 token 选择?
这一章的目标,就是把“结构化约束如何真正卡进 sampler”讲清楚。
为什么这章不是重复结构化生成章节#
结构化生成章节回答的是:
- 约束对象怎样创建
- grammar backend 怎样维护状态
- scheduler 怎样在 decode 前等待 grammar ready
这一章回答的则是:
- 一旦这些对象已经 ready,并进入
SamplingBatchInfo - sampler 在同一个
forward()里到底怎样对待它们
这已经是纯 execution-side 的问题。
一张图:约束在 sampler 看来,不是“额外功能”,而是输入分布的一部分#
这张图解决的理解障碍是:很多读者把结构化输出想成外部 validator,而不是 sampler 真实输入的一部分。
flowchart LR
Params["SamplingParams / response_format / tool constraint"] --> SBI["SamplingBatchInfo"]
SBI --> Mask["vocab_mask + apply_mask_func"]
Mask --> Logits["masked logits"]
Logits --> Sample["Sampler token selection"]图比纯文字多解释的一点是:约束一旦进入 execution,不再是“对输出结果做检查”,而是直接改写 logits 空间。
SamplingBatchInfo.update_regex_vocab_mask() 真正做了什么#
这是整条桥最值得盯住的函数之一。它会:
- 先检查
self.grammars是否存在 - 找到第一个可用 grammar 作为 mask allocator
- 为整个 batch 分配
vocab_mask - 对每个 request 的 grammar 执行
fill_vocab_mask(...) - 把 mask 移到执行设备上
这个顺序说明 grammar 对 sampler 的影响不是单请求即兴判断,而是一套正式的 batch-level 预处理。
apply_logits_bias(...) 是约束真正生效的关口#
在 SamplingBatchInfo.apply_logits_bias(...) 里,会按顺序应用:
- additive penalties
- scaling penalties
- orchestrator penalties
vocab_masklogit_bias
这里最关键的点是:vocab_mask 不在 sampler 外部单独处理,而是和其他采样前置变换一起进入统一的 logits 改写关口。
这说明从 execution 角度看,结构化约束不是旁路功能,而是与 penalty、logit bias 平级的一等采样前置变换。
为什么这会影响你对“采样参数”的理解#
如果你只是把采样理解成温度、top-p、top-k 的数学变换,那么结构化约束会看起来像一个后补丁。但源码组织已经明确表明:
- 温度 / top-p / top-k
- penalty
- logit bias
- vocab mask
都在执行边界汇合。也就是说,Sampler 在本质上是在一个“被多层约束重写过的 logits 空间”里选 token。
need_top_p_sampling 和 grammar mask 为什么不是同一类条件#
这一点也值得写清楚:
need_top_p_sampling、need_top_k_sampling主要改变 sampler 走哪种抽样算法- grammar mask 主要改变“候选空间中哪些 token 还活着”
它们都影响采样,但不是同一维度的影响。好技术书应该把这种维度区分讲清楚,否则读者容易把所有采样复杂度都看成一回事。
tool-call constraint 进入这条链后又发生了什么#
前面结构化生成章节已经写过,OpenAI chat 请求会把 tool-call constraint 转成 json_schema 或 structural_tag 再注入 sampling_params。到了 execution 层,重要的不是“它最初来自工具调用”,而是“它现在已经退化成 grammar / mask 语义的一种变体”。
也就是说,一旦到了 sampler,这些不同来源的约束会开始呈现统一形状:
- 要么改变 vocab mask
- 要么改变 logits 空间
这正是 system design 中“多种上游语义在低层收敛成同一执行形式”的经典例子。
这一层最容易出现的误判#
1. 以为 structured output 主要在输出后检查#
execution 侧已经把它变成采样前的 logits 改写了。
2. 以为 grammar mask 只是一个小辅助字段#
它会真正改变 token 候选空间,并触发更保守的同步语义。
3. 以为 sampler 的复杂度只来自 top-p/top-k#
实际上结构化约束和 penalties 也在同一关口汇合。
如果你想顺着这条桥读源码,先怎么走#
建议按这个顺序:
- 看
SamplingParams里哪些字段能承载结构化约束。 - 看
SamplingBatchInfo.from_schedule_batch(...) - 再看
update_regex_vocab_mask()和apply_logits_bias() - 最后回到
Sampler.forward()理解这些改写后的 logits 是怎样被采样的。
小结#
这一章真正想补齐的,是 execution model 里最容易被忽略的一点:
- 结构化约束不是停在 scheduler 边上
- 它会真正进入 sampler 视野,变成 logits 空间的一部分
- 也正因为这样,约束代价与采样代价在 execution 层会真正叠加
到这里,执行模型对“结构化约束如何进入采样”才真正形成闭环。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。