PrefillAdder、new_token_ratio 与准入预算#
这章解决什么问题#
前面的调度与内存章节已经解释了 waiting queue 重排、disaggregation 队列、retract、自检和 cache 复用,但还有一层对 batch 形成最关键的逻辑没有单独展开:在 waiting queue 已经排序完之后,系统到底用什么预算规则决定“再放一个请求进来是否还安全”?这正是 PrefillAdder 和 new_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_ratiorem_input_tokensrem_chunk_tokensmax_running_requestsprefill_max_requestspriority_scheduling_preemption_thresholdmixed_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_tokens、cur_rem_tokens、rem_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_tokensreal_input_tokensprefill_max_requests- context parallel / NSA 约束
- priority preemption 是否值得触发
也就是说,这两个函数虽然都叫 “add”,但其实对应的是两类不同 admission 情境。
为什么 PrefillAdder 和 priority 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 形成太保守或太激进,先怎么查#
建议按这个顺序:
- 看当前
new_token_ratio是多少,以及是否刚发生过 retract。 - 看
chunked_prefill_size是固定值还是动态值。 - 看
PrefillAdder当前的rem_total_tokens / cur_rem_tokens / rem_input_tokens。 - 看请求是在
add_one_req()还是add_chunked_req()里被挡下。 - 最后再看 priority preemption 是否在 admission 最后一道门被触发。
小结#
这一章真正想补齐的,是调度章节里最容易被隐形处理的一层:
- waiting queue 排序之后,真正决定“能不能进 batch”的是 admission budget
PrefillAdder正是这层预算门- 它把未来 token 成本、chunked prefill、priority preemption 和页对齐开销一起压进了 batch 形成逻辑
到这里,调度与内存章节对“batch 为什么会长成这样”就更完整了。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。