SamplingParams 的规范化、互斥约束与 stop 语义#

执行模型前面已经讲了 SamplingBatchInfo、vocab mask、grammar sync 和 structure-aware sampling,但如果没有一章把 SamplingParams 自己的“预编译行为”单独讲清楚,读者仍然很容易误解一件事:sampler 看到的并不是调用方原样传进来的那份参数,而是一份已经被构造器、verify()normalize() 主动整理过的内部采样语义。

这章真正补的是 execution model 里很基础、却最容易被忽略的一层:参数并不是被动配置,而是会在进入 batch 之前先被验证、收敛、改写,并提前决定 stop 相关的执行边界。只要这一层读稳了,后面的 sampler、grammar 和 output processor 行为就更不容易看成“很多偶然分支”。

先把这层放回采样主线#

很多人天然会把 SamplingParams 想成一个字段袋子:temperature、top-p、top-k、stop,最后交给 sampler 用。源码里真正更重要的地方是,中间还隔着一次参数语义整理。下面这张图的作用,就是把这一步显式化:

flowchart LR
    Raw["raw sampling fields"] --> SP["SamplingParams"]
    SP --> Verify["verify()"]
    SP --> Norm["normalize()"]
    Verify --> SBI["SamplingBatchInfo"]
    Norm --> SBI
    SBI --> Sampler["Sampler.forward()"]

图里最重要的一点是:执行层真正消费的,是“经过验证与规范化之后的采样语义”,而不是调用方原样给出的字段拼盘。

SamplingParams 构造阶段就已经在改写行为#

最值得技术书点明的,不是字段多,而是对象构造阶段已经开始主动收敛语义。例如:

  • 如果 0 <= temperature < _SAMPLING_EPS,系统会直接把它视作 greedy sampling,并设置 top_k = 1
  • 如果 top_k == -1,系统会把它改写成统一内部语义 TOP_K_ALL

这说明很多执行层行为,在 sampler 真正启动之前就已经被配置层改写过了。运行时并不要求每个下游模块都去理解“用户写的 -1 代表什么”或“几乎零温度意味着什么”,而是尽量在参数对象内部就完成统一收口。

这类设计特别值得写进书里,因为它能帮助读者建立一个稳定判断:execution model 里很多后续分支,并不是从 sampler 开始凭空长出来的,而是从 SamplingParams 这里就已经被预先编好了。

verify() 真正在保护哪些边界#

如果说构造阶段主要在做收敛,那么 verify() 的重点就是主动拒绝危险或自相矛盾的参数组合。它会检查:

  • 温度、top-p、min-p 的范围
  • repetition / frequency / presence penalty 的范围
  • min_new_tokens <= max_new_tokens
  • logit_bias 的 token id 是否落在词表内
  • json_schema / regex / ebnf 是否互斥

这里最值得单独强调的,是最后一条。因为它说明系统已经明确承认:不同来源的结构化约束虽然都长得像“输出限制”,但在 runtime 里并不能并列成立。对读者来说,这一点尤其重要,因为它直接决定了怎样理解“我给了更多约束,为什么系统反而报错或只认了一部分”。

structural_tag 为什么不在同一条互斥线上#

这本身也是一个很好的系统信号。当前互斥检查重点针对:

  • json_schema
  • regex
  • ebnf

structural_tag 走的是另一条协议桥与 grammar backend 路线。这说明系统内部已经对“哪些约束被视作同一族”有了自己的分层,而不是把所有 structured field 混成一锅。这种细微但明确的边界,正是一部技术书应该主动替读者点破的地方。

normalize() 最值得认真看的,其实是 stop 语义预处理#

很多人会把 normalize() 当作小修小补。更稳的理解是,它真正值钱的地方在于:它把 stop 相关字段提前变成了 execution layer 能直接消费的内部表示。

对于 stop string,系统会先把单字符串统一成列表;如果有 tokenizer,还会把这些 stop string 编成 token ids,并进一步估计最大长度。这说明系统不是在真正生成时才临时猜“我应该回看多少文本”,而是提前为 stop 检查准备好尾部窗口。

对于 stop regex,更有意思。系统会调用 get_max_seq_length(regex_str) 去算一个严格上界,用来估计未来为了不漏匹配,至少要在尾部保留多长的字符串或序列。也就是说,stop regex 不是“后处理多做一个 re.search”那么简单,它会反过来直接影响执行安全边界。

get_max_seq_length() 特别适合拿来解释“为什么 stop regex 不是轻功能”#

这段代码用 sre_parse 递归分析 regex token,并给出严格上界。它在书里的价值,不在于证明实现有多复杂,而在于替读者建立一个更稳的工程判断:

  • stop regex 不是输出后的小检查
  • 它需要在更早阶段就参与执行边界估计

也正因为这样,stop string / stop regex 虽然长得像普通请求参数,但后果早就已经进入 execution semantics。

这章和后面的 sampler / grammar 章节怎样闭环#

现在可以把前后的逻辑重新接起来了:

  • SamplingParams 先把调用方输入编译成统一内部语义
  • SamplingBatchInfo 再把这些语义压成 batch 级采样元数据
  • grammar / vocab mask / penalties / stop 边界再继续进入 sampler

换句话说,如果前面章节讲的是“采样边界会怎样工作”,这一章讲的就是“这些边界为什么在一开始会长成这个样子”。没有这层,execution model 仍然会缺少一个很关键的上游解释面。

最容易出现的三种误判#

第一,误以为 SamplingParams 只是字段袋子。
实际上它在构造、验证和规范化时已经主动改写了内部语义。

第二,误以为 stop regex 只是输出后的一个小检查。
系统会更早地为它计算长度上界,这已经影响执行边界。

第三,误以为多个 grammar 字段一起设只会让系统更严格。
源码明确把某些组合视作非法,而不是“更安全”。

真正怀疑问题出在参数语义本身时,先怎么查#

更稳的顺序通常是:

  1. 先看原始请求里到底带了哪些采样字段。
  2. 再看 SamplingParams.__init__() 是否已经改写了其中一些值。
  3. 再看 verify() 是否把某些组合视作非法。
  4. 再看 normalize() 是否改变了 stop 相关字段的内部表示。
  5. 最后才回到 SamplingBatchInfo 和 sampler,看这些规范化结果怎样被消费。

这条顺序最重要的意义,是它先验证“参数语义有没有先被编对”,再去怀疑后面的执行层。否则你很容易把配置层问题误判成 sampler 问题。

小结#

SamplingParams 值得单独成章,不是因为它字段多,而是因为它在 execution model 里承担了更靠前的一次“参数语义编译”:用户输入先在这里被收敛、验证、归一化,再以统一内部语义进入 batch 和 sampler。

把这一层讲清楚之后,execution model 对“采样参数怎样真正变成运行时行为”才算真正闭合。