清晨六点,城市图书馆刚开门,管理员老周就犯难了。今天有三场大型讲座将同时进行,每场讲座需要占用不同数量的参考资料——有的需要 20 本,有的需要 50 本。按照老规矩,图书馆会给每场讲座预留一整排固定书架,但这样一来,有的书架挤得满满的,有的却空了一大半。更麻烦的是,如果讲座 A 临时增加了资料需求,而它旁边的书架已被讲座 B 占满,管理员只能把书搬到更远的地方。这种“整块预留”的管理方式,让本就紧张的图书馆空间浪费了近一半。 老周不知道,他遇到的这个难题,和大模型推理时 GPU 显存的困境一模一样。当大模型生成每一个 token(文字片段)时,都需要保存之前所有 token 的“记忆”——也就是 KV Cache(键值缓存)。就像每场讲座的参考资料一样,KV Cache 的大小是动态变化的,而传统方案会为每个请求预分配一整块连续显存,导致大量空间浪费,甚至根本放不下长文本的请求。 好在,一个叫 vLLM 的团队发明了 PagedAttention(分页注意力)。它就像图书馆引入了“按需分配”的新规:不再给每场讲座预留整排书架,而是把书架拆成许多小格子,讲座需要多少资料就分配多少格子,甚至可以跨区域散落存放。这样一来,图书馆的空间利用率大幅提升,能同时服务的讲座数翻了好几倍。PagedAttention 的核心思想,正是借鉴了计算机操作系统中的虚拟内存分页技术。

岗位关联

掌握 PagedAttention,你就掌握了现代大模型推理引擎(如 vLLM)吞吐量碾压传统方案的核心秘密。无论你是做模型部署、推理优化还是 AI 基础设施,这都是面试和实战中的硬通货。

目录
  1. 可视化演示
  2. KV Cache 内存困境
  3. 虚拟内存分页
  4. PagedAttention 核心
  5. 块表管理机制
  6. 共享内存与解码
  7. 吞吐量优化效果
  8. 工程实现要点
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示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。要打破这个僵局,需要一种完全不同的管理哲学——不再追求连续,而是拥抱离散。

显存利用率=∑i=1n实际使用的 K/V 大小∑i=1n预分配的连续空间大小≤40%\displaystyle \text{显存利用率} = \frac{\sum_{i=1}^{n} \text{实际使用的 K/V 大小}}{\sum_{i=1}^{n} \text{预分配的连续空间大小}} \leq 40\%
这个公式说明:预分配空间越大、越不精准,浪费就越严重。PagedAttention 的核心目标,就是让分母无限接近分子。
  1. 分子——实际使用量所有请求真正用到的 K/V 向量总大小。在分页方案中,这部分显存被精准占用,没有任何浪费。
    ∑i=1nactual_kv_sizei\displaystyle \sum_{i=1}^{n} \text{actual\_kv\_size}_i
  2. 分母——预分配总量传统方案为所有请求预留的连续显存总和。由于按最大长度预留,实际使用往往远小于预留值。
    ∑i=1nreserved_contiguous_sizei\displaystyle \sum_{i=1}^{n} \text{reserved\_contiguous\_size}_i
  3. 比值的意义利用率越低,说明浪费越严重。传统方案通常只有 20%-40%,而 PagedAttention 可以将其提升到 90% 以上。

虚拟内存分页

老周苦思冥想,突然想起计算机课上讲过的虚拟内存技术。操作系统的物理内存同样有限,但程序运行时需要的内存是动态增长的。虚拟内存的解决方案是:把物理内存切成固定大小的“页框”(page frame),每个程序按需申请页框,通过“页表”记录程序的逻辑地址到物理页框的映射关系。程序不需要知道自己的数据到底在物理内存的哪个角落,只要查页表就能找到。

这套思想完美对应着推理引擎的困境。既然无法预知 KV Cache 的最终大小,那就不要预知——把显存切成固定大小的“物理块”(block),每个请求开始时只分配一个块,随着 token 生成,再增量分配更多的块。这些块不需要连续,甚至可以分散在显存的各个角落。关键是引入一张“块表”(block table),记录每个请求的逻辑块编号到物理块编号的映射。

操作系统分页思想的核心是“隔离”,而 PagedAttention 把它用在了显存管理上。每个请求的 KV Cache 被拆分成一个个独立管理的块,请求之间互不干扰,GPU 显存的每一寸空间都被精确利用。

LogicalBlocki→Block TablePhysicalBlockπ(i)\displaystyle \text{LogicalBlock}_i \xrightarrow{\text{Block Table}} \text{PhysicalBlock}_{\pi(i)}
块表就是 KV Cache 世界的“页表”,它解除了“逻辑连续”对“物理连续”的依赖。
  1. 逻辑块编号这是从请求视角看到的“第几个块”,逻辑上从 0 开始递增。比如第 5 个逻辑块,表示这个请求的第 5 段 KV Cache。
    LogicalBlocki,i=0,1,2,…\displaystyle \text{LogicalBlock}_i, \quad i = 0, 1, 2, \ldots
  2. 块表映射函数$\pi(i)$ 表示第 $i$ 个逻辑块实际存放在哪个物理块中。这个映射由块表记录,可以在每次分配时动态决定。
    π:LogicalBlock→PhysicalBlock\displaystyle \pi: \text{LogicalBlock} \to \text{PhysicalBlock}
  3. 映射的意义只要查询块表,就能在 $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 矩阵,而是按块读取、按块计算,最终拼接成完整的注意力输出。

NumBlocks=⌈TB⌉\displaystyle \text{NumBlocks} = \left\lceil \frac{T}{B} \right\rceil
块大小 $B$ 是一个关键超参数:$B$ 太小则块表开销大,$B$ 太大则接近传统方案、碎片问题重现。vLLM 在实践中通常取 $B = 16$。
  1. 总 token 数$T$ 是请求生成的 token 总数(包括输入提示词)。这是动态增长的,无法预先确定。
    T=Tprompt+Tgenerated\displaystyle T = T_{\text{prompt}} + T_{\text{generated}}
  2. 块大小$B$ 是每个块能容纳的 token 数。$B$ 决定了内存管理的粒度,是时间-空间权衡的核心参数。
    B=16 tokens/block\displaystyle B = 16 \text{ tokens/block}
  3. 向上取整$\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[i]=pi⟺LogicalBlocki↦PhysicalBlockpi\displaystyle \text{BlockTable}[i] = p_i \quad \Longleftrightarrow \quad \text{LogicalBlock}_i \mapsto \text{PhysicalBlock}_{p_i}
块表让“逻辑连续”变成了纯概念上的连续:从请求视角看,KV Cache 依然是一个完整的、按 token 顺序排列的序列;但在物理显存中,它们可以散落在任何位置。
  1. 块表数组BlockTable 是每个序列独立的数组,数组长度等于该序列当前的逻辑块数。初始时只有 1 块,随着生成过程不断增长。
    BlockTable=[p0,p1,p2,…,pn−1]\displaystyle \text{BlockTable} = [p_0, p_1, p_2, \ldots, p_{n-1}]
  2. 映射语义$p_i$ 表示第 $i$ 个逻辑块存放的物理块号。$i$ 是连续从 0 到 $n-1$ 的,但 $p_i$ 之间完全没有大小或位置关系。
    pi∈{0,1,2,…,M−1},M=GPU 上物理块总数\displaystyle p_i \in \{0, 1, 2, \ldots, M-1\}, \quad M = \text{GPU 上物理块总数}
  3. 查询复杂度数组按下标访问是 $O(1)$,这意味着注意力计算可以高效地定位任意 token 的 K/V,不会因为离散存放而牺牲性能。

共享内存与解码

图书馆来了三场同时进行的讲座,主题都是“区块链入门”。老周发现,三场讲座的开场白资料完全一样——都是 10 本书的基础介绍。如果给每场讲座各分配 10 个格子存放这些重复的书,那就浪费了 20 个格子的空间。聪明的做法是:只存一份“开场白资料”,三场讲座的卡片上都指向这份共享资料,等讲到各自独有的内容时再各自分配新格子。

这正是大模型推理中“并行采样”(parallel sampling)的典型场景。当用户输入一个问题时,模型会以多种不同的随机种子并行生成多个候选回答,最终选出最优的一个。这些并行序列共享相同的前缀(prompt),但后续生成各不相同。在传统方案中,每个序列都要完整存放一份 prompt 的 KV Cache,显存浪费明显。PagedAttention 支持“共享块”:多个序列的块表可以指向同一个物理块!

共享内存配合“写时复制”,让 PagedAttention 在处理并行采样时既能享受内存共享的巨大优势,又保证序列之间的独立性。在 vLLM 的实现中,当 beam search(束搜索)或并行采样需要分叉时,系统自动创建共享块的副本,整个过程对上层调用完全透明。

RefCount[p]≥1表示物理块 p 被多个序列共享\displaystyle \text{RefCount}[p] \geq 1 \quad \text{表示物理块 } p \text{ 被多个序列共享}
引用计数(refcount)是共享安全的关键:只有引用计数降为 0 时,物理块才能被回收。这就像图书馆的书只有被所有借阅者归还后才能放回空闲池。
  1. 引用计数每个物理块维护一个计数器,初始为 0。当某个序列的块表首次映射到该块时,计数器加 1。
    RefCount[p]←RefCount[p]+1\displaystyle \text{RefCount}[p] \leftarrow \text{RefCount}[p] + 1
  2. 共享前缀N 个并行序列共享前 $k$ 个逻辑块时,这些物理块的引用计数都等于 N。显存占用从 $N \times k$ 降为 $k$。
    显存节省=(N−1)×k×BlockSize\displaystyle \text{显存节省} = (N - 1) \times k \times \text{BlockSize}
  3. 写时复制当某个需要独特内容的序列要修改共享块时(比如生成了新 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 在实际应用中的可用范围。

Throughput=Number of Completed RequestsTotal Time∝GPU Memory CapacityAvg KV Cache per Request\displaystyle \text{Throughput} = \frac{\text{Number of Completed Requests}}{\text{Total Time}} \propto \frac{\text{GPU Memory Capacity}}{\text{Avg KV Cache per Request}}
这个公式揭示了吞吐量的本质:当 GPU 计算能力不是瓶颈时,吞吐量几乎完全由显存容量决定——能塞进多少请求,就能完成多少请求。
  1. 吞吐量定义单位时间内完成的请求数。这是衡量推理系统效率的核心指标,直接影响服务成本和用户体验。
    Throughput=Requests CompletedTime\displaystyle \text{Throughput} = \frac{\text{Requests Completed}}{\text{Time}}
  2. 与显存的关系在算力充足的前提下,GPU 能并发处理的请求数 ≈ GPU 总显存 / 每个请求的平均显存占用。分母越小,能并发的请求就越多。
    Concurrency≈⌊GPU MemoryAvg Memory per Request⌋\displaystyle \text{Concurrency} \approx \left\lfloor \frac{\text{GPU Memory}}{\text{Avg Memory per Request}} \right\rfloor
  3. 效果量化传统方案的显存浪费约 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 获得极致性能的必要代价。

Attention(Q,K,V)=Softmax(QKTdk)VwhereK,V are fetched block-wise\displaystyle \text{Attention}(Q, K, V) = \text{Softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V \quad \text{where} \quad K, V \text{ are fetched block-wise}
块大小为 $B$ 时,注意力计算被拆分为多个块级子问题:每个块内执行标准的缩放点积注意力,块之间通过在线 softmax 技巧正确合并。
  1. 块级注意力对于每个查询 token,需要计算出所有 KV 块的注意力分数。每个逻辑块的贡献被独立计算,然后按 softmax 的分配律合并。
    Attni=∑j=1NumBlocksSoftmaxj(QKjT)Vj\displaystyle \text{Attn}_i = \sum_{j=1}^{\text{NumBlocks}} \text{Softmax}_j(QK_j^T) V_j
  2. 硬件并行GPU 拥有数千个计算核心,可以将不同块的注意力计算分配给不同的 SM(流式多处理器)并行执行。块的大小 $B$ 决定了并行度:$B$ 越小,块数越多,并行度越高。
    Parallelism=TB\displaystyle \text{Parallelism} = \frac{T}{B}
  3. 块大小权衡$B$ 太小(如 4)会导致块表过长、管理开销变大;$B$ 太大(如 64)则接近传统连续分配方案,碎片问题重新出现。vLLM 在实验中选取 $B=16$ 作为性能拐点。
python9 行
# 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 演示

python121 行
"""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()
目的:模拟三个并行采样序列共享同一 prompt 前缀的 KV Cache。可以看到,naive 方案需要 6 个块,而共享后只需 4 个块,节省 33% 显存。
  1. class BlockAllocator模拟 vLLM 的物理块分配器,维护 free_list 空闲块列表,allocate 时弹出任意空闲块并递增引用计数。
  2. def copy_on_write实现写时复制。当序列需要修改被共享的块时,先让原块引用计数减 1,再分配新块复制数据,保证序列间互不干扰。
  3. def append_token按需分配。块写满时才申请新块,token_id 会写入当前块的 data 字典(原子化模拟 K/V 存储)。
  4. 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 的核心思想借鉴了哪个领域的经典技术?

单选题 · mid

  • A.数据库的 B+ 树索引
  • B.操作系统的虚拟内存分页← 正确答案
  • C.编译器的寄存器分配
  • D.网络协议的滑动窗口

答案:B。PagedAttention 的核心是引入块表和物理块,将 KV Cache 的逻辑连续性与物理离散性解耦,这正是虚拟内存分页思想。B+ 树索引解决的是数据检索问题,寄存器分配解决的是变量到寄存器的映射,滑动窗口解决的是流量控制,它们都与分页机制无直接关联。

Q2. 传统 KV Cache 管理方案的主要问题有哪些?

多选题 · mid

  • A.内部碎片:预分配空间大于实际需求← 正确答案
  • B.外部碎片:空闲空间不连续导致无法利用← 正确答案
  • C.无法共享 prompt 的 KV Cache← 正确答案
  • D.计算速度过慢导致 GPU 利用率低

答案:ABC。传统方案为每个请求预留连续显存且不共享,因此会产生内部碎片(预分配过大)、外部碎片(显存空洞无法利用)、无法共享 prompt。计算速度慢是模型自身特性,与 KV Cache 管理方式无关,故 D 不正确。

Q3. 块大小(block size)B 的选择对 PagedAttention 性能的影响是?

单选题 · mid

  • A.B 越大,块表占用内存越少,碎片越小
  • B.B 越小,块表占用内存越多,碎片越多
  • C.B 越大,块表占用内存越少,但片段浪费可能增加← 正确答案
  • D.B 的大小对性能没有任何影响

答案:C。B 越大,逻辑块数越少,块表的存储和查询开销越低;但每个块内的最后一部分 token 可能会浪费空间(内部碎片)。B 越小,管理越精细但块表开销越大。因此 B 需要在管理开销和空间利用率之间权衡。A、B 说法都过于极端,D 明显错误。

Q4. 在并行采样(parallel sampling)场景中,多个序列共享 prompt 前缀的 KV Cache,是通过什么机制保证安全性的?

单选题 · mid

  • A.每个序列复制一份完整的 KV Cache
  • B.采用写时复制,只在需要修改时才复制物理块← 正确答案
  • C.只允许读取,不允许任何写操作
  • D.使用全局锁保护所有共享块

答案:B。写时复制(copy-on-write)保证了共享块的线程安全:序列共同读取共享块,只有当某一序列需要写入独有内容时才复制出一份新块进行修改。A 浪费显存,C 不现实(生成过程必然要写入),D 会导致性能瓶颈。

Q5. 以下哪些因素可以解释 vLLM 吞吐量相比传统方案提升 2-4 倍?

多选题 · hard

  • A.显存利用率提升使同一 GPU 能容纳更多请求← 正确答案
  • B.更大的批处理规模充分利用 GPU 算力← 正确答案
  • C.KV Cache 共享减少冗余内存访问← 正确答案
  • D.模型本身的计算浮点精度被降低

答案:ABC。PagedAttention 提高显存利用率→可并发的请求增多→批处理规模变大→GPU 利用率提升;共享 KV Cache 减少了内存占用和访问量。但 vLLM 不改变模型的计算精度(如 FP16 保持不变),因此 D 不正确。

Q6. 块表(block table)的作用是?

单选题 · easy

  • A.记录 KV Cache 中每个 token 的值
  • B.将请求的逻辑块号映射到物理块号← 正确答案
  • C.存储模型的全部权重参数
  • D.保存注意力分数矩阵

答案:B。块表是 PagedAttention 的核心数据结构,数组下标为逻辑块号,值为物理块号。作用类似于操作系统中的页表。A、C、D 都与其他数据结构相关,与块表的功能无关。

Q7. 物理块的引用计数(refcount)降为 0 表示什么?

单选题 · hard

  • A.该块损坏,需要立即修复
  • B.该块被所有序列共享,暂时不能释放
  • C.没有任何序列在使用该块,可以回收← 正确答案
  • D.该块中的数据已复制到 CPU 内存

答案:C。引用计数为 0 意味着所有引用它的序列都已释放对该块的映射,块可以安全归还到空闲列表。B 描述的是 refcount > 1,A 和 D 与引用计数的语义无关。

Q8. PagedAttention 在工程实现中面临的挑战包括?

多选题 · mid

  • A.需要编写专用 CUDA kernel 处理离散块← 正确答案
  • B.调度器需精确追踪块分配与释放← 正确答案
  • C.块表查询增加了注意力计算延迟← 正确答案
  • D.GPU 显存带宽需求自动消失

答案:ABC。专用 CUDA kernel 是必须的(标准 kernel 假定连续内存);调度器对块表的管理是核心功能;块表查询确实引入了额外的间接寻址开销。显存带宽需求不会自动消失,D 错误。

Q9. PagedAttention 最终实现了什么核心目标?

单选题 · easy

  • A.提升模型的推理准确率
  • B.减少 GPU 显存浪费,提升推理吞吐量← 正确答案
  • C.压缩模型体积同时保持效果
  • D.将模型从 GPU 迁移到 CPU 运行

答案:B。PagedAttention 的目标是显存管理优化,通过减少碎片(浪费)来提升吞吐量。它不改变模型输出的质量(准确率),不压缩模型权重,也不负责跨设备迁移。它是“同样的模型、更快的服务”。

Q10. 关于 PagedAttention 的共享内存机制,以下说法正确的是?

多选题 · hard

  • A.共享块被多个序列引用时,引用计数会递增← 正确答案
  • B.序列要修改共享块时,必须触发 copy-on-write← 正确答案
  • C.共享机制适用于所有解码策略(如贪心解码也能受益)
  • D.共享块的释放需要等到所有引用解除← 正确答案

答案:ABD。引用计数随共享增加;写时复制保证独立性;释放需等所有引用解除。但贪心解码(greedy decoding)每个序列只有一个,没有共享的需求,因此 C 不正确。共享主要面向 parallel sampling / beam search。