2.3 PagedAttention:把页表搬进显存 本节摘要:KV Cache 的体量在 2.2 被证明确实能压垮显存,可多数推理框架连这块显存都用得稀烂——它们给每个请求预留一整条连续空间,按最大可能长度算,结果一半以上是空仓位。PagedAttention 借了操作系统的页表思路:把缓存切成固定大小的逻辑块,用一张块表映射到零散的物理块,按需分配、还能让不同请求共享同一段前缀。这一节先算清连续分配浪费了多少,再看块表三件套怎么把这浪费吃回去。 连续显存分配的隐形税 先看清旧做法交了多少"租金"。早期推理服务给每个请求在显存里划一整条连续区域,长度按该请求可能达到的上限预留——典型就是模型支持的最大上下文,比如 4096 或 8192 个 token。请求没写完,这条区域就一直占着;
本节摘要:KV Cache 的体量在 2.2 被证明确实能压垮显存,可多数推理框架连这块显存都用得稀烂——它们给每个请求预留一整条连续空间,按最大可能长度算,结果一半以上是空仓位。PagedAttention 借了操作系统的页表思路:把缓存切成固定大小的逻辑块,用一张块表映射到零散的物理块,按需分配、还能让不同请求共享同一段前缀。这一节先算清连续分配浪费了多少,再看块表三件套怎么把这浪费吃回去。
先看清旧做法交了多少"租金"。早期推理服务给每个请求在显存里划一整条连续区域,长度按该请求可能达到的上限预留——典型就是模型支持的最大上下文,比如 4096 或 8192 个 token。请求没写完,这条区域就一直占着;写完了,这条区域才释放。
问题在"按上限预留"。真实对话里,十个请求九个用不满上限。假设服务预留 4096,平均一个请求实际生成 1500 就停了,那每条区域里有 2596 个 token 位是空仓位,内部碎片率约 63%。这不是个别现象,是结构性浪费:你为"可能很长"付了全款,却只为"实际这么长"在用。
更糟的是外部碎片。连续分配要求一整条不能有缝,于是当显存放着几条长短不一的已释放区域,哪怕它们加起来够装一个新请求,只要不连成一条,就装不进——系统报"显存不足",可真实空闲块散落各处用不上。连续分配还顺手堵死了共享:两个请求前面一千个 token 是同一份系统提示词,旧做法各留各的连续区,两份一模一样的 K、V 在显存里并存,谁也借不到谁。
把数字摆上桌。并发 8 路、每路预留 4096、平均实际 1500。预留总量 = 8 × 4096 = 32768 个 token 位;实际用到 = 8 × 1500 = 12000。浪费 = 20768 个 token 位,浪费率 63%。换算到 7B 的 0.5MB/token,这就是白白占着约 10GB 显存却什么都不干。这 10GB 本来能多塞进两三个并发请求——连续分配直接把它扔进了下水道。
PagedAttention 的解法和操作系统管内存如出一辙:不预留连续长条,把缓存切成固定大小的"块",每块装 16 个 token 的 K、V。每个请求维护一张"块表",记下自己的逻辑块序号对应到哪块物理块。请求生成到哪,就按需领一块新物理块挂上块表;没生成到的地方,不占物理显存。
逻辑块是请求视角的连续编号(第 0 块、第 1 块……),物理块是显存里实际散落的位置。块表就是那张"逻辑→物理"的映射。注意力计算时,内核按块表把分散的物理块拼成逻辑上连续的数据流,对外看起来仍是一整条——GPU 显存零碎,计算视角完整。
这张图左边是连续分配的浪费链,右边是 PagedAttention 的按需链。右边把"按上限付费"换成了"按实际付费",碎片从"整条预留的剩余"缩小到"最后一块没填满的那几个 token",粒度从几千降到 16 以内。
和操作系统页表有三个差别必须点明,否则容易想偏。第一,物理块永远在 GPU 显存里,不存在"换出到内存再换回"的缺页机制——KV 数据离 GPU 太远就失去缓存意义,所以 PagedAttention 的块表没有 swapping 这一层。第二,块大小是 16 个 token 而非 4KB 页,这是为 GPU 的内存访问特性调过的,太大碎不起来、太小访存次数爆炸。第三,也是最有价值的差别:块可以共享。两份请求若前缀相同,它们的逻辑块 0、1、2 可以指向同一组物理块,块表里的"引用计数"加一即可,不需要复制。这点操作系统页表也能做(写时复制),但 PagedAttention 把它用在了"共享提示词前缀"这个推理专属场景上,后面 2.4 的 RadixAttention 正是站在这块共享能力之上。

碎片降下来,直接好处是能塞进更多并发。连续分配卡在"显存总量够、但凑不出一条连续"的假性不足;PagedAttention 把显存当成可任意拼接的块池,几乎不挑形状。同一块显卡,能同时服务的请求数上去了,GPU 计算单元被喂得更满——大模型推理的瓶颈常在"等显存、等批",批越大利用率越高。
共享前缀又叠加一层增益。一批请求若都带着同一份长系统提示词,连续分配要为每个请求复制这份 K、V;PagedAttention 让它们指向同一组物理块,省下的正是 2.2 算过的"每 token × 序列长 × 批大小"里"前缀 ×(批大小减一)"那部分。批里请求越多、共享前缀越长,这层省得越狠。
还有个隐性收益:调度变灵活。连续分配要等一个请求整条预留到位才能起,PagedAttention 来一个请求先给一块,边生成边补,配合连续批处理能把刚空出的块立刻转给新请求。显存周转快了,单位时间吞吐自然高。
共享前缀听着美好,落地要一套记账。每个物理块带一个引用计数:几个逻辑块指向它,计数就是几。新请求来了,若它的前缀和已有请求重合,块表直接把对应物理块的引用加一,不复制数据——这是写时复制的思想:只读时共用,一旦某个请求要开出不同的新块(多轮回溯、分支改写会出现),才真正分配新物理块。引用降到零,块立刻可回收,腾出的位置马上能转给别的请求。
真正的压力点在物理块池见底。并发高、共享少时,池子也会被填满。这时 PagedAttention 必须有淘汰策略:把最久未被引用的块换出去。和操作系统不同,它通常不换到 CPU 内存——KV 离 GPU 太远就失去缓存意义,所以多是直接丢弃。丢了意味着对应前缀的命中失效,下次那段要重算 Prefill。淘汰顺序很讲究:基数树(2.4)里被越多请求共享的块越该留,孤立的冷块先走。PagedAttention 管"块在哪",淘汰策略管"块去留",二者配合才让池子在高并发下不崩。
还有一个工程细节:块大小取 16 是经验值,不是铁律。块太大,最后一块浪费的 token 多,碎片率回升;块太小,块表变长、映射开销和访存次数上升,GPU 的合并访问优势减弱。16 在多数模型上平衡了碎片与开销,但小模型或超长上下文常调到 32 甚至 64。调这个参数,本质是在"碎片"和"查表成本"之间找拐点,和操作系统页大小的选择是同一个数学。理解块大小,就不难理解为什么 PagedAttention 的吞吐提升在"请求长度参差不齐"时最明显——整齐的负载反而体现不出按需分配的价值,参差才显出它的威力。
PagedAttention 解决的是"分配效率"——让缓存这块显存不被碎片和重复拷贝糟蹋。它不解决"总量上限":显存再会省,也省不出比物理容量更多的空间。当共享和碎片优化都榨干后,缓存仍可能见顶,那时就得靠 2.4 把相同前缀组织得更聪明(让命中率更高、重算更少),以及 2.5 在架构和精度上直接瘦身。
换句话说,PagedAttention 是"把仓库用干净",RadixAttention 是"让更多客人共用同一批货",压缩三路是"把每件货做小"。三者接力,才把"算力预付、缓存回收"这件事务实落地。下一节看第二棒:前缀怎么被组织成可命中的缓存。