PrefillAdder、new_token_ratio 与准入预算#

这章解决什么问题#

前面的调度与内存章节已经解释了 waiting queue 重排、disaggregation 队列、retract、自检和 cache 复用,但还有一层对 batch 形成最关键的逻辑没有单独展开:在 waiting queue 已经排序完之后,系统到底用什么预算规则决定“再放一个请求进来是否还安全”?这正是 PrefillAddernew_token_ratio 在解决的问题。

这一章的目标,就是把这层 admission logic 讲透。

为什么 PrefillAdder 值得单独成章#

因为它不是普通 helper。它处在一个非常关键的位置:

  • 之前:waiting queue 已经排好顺序
  • 之后:ScheduleBatch.init_new(...) 真正创建 batch

也就是说,它就是“排序”与“形成 batch”之间的最后一道门。优秀技术书如果不把这道门讲出来,调度章节会显得像“排完队之后 batch 就自己形成了”。

一张图:PrefillAdder 位于 waiting queue 和真正 batch 之间#

这张图解决的理解障碍是:很多读者会把 batch 形成想成 waiting queue 的自然切片,但实际还有一层独立预算器在做准入。

flowchart LR
    WQ["waiting queue (already sorted)"] --> Add["PrefillAdder"]
    Add --> Budget["token / request / chunk budget check"]
    Budget --> Batch["new prefill batch"]
    Budget --> Chunk["new_chunked_req / preempt_list"]

图比纯文字多解释的一点是:batch 形成不是被动切片,而是主动预算和准入。

PrefillAdder 初始化时就把什么预算收进来了#

构造参数里最关键的有:

  • new_token_ratio
  • rem_input_tokens
  • rem_chunk_tokens
  • max_running_requests
  • prefill_max_requests
  • priority_scheduling_preemption_threshold
  • mixed_with_decode_tokens

这说明准入逻辑不是单看“还剩多少 token”,而是在同时考虑:

  • 新请求输入成本
  • 未来可能产生的新 token 成本
  • chunked prefill 限额
  • 运行中请求数
  • 与 decode 共存时已经占掉的预算

这非常像系统书里应该单独抽出来讲的“预算层”。

new_token_ratio 到底在做什么#

_get_running_request_total_token_offset(...) 和 scheduler 主循环周围可以看出,new_token_ratio 是一种对未来 decode 扩张成本的保守估计。它不是精确预测,而是 admission 时用来保留未来 token 空间的启发式因子。

这说明系统的准入预算并不只看当前输入长度,还显式把“这些请求接下来还会继续长”纳入预算。好处是更稳;代价是会更保守。

rem_total_tokenscur_rem_tokensrem_input_tokens 这几组预算为什么要分开#

这是 PrefillAdder 最值得技术书展开的地方之一:

  • rem_total_tokens 更像全局总预算
  • cur_rem_tokens 更像当前这轮局部还能用的预算
  • rem_input_tokens 更像专门为新增输入保留的预算

把它们分开,系统才能在 admission 时同时保护:

  • 这轮 prefill 还能不能安全推进
  • 后面 decode 是否还有伸缩余地
  • chunked prefill 是否会一次性吃太多输入 budget

_update_prefill_budget(...) 为什么那么关键#

这段逻辑明确说明,准入不是只扣输入 token 本身。它还会额外扣:

  • page 对齐带来的 overhead
  • 预估的 max_new_tokens
  • chunk / dllm 相关预算

也就是说,预算扣减本身已经包含了“未来成本”和“底层页粒度成本”。

这非常值得写进书里,因为它解释了为什么你明明看起来还有一些空闲槽位,但 admission 仍然拒绝新请求。

add_chunked_req(...)add_one_req(...) 的差别#

add_chunked_req(...)#

它更像是对已经确定要 chunked 的请求继续推进下一段输入,重点在于:

  • 还能塞多少 chunk
  • 是否需要把它保留成 new_chunked_req
  • 未来 max_new_tokens 是否暂时不应继续预留

add_one_req(...)#

它更像是普通 waiting request 的准入判定,会综合:

  • total_tokens
  • real_input_tokens
  • prefill_max_requests
  • context parallel / NSA 约束
  • priority preemption 是否值得触发

也就是说,这两个函数虽然都叫 “add”,但其实对应的是两类不同 admission 情境。

为什么 PrefillAdderpriority preemption 会在这里相遇#

如果 running_batch.batch_is_full,系统不会立刻放弃,而会判断:

  • 当前是否启用 priority preemption
  • PrefillAdder.preempt_to_schedule(...) 是否能腾出足够预算

这说明 priority 不只影响 waiting queue 排序,还会在 admission 最后一道门再次介入。这是一个很重要的跨章回扣:

  • 前一章讲的是 priority 如何排队
  • 这一章讲的是 priority 如何改变“最终能不能进 batch”

chunked prefill 与动态 chunking 为什么在这层最值得看#

scheduler 在创建 PrefillAdder 前,会先决定当前的 chunked_prefill_size,甚至在 enable_dynamic_chunking 场景下动态预测下一块大小。然后 PrefillAdder 再据此决定:

  • 当前请求要不要被截断
  • 截断后是否形成 new_chunked_req
  • 剩余 chunk budget 是否还能继续容纳别的请求

这说明 chunked prefill 不是单纯的执行优化,而是 batch admission 策略的一部分。

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

1. 以为 waiting queue 排完序就等于可以进 batch#

中间还隔着 PrefillAdder 这层预算门。

2. 以为预算只看当前输入长度#

其实它还看未来 token 成本、页对齐成本和 chunk 限额。

3. 以为 priority 只影响队列顺序#

在 admission 的最后一层,它还可能触发 preemption。

如果你怀疑 batch 形成太保守或太激进,先怎么查#

建议按这个顺序:

  1. 看当前 new_token_ratio 是多少,以及是否刚发生过 retract。
  2. chunked_prefill_size 是固定值还是动态值。
  3. PrefillAdder 当前的 rem_total_tokens / cur_rem_tokens / rem_input_tokens
  4. 看请求是在 add_one_req() 还是 add_chunked_req() 里被挡下。
  5. 最后再看 priority preemption 是否在 admission 最后一道门被触发。

小结#

这一章真正想补齐的,是调度章节里最容易被隐形处理的一层:

  • waiting queue 排序之后,真正决定“能不能进 batch”的是 admission budget
  • PrefillAdder 正是这层预算门
  • 它把未来 token 成本、chunked prefill、priority preemption 和页对齐开销一起压进了 batch 形成逻辑

到这里,调度与内存章节对“batch 为什么会长成这样”就更完整了。