想象一家社区图书馆。旧管理员定下一条规矩:每位读者进馆,都要领一本空白手抄本,图书馆按“你有可能读到的最多页数”,提前在固定书架上占好一整块位置——哪怕你只翻了十页,也占走百页的空间。读者一多,书架就被塞满半空的手抄本,新读者反而借不到书。新管理员换了一套玩法:把所有书拆成标准大小的活页,装进统一的信封袋,散放在公共书架上;读者读到哪一页,才取哪一袋;读同一本书前几章的人,可以合看同一叠活页;谁想在自己的页上写批注,就先复印一份再动笔。同样一面书架墙,能同时服务的读者翻了好几倍。PagedAttention(分页注意力),就是大模型推理世界里的这位新管理员:它把大模型推理时那份跟随序列增长的“上下文记忆”,像活页一样分块存放、按需分配,让相同前缀的请求共享同一批块,必要时才写时复制。理解它,你就拿到了大模型推理引擎显存优化的一把钥匙。

岗位关联

掌握 PagedAttention,你就能看懂 vLLM 等推理引擎的内存管理内核,学会用“分页 + 共享”思维调优大模型部署的显存与吞吐,这是 LLM 推理工程师最值钱的核心技能之一。

目录
  1. 可视化演示
  2. 显存痛点
  3. 分页核心思想
  4. 块表映射机制
  5. 前缀共享机制
  6. 显存收益分析
  7. vLLM 工程落地
  8. 应用与部署
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示PagedAttention的块表映射机制、跨请求前缀KV块共享与引用计数、以及连续批处理动态调度过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

块表映射机制 逻辑块通过块表映射到物理块 灵感来自操作系统虚拟内存分页 逻辑块i → 块表[i] → 物理块 非连续物理存储消除显存碎片

显存痛点

故事从旧管理员的“手抄本”开始:每位读者按“可能读到的最大页数”领一整块固定书架,哪怕实际只读十页。传统的大模型推理框架也是如此——它不知道用户会问多长,只能按最大序列长度一次性预分配一块连续显存,这笔账就记在名叫 KV Cache 的“上下文记忆账本”上。

Transformer 每生成一个新 token,都要重新计算它与历史所有 token 的注意力。为了避免重复算,推理引擎把每个历史 token 的 Key(键)和 Value(值)张量缓存在显存里,这就是 KV Cache。它的体积随并发请求数、序列长度线性增长,是大模型推理中最主要的显存消耗者之一。

然而传统分配方式有两个致命问题:一是内部碎片——按最大长度申请、实际只用了很少一部分,剩下的空间空置;二是外部碎片——不同请求先后结束,释放出大小不一的零散块,彼此不连续,新请求的大块需求再也拼不回去,实际显存利用率常常只有 20% 到 40%。

旧管理员的账本越来越厚,直到新管理员发现:与其按页数囤书,不如按需取页——这正是 PagedAttention 改革的起点。

MKV=2×L×dkv×S×b×bytes\displaystyle M_{KV} = 2 \times L \times d_{kv} \times S \times b \times \text{bytes}
这条公式告诉你 KV Cache 的显存账单有多大:双份矩阵、每层一份、随序列和并发线性膨胀。
  1. 双份矩阵注意力既要 Key 又要 Value,两个矩阵都要缓存,所以最前面乘 2。
    2\displaystyle 2
  2. 层数累加Transformer 每一层都有独立的 KV 缓存,L 代表模型层数。
    L\displaystyle L
  3. 每层维度d_kv 是每层 KV 张量的特征维度,由 KV 头数与每个头的维度决定。
    dkv\displaystyle d_{kv}
  4. 总 token 数S 是序列长度,b 是批大小,二者相乘得到需要缓存的总 token 数。
    S×b\displaystyle S \times b
  5. 精度字节bytes 是每个元素占用的字节数,FP16 精度下为 2 字节。
    bytes\displaystyle \text{bytes}

分页核心思想

新管理员把书拆成标准活页、装进统一信封袋。PagedAttention 的灵感正来自这个朴素直觉,以及操作系统里更古老的前辈——虚拟内存分页:进程的逻辑地址空间可以很大,但物理内存按固定大小的页框分配,用多少占多少。

PagedAttention 把 KV Cache 切成固定大小的块(Block),每块默认装 16 个 token 对应的 KV 数据。请求侧不再按“最大长度”申请,而是随生成进度按需分配:每生成一批 token,只申请装得下它们的块。从逻辑上看,请求的序列仍然是 0、1、2…连续编号的;但物理上,这些块可以被放在显存的任意空闲位置,中间可以有空隙——就像书架上散落的信封袋,中间空着几个格子完全不影响借阅。

逻辑连续、物理分散,这是分页思想最核心的一步。相比旧方案“必须找一整块连续显存”,新方案只需要“有位置放下一个块”即可。

从此,显存账本从“按最大页数囤书”变成“按实际页数取袋”,只是散落的信封袋要靠一张借阅清单才能找回——那就是下一节的块表。

N=⌈SusedBblock⌉\displaystyle N = \left\lceil \frac{S_{used}}{B_{block}} \right\rceil
一个请求到底占用几个块?用实际 token 数除以块大小,向上取整即可。
  1. 实际使用量S_used 是请求当前实际生成的 token 数,而不是预留的最大值。
    Sused\displaystyle S_{used}
  2. 块大小B_block 是每个物理块的容量,vLLM 默认 16 个 token 一块。
    Bblock\displaystyle B_{block}
  3. 向上取整最后一块往往装不满,但也要占一个块,所以用向上取整。
    ⌈⋅⌉\displaystyle \left\lceil \cdot \right\rceil

块表映射机制

读者手里的借阅清单记录了“我在读的第几页,放在书架的哪个信封袋”。PagedAttention 为每个请求维护一张块表(Block Table),本质上就是逻辑块号到物理块号的映射数组。

计算过程分两步:给定一个逻辑块号 i,查块表得到物理块地址;再算出该 token 在块内的偏移。因为块大小固定,偏移只需一次取模运算,开销极小。传统方案里,Attention 计算要求 KV 在显存中连续摆放,而 PagedAttention 的注意力算子会拿着块表去“按图索骥”——先定位物理块,再读块内数据,物理上是否连续已经不重要了。

这彻底解除了“连续大块”的紧箍咒:一块显存刚被释放,无论它夹在哪两块之间,都能立刻被新请求复用,外部碎片问题迎刃而解。

有了这张借阅清单,散落的信封袋就能按序被找到。而清单带来的另一份惊喜是:既然大家都拿着同一个信封袋的编号,那这袋活页是不是可以一起看?

pid=block_table[i],off=j mod Bblock\displaystyle pid = \text{block\_table}[i], \qquad off = j \bmod B_{block}
逻辑块号查表拿物理块号,块内位置用取模算出,两步即可定位任意 token 的 KV。
  1. 查表得物理块号逻辑块号 i 作为下标,从块表中取出物理块号 pid。
    pid=block_table[i]\displaystyle pid = \text{block\_table}[i]
  2. 块内偏移token 在整条序列里的位置 j,对块大小取模,得到它在块内的偏移 off。
    off=j mod Bblock\displaystyle off = j \bmod B_{block}

前缀共享机制

图书馆里很多人读的是同一本书,至少前几章完全一样。旧管理员给每人发一整套手抄本,纯属浪费;新管理员允许大家合看同一叠活页,只有当某人要在上面写自己的批注时,才复印一份再动笔。

大模型场景中同样存在大量“相同前几章”:聊天机器人的系统提示词、少样本示例、RAG 里固定的问题模板,都是所有请求共享的公共前缀。由于它们的 token 序列完全一致,产生的 KV 块也完全一致——PagedAttention 允许这些请求共享同一批物理块。

共享通过引用计数(Ref Count)管理:每块记录当前有几个请求在引用。共享块是只读的。当某个请求需要在一个已被共享的块上继续写新 token 时,不能直接改,而要触发写时复制(Copy-on-Write, COW):新分配一个物理块,把旧块内容拷贝进去,再写入新 token,同时把旧块的引用计数减一。

合看活页让同样的书架服务更多人,这正是 PagedAttention 在真实负载中显存收益的最大来源之一。

ref(b)>1⇒COW,Msaved=Np×Bblock×bytes×(R−1)\displaystyle \text{ref}(b) > 1 \Rightarrow \text{COW}, \qquad M_{saved} = N_{p} \times B_{block} \times \text{bytes} \times (R-1)
一个前缀被 R 个请求共享,只有第一个请求付出完整代价,其余 R-1 个请求全部免单。
  1. 引用计数ref(b) 表示物理块 b 被多少请求共同引用,是判断能否直接写的关键。
    ref(b)>1\displaystyle \text{ref}(b) > 1
  2. 写时复制引用数大于 1 时块只读,写入前必须复制新块,保证互不干扰。
    COW\displaystyle \text{COW}
  3. 节省的显存共享前缀共 N_p 个块,每个块占 B_block × bytes 字节,R 个请求共享相比各自独享,省下 R-1 份。
    Msaved=Np×Bblock×bytes×(R−1)\displaystyle M_{saved} = N_{p} \times B_{block} \times \text{bytes} \times (R-1)

显存收益分析

新管理员的改革成效如何?看看书架的“空置率”就一目了然。旧方案的书架被三样东西占着:真正的内容、预留下的空白页、以及拼不回完整区间的缝隙。分页后,前两项被大幅压缩,最后一项几乎清零。

内部碎片:不再按最大长度预分配,只按实际使用申请块,唯一的浪费是每个请求最后那个没装满的块——平均半块而已。请求多了,这笔开销被摊薄到可以忽略。外部碎片:物理块可以放在任意空闲位置,不存在“必须有连续大块”的限制,彻底消除。

vLLM 论文在真实负载上的报告显示:相比 FasterTransformer、Orca 等当时的主流系统,vLLM 的吞吐量提升了 2 到 4 倍,KV Cache 显存浪费从传统方式的 60% 到 80% 降到了接近零——书架从“半空状态”变成“几乎满员运营”。

同样的显存,现在能吞吐更多请求——收益的兑现,最终落到工程实现上。

ηwaste=Malloc−MusedMalloc,ηint′≈12Nblocks\displaystyle \eta_{waste} = \frac{M_{alloc} - M_{used}}{M_{alloc}}, \qquad \eta'_{int} \approx \frac{1}{2N_{blocks}}
改革前浪费率动辄过半;改革后,内部碎片只剩“平均半块/请求”的量级。
  1. 浪费率定义分配出去但没被用上的显存占比,传统方案里这就是空洞的度量。
    ηwaste=Malloc−MusedMalloc\displaystyle \eta_{waste} = \frac{M_{alloc} - M_{used}}{M_{alloc}}
  2. 分页后的内部碎片每个请求只有最后一个块平均空半块,占比约为 2 倍块数分之一。
    ηint′≈12Nblocks\displaystyle \eta'_{int} \approx \frac{1}{2N_{blocks}}

vLLM 工程落地

新管理员的玩法要落地,光有活页还不够,还需要一套图书管理系统:谁在借哪袋、哪袋空闲、哪袋被多少人共用,都要实时记账。大模型推理框架 vLLM 就是这个“管理系统”。

vLLM 是第一个把 PagedAttention 工程化的推理引擎,包含三个关键角色:调度器(Scheduler)决定下一轮让哪些请求运行;块管理器(Block Manager)负责物理块的分配、释放、共享与写时复制记账;PagedAttention 算子以 CUDA kernel 的形式实现“按块表寻址”的分块注意力计算,每次前向都高效定位分散的 KV 块。

部署调优时,最常碰到的参数有:--block-size(默认 16,调节分块粒度)、--gpu-memory-utilization(默认 0.9,控制 KV 可用显存上限)、--max-num-seqs(最大并发序列数)以及 --enable-prefix-caching(开启前缀缓存,让相同前缀的块直接命中复用)。

系统就绪后,真正的收益要在具体业务场景中兑现——活页图书馆终于可以开门迎客了。

Ukv=MallocMgpu×uutil\displaystyle U_{kv} = \frac{M_{alloc}}{M_{gpu} \times u_{util}}
观测 U_kv 是否接近 1,就能判断 KV 显存是否被充分利用——这是分页是否生效的晴雨表。
  1. 已分配显存M_alloc 是当前已经分配给 KV 块的显存总量。
    Malloc\displaystyle M_{alloc}
  2. 可用上限M_gpu 是 GPU 总显存,u_util 是允许 KV 占用的比例上限(默认 0.9)。
    Mgpu×uutil\displaystyle M_{gpu} \times u_{util}
  3. 利用率指标两者相除得到 KV 显存利用率,越接近 1 说明分页调度越紧凑。
    Ukv\displaystyle U_{kv}

应用与部署

图书馆开门后最热闹的三个服务区:一是共读会,二是会议室问答,三是考前刷题班——它们分别对应多轮对话、RAG 文档问答和批量评测。

多轮对话中,所有会话共享同一段系统提示词(常占 prompt 的 30% 到 50%),大家“合看”这段前缀的 KV 块,只有各自的对话内容按需分块。RAG 长文档问答中,一篇文档被多个问题反复引用,文档的 KV 只需计算一次,所有问题共享对应物理块,省下的不仅是显存还有重复计算。批量评测时,同一份指令模板被喂给成千上万个测试用例,前缀缓存命中率极高,吞吐显著提升。

调优时建议盯住两个指标:前缀命中率(提示词里有多少 token 的 KV 直接命中缓存)和KV 利用率(上一节的 U_kv)。实践上把公共指令固定在一段、把易变内容放在后面,就能让共享收益最大化。

从“每人一本手抄本”到“合看活页、按需取袋”,这座图书馆终于让相同显存服务了更多读者——这就是 PagedAttention 的全部故事。

H=ShitSprompt\displaystyle H = \frac{S_{hit}}{S_{prompt}}
前缀命中率越高,说明越多请求在“合看活页”,而不是重复抄写。
  1. 命中 token 数S_hit 是提示词中直接命中共享缓存的 token 数量。
    Shit\displaystyle S_{hit}
  2. 提示词总长S_prompt 是完整提示词的长度,两者比值衡量前缀共享的甜蜜程度。
    Sprompt\displaystyle S_{prompt}

AI 应用场景

多轮对话服务的系统提示词共享

阶段:推理部署与在线服务

解决的问题:传统服务中每个并发会话独立分配完整 KV Cache,系统提示词和开场白被每个用户重复存一份,显存随会话数直线爆炸。PagedAttention 让所有会话共享相同系统提示词的物理块,只有用户各自的对话部分按需分块,显存峰值明显下降。

具体案例:某客服机器人线上并发 2000 路会话,系统提示词约 800 token,占总 prompt 的 40%。切到 vLLM 并开启前缀缓存后,KV 显存峰值下降约 35%,单卡可承载并发会话数从 80 提升到 140,P95 首 token 延迟保持不变。

RAG 长文档批量问答的前缀复用

阶段:推理优化与检索增强生成

解决的问题:RAG 场景中多个问题共享同一篇长文档,传统做法是每个问题都重新编码一遍文档,算力与显存双重浪费。分页注意力让文档的 KV 只计算一次,所有问题共享对应的物理块,代价仅是引用计数加一。

具体案例:法律问答平台将一份 10 万字的合同切片后作为公共上下文,批量回答 100 个相关问题。启用共享前缀后,文档编码次数从 100 次降为 1 次,整体吞吐提升约 2.5 倍,单次问答的首 token 时延从 4 秒降到 1.2 秒。

离线评测与大模型压测

阶段:模型评测与性能压测

解决的问题:跑公开基准时,所有测试样例共享同一份指令前缀;普通框架重复编码指令,评测吞吐被无效计算拖累。PagedAttention 的前缀共享让指令 KV 只算一次,测试样例快速复用,batch 可以开得更大。

具体案例:评测团队用 vLLM 给 7B 模型跑 MMLU 一万题,开启 --enable-prefix-caching 后,相同指令前缀命中缓存,评测吞吐从每秒 180 条提升到 420 条,GPU 显存占用反而下降了近三成。

Python 演示

python85 行
import math  class BlockManager:    """模拟 PagedAttention 的物理块管理与共享"""     def __init__(self, total_blocks, block_size):        self.total_blocks = total_blocks        self.block_size = block_size        self.refs = [0] * total_blocks              # 物理块引用计数        self.free_pool = list(range(total_blocks))  # 空闲物理块队列        self.tables = {}                            # 请求 -> 逻辑块到物理块映射        self.used = {}                              # 请求 -> 已写入 token 数     def _alloc(self):        pid = self.free_pool.pop(0)        self.refs[pid] += 1        return pid     def prefill(self, req_id, num_tokens):        """预填充提示词 KV, 按需申请 ceil(num_tokens/block_size) 个块"""        need = math.ceil(num_tokens / self.block_size)        self.tables[req_id] = [self._alloc() for _ in range(need)]        self.used[req_id] = num_tokens        return self.tables[req_id]     def share(self, req_id, src_id, num_tokens):        """前缀共享: 复用 src 请求前 num_tokens 对应的物理块, 引用计数加一"""        n = math.ceil(num_tokens / self.block_size)        shared = self.tables[src_id][:n]        for pid in shared:            self.refs[pid] += 1        self.tables[req_id] = list(shared)        self.used[req_id] = num_tokens        return shared     def append_token(self, req_id):        """生成一个新 token: 满块则分配新块, 共享块则写时复制"""        blocks = self.tables[req_id]        pos = self.used[req_id] % self.block_size        if pos == 0:            blocks.append(self._alloc())        else:            last = blocks[-1]            if self.refs[last] > 1:                new_pid = self._alloc()   # 先复制内容到新块再写入                self.refs[last] -= 1                blocks[-1] = new_pid        self.used[req_id] += 1     def free(self, req_id):        """请求结束, 归还其占用的所有物理块"""        for pid in self.tables.get(req_id, []):            self.refs[pid] -= 1        del self.tables[req_id]        del self.used[req_id]  def show_table(mgr, req_id):    blocks = " | ".join("L%d->P%d" % (i, p) for i, p in enumerate(mgr.tables[req_id]))    print("%s 块表: %s" % (req_id, blocks))  def main():    mgr = BlockManager(total_blocks=12, block_size=4)    mgr.prefill("sys", 8)                 # 系统提示词, 占 2 块    mgr.share("A", "sys", 8)              # A 共享前缀    for _ in range(3):        mgr.append_token("A")             # A 生成 3 个 token, 按需取第 3 块    mgr.share("B", "sys", 8)              # B 也共享相同前缀    for _ in range(2):        mgr.append_token("B")             # B 生成 2 个 token    mgr.share("C", "A", 9)                # C 从 A 的第 9 token 处分支, 模拟 beam search    mgr.append_token("C")                 # 触发写时复制, 新块替换共享块     print("物理块引用计数 refs:", mgr.refs)    for rid in ("sys", "A", "B", "C"):        show_table(mgr, rid)    no_share = sum(math.ceil(mgr.used[r] / mgr.block_size) for r in mgr.tables)    used = sum(1 for r in mgr.refs if r > 0)    print("无共享需 %d 块, 分页+共享只用 %d 块, 节省 %d 块" % (no_share, used, no_share - used))  if __name__ == "__main__":    main()
目的:用纯 Python 模拟 PagedAttention 的块表、按需分配、前缀共享与写时复制,直观展示“无共享需要 11 块,分页+共享只用 5 块”的显存收益。
  1. self.refs = [0] * total_blocks每个物理块一个引用计数,数组下标就是物理块号,像图书馆登记每袋活页被几个读者合看。
  2. pid = self.free_pool.pop(0)从空闲队列取出一个物理块号,保证按需分配且永不重复。
  3. need = math.ceil(num_tokens / self.block_size)向上取整算出需要几个块,最后一块没装满也算一整块。
  4. self.refs[pid] += 1前缀共享的关键:每块引用计数加一,多个请求共用而不复制数据。
  5. new_pid = self._alloc()写时复制的核心:共享块不能改,先分配一个新物理块。
  6. self.refs[last] -= 1复制完成后原共享块引用计数减一,相当于该请求从共享中退出。
  7. no_share = sum(math.ceil(mgr.used[r] / mgr.block_size) for r in mgr.tables)假设完全不复用,算出朴素方案需要的总块数,用于对比收益。
  8. used = sum(1 for r in mgr.refs if r > 0)统计当前真正被占用的物理块数,二者相减就是共享省下的显存。

常见误区

误区PagedAttention 把 KV Cache 压缩小了,所以显存占用才降下来——它一定是个类似量化的压缩算法。

正解分页一点也不压缩 KV 数据本身,K 和 V 张量一个不少、精度不变。它做的是内存管理:把整块连续预分配改成定长块按需分配,把“申请了没用”的内部空洞和“散了拼不回”的外部碎片解救出来。类比图书馆:新管理员不是把书页内容改小,而是让多人合看同一叠活页、只取需要的页。

为何常见:看到“减少显存浪费”就直觉联想到压缩,忽略了分页本质是操作系统的内存管理思想,而不是数据压缩技术。

误区既然多个请求共享同一个物理块,那谁都能在自己的逻辑页上继续写新 token,反正有块表。

正解共享块是只读的。一旦引用计数大于 1,任何写入都必须先触发写时复制:新分配一块、拷贝旧内容、再写入新 token,同时把原块引用计数减一。就像合租书架上公共层的活页,谁要贴自己的标签,得先复印一份再动笔,否则就把别人的笔记涂掉了。

为何常见:日常编程里“拿到地址就能写”的习惯根深蒂固,忽略了并发共享场景下写入冲突的破坏性。

误区块设得越小,尾部浪费越少,所以 block-size 越小越优。

正解块小确实让尾部内部碎片变少,但块数变多,块表变大、寻址开销上升、GPU 按块并行的效率也会下降;块大则尾部浪费增加。vLLM 默认 16 token 是大量实验后的权衡值,调参时应综合吞吐和显存实测,而不是单看碎片指标。

为何常见:只盯着“内部碎片”这一个指标做局部优化,忽略了块表与 kernel 开销的全局代价,犯了只见树木不见森林的错。

误区PagedAttention 只对长对话和长文档有用,短请求 token 少,分不分页无所谓。

正解短请求同样受益。即使没有前缀共享,按需分配也消除了预分配带来的内部空洞,而物理块任意放置让释放的碎片立刻可复用,外部碎片对所有请求一视同仁地消失。共享收益看的是“前缀占比 × 共享请求数”,前缀短时收益小,但内存管理收益始终存在。

为何常见:只看到论文里长上下文的大数字,低估了碎片消除这种“默认收益”的普适价值。

面试题

Q1. PagedAttention 解决大模型推理中的什么核心问题?

单选题 · easy

  • A.训练时的梯度消失
  • B.KV 显存碎片和浪费← 正确答案
  • C.模型权重精度下降
  • D.输入数据加载太慢

答案:B。PagedAttention 是显存管理技术,目标是消灭 KV Cache 预分配造成的内部碎片和连续分配造成的外部碎片。它不涉及梯度、权重精度或数据加载,就像新管理员解决的是书架占用问题,而不是书的内容问题。

Q2. 大模型推理中的 KV Cache 主要存储什么?

单选题 · easy

  • A.缓存每层的键值张量← 正确答案
  • B.存储模型全部权重
  • C.记录输入词表映射
  • D.保存优化器动量

答案:A。KV Cache 缓存的是 Transformer 每层产生的 Key 和 Value 张量,用于避免生成新 token 时重复计算历史注意力。模型权重属于静态参数,词表映射和优化器动量都属于训练期概念,与推理缓存无关。

Q3. 传统 KV Cache 分配方式的主要缺陷是什么?

单选题 · easy

  • A.按最大长度一次性预分配← 正确答案
  • B.按实际长度动态扩缩
  • C.使用更高精度数据类型
  • D.在 CPU 上保存缓存

答案:A。传统方案为每个请求按最大可能序列长度预分配一整块连续显存,实际往往只用一小部分,产生大量内部空洞;请求结束释放出的零散碎片又无法拼接复用,形成外部碎片。B 恰好是分页想要实现的目标,C 和 D 与缺陷成因无关。

Q4. 块表(Block Table)的核心作用是什么?

单选题 · mid

  • A.记录逻辑块到物理块的映射← 正确答案
  • B.保存注意力权重参数
  • C.压缩 Key 和 Value 数据
  • D.计算生成 token 的概率

答案:A。块表是每个请求维护的映射数组,逻辑块号通过查表得到物理块号,配合块内偏移即可定位任意 token 的 KV。它既不存权重,也不压缩数据,更不参与概率计算,只负责“地址翻译”,相当于读者手里的借阅清单。

Q5. 某个物理块被多个请求共享(引用计数大于 1),一个新请求要往里写入 token 时会怎样?

单选题 · mid

  • A.直接在共享块上覆写
  • B.系统报错并终止推理
  • C.写时复制分配新块← 正确答案
  • D.把当前块大小减半

答案:C。共享块是只读的,直接覆写会破坏其他请求的数据。正确做法是写时复制:新分配一个物理块,把原块内容拷贝过去再写新 token,同时原块引用计数减一。这是操作系统 COW 思想在推理引擎中的移植。

Q6. PagedAttention 相比传统方案带来的收益包括哪些?

多选题 · mid

  • A.消除内部碎片浪费← 正确答案
  • B.消除外部碎片限制← 正确答案
  • C.无需再维护块表
  • D.支持前缀块共享← 正确答案

答案:ABD。按需分配消除预分配导致的内部碎片,物理块任意放置消除外部碎片,引用计数让相同前缀的物理块可被多请求共享。但块表恰恰是分页新增的管理结构,不可能“无需维护”,所以 C 错误。

Q7. 关于块大小 B 的选取,正确的权衡认识是什么?

单选题 · hard

  • A.B 越小块表越大且越长← 正确答案
  • B.块越大寻址速度越快
  • C.B 与模型层数严格正比
  • D.B 越小 KV 精度越低

答案:A。块越小,相同 token 数对应的物理块数越多,块表越长、开销越大;块大则尾部浪费增加,所以需要折中。寻址速度由块表查找机制决定,与块大小不构成“越快”关系;块大小与层数、KV 精度都没有正比关系。

Q8. 以下哪些场景适合利用前缀共享?

多选题 · hard

  • A.固定系统提示词对话← 正确答案
  • B.少样本示例前缀← 正确答案
  • C.RAG 长文档问答← 正确答案
  • D.每个请求随机话题

答案:ABC。前缀共享的前提是请求间存在完全相同的 token 序列前缀。固定系统提示词、少样本示例、RAG 文档都是典型的公共前缀,而“随机话题”意味着每个请求前缀各不相同,共享收益接近于零。

Q9. 关于写时复制(COW)机制,正确的说法有哪些?

多选题 · hard

  • A.共享块内容不允许直接修改← 正确答案
  • B.复制后原块引用计数减一← 正确答案
  • C.每次生成都必须复制
  • D.复制出的新块归请求独占← 正确答案

答案:ABD。COW 的前提是引用计数大于 1,此时共享块只读;复制出新块后原块引用减一,新块由发起请求独占写入。只有写入共享块时才复制,独占块直接写,所以“每次生成都复制”错误。

Q10. 某请求已生成 45 个 token,块大小为 16,至少需要多少个物理块?

单选题 · extreme

  • A.2 个物理块
  • B.3 个物理块← 正确答案
  • C.4 个物理块
  • D.16 个物理块

答案:B。物理块数等于 ceil(45 / 16) = 3。前两块各装 16 个 token 恰好装满,第三块只装 13 个 token,产生半个块量级的内部碎片——这正是分页后唯一残留的尾部浪费,远小于传统方案按最大长度预分配的空洞。