深入篇 24 / nano-vllm 源码追踪 · 2026-11-08

跟踪一个请求:token 数、已缓存数与下一次计算

用状态表走完 chunked prefill 和两次 decode,定位每一次计数器更新。

核心阅读 12–18 分钟练习另需 10–20 分钟前置:21;原课01、03

本课只拿下这一件事

用状态表走完 chunked prefill 和两次 decode,定位每一次计数器更新。

同一请求每轮经过相同的控制环。chunk 未完成时虽然 runner 返回候选,postprocess 不追加它。
chunk 未完成时虽然 runner 返回候选,postprocess 不追加它。

固定一个足够小的例子

假设 prompt 长度7、prefill 每轮预算4、KV容量足够、没有prefix命中,生成至少3个token且不遇到EOS。下表中的状态都是 postprocess 完成之后;N=num_tokens,C=num_cached_tokens,S=num_scheduled_tokens。P代表prompt,g代表输出。

轮次送入模型NCS追加输出?
入队无700否
prefill 1P0…P3740否
prefill 2P4…P6870g0
decode 1g0(位置7)980g1
decode 2g1(位置8)1090g2

源码顺序决定这张表

schedule 选中等待队列头部,令 S=min(剩余prompt,剩余预算);完成prefill所需token全部被调度时将它从waiting移到running。这是调度决定,不等于GPU已经完成。model_runner 以start=C、end=C+S打包输入。postprocess首先hash刚完成的整块,再C+=S、S=0;若是prefill且C<N,continue,不追加候选。否则append_token,让N再加1。

因此“已经在running队列”与“已经拿到首token”不是同一个瞬间;看状态前要注明是在schedule之后还是postprocess之后。同步nano实现很快走完这段,vLLM异步流水线会把这类区分放大。

抢占不是把 token IDs 删掉

preempt把状态改回WAITING、is_prefill=True,释放块表并将C清零;已知token_ids仍保留,包括生成历史。重入时相当于用更长的已知前缀重算KV,可能再次利用仍可命中的完整prefix块。它不是从用户原始prompt重新采样一条不同的生成历史。

这个最小调度器的限制

此版本只要本轮排出了prefill就立即返回,所以同轮不混decode。分块prefill只允许当前scheduled列表的第一条序列使用剩余预算;之后遇到装不下的序列会break。can_allocate先为请求当前全部token预留所需块,chunked计算预算并没有自动改成按chunk分配显存。读小框架时应把这些简化和生产引擎区别开。

不要混淆三种“进度”。真实数值见正文表;红线不是生成边界,计数器才是契约。
真实数值见正文表;红线不是生成边界,计数器才是契约。

数值练习:先算,再核对

把prompt长度改成9,预算仍4。写出每轮N/C,直到第一个输出产生。然后用下面源码摘录逐行验证。若你有自己的日志,只记录seq_id/N/C/S/队列即可,先不要记录模型张量。

闭卷检索

不要回看正文。答错后先解释原因,再重新打开这一课。

把结论落到源码

  1. 对照 schedule L48–55 与 postprocess,标注入running与追加首token的两个时间点。
  2. 沿 preempt → deallocate → allocate 追踪 block_table、C与token_ids,解释重算输入。

以下行号由本地 vendor 文件生成;上游链接固定完整 commit,离线摘录可直接打开。

优先阅读:本地 scheduler.py 全文,结合原课03。

速查:形状与公式 · 三框架映射 · 术语表 · 复习与实战路线 · 4070 Ti本地实验

记录你此刻能做到什么

只在当前浏览器保存自评;不会自动写成已掌握的 learning record。

随时追问老师。把“第 24 课 + 哪个公式 / 哪个张量 / 哪一行 + 你的推导”发给我,我们可以继续拆到每一步。