清晨六点,城市图书馆刚开门,管理员老周就犯难了。今天有三场大型讲座将同时进行,每场讲座需要占用不同数量的参考资料——有的需要 20 本,有的需要 50 本。按照老规矩,图书馆会给每场讲座预留一整排固定书架,但这样一来,有的书架挤得满满的,有的却空了一大半。更麻烦的是,如果讲座 A 临时增加了资料需求,而它旁边的书架已被讲座 B 占满,管理员只能把书搬到更远的地方。这种“整块预留”的管理方式,让本就紧张的图书馆空间浪费了近一半。 老周不知道,他遇到的这个难题,和大模型推理时 GPU 显存的困境一模一样。当大模型生成每一个 token(文字片段)时,都需要保存之前所有 token 的“记忆”——也就是 KV Cache(键值缓存)。就像每场讲座的参考资料一样,KV Cache 的大小是动态变化的,而传统方案会为每个请求预分配一整块连续显存,导致大量空间浪费,甚至根本放不下长文本的请求。 好在,一个叫 vLLM 的团队发明了 PagedAttention(分页注意力)。它就像图书馆引入了“按需分配”的新规:不再给每场讲座预留整排书架,而是把书架拆成许多小格子,讲座需要多少资料就分配多少格子,甚至可以跨区域散落存放。这样一来,图书馆的空间利用率大幅提升,能同时服务的讲座数翻了好几倍。PagedAttention 的核心思想,正是借鉴了计算机操作系统中的虚拟内存分页技术。
掌握 PagedAttention,你就掌握了现代大模型推理引擎(如 vLLM)吞吐量碾压传统方案的核心秘密。无论你是做模型部署、推理优化还是 AI 基础设施,这都是面试和实战中的硬通货。
可视化演示
展示KV Cache分页管理、逻辑到物理内存映射及传统与分页方案吞吐对比,理解vLLM分页注意力优化原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
KV Cache分页管理 逻辑序列的token映射到固定大小的物理页 灰色空闲页可被其他序列共享,减少显存碎片 Page = Block(size=16) 每个页固定大小,按需分配非连续
KV Cache 内存困境
回到图书馆的场景。老周发现,问题不只是空间浪费,更可怕的是“碎片化”。当一场讲座结束后,它占用的那一整排书架空了出来,但下一场讲座可能需要的是更大或更小的空间。时间一长,图书馆里到处是“塞不满的书架”和“塞不下的空隙”,老周只能眼睁睁看着空间在指尖溜走。
在大模型推理的世界里,这个困境被放大了无数倍。当模型生成第 100 个 token 时,它需要前 99 个 token 的 Key 和 Value 向量来计算注意力分数。这些 K/V 向量必须常驻显存,统称为 KV Cache。传统推理框架的做法是:在请求开始时,根据预估的最大长度(比如 2048 个 token)一次性分配一块连续显存。但实际生成的 token 数可能只有几百个,于是大量显存被白白占用——这就是“内部碎片”。
更棘手的是“外部碎片”。假设显存中已经有三个请求各占了一块空间,第四个请求需要一块更大的连续空间,但显存中已经没有足够大的连续空洞了。虽然所有碎片加起来空间足够,但系统无法利用它们。据统计,传统方案中 KV Cache 的显存浪费高达 60% 以上,这意味着每块 GPU 的实际有效容量被打了四折。
这个困境的本质是“静态预留”与“动态需求”之间的矛盾。就像图书馆不能预知每场讲座到底需要多少本书一样,推理引擎也无法预知一个请求到底会生成多少个 token。要打破这个僵局,需要一种完全不同的管理哲学——不再追求连续,而是拥抱离散。
- 分子——实际使用量所有请求真正用到的 K/V 向量总大小。在分页方案中,这部分显存被精准占用,没有任何浪费。
- 分母——预分配总量传统方案为所有请求预留的连续显存总和。由于按最大长度预留,实际使用往往远小于预留值。
- 比值的意义利用率越低,说明浪费越严重。传统方案通常只有 20%-40%,而 PagedAttention 可以将其提升到 90% 以上。
虚拟内存分页
老周苦思冥想,突然想起计算机课上讲过的虚拟内存技术。操作系统的物理内存同样有限,但程序运行时需要的内存是动态增长的。虚拟内存的解决方案是:把物理内存切成固定大小的“页框”(page frame),每个程序按需申请页框,通过“页表”记录程序的逻辑地址到物理页框的映射关系。程序不需要知道自己的数据到底在物理内存的哪个角落,只要查页表就能找到。
这套思想完美对应着推理引擎的困境。既然无法预知 KV Cache 的最终大小,那就不要预知——把显存切成固定大小的“物理块”(block),每个请求开始时只分配一个块,随着 token 生成,再增量分配更多的块。这些块不需要连续,甚至可以分散在显存的各个角落。关键是引入一张“块表”(block table),记录每个请求的逻辑块编号到物理块编号的映射。
操作系统分页思想的核心是“隔离”,而 PagedAttention 把它用在了显存管理上。每个请求的 KV Cache 被拆分成一个个独立管理的块,请求之间互不干扰,GPU 显存的每一寸空间都被精确利用。
- 逻辑块编号这是从请求视角看到的“第几个块”,逻辑上从 0 开始递增。比如第 5 个逻辑块,表示这个请求的第 5 段 KV Cache。
- 块表映射函数$\pi(i)$ 表示第 $i$ 个逻辑块实际存放在哪个物理块中。这个映射由块表记录,可以在每次分配时动态决定。
- 映射的意义只要查询块表,就能在 $O(1)$ 时间内从逻辑位置定位到物理位置,而且物理块可以任意离散分布。
PagedAttention 核心
老周终于想通了。他决定把图书馆的整排书架全部拆掉,换成统一大小的方形储物格——每个格子能放 10 本书。讲座开始时只分配 1 个格子,之后每增加 10 本书就多分配 1 个格子。格子在哪个区域没关系,只要在登记簿上记录“讲座的第 1 格在 C 区 3 排 5 号,第 2 格在 A 区 1 排 2 号”就行。这样一来,空间利用率接近 100%,而且没有任何碎片。
PagedAttention 的技术实现正是如此。首先,定义块大小(block size)为 $B$,即每个块能存储 $B$ 个 token 的 K/V 向量。一个请求生成的 token 数为 $T$ 时,它需要的逻辑块数为 $\lceil T / B \rceil$。每个逻辑块在物理显存中占据一个等大的块,但物理位置完全由分配器决定。注意力计算时,通过块表把逻辑块映射到物理块,然后在物理块上进行计算。
PagedAttention 的创新之处在于,它把“管理连续内存”变成了“管理离散块”。注意力计算的核心公式——Softmax(QK^T)V——不再需要连续的 K/V 矩阵,而是按块读取、按块计算,最终拼接成完整的注意力输出。
- 总 token 数$T$ 是请求生成的 token 总数(包括输入提示词)。这是动态增长的,无法预先确定。
- 块大小$B$ 是每个块能容纳的 token 数。$B$ 决定了内存管理的粒度,是时间-空间权衡的核心参数。
- 向上取整$\lceil \cdot \rceil$ 表示即使最后一个块只放了 1 个 token,也要占一个完整的物理块。但最多只浪费 $B-1$ 个 token 的空间,相比传统方案的 60% 浪费完全可以忽略。
块表管理机制
图书馆的登记簿成了最关键的物品。老周在登记簿上为每场讲座建立一张卡片,卡片上按顺序写着:第 1 格 → C 区 3 排 5 号;第 2 格 → A 区 1 排 2 号……每次讲座需要新增资料时,管理员从空闲格子池中取出一个格子,把位置记录在卡片末尾。讲座结束时,卡片上所有格子被归还到空闲池,供其他讲座使用。
在 vLLM 中,这张“卡片”就是块表(block table)。每个序列(sequence)维护一个块表,数组下标是逻辑块号,数组值是对应的物理块号。当注意力计算需要第 $i$ 个逻辑块的 K/V 时,只需 $O(1)$ 时间查询块表得到物理块号,再从显存中读取。分配新块时,从全局的空闲块列表(free block list)中弹出任意一个空闲块,将物理块号追加到块表末尾——这就是“按需增量分配”。
块表机制还带来一个额外好处:释放与回收极其高效。当一个序列生成完毕(如遇到结束符),只需遍历它的块表,将所有物理块归还到空闲池。相比传统方案需要处理“空洞合并”的复杂操作,分页方式的回收就是简单的数组遍历,时间和逻辑开销都极小。
- 块表数组BlockTable 是每个序列独立的数组,数组长度等于该序列当前的逻辑块数。初始时只有 1 块,随着生成过程不断增长。
- 映射语义$p_i$ 表示第 $i$ 个逻辑块存放的物理块号。$i$ 是连续从 0 到 $n-1$ 的,但 $p_i$ 之间完全没有大小或位置关系。
- 查询复杂度数组按下标访问是 $O(1)$,这意味着注意力计算可以高效地定位任意 token 的 K/V,不会因为离散存放而牺牲性能。
共享内存与解码
图书馆来了三场同时进行的讲座,主题都是“区块链入门”。老周发现,三场讲座的开场白资料完全一样——都是 10 本书的基础介绍。如果给每场讲座各分配 10 个格子存放这些重复的书,那就浪费了 20 个格子的空间。聪明的做法是:只存一份“开场白资料”,三场讲座的卡片上都指向这份共享资料,等讲到各自独有的内容时再各自分配新格子。
这正是大模型推理中“并行采样”(parallel sampling)的典型场景。当用户输入一个问题时,模型会以多种不同的随机种子并行生成多个候选回答,最终选出最优的一个。这些并行序列共享相同的前缀(prompt),但后续生成各不相同。在传统方案中,每个序列都要完整存放一份 prompt 的 KV Cache,显存浪费明显。PagedAttention 支持“共享块”:多个序列的块表可以指向同一个物理块!
共享内存配合“写时复制”,让 PagedAttention 在处理并行采样时既能享受内存共享的巨大优势,又保证序列之间的独立性。在 vLLM 的实现中,当 beam search(束搜索)或并行采样需要分叉时,系统自动创建共享块的副本,整个过程对上层调用完全透明。
- 引用计数每个物理块维护一个计数器,初始为 0。当某个序列的块表首次映射到该块时,计数器加 1。
- 共享前缀N 个并行序列共享前 $k$ 个逻辑块时,这些物理块的引用计数都等于 N。显存占用从 $N \times k$ 降为 $k$。
- 写时复制当某个需要独特内容的序列要修改共享块时(比如生成了新 token 需要写入 K/V),不能直接修改,而要复制一份新的物理块再写入——这就是“写时复制”(copy-on-write)。原块的引用计数减 1,新块引用计数置 1。
吞吐量优化效果
图书馆新规实施一个月后,老周算了一笔账:以前一天最多接待 10 场大型讲座,现在能接待 35 场;以前每次讲座因为找不到足够空间而取消的情况每周都有,现在几乎绝迹。更重要的是,空间利用率从不到 40% 提升到了 95% 以上。消息传开,更多讲座主办方愿意来这个图书馆办活动。
在大模型推理的世界里,vLLM 交出的成绩单同样惊艳。根据 vLLM 团队在论文中报告的实验结果,相比传统的推理框架(如 FasterTransformer、Orca),PagedAttention 在同样硬件条件下实现了 2-4 倍的吞吐量提升。吞吐量(throughput)的定义是:单位时间内完成的请求数。吞吐量提升的直接原因有两个:一是显存利用率提升,使得同一块 GPU 能同时容纳更多请求;二是批处理规模增大,GPU 的并行计算能力被更充分地利用。
除了吞吐量,PagedAttention 还让大模型推理得以支持更长的上下文。传统方案中,一个长度为 32K 的请求可能因为找不到足够大的连续显存而被直接拒绝;而在分页方案中,只要显存中空闲块的总量足够,请求就能被接受——因为物理块可以分布在任意位置。这极大地扩展了 LLM 在实际应用中的可用范围。
- 吞吐量定义单位时间内完成的请求数。这是衡量推理系统效率的核心指标,直接影响服务成本和用户体验。
- 与显存的关系在算力充足的前提下,GPU 能并发处理的请求数 ≈ GPU 总显存 / 每个请求的平均显存占用。分母越小,能并发的请求就越多。
- 效果量化传统方案的显存浪费约 60%,PagedAttention 将浪费降到接近 0%,等效于显存容量扩大 2.5 倍,直接带来吞吐量的成倍提升。
工程实现要点
图书馆新规的成功不是没有代价的。老周发现,管理员的工作变得更复杂了:每天要维护空闲格子池、更新讲座卡片、处理共享资料的引用计数……如果图书馆还雇了 4 个图书管理员分管不同区域,那么任何一个管理员查找资料时都要先查总登记簿,再跑到指定区域取书,协调成本显著上升。
在工程实现层面,PagedAttention 同样面临着类似的复杂性挑战。最大的挑战在于:标准的 FlashAttention 等注意力核函数假定 K/V 在内存中连续存放,而分页后的 K/V 是离散的。因此 vLLM 必须编写专用的 CUDA 核函数(kernel),在计算注意力时按块表逐个块地读取 K/V,这增加了显存访问的间接性(indirection),对 GPU 的并行计算能力提出了更高的要求。
除了 CUDA 核函数,vLLM 的调度器(scheduler)也与 PagedAttention 深度耦合。调度器需要精确追踪每个序列的块表、预测未来的显存需求,并在显存耗尽时执行 preemption(抢占——把某些序列的 KV Cache 暂时换出到 CPU 内存)。这些工程细节虽然复杂,但都是 PagedAttention 获得极致性能的必要代价。
- 块级注意力对于每个查询 token,需要计算出所有 KV 块的注意力分数。每个逻辑块的贡献被独立计算,然后按 softmax 的分配律合并。
- 硬件并行GPU 拥有数千个计算核心,可以将不同块的注意力计算分配给不同的 SM(流式多处理器)并行执行。块的大小 $B$ 决定了并行度:$B$ 越小,块数越多,并行度越高。
- 块大小权衡$B$ 太小(如 4)会导致块表过长、管理开销变大;$B$ 太大(如 64)则接近传统连续分配方案,碎片问题重新出现。vLLM 在实验中选取 $B=16$ 作为性能拐点。
# vLLM 中 PagedAttention 的调度示意class Scheduler: def schedule(self, seqs): for seq in seqs: # 检查是否有足够的空闲块 if self.free_blocks >= seq.required_blocks(): self.allocate_blocks(seq) else: self.preempt(seq)
AI 应用场景
场景一:大规模 LLM 服务部署
阶段:模型推理与部署
解决的问题:传统推理框架(如 HuggingFace 原生实现)在服务多用户时,KV Cache 占用大量显存且利用率低。当并发请求超过一定数量时,GPU 显存直接被占满,后续请求被阻塞排队,用户体验急剧下降。PagedAttention 通过分页管理将显存利用率从 40% 提升到 95%,同等显存下可服务的并发用户数增加 2-4 倍。
具体案例:某 AI 公司部署 Llama 2 70B 模型,使用 4 张 A100 80GB GPU。传统框架下并发数达到 50 时即触发显存溢出,改用 vLLM(基于 PagedAttention)后,并发数提升至 150,且单请求延迟无明显恶化。公司无需购买额外 GPU 即实现了 3 倍的业务容量增长。
场景二:长文本生成与文档问答
阶段:推理优化与上下文管理
解决的问题:长文档问答(如 10 万字合同审查)需要模型处理极长的输入序列。传统方案要求为整个序列分配连续显存,长文本请求经常因显存碎片化而被拒绝。PagedAttention 按块分配,只要显存空闲块总量足够即可受理请求,使长文本应用成为可能。
具体案例:某法律科技公司构建合同审查助手,需处理平均 5 万字的合同。采用 vLLM 后,长文档请求的接受率从 60% 提升到 98%,P95 响应时间从 40 秒降至 18 秒。分页机制还让多个用户的共享合同前缀(如统一的法律条款模板)实现内存共享,节省 30% 额外显存。
场景三:大模型并行采样与自洽性检查
阶段:采样生成与质量控制
解决的问题:生成高质量回答时,常采用并行采样(temperature > 0 多次采样)或多路径束搜索,选出最优结果。这些序列共享相同的提示词前缀。传统方案为每个序列单独存储一份前缀 KV Cache,显存浪费数倍。PagedAttention 的块级共享与写时复制让多序列共享前缀成为现实。
具体案例:某客服机器人使用温度 0.8 并行采样 4 条候选回答,选择语义最合理的一条。vLLM 的共享机制使 4 个序列的显存占用仅为独立方案的 30%+(前缀部分从 4 份降为 1 份)。配合自洽性检查(多次采样比对答案),机器人回答准确率提升了 12 个百分点。
Python 演示
"""PagedAttention 分页管理模拟器 用 Python 模拟 vLLM 中 PagedAttention 的 KV Cache 分页管理核心逻辑,包括块表、空闲块分配、共享引用计数和写时复制。"""import mathimport random class PhysicalBlock: """模拟一个物理内存块""" def __init__(self, pid, capacity=16): self.pid = pid self.capacity = capacity self.ref_count = 0 self.data = {} self.owner = None def write_kv(self, token_id, key_vec, val_vec): if len(self.data) >= self.capacity: raise RuntimeError("Block is full!") self.data[token_id] = (key_vec, val_vec) def add_ref(self): self.ref_count += 1 def remove_ref(self): self.ref_count = max(0, self.ref_count - 1) class BlockAllocator: """物理块分配器""" def __init__(self, total_blocks, block_size=16): self.block_size = block_size self.blocks = {pid: PhysicalBlock(pid, block_size) for pid in range(total_blocks)} self.free_list = list(range(total_blocks)) def allocate(self, owner): if not self.free_list: raise MemoryError("No free blocks available!") pid = self.free_list.pop() blk = self.blocks[pid] blk.owner = owner blk.add_ref() return blk def free(self, pid): blk = self.blocks[pid] if blk.ref_count == 0: self.free_list.append(pid) blk.owner = None def copy_on_write(self, pid, owner): """写时复制:从共享块复制出一个新块""" src_blk = self.blocks[pid] src_blk.remove_ref() new_blk = self.allocate(owner) new_blk.data = dict(src_blk.data) return new_blk class Sequence: """多序列,共享提示词前缀,后续独立生成""" def __init__(self, seq_id, allocator): self.seq_id = seq_id self.allocator = allocator self.block_table = [] self._positions = [] def append_token(self, token_id): need_new_block = not self.block_table or \ len(self.block_table[-1].data) >= self.allocator.block_size if need_new_block: blk = self.allocator.allocate(owner=self) self.block_table.append(blk) blk = self.block_table[-1] blk.write_kv(token_id, random.random(), random.random()) def demo_parallel_sampling(): """三个并行序列共享 prompt 前缀(前 32 个 token)""" allocator = BlockAllocator(total_blocks=64, block_size=16) shared_prefix_len = 32 seq_a = Sequence("A", allocator) for _ in range(shared_prefix_len): seq_a.append_token(random.randrange(1000)) prompt_blocks = list(seq_a.block_table) print(f"prompt 占用的物理块: " f"{[b.pid for b in prompt_blocks]}") shared_blocks = len(prompt_blocks) total_blocks_used = shared_blocks seq_b = Sequence("B", allocator) seq_c = Sequence("C", allocator) for seq in (seq_b, seq_c): seq.block_table = list(prompt_blocks) for blk in prompt_blocks: blk.add_ref() seq_a.append_token(random.randrange(1000)) seq_b.append_token(random.randrange(1000)) seq_c.append_token(random.randrange(1000)) total_blocks_used = len( {b.pid for b in seq_a.block_table} | {b.pid for b in seq_b.block_table} | {b.pid for b in seq_c.block_table} ) naive_blocks = (len(seq_a.block_table) * 3) print(f"三个序列各独立存储需要物理块: {naive_blocks}") print(f"共享后实际占用物理块: {total_blocks_used}") print(f"节省比例: {1 - total_blocks_used / naive_blocks:.1%}") if __name__ == "__main__": demo_parallel_sampling()
class BlockAllocator模拟 vLLM 的物理块分配器,维护free_list空闲块列表,allocate时弹出任意空闲块并递增引用计数。def copy_on_write实现写时复制。当序列需要修改被共享的块时,先让原块引用计数减 1,再分配新块复制数据,保证序列间互不干扰。def append_token按需分配。块写满时才申请新块,token_id会写入当前块的data字典(原子化模拟 K/V 存储)。demo_parallel_sampling主流程。序列 B、C 共享 A 的 prompt 块(引用计数从 1 变为 2),后续各序列独立生成时按需增量分配。
常见误区
误区有人认为 PagedAttention 的核心就是把显存切成小块,只要切得足够小,浪费自然就消失了。有这种想法的人可能会觉得,那直接用固定大小的小块连续分配不就行了?
正解分块只是手段,核心是块表映射 + 按需增量分配 + 引用计数共享的完整机制。单纯的小块连续分配仍然无法解决“请求需要变的连续空间”问题——请求的 KV Cache 是动态增长的,任一小块都会用满,但不同请求的块之间依然会产生碎片。PagedAttention 的关键在于逻辑块到物理块的解耦映射,让物理块可以离散分布,才真正消灭了碎片。
为何常见:人们容易把“分页”理解为单纯的“划分格子”,忽略了与现代 OS 虚拟内存的深层类比——页表映射才是分页的精髓,而不是格子大小本身。
误区很多初学者看到“显存利用率从 40% 提升到 90%”,就认为吞吐量也应该提升 2.25 倍——“显存大两倍,吞吐就大两倍”。
正解吞吐量还受限于其他因素:GPU 计算吞吐(算力)、显存带宽(copy 和读写速度)、调度器效率、软流水等。当计算资源成为瓶颈时,显存利用率的提升不会直接转化为吞吐量提升。vLLM 论文中 2-4 倍的提升是在“显存瓶颈”场景下的结果——当 GPU 算力足够、显存是主要瓶颈时,吞吐量才接近线性改善。
为何常见:从“显存利用率 = 吞吐量”这个直觉出发很自然,但忽略了推理系统的多瓶颈特性。Amdahl 定律告诉我们,系统加速受限于最慢的瓶颈环节。
误区认为只要用了 vLLM,所有的注意力机制(GQA、MQA、FlashAttention、滑动窗口注意力等)都能自动获得 PagedAttention 的性能提升。
正解PagedAttention 需要针对不同的注意力架构编写专门的 CUDA kernel。GQA/MQA 涉及共享 K/V 头,需要特殊的索引逻辑;FlashAttention 的 IO-aware 块计算也需要与分页机制融合。vLLM 团队在实际实现中为不同模型架构编写了多套 kernel,并非“一次分页,处处适用”。部署新架构时,可能需要自定义 kernel 才能兼顾分页与性能。
为何常见:用户把 PagedAttention 理解为一个“透明的优化层”,而它实际上是一个需要与模型结构紧密耦合的底层引擎。GPU 上的 kernel 是“硬编码”的,新的注意力模式需要新的 kernel 适配。
误区有人看到“共享 KV Cache 节省 N 倍显存”,就认为共享是“免费的午餐”,没有注意到复杂性。
正解引用计数、写时复制、块归属追踪都带来了显著工程成本。每次共享/释放都需要原子操作维护计数,每次写共享块都要触发 copy-on-write(复制整个块的 K/V 向量),增加了显存带宽压力。同时,调度器需要精确管理“哪些块属于哪些序列”,复杂度大幅上升。在实践中,vLLM 限制了共享场景的使用范围(如仅在 parallel sampling 时启用),避免过度共享带来的管理开销。
为何常见:从纯“节省空间”的角度看,共享的优势极其诱人;但忽略了分布式环境下一致性维护、并发控制、失败恢复等隐性成本。
误区短文本请求的 KV Cache 很小,似乎没必要引入分页的复杂性。有人认为分页是为处理超长上下文(如 32K token)才需要的优化。
正解碎片问题是系统级的,而非单请求级的。即使每个请求都很短,大量短请求交错执行也会在显存中产生大量离散空洞。传统方案要求每个请求的 KV Cache 连续存放,空洞无法被跨请求利用。PagedAttention 的块级管理让所有请求共享同一块空闲池,短请求也能填补大请求之间的碎片空洞。这就是为什么 vLLM 在短文本场景下也能获得显著吞吐提升(论文中短文本提升约 2 倍)。
为何常见:将碎片问题理解成“单个请求内部的问题”,而没有意识到推理系统是大量请求交错执行的。碎片本质是“时间维度上不同请求的映射重叠问题”。
面试题
Q1. PagedAttention 的核心思想借鉴了哪个领域的经典技术?
- A.数据库的 B+ 树索引
- B.操作系统的虚拟内存分页← 正确答案
- C.编译器的寄存器分配
- D.网络协议的滑动窗口
答案:B。PagedAttention 的核心是引入块表和物理块,将 KV Cache 的逻辑连续性与物理离散性解耦,这正是虚拟内存分页思想。B+ 树索引解决的是数据检索问题,寄存器分配解决的是变量到寄存器的映射,滑动窗口解决的是流量控制,它们都与分页机制无直接关联。
Q2. 传统 KV Cache 管理方案的主要问题有哪些?
- A.内部碎片:预分配空间大于实际需求← 正确答案
- B.外部碎片:空闲空间不连续导致无法利用← 正确答案
- C.无法共享 prompt 的 KV Cache← 正确答案
- D.计算速度过慢导致 GPU 利用率低
答案:ABC。传统方案为每个请求预留连续显存且不共享,因此会产生内部碎片(预分配过大)、外部碎片(显存空洞无法利用)、无法共享 prompt。计算速度慢是模型自身特性,与 KV Cache 管理方式无关,故 D 不正确。
Q3. 块大小(block size)B 的选择对 PagedAttention 性能的影响是?
- A.B 越大,块表占用内存越少,碎片越小
- B.B 越小,块表占用内存越多,碎片越多
- C.B 越大,块表占用内存越少,但片段浪费可能增加← 正确答案
- D.B 的大小对性能没有任何影响
答案:C。B 越大,逻辑块数越少,块表的存储和查询开销越低;但每个块内的最后一部分 token 可能会浪费空间(内部碎片)。B 越小,管理越精细但块表开销越大。因此 B 需要在管理开销和空间利用率之间权衡。A、B 说法都过于极端,D 明显错误。
Q4. 在并行采样(parallel sampling)场景中,多个序列共享 prompt 前缀的 KV Cache,是通过什么机制保证安全性的?
- A.每个序列复制一份完整的 KV Cache
- B.采用写时复制,只在需要修改时才复制物理块← 正确答案
- C.只允许读取,不允许任何写操作
- D.使用全局锁保护所有共享块
答案:B。写时复制(copy-on-write)保证了共享块的线程安全:序列共同读取共享块,只有当某一序列需要写入独有内容时才复制出一份新块进行修改。A 浪费显存,C 不现实(生成过程必然要写入),D 会导致性能瓶颈。
Q5. 以下哪些因素可以解释 vLLM 吞吐量相比传统方案提升 2-4 倍?
- A.显存利用率提升使同一 GPU 能容纳更多请求← 正确答案
- B.更大的批处理规模充分利用 GPU 算力← 正确答案
- C.KV Cache 共享减少冗余内存访问← 正确答案
- D.模型本身的计算浮点精度被降低
答案:ABC。PagedAttention 提高显存利用率→可并发的请求增多→批处理规模变大→GPU 利用率提升;共享 KV Cache 减少了内存占用和访问量。但 vLLM 不改变模型的计算精度(如 FP16 保持不变),因此 D 不正确。
Q6. 块表(block table)的作用是?
- A.记录 KV Cache 中每个 token 的值
- B.将请求的逻辑块号映射到物理块号← 正确答案
- C.存储模型的全部权重参数
- D.保存注意力分数矩阵
答案:B。块表是 PagedAttention 的核心数据结构,数组下标为逻辑块号,值为物理块号。作用类似于操作系统中的页表。A、C、D 都与其他数据结构相关,与块表的功能无关。
Q7. 物理块的引用计数(refcount)降为 0 表示什么?
- A.该块损坏,需要立即修复
- B.该块被所有序列共享,暂时不能释放
- C.没有任何序列在使用该块,可以回收← 正确答案
- D.该块中的数据已复制到 CPU 内存
答案:C。引用计数为 0 意味着所有引用它的序列都已释放对该块的映射,块可以安全归还到空闲列表。B 描述的是 refcount > 1,A 和 D 与引用计数的语义无关。
Q8. PagedAttention 在工程实现中面临的挑战包括?
- A.需要编写专用 CUDA kernel 处理离散块← 正确答案
- B.调度器需精确追踪块分配与释放← 正确答案
- C.块表查询增加了注意力计算延迟← 正确答案
- D.GPU 显存带宽需求自动消失
答案:ABC。专用 CUDA kernel 是必须的(标准 kernel 假定连续内存);调度器对块表的管理是核心功能;块表查询确实引入了额外的间接寻址开销。显存带宽需求不会自动消失,D 错误。
Q9. PagedAttention 最终实现了什么核心目标?
- A.提升模型的推理准确率
- B.减少 GPU 显存浪费,提升推理吞吐量← 正确答案
- C.压缩模型体积同时保持效果
- D.将模型从 GPU 迁移到 CPU 运行
答案:B。PagedAttention 的目标是显存管理优化,通过减少碎片(浪费)来提升吞吐量。它不改变模型输出的质量(准确率),不压缩模型权重,也不负责跨设备迁移。它是“同样的模型、更快的服务”。
Q10. 关于 PagedAttention 的共享内存机制,以下说法正确的是?
- A.共享块被多个序列引用时,引用计数会递增← 正确答案
- B.序列要修改共享块时,必须触发 copy-on-write← 正确答案
- C.共享机制适用于所有解码策略(如贪心解码也能受益)
- D.共享块的释放需要等到所有引用解除← 正确答案
答案:ABD。引用计数随共享增加;写时复制保证独立性;释放需等所有引用解除。但贪心解码(greedy decoding)每个序列只有一个,没有共享的需求,因此 C 不正确。共享主要面向 parallel sampling / beam search。