Skip to content

05 · BlockManager:分页 KV Cache

05 · BlockManager:分页 KV Cache

先建立直觉:为什么 KV 不必连续摆放?

如果一个请求的 token 在逻辑上连续,它的 KV Cache 是否也必须占一整段连续 GPU 显存?

先观看这段动画。它把“连续预留为何浪费”“11 个 token 怎样变成三张逻辑页”“物理 block 可以离散”“追加时何时需要新页”以及“共享 prefix 何时释放”串成同一条因果链。

观看时只抓住一个结论block_table 保存的是“逻辑页顺序 → 物理 block id”的映射;它让 token 的访问顺序保持连续,而不要求物理 KV 地址连续。固定大小页与页表映射正是 PagedAttention 内存管理思想的关键。1

一个请求怎样拿到页

block_size = 4。长度为 11 的请求需要 ceil(11 / 4) = 3 个逻辑 block:前两页各放 4 个 token,最后一页放 3 个 token,并留下 1 个空位。这个空位是内部碎片;它没有消失,但代价被限制在最后一页,而不是为每条请求预留最大长度的一整段连续显存。

分配时,BlockManager 从 free_block_ids 取可用物理 id,写入 Sequence 的 block_table,同时把这些 id 视为已用。物理 id 可以是 [7, 2, 14] 这样离散的序列;Attention 路径只要依据 table 取页,逻辑 token 的历史顺序不会丢失。

你看到的对象它回答的问题不应混淆为
block_size一张逻辑页最多容纳几个 token?请求的总长度上限
seq.block_table第 0、1、2 张逻辑页分别在哪个物理 block?一段连续显存地址
free_block_ids现在还能分配哪些物理页?某个 Sequence 已经拥有的页
最后一页空位固定页带来的内部碎片有多少?外部碎片或请求失败

追加第 5 个 token 时,发生了两件不同的事

对一个长度为 4、block_size = 4 的请求,已有逻辑页已经填满。第 5 个 token 并不是立即“写进缓存”;系统先需要确认它会跨入一张新逻辑页。

  1. can_append()检查:若追加会跨页,它需要确认空闲队列中仍有可分配 block。
  2. may_append()动作:条件满足后,真正取一个物理 id,并将它追加到 block_table

这两个函数刻意分开,正是为了让调度器能先判断资源是否满足,再改变共享的内存状态。阅读边界条件时,要同时观察 len(seq)block_size 与“新 token 是已经追加还是尚未追加”。

共享 prefix 为什么不能直接释放

Prefix Cache 可以让多个 Sequence 的 table 指向同一个完整 prefix block。这里的共享不是复制整页 KV,而是多个引用指向同一物理页;因此 block 的 ref_count 必须记录还有多少 Sequence 在使用它。第一个请求结束时,计数从 2 变为 1,物理页仍不能回收;最后一个引用离开后,计数变为 0,id 才能回到 free_block_ids

这让“分配”“追加”“释放”成为一条完整资源生命周期:

text
free_block_ids → allocate → seq.block_table → can_append / may_append → ref_count = 0 → deallocate → free_block_ids

代码锚点:去哪里验证这条链路

下面是概念化阅读路径,不等同于上游源码的逐行转录。先追一个字段,再看它被谁检查、何时改变,最后回到实验解释状态。

阅读锚点先问的问题实验里的可观察结果
seq.block_table某张逻辑页写入了哪个物理 id?A 的离散页 id 与逻辑页顺序同时出现。
can_append()追加前是否有足够的空闲物理页?B 填满一页后,先显示检查结果。
may_append()检查通过后,哪一个 id 被真正加入 table?B 从 4 变 5 token,table 多出一个 id。
deallocate()ref_count何时能把物理页放回 free queue?释放请求后,已用页下降、空闲队列恢复。

动手验证:预测 → 运行 → 解释

不要先点击“分配”。先把 token 长度换算成逻辑页,再用实验中的 block table 验证。如果预测错误,重点不是记住答案,而是指出究竟把“逻辑页、物理页、空位、检查和分配”的哪两个概念混在了一起。

步骤你要做什么你应该观察什么
1. 预测装载默认任务:block_size=4、A=11 token。先选择 A 需要的 block 数、最后一页空位数;再判断 B 从完整一页追加 1 token 时是否需要新页。使用 ceil(长度 / block_size) 预测页数;区分“逻辑连续”与“物理 id 连续”。
2. 运行依次分配 A、分配填满页的 B、追加 B 的第 5 个 token。随后分别释放 A 与 B。A 的 block_table 为离散 id;B 追加前先检查空闲页,追加后才增加 table 项;释放会改变 free queue。
3. 解释点击“核对我的预测”,再用一句话说明 B 为什么不能复用原页。用“跨入新逻辑页、can_append()may_append()block_table”解释,而不是只说“因为页满了”。
HTML INTERACTIVE LAB05 · BlockManager:分页 KV Cache
单独打开 ↗
课后习题等待完成

当 `block_size=4`、请求长度从 4 增加到 5 时,为什么需要先检查空闲 block?

动手任务

完成默认对比任务后,把 block_size 改成 8,重新预测 A=11 时的 block 数和最后一页空位;比较两次结果,并解释内部碎片为何变化。

下一节:Prefix Cache

社区教程,与 nano-vLLM 上游项目无官方隶属关系。