speculative acceptance、new_accepted_len 与 finish 传播#

这章解决什么问题#

执行模型前面的章节已经讲了 speculative decoding 的存在、Spec V2 带来的复杂度升级,以及 Req.check_finished() 怎样把输出序列转成结束语义,但还有一层非常关键的连接没有被单独讲透:

  • speculative 路径到底怎样决定“这一轮真正接受了多少 token”
  • 这个 accepted length 又怎样继续影响:
    • output_ids
    • finish 判断
    • logprobs / output processor
    • speculative 统计指标

如果不把这一层讲清楚,读者会知道 speculative 有 draft / verify 两步,却仍然很难准确回答:

  • 为什么 new_accepted_len 会直接改变 finish 语义
  • 为什么 spec_accepted_tokensspec_verify_ct 和 acceptance histogram 必须在输出阶段继续被维护

为什么这层值得单独成章#

因为 speculative 执行的真正复杂度,不只在“多跑了一条 draft 路径”,而在于:

  • 它把“本轮到底算接受了几个 token”这件事变成了一个显式运行时状态

而这个状态又不会只停留在 speculative worker 内部。它会继续流入:

  • Req.output_ids
  • Req.check_finished(new_accepted_len)
  • BatchTokenIDOutput
  • request-level speculative metrics

这意味着 acceptance 不是 speculative 的局部细节,而是执行后半段的核心传播变量。

一张图:speculative 的关键不是 draft 和 verify 本身,而是 accepted length 如何沿链路往后传#

这张图解决的理解障碍是:很多读者会把 speculative 想成“验证后挑一个结果”,但真正重要的是“这个结果如何继续影响后面的 finish 和输出语义”。

flowchart LR
    Draft["draft path"] --> Verify["target verify"]
    Verify --> Accept["accepted length / accepted tokens"]
    Accept --> Out["req.output_ids append"]
    Accept --> Finish["Req.check_finished(new_accepted_len)"]
    Accept --> Stats["spec_accepted_tokens / histogram"]
    Finish --> Meta["finish_reason / output meta"]

图比纯文字多解释的一点是:acceptance 不是 speculative worker 的终点,而是 output processor 和 finish 传播的起点。

为什么 new_accepted_len 是这条链的真正关键字段#

schedule_batch.py::check_finished(new_accepted_len=1) 这个签名本身就已经很说明问题:

  • 普通路径默认只接受一个新 token
  • speculative 路径则可能一次接受多个 token

这意味着 finish 判断的粒度天然依赖 acceptance 结果,而不是写死为“最后一个 token 看一眼就行”。

从系统设计角度看,这非常合理,因为:

  • stop token
  • stop string
  • grammar termination
  • max_new_tokens

这些条件都可能在“这一轮新接受的多个 token”里面中途命中。

如果 still 用单 token 视角,finish 语义就会被错误传播。

Req.check_finished(new_accepted_len) 为什么在 speculative 路径里更重要#

你可以把它理解成一个“窗口化 finish 检查”:

  • 普通 decode:窗口通常就是最后一个 token
  • speculative:窗口变成“这一轮新接受的多个 token”

因此,new_accepted_len 真正改变的是 finish 检查的观察范围,而不是只改一个计数器。

这也解释了为什么 speculative 路径下 finish 传播会更复杂:

  • 不是看“最后一个 token 是什么”
  • 而是看“刚刚被确认进入输出序列的这一段里发生了什么”

为什么 acceptance 会继续影响 logprobs 与输出边界#

scheduler_output_processor_mixin.py 里,输出侧会按:

  • send_output_token_logprobs_offset
  • output_ids_

来决定当前该把哪一段 output logprobs 发出去。

一旦 speculative 一轮接受了多个 token,这里的“当前新输出边界”就不再是简单 +1,而是会被 accepted length 推进得更远。

也就是说,acceptance 结果不只影响 finish,也会影响:

  • 本轮哪些 output token 算“新 token”
  • 哪些 logprobs / top-logprobs / token-id-logprobs 应该一并推进

这就是为什么 acceptance 必须被放在执行后半段来讲,而不是只留在 speculative worker 章节。

spec_accepted_tokensspec_verify_ct 和 acceptance histogram 为什么要跟着请求走#

你在:

  • speculative worker
  • scheduler_output_processor_mixin.py
  • BatchTokenIDOutput

里都会看到这些统计值被继续维护。

这说明它们不是“调试时顺手看看”的临时变量,而是请求级正式统计:

  • spec_verify_ct:做了多少次验证
  • spec_accepted_tokens:总共接受了多少 token
  • acceptance histogram:每次接受长度的分布

从技术书角度看,这很值得强调,因为它说明上游并不把 speculative 只当性能黑盒,而是保留了能解释其行为的 request-level 统计语义。

为什么这章和 5.2 / 5.7 / 5.8 不重复#

这三章各自回答的是不同层级的问题:

  • 5.2:speculative decoding 作为执行模式整体是什么
  • 5.7:多模式叠加后,复杂度为什么升级
  • 5.8:采样输出怎样变成 finish 和对外语义

而这一章补的是它们之间的中间桥:

  • speculative acceptance 如何把“模式差异”真正传播到 output 和 finish 语义里

也就是说,这章讲的是:

  • speculative 的局部结果怎样长成后半段的正式输入

这一层最容易出现的误判#

1. 以为 speculative 只是验证后决定“用哪几个 token”#

它还会继续影响 finish 判断窗口和输出侧统计边界。

2. 以为 new_accepted_len 只是辅助计数#

它直接决定 Req.check_finished() 看哪一段新 token。

3. 以为 speculative 指标只是性能调试信息#

源码已经把它们做成了 request-level 正式输出统计的一部分。

如果你要顺着源码读这条 acceptance 传播链,推荐顺序是什么#

建议按下面顺序:

  1. speculative worker 里 accepted length / accepted tokens 的产出点
  2. Req.check_finished(new_accepted_len)
  3. scheduler_output_processor_mixin.py 里 output/logprob 边界推进
  4. BatchTokenIDOutput 里 speculative 统计字段

这样读,你先看到 acceptance 怎么来,再看到它如何继续影响后半段,不容易把 speculative 和 finish 语义人为切断。

小结#

这一章真正要补齐的,是 speculative 执行路径里此前仍然隐含的一条核心传播链:

  • verify 之后最重要的结果不是“通过或失败”本身
  • 而是这一轮究竟接受了多少 token
  • 这个 accepted length 会继续决定 finish、输出边界和 request-level speculative 统计

到这里,执行模型里关于 speculative 的解释就不只停在模式与复杂度,也真正覆盖了它怎样改变后半段输出语义。