penalty、logit_bias 与 custom logit processor 的汇合#

执行模型前面已经把 SamplingBatchInfo、vocab mask 和结构化约束怎样进入 sampler 讲得比较深了,但如果没有一章把“采样前改写汇合点”收束出来,读者仍然会把这些能力理解成几种互不相关的功能开关:

  • presence_penalty
  • frequency_penalty
  • repetition_penalty
  • logit_bias
  • custom_logit_processor

这章真正要补的,就是 execution model 里这条很关键的统一视图:这些来源虽然来自不同层,但最后都在回答同一个问题,namely 在 sampler 真正选择 token 之前,logits 空间到底要被怎样改写。

先把这条改写栈放回采样边界#

如果只从 API 参数或 feature 名字去看,这几类能力很容易被分散理解。但从执行边界看,它们最终都会落到同一条采样前变换栈里。下面这张图的价值,就是把这条栈明确压出来:

flowchart LR
    Raw["raw logits"] --> Custom["custom logit processor"]
    Custom --> Penalty["additive/scaling penalties"]
    Penalty --> Mask["grammar vocab mask"]
    Mask --> Bias["logit_bias"]
    Bias --> Sampler["Sampler.forward()"]

这张图最重要的作用,不是告诉你顺序有五层,而是提醒你:sampler 并不是直接吃“原始 logits”,它看到的 logits 已经经历了一次很完整的运行时改写。

SamplingBatchInfo 才是这些来源真正汇合的地方#

如果要只抓一个对象,最稳的就是 SamplingBatchInfo。因为从 sampling_batch_info.py 看,这一层已经把几类本来分散的能力收口成同一个 batch-level sampling metadata:

  • penalizer_orchestrator
  • acc_additive_penalties
  • acc_scaling_penalties
  • logit_bias
  • custom_logit_processor
  • vocab_mask

也就是说,系统内部已经明确做了“语义归并”:无论这些东西来自历史输出、调用方显式偏置、结构化约束,还是用户自定义 processor,最终都会在这里汇成 sampler 前的一套统一工作集。技术书在这里最该做的,正是把这种内部收口讲明,而不是继续沿功能名称分散解释。

penalties 为什么又分 additive 和 scaling#

这不是实现层的洁癖,而是它们在数学和语义上本来就不同:

  • additive penalties 更像往 logits 上直接加减偏置
  • scaling penalties 更像按比例缩放某些 token 的相对得分

BatchedPenalizerOrchestrator 会分别累计和管理这两类 penalty,这一点很值得写进书里。因为它能帮读者建立一个更稳的判断:系统不是把所有“惩罚项”粗暴混在一起,而是在底层保留了它们作用方式的差异。这也解释了为什么某些 penalty 在 overlap / non-overlap 场景里的处理方式会不一样。

BatchedPenalizerOrchestrator 更像动态状态管理器,而不是参数翻译器#

如果只把它看成“管理几个 penalizer class”,会低估它的运行时地位。更准确的理解是,它负责:

  1. 在 batch 级别准备各 penalizer
  2. 把 penalty 累计进预分配缓冲
  3. 在 filter / merge batch 时同步维护 penalizer 状态
  4. 在 speculative 或重复展开场景下,把 penalty 正确扩展到新的 token layout

也就是说,这层已经不只是配置翻译,而是在 execution model 里承担了真正的动态状态管理职责。把这一点讲清楚,读者就不容易再把 penalty 看成静态参数,而会意识到它们会随着请求状态持续演化。

logit_bias 来源更显式,但落点和 penalty 一样#

logit_bias 和 penalty 最大的不同,在于它不是根据输出历史动态算出来的,而更像调用方对某些 token 的直接偏置意图。SamplingBatchInfo.from_schedule_batch(...) 会把 per-request 的 logit_bias 合成 batch 级 tensor,然后在 apply_logits_bias() 的最后一步加上。

这说明:

  • logit_bias 不是历史依赖的运行时状态
  • 但它仍然属于采样前改写栈的一部分

也就是说,来源不同,并不意味着执行落点不同。对一本系统书来说,这种“来源分散,落点统一”的现象恰恰最值得强调。

custom logit processor 是这条栈里最开放、也最需要顺序感的一层#

custom processor 和 penalties / logit_bias 最大的区别,是它允许调用方把 callable 级逻辑直接注入 execution model。sampling_batch_info.py 会先按 processor string 做去重和 request-mask 组织,然后 sampler.py::_preprocess_logits(...) 再在最前面调用 apply_custom_logit_processor(...)

这说明系统在设计上明确承诺了一件事:用户自定义规则会优先于通用 penalty 与 grammar mask 生效。这个排序本身就是一种 execution 语义。如果不把它说出来,读者就很难判断“为什么我自定义的 processor 看起来会覆盖后面的某些约束”。

apply_logits_bias() 是阅读这条栈的最佳总入口#

比起散着读各文件,更稳的方式通常是先看 apply_logits_bias(),因为它几乎把整条栈都串起来了:

  1. additive penalties
  2. scaling penalties
  3. orchestrator apply
  4. grammar mask
  5. logit_bias

这也是这章最像“系统书”而不是“参数文档”的地方:它不是一一解释每个功能点,而是先给你一个统一汇合点,再带你回头拆来源。只要这个汇合点站稳,execution model 的整体复杂度就不再显得像散落功能,而像一条有顺序的改写管线。

这章和结构化约束章节怎样自然回扣#

前面已经写过 grammar / vocab mask 会在这里生效。现在把 penalty、logit_bias 和 custom processor 也一起拉进来之后,execution model 才更完整地告诉你:

  • sampler 吃到的从来不是原始 logits
  • 而是经过多层采样前改写之后的 logits

这正是 system-level 的视角。它能把“结构化约束”“采样策略”“用户显式偏置”“自定义逻辑”重新看成同一条执行边界上的不同来源。

最容易出现的三种误判#

第一,误把 penalty、logit_bias、custom processor 看成三套平行机制。
从执行边界看,它们最终在同一条改写栈里汇合。

第二,误以为 custom processor 只是 API 扩展点。
它在 sampler 前的位置非常关键,会真实改写后面所有逻辑看到的 logits。

第三,误以为 grammar mask 是唯一会改 logits 空间的结构化机制。
penalty 和 logit_bias 一样也在改写空间,只是语义不同。

真正怀疑采样行为异常时,更稳的检查顺序#

建议按这个顺序:

  1. SamplingBatchInfo 是否准备了 penalties、bias、custom processor。
  2. _preprocess_logits(...) 是否先应用了 custom processor。
  3. apply_logits_bias() 里的各层是否都在生效。
  4. 最后才去判断 sampler 自身的 top-k / top-p / temperature 路径。

这条顺序的价值,在于它先确认“采样前空间有没有被改对”,再去怀疑“采样器有没有按这个空间做选择”。

小结#

这一章真正补齐的,是 execution model 里最后一条很值钱的统一视图:penalty、logit_bias、custom processor 和 grammar mask 并不是分散功能,而是共同构成了 sampler 之前的 logits 变换栈。

只要这条栈读清楚,采样执行的真实复杂度就会完整显现出来。