🏛️ 第33讲:显存碎片吞噬了60%算力?——KV Cache 内存管理演进:PagedAttention、Continuous Batching 与 Chunked Prefill 深度解构
主讲人:👓 Ringi(大厂 AI Infrastructure 工程师)
所属模块:Module 05: LLM 在线推理系统与性能工程
篇章范式:🚀 LLM 推理服务与高性能 Serving 篇(Inference Serving & Systems Paradigm)
核心导读:在上一讲中,我们建立起了 Prefill 与 Decode 两阶段物理特性的坐标系,并得出了一个冷酷的结论——Decode 是严重的访存受限型任务,而提升硬件利用率的唯一生命线就是“做大 Batch Size”。然而,当你兴冲冲地试图把并发请求塞进单张 80GB 的 GPU 时,现实却迎头泼来一盆冷水:系统往往只跑了不到 20 个并发就惨遭 CUDA OOM(显存溢出)崩溃,而nvidia-smi却显示实际用到的显存甚至还不到一半!到底是谁在暗中吞噬宝贵的显存?为什么操作系统几十年前发明的“虚拟内存分页”思想,在大模型时代成为了价值百亿美金的系统级突破?本讲将带你深入显存碎片的微观世界,从硬件指针与 Block Table 映射手算出发,彻底解构 PagedAttention、Continuous Batching(连续批处理)与 Chunked Prefill 的协同物理机制,并带你摸清显存耗尽时系统抢占(Swapping vs Recompute)的生死抉择。

📑 目录导航
- 0. Ringi 开场:生产真实现场与痛点冲突
- 1. 连续分配的原罪:从“餐厅十人桌预留”到显存内外碎片深渊
- 2. PagedAttention 核心架构:操作系统虚拟分页思想向 GPU 显存的史诗级迁徙
- 3. 调度范式革新:Continuous Batching(迭代级动态组批)的物理装箱原理
- 4. 削峰填谷之战:Chunked Prefill 与 vLLM V1 统一 Token 预算调度器
- 5. 极端显存危机下的保命防线:Preemption(抢占)、Swapping(换出)与 Recomputation(重算)
- 6. 高阶前缀复用:Prefix Caching 与 Radix Tree 块管理内核
- 7. 动手实战与代码实验室(Minimal Runnable Code)
- 8. Ringi 避坑指南与生产黄金准则
- 9. Ringi 5 点核心速记口诀、自我检验清单与课后深度思考题
- 10. 📚 参考资料与核心源码/经典论文指引
- 附录:Appendix A — 大厂硬核高频面试题与白板推导(Interview Drill)
- 🎨 【配图工坊生图 Prompt 暂存区 · 生成配图后可一键整块删除】
0. Ringi 开场:生产真实现场与痛点冲突
0.1 真实工程矛盾:为什么 80GB 显存还剩 30GB“可用”,新请求一进却当场报 CUDA OOM?
在大模型推理系统搭建的早期,许多工程师都曾经历过一次极度崩溃的“显存幽灵事件”: 你手头有一台装配了 8 张 NVIDIA A100-80GB 的服务器,部署了一个 13B 参数的对话模型。按理论计算:- 模型静态权重占用约 显存;
- 单卡 80GB 显存扣除权重后,还足足剩下 的巨额显存空间专门给 KV Cache 使用;
- 按上一讲的手算结果,13B 模型单 Token KV Cache 仅占约 ,哪怕平均会话长达 2048 Token,一个请求也就占约 ;
- 算盘打得噼里啪啦响: 。也就是说,单卡理论上至少应该能轻松扛起 150 个并发请求!
“48.12 GiB already allocated; 29.80 GiB free” —— 显存明明还空余着整整 29.8 GB,系统却连一个 1.25 GB 的张量都申请不出来,直接当场暴毙挂掉!钱花在了哪里?为什么明明有空闲显存却用不了? 答案就在于:在传统的连续显存分配框架下,显存被切成了无数彼此孤立的碎块。大模型面对未知的生成长度,必须按最坏情况预留连续物理空间;而真实的会话长度天差地别、生命周期参差不齐,这使得物理显存很快退化成千疮百孔的“蜂窝煤”! 空闲显存总量看起来虽然很大,但没有任何一块连续的物理空间能塞下下一个请求。这种“看得见、摸不着”的显存碎片,直接吃掉了系统超过 60% 到 70% 的理论算力潜能!
0.2 线上真实事故复盘:某电商大模型对话集群在促销前夕因连续显存预分配导致请求并发断崖崩塌
2023 年秋,某头部电商大厂为备战大促,上线了一套基于开源 LLM 架构的智能导购助手。底层推理引擎基于早期未集成 PagedAttention 的自研 Serving 框架构建,单机部署 8 卡 H800。 事故现场还原:- 流量激增:晚间 20:00 促销活动开闸,外部网关流量从平时的 20 QPS 骤升至 180 QPS;
- 保守配置:为了防止线上请求因上下文超长而截断,业务配置了
max_sequence_length = 4096; - 连锁崩塌:
- 系统为每个涌入的 HTTP 请求按照 4096 Token 的最大尺寸在 GPU 显存中预开辟连续张量缓冲区;
- 绝大部分用户的真实咨询非常简短(如“这件衣服有 M 码吗?”、“包邮吗?”),实际只聊了 150~300 个 Token 就主动关闭了会话;
- 但因为请求退出时释放的大块内存与其它未结束请求交错,导致物理显存中留下了海量无法拼接的离散缝隙;
- 在并发数仅仅达到 42 时,新进来的第 43 个请求由于在显存中找不到连续的 4096 空间,直接触发 CUDA OOM 异常;
- 异常未被优雅捕获,导致 Serving 守护进程挂掉重启;其他可用节点瞬间遭遇流量倾泻,引发全集群连环雪崩!
- 损失定级:
- 导购助手全网瘫痪 28 分钟,P99 响应超时率达 94.2%,大促前置转化率直接下跌 18%。
- 最大稳定并发承载数从 42 飙升至 280(提升 6.6 倍);
- 显存碎片率从惊人的 68% 骤降至 3.2%;
- 硬件采购成本直接节约了数千万元!
0.3 传统连续分配 vs PagedAttention vs Continuous Batching 全维度对照速查表
1. 连续分配的原罪:从“餐厅十人桌预留”到显存内外碎片深渊
💡 架构全景速览:在深潜源码前,先在白板上建立坚不可摧的 KV Cache 内存管理演进、PagedAttention 与 Continuous Batching 物理底账。
1.1 内部碎片(Internal Fragmentation):未知的输出长度与最坏打算预分配
在计算机体系结构中,内部碎片(Internal Fragmentation) 指的是:系统分配给某个任务的物理内存空间,远大于该任务实际消耗的空间,而这部分多余的空间被该任务强行霸占,无法被任何其他任务使用。 在大模型自回归推理中,内部碎片之所以致命,是因为一个纯粹的算法特性:在生成结束之前,全世界没有任何人能提前预测一个大语言模型到底会输出多少个字!
- 用户发送了一个 Prompt,设置了参数
max_new_tokens = 2048; - 原生 PyTorch 算子要求注意力计算的张量必须在物理显存上呈现连续的内存布局;
- 引擎为了防止在生成到第 1500 个 Token 时因显存不足而报错,只能在请求进来的第 0 步,就硬生生在显存里划出整整 2048 个 Token 的连续空间;
- 最终结果:模型可能只回答了一个 JSON 结构体或一句简短的“好的”,在第 80 个 Token 输出了
<|endoftext|>; - 剩下的 1968 个 Token 物理槽位,在整个会话执行期间全部在显存中“死躺”,有效利用率仅有区区 3.9%!
1.2 外部碎片(External Fragmentation):可变生命周期造成的显存“蜂窝煤”
如果说内部碎片是单个请求内部的浪费,那么 外部碎片(External Fragmentation) 则是多个并发请求在物理时间轴上交错交织引发的系统性崩盘。 在大模型在线服务中,不同请求的生命周期高度异构:- 请求 A 处理快速问答,50 步结束退出;
- 请求 B 进行代码分析,800 步结束退出;
- 请求 C 处理长文翻译,2000 步结束退出。
1.3 预留等待碎片(Virtual Reservation Waste):长会话渐进增长的动态悲剧
除了内部碎片和外部碎片,传统 Serving 架构还存在第三重极度隐蔽的浪费——预留等待碎片(Reservation Waste)。 即使一个长请求最终确实生成了 2048 个 Token,它的生成也是一步一步(逐字自回归)发生的:- 在第 1 步,它实际只用了 1 个槽位,剩下的 2047 个槽位虽然未来会被填满,但在当前这一时刻它们是纯粹的空闲显存;
- 在第 100 步,它用了 100 个槽位,剩下的 1948 个槽位依旧处于闲置状态;
- 直到生成即将结束的那最后几步,这片显存才真正达到了 100% 的利用率。
1.4 公式五步穿透:连续分配显存有效利用率上限推导与数学证据
① Why(为什么需要算它?)
在向管理层或架构评审会汇报系统重构价值时,绝不能只说“连续分配不好”,必须给出严格的数学利用率上限推导,证明在统计学意义上,连续分配为什么注定会杀死 60% 以上的硬件吞吐。② Mental Model(物理直觉比喻)
假设一家拥有 100 个车位的停车场:每辆车进场时,管理员根本不看车型,一律按加长重型卡车的标准(占 5 个车位)给每辆车画专属隔离带。哪怕开进来的是一辆微型老年代步车,旁边 4 个车位也空着不准别人停。哪怕整座停车场只停了 20 辆小轿车,管理员就必须在门口挂出“车位已满”的牌子。③ Tiny Calculator(极简数字手算)
假设系统最大上下文预分配长度为 。 真实业务流量的实际生成长度 服从均匀分布,介于 到 之间(平均值 Token)。- 单个请求平均实际消耗槽位: ;
- 单个请求强制预分配槽位: ;
- 单请求内部利用率:
- 即使考虑会话过程中逐步追加写入的动态积分,有效时间-空间乘积(Area-Time Utilization)在数学期望上:
④ Formal Model(标准公式与数学证明)
假设请求的实际生成长度 服从概率密度函数 ,定义域为 。系统采用最大长度连续预分配策略。 整个在线推理集群的 显存空间静态利用率期望值 为: 在真实互联网大模型会话场景中(如 Chatbot、Search 等),实际生成长度长尾分布极强,平均生成长度 通常在 Token 之间,而系统配置的 通常为 。 代入真实参数: 如果再加上外部碎片导致的不可分配损耗系数 :⑤ Sanity Check(数量级校验)
残酷的数学事实:在不采用虚拟内存分页的前提下,大模型推理集群在物理显存上的有效利用率在数学上被死死锁死在 10% ~ 20% 的超低区间。这就意味着:你花了 1000 万元买的 GPU 集群,有 800 万元纯粹是在为连续内存的愚蠢假设买单!2. PagedAttention 核心架构:操作系统虚拟分页思想向 GPU 显存的史诗级迁徙
2.1 体系结构对照:从 OS MMU/Page Table 到 vLLM Block Table
面对连续内存碎片的百年困局,计算机科学史早在几十年前就给出了终极答案——虚拟内存分页(Virtual Memory Paging)。 在现代操作系统(如 Linux)中,进程执行malloc(1GB) 时,操作系统根本不会真的在物理内存里找一块连续的 1GB 空间;操作系统的内存管理单元(MMU)只是在进程的虚拟地址空间中画了一张饼,背后将这 1GB 拆解成数万个大小为 4KB 的虚拟页(Virtual Pages)。这些物理页散落在 RAM 的任意角落,甚至可以不连续、乱序存放,全靠一张 页表(Page Table) 进行物理地址动态寻址。
伯克利团队的划时代洞察在于:
大语言模型的 Prompt 和输出 Token 序列,在逻辑上是连续的文本流;但在 GPU 物理显存里,它们凭什么非得连续存放?为什么不能把操作系统的那套页表照搬进 GPU?这就是 PagedAttention 的灵魂所在。
2.2 核心数据结构与 Shape 拓扑:Logical Block vs Physical Block vs Block Table
在 PagedAttention 的世界中,显存管理被重构为三层精密的抽象结构:1. 块大小(Block Size, )
系统将每个物理块能够容纳的连续 Token 数量定义为 Block Size(在 vLLM 中,标准基线通常取 或 )。- 为什么不选 ?如果每个 Token 都是一个独立块,块表的长度和管理开销会过大,且无法利用 CUDA 内存合并访存(Coalesced Memory Access);
- 为什么不选 ?块过大又会导致最后一个块内的内部碎片回潮。实测证明 和 是碎片控制与访存吞吐的黄金平衡点。
2. 显存物理块张量形状(Tensor Shape)
在 GPU 显存底层,vLLM 会在服务启动时开辟一个庞大的物理块池,其张量形状被固化为: 其中 是为了满足 GPU 向量化加载(如 16 字节float4 内存指令)设置的内嵌重排维度(通常为 8)。
这个巨大的连续张量一旦分配,就再也不进行任何销毁与重分配,彻底规避了向操作系统反复申请释放显存的巨大开销。
3. 块表(Block Table)的运作拓扑
每一个正在运行的推理请求,在 CPU 端由调度器维护一个动态递增的一维整型数组:block_table = [p_block_id_0, p_block_id_1, ...]。
2.3 CUDA Kernel 执行层穿透:Warp 级物理间接寻址与非连续 SRAM 读取
有人可能会产生一个深层怀疑:“把数据切成散落在各处的碎块,在数学和逻辑上确实省了显存,但在 GPU 上计算 Attention 时,难道不需要先把它们拷贝成一块连续的物理张量吗?如果要拷贝,那性能岂不是全毁了?”答案是:绝对不需要拷贝!PagedAttention 写出了一个专门的 CUDA Kernel,直接在算子内部通过间接寻址完成非连续读取! 在 Decode 阶段执行 Attention 时:
- 线程网格分配:每个 CUDA Thread Block 负责处理一个或多个 Query Head 的注意力计算;
- 块表送入高速缓存:当前请求对应的
block_table(一小串整数数组)在启动 Kernel 时被复制进 GPU 的常量内存(Constant Memory)或 SM 片上共享内存(Shared Memory); - 外层循环按块推进:
- Warp(32 个并行线程)在外层循环中逐一遍历该请求的物理块号;
- 线程们直接拿到物理块号,通过物理步长(Stride)算出的地址,直接用全局访存指令从 HBM 的分散物理块中读取这一批 16 个 Token 的 Key 向量进 SRAM;
- 在片上寄存器中与当前的 向量完成点乘累加(Online Softmax);
- 随后再从对应的 Value 物理块中加载 16 个 Value 向量完成加权乘加;
- 完全零额外内存拷贝:从 HBM 到片上 SRAM 的全流程中,根本没有任何多余的张量拼接开销!
2.4 意外之喜:Copy-on-Write(写时复制)与多分支/并行采样零开销共享
PagedAttention 的虚拟分页架构,还无心插柳地解锁了大模型系统中最令人惊叹的黑魔法——内存写时复制(Copy-on-Write, CoW)。 在大模型应用中,经常会遇到以下场景:- 并行采样(Parallel Sampling / Best-of-N):用户给出一个 Prompt,要求大模型同时生成 4 个不同风格的回答供挑选;
- 束搜索(Beam Search):在复杂推理或机器翻译中,每一步维护 Top-K 条候选搜索分支;
- Agent 多分支回溯:智能体在决策树中尝试多条推演路径。
- 只读共享期:
- 4 个子任务的
block_table在创建那一刻,直接指向那几个已经算好的相同的物理块; - 每个物理块维护一个轻量级的 引用计数(
ref_cnt),此时ref_cnt = 4; - 显存复制量:严格为 0!创建 4 个分支只需要复制几十字节的整数映射表!
- 4 个子任务的
- 写入分叉期(CoW 触发):
- 当任务 1 生成了一个独特的词,试图向最后一个物理块写入新内容时,系统检测到该物理块的
ref_cnt > 1; - 此时系统才触发 写时复制:从空闲池中申请一个新的物理块,把被修改的这一个块的内容复制过去,成为任务 1 的私有块,其
ref_cnt设为 1,同时原物理块的ref_cnt减 1; - 任务 1 更新自己的块表,后续写入完全独立,其他 3 个任务毫无察觉!
- 当任务 1 生成了一个独特的词,试图向最后一个物理块写入新内容时,系统检测到该物理块的
fork() 进程如出一辙的设计,让束搜索和并行采样的显存开销锐减了 55% 以上,系统吞吐量翻倍提升!
3. 调度范式革新:Continuous Batching(迭代级动态组批)的物理装箱原理
3.1 Static Batching 的“木桶之锁”:被最长序列霸凌的已完成请求
如果说 PagedAttention 是彻底重构了“空间(显存)”的微观布局,那么 Continuous Batching(连续批处理) 则是彻底解放了“时间(计算调度)”的宏观流水线。 在上一章我们痛斥过 Static Batching(静态批处理) 的罪状。让我们用一个更残酷的时间步视图来看看它的算力空转:- 只要最慢的那个人还在跑,其他已经回答完毕的用户其结果不能及时返回给客户端(除非上层强行做流式 hack,但底层显卡槽位无法交还);
- 排在等待队列里的请求 5、请求 6 只能眼睁睁看着空闲槽位在做无意义的
<PAD>矩阵乘法,系统吞吐死死卡在三成以下。
3.2 Iteration-level Scheduling:以 Token 生成步为粒度的动态进出状态机
2022 年,微软团队在 OSDI 发表的奠基性论文 Orca 提出了一个直击灵魂的解决方案:为什么必须把一整批请求‘同生共死’地绑在一起?大模型前向推理本来就是逐步推进的,为什么不在每一次生成单个 Token 的迭代步(Iteration / Step)之后,重新组装一次 Batch?这就是 Continuous Batching(连续批处理 / 动态迭代级组批)。 它的核心机制由一个严密运转的请求生命周期状态机驱动:
- 完成即退(Instant Eviction):在步 ,若请求 1 吐出了
<|endoftext|>,它在这一步结束的微秒级瞬间被当场剔除,其占用的槽位和显存 Block 立刻宣布释放! - 随到随补(Immediate Admission):在步 ,调度器立刻从等待队列中拉入一个排队中的新请求 5。新请求 5 顺畅地与正在继续跑第 51 步的请求 2 拼在同一个物理张量中进 GPU 计算!
- 车轮永动:GPU 内部的计算流水线就像一趟“招手即停、到站即下”的高频地铁,永远保持车厢满载,彻底消灭了无所事事的“幽灵等待”。

3.3 为什么必须配合 PagedAttention?细粒度即时回收与按需分配的“水乳交融”
这里必须强调一个被许多浅层文章忽略的体系结构绝配关系:Continuous Batching 和 PagedAttention 不是两项孤立的技术,它们是必须合体才能产生核聚变的孪生兄弟!思考一个问题:如果只有 Continuous Batching,却没有 PagedAttention,系统会发生什么?
- 假设请求 1 在第 50 步退出了,空出了一片显存;
- 但如果显存分配依然是传统的连续大块,新来的请求 5 需要 2000 个 Token 的空间;
- 请求 1 留下的“小洞”根本塞不下请求 5!
- 调度器虽然想在下一个 Step 拉新请求上车,但由于显存碎片无法被即时细粒度复用,Continuous Batching 的动态进出机制将完全沦为空想!
- 任何一个请求退出,归还的都是几个标准的通用零件;
- 任何一个新请求进来,或者任何一个老请求长大,都是直接抓取这几个零件拼装;
- 正是 PagedAttention 提供的超细粒度、零碎片的物理显存弹性,才使得 Continuous Batching 的“随到随拼”成为了现实!
3.4 算力利用率从 30% 跃迁至 80% 的硬件物理归因
在上一讲的 Roofline 模型推导中,我们证明了 Decode 阶段的核心困局是: 单步计算必须从显存搬运完整的模型权重,唯有做大并发 ,才能平摊权重搬运的开销。- Static Batching 的死结:为了防止 OOM,受制于显存碎片和木桶短板,静态 Batch Size 往往只能保守地设为 。算术强度被压制在极低水平,GPU 算力利用率(MFU)惨淡地徘徊在 20% ~ 30%;
- Continuous Batching + PagedAttention 的飞跃:消除了碎片和等待,同一张卡可以常态化维持 的超大并发批次。
- 物理回报:算术强度成倍攀升,逼近 H100 的 295 FLOP/Byte 平衡点,GPU 计算核心的有效利用率瞬间跃升至 75% ~ 85%! 这就解释了为什么 vLLM 在 2023 年一经推出,全球各大云厂商立刻排队废弃旧架构。
4. 削峰填谷之战:Chunked Prefill 与 vLLM V1 统一 Token 预算调度器
4.1 长 Prefill 的“推土机效应”:同批 Decode 线程遭遇毫秒级断崖停摆
然而,历史的螺旋式上升从不停歇。当我们沉浸在 Continuous Batching 的高吞吐喜悦中时,我们在第 0.2 节遭遇的惨烈事故再次敲响了警钟:Prefill 与 Decode 的物理冲突在 Continuous Batching 内部引爆了! 在没有流量管控的系统中:- 调度器每一步都在 Running 队列里装载了 32 个正在 Decode 的请求;
- 此时 Waiting 队列里冷不丁冒出一个带有长上下文的请求(如一段长达 8,000 Token 的文献翻译);
- 调度器机械地判定:“显存够,把这个请求塞进这一步的批次里”;
- 推土机进场:为了在单步内算完这 8,000 个 Token 的前向传播,GPU 的计算耗时从平时的 20ms 瞬间暴涨至 500ms 以上;
- 同处该 Step 的 32 个无辜的 Decode 请求,全部被强行绑架在 GPU 物理执行流水线上,整整停顿半秒钟无法吐字!
4.2 Chunked Prefill 算法本质:将非弹性长 Prompt 物理切片
破解之道,正是微软与清华等学者提出的 Chunked Prefill(分块预填充)。 它的第一性原理极其纯粹:如果一个巨型集装箱会压垮桥梁,就必须在收费站前将其拆解为若干个标准托盘,以均匀的间隙放行!系统不再允许任何长 Prompt“整存整取”地霸占 GPU 单步时间。
- 设定一个固定的分块切片阈值(如 );
- 当一个 4000 Token 的长 Prompt 到达时,调度器将其切为 个连续的 Chunk;
- 第 1 步:仅执行 Chunk 0(计算 512 个 Token 的 Attention,将其写入 KV Cache),并与批次内的其他 Decode 请求一同执行,单步耗时稳定在 35ms;
- 第 2 步:继续执行 Chunk 1(此时利用已经算好的 Chunk 0 的 KV Cache 作为历史进行掩码注意力),耗时依然为 35ms;
- ……
- 第 8 步:最后一个 Chunk 7 执行完毕,产出首个输出 Token,该请求正式无缝切换为 Decode 模式!
4.3 vLLM V1 调度核心演进:抹平 Prefill 与 Decode 阶段二分
在 vLLM 最新的 V1 生产级调度架构 中,工程实现跨越到了一个前所未有的高度:彻底抹平阶段界限。 在 V1 引擎源码(vllm/v1/core/sched/scheduler.py)中,传统的“Prefill 调度队列”与“Decode 调度队列”被全部废弃。取而代之的是一个无比凝练的统一抽象:
对底层 GPU 执行器而言:
- 一个做 Decode 的请求,本质是:这一步需要处理 1 个 Token;
- 一个做 Chunked Prefill 的请求,本质是:这一步需要处理 512 个 Token;
- 一个做短 Query Prefill 的请求,本质是:这一步需要处理 64 个 Token;
- 一个在做投机解码验证的请求,本质是:这一步需要处理 5 个候选 Token!
4.4 优先级裁决树:Decode 先行保流速,余量装箱切 Prefill
调度器在每一步调度时的核心伪代码与决策树逻辑如下:5. 极端显存危机下的保命防线:Preemption(抢占)、Swapping(换出)与 Recomputation(重算)
5.1 物理块池耗尽时的系统抉择:OOM 拒绝服务 vs 优雅抢占
尽管 PagedAttention 消除了绝大多数无谓的显存碎片,但在极端高并发或超长会话突增的流量洪峰下,依然会遭遇物理现实的无情撞击:GPU 上的所有物理 Block 真的被彻底用光了! 当批次中有 64 个 Decode 请求正在推进,这一步它们各自需要生成一个新 Token,而物理块池中恰好连 1 个空闲 Block 都没有了,系统该怎么办?- 选型 A:直接向客户端抛出
Internal Server Error: Out of Memory,掐断连接(直接杀死用户体验); - 选型 B:像 Linux 调度进程一样,临时**抢占(Preemption)**一部分倒霉的请求,剥夺它们的物理显存归还给公池,确保剩下的请求能够安全跑完!
5.2 换出(Swapping):GPU HBM Host RAM 跨 PCIe 搬运的成本与瓶颈
在抢占机制中,第一种经典的救火方案叫做 Swapping(换出):- 调度器按照“后进先出(LIFO)”或“优先级队列”挑出一个最年轻的牺牲品请求;
- 将该请求在 GPU HBM 显存中持有的所有物理 Block,通过 PCIe 总线 异步搬运拷贝到宿主机(Host CPU)的内存(RAM)中暂存;
- 将该请求在 GPU 上的物理 Block 标记为释放,供高优先级的请求继续执行;
- 等到高优先级请求完成退出、GPU 显存重新充裕后,再通过 PCIe 把这些 Block 从 CPU 内存拷贝回 GPU 显存,唤醒该请求继续 Decode。
5.3 重计算(Recomputation):丢弃 KV Cache 重新 Prefill 的经济性比较
针对 Swapping 的带宽瓶颈,第二种硬核方案应运而生——Recomputation(丢弃重算):- 调度器直接将牺牲品请求在 GPU 中的所有 KV Block 当场清空抹去,完全不往 CPU 内存搬运;
- 将该请求打回 Waiting 队列的最前端;
- 把该请求截至目前所生成的所有文本(原 Prompt + 已生成的输出 Token)全部打包,视作一段全新的大 Prompt;
- 等显存空出来后,让它重新跑一次 Prefill!
5.4 临界抉择推导:计算算力 vs PCIe 带宽的交叉点手算
究竟是 Swapping 划算,还是 Recomputation 划算? 这是一个大厂资深架构师必须能够信手拈来的体系结构量化推导题。 设某请求已经累积生成的上下文长度为 Token,模型参数量为 :- 方案 1:Swapping 跨 PCIe 搬运的总耗时(以 PCIe 4.0 32 GB/s 计):
- 方案 2:Recomputation 重新 Prefill 跑一次的计算耗时(以 H100 实际 MFU 下算力 计):
- 重新计算该模型 1000 Token Prefill 的耗时:
- 而跨 PCIe 4.0 换出再换入这 1000 个 Token 的 KV(约 )的耗时:
- 在中小模型或短上下文场景:GPU 重算极快,Recomputation 简单高效,不吃 Host 内存;
- 在大模型或极长上下文场景:重新跑一次大 Prefill 的算力开销极其昂贵,Swapping 换出的速度比重算快整整一个数量级以上! 因此工业引擎在 70B 级以上模型中普遍倾向于优先采用 Swapping 机制保命。
6. 高阶前缀复用:Prefix Caching 与 Radix Tree 块管理内核
6.1 多轮对话与统一 System Prompt 的重复计算浪费
在智能客服、多轮对话助手以及 Agent 应用中,流量存在极其恐怖的上下文冗余:- 某企业客服机器人的 System Prompt(包含企业规章、免责声明、工具定义)长达 3,000 Token;
- 用户问第一句话:“你们营业时间是几点?”(10 Token);
- 机器人回答后,用户追问第二句:“周日开门吗?”(10 Token);
- 如果不作任何处理,在处理第二句追问时,系统必须把前序的 3,000 Token System Prompt + 第一轮对话的历史 完完整整重新跑一次 Prefill!
6.2 Radix Tree(基数树)物理机制:Token ID 路径匹配与 Block 引用计数维护
为了终结这种野蛮的算力浪费,以 SGLang(RadixAttention)为代表的技术创新将 Radix Tree(基数树 / 前缀树) 引入了 KV Cache 物理块管理。 其核心机制是:- 树节点即 Block 序列:树的每一条分支路径代表一段特定的 Token ID 序列,叶子节点和内部节点直接索引着物理显存中对应的 Physical Block ID;
- 前缀秒级匹配:
- 新请求到达后,调度器首先将其 Token ID 序列放入 Radix Tree 中进行最长前缀匹配(Longest Prefix Match);
- 若命中了一个包含 3,000 Token 的系统前缀,调度器直接在物理显存中找到这批已经算好的 Physical Blocks,并将当前请求的
block_table前半段直接指向它们; - 该请求的 Prefill 计算量瞬间缩减 99%,TTFT 从数秒直接暴降至几毫秒!
- 引用计数与驱逐:
- 只要有会话在使用该分支,树节点所指向的物理块其
ref_cnt > 0,不可被释放; - 当会话退出后,物理块并不立刻抹去,而是保留在树上,其
ref_cnt归零,成为“可被回收的缓存节点”。
- 只要有会话在使用该分支,树节点所指向的物理块其

6.3 带有有效内容的“空闲块”管理:双向链表 FreeKVCacheBlockQueue 的设计玄机
这里隐藏着 vLLM V1 引擎源码中一个极其令人拍案叫绝的工程细节:
在 vllm/v1/core/block_pool.py 中,管理所有物理空闲块的数据结构,为什么被专门设计成了一条 双向链表(Doubly Linked List),而不是简单的 Python 列表或栈?
背后深层原因:
当开启了 Prefix Caching 之后,“空闲块”被赋予了双重身份:
- 纯净空闲块:从未被使用过,内部没有任何有效数据;
- 带缓存的空闲块(Cached Free Block):前序请求已经退出,当前没有任何人引用它(
ref_cnt = 0),但它里面依然完好无损地保存着之前算好的 KV 数据!
- 调度器必须从空闲链表的中间任意随机位置,把那几个命中特定前缀的 Block 给精准“揪”出来重新激活(将其
ref_cnt从 0 变成 1); - 如果使用普通的数组或队列,从中间删除一个元素的复杂度是低效的 ;
- 而使用双向链表,通过哈希表定位节点后,可以在 绝对常数时间内完成任意节点的摘除与重插入!
7. 动手实战与代码实验室(Minimal Runnable Code)
7.1 实验一:纯 Python 原生复刻 PagedAttention 块分配器与 Block Table 映射器
本实验完整模拟真实大模型推理过程中的显存碎片形成过程。对比在相同的随机生成长度序列下,传统连续预分配与 Paged 分页分配的显存有效利用率,用数字验证为什么分页能够消灭 60% 以上的碎片浪费。7.2 实验二:Continuous Batching 调度状态机极简仿真器
本实验完整模拟 Static Batching 与 Continuous Batching 在处理一批动态完成序列时的调度时空图,直接输出两个模式完成所有任务的总步数对比与槽位空转率。8. Ringi 避坑指南与生产黄金准则
8.1 7 大常见小白认知误区 vs 大厂 AI Infra 正确物理认知
8.2 生产 KV Cache 与并发调度黄金 Checklist
- 1. Block Size 生产基准核定:
- 在绝大多数主流开源大模型(LLaMA、Qwen、Mistral)上,生产环境严格保持
block_size = 16(针对极长文本大并发吞吐优化可测试评估block_size = 32,严禁随意设为 4 或 64 以上)。
- 在绝大多数主流开源大模型(LLaMA、Qwen、Mistral)上,生产环境严格保持
- 2. 显存利用率红线防御:
gpu_memory_utilization推荐基准设为 0.90(若启用了复杂 Speculative Decoding 建议下调至 0.85),强制为非 KV 运行时预留至少 6GB 以上物理缓冲。
- 3. 调度统一 Token 预算(Token Budget)刚性落地:
- 生产强制启用 Chunked Prefill;
- 推荐配置
max_num_batched_tokens:A100 上配置 512,H100 上配置 2048,严禁允许未分块的超万字 Prompt 裸奔注入引擎。
- 4. 生产抢占策略熔断基线:
- 明确配置抢占模式:70B 以上大模型且 Host 内存充足时开启 Swapping;中小模型且网络带宽受限时开启 Recomputation;
- 严密监控
vllm:num_preemptions指标,若每分钟抢占次数 ,必须立即触发集群自动水平扩容(HPA)。
- 5. 前缀复用(Prefix Caching)场景化开关:
- 在多轮 Agent、智能客服、知识库 RAG 等具有公共前缀的场景,必须开启
enable_prefix_caching=True; - 对纯随机输入、无公共 System Prompt 的离线翻译/标注任务,显式关闭前缀缓存,释放全部显存给动态 Block 池。
- 在多轮 Agent、智能客服、知识库 RAG 等具有公共前缀的场景,必须开启
9. Ringi 5 点核心速记口诀、自我检验清单与课后深度思考题
9.1 5 点押韵核心速记口诀
9.2 10 条白板自我检验清单
- 什么是内部碎片与外部碎片?为什么大模型推理中内部碎片尤为严重?
- 画出 PagedAttention 中逻辑块(Logical Block)、物理块(Physical Block)与块表(Block Table)的映射架构图。
- 为什么 PagedAttention 的 CUDA Kernel 在执行非连续块读取时不需要提前在显存做物理张量拷贝?
- Copy-on-Write(写时复制)在 PagedAttention 中是如何工作的?它对 Beam Search 有什么价值?
- 画出 Continuous Batching 的生命周期状态机,说明它为什么打破了 Static Batching 的“木桶短板”。
- 为什么说“没有 PagedAttention,Continuous Batching 就是空中楼阁”?二者如何紧密协同?
- 突发长 Prefill 是如何摧毁同批正在 Decode 请求的 TPOT 稳定性的?Chunked Prefill 如何化解它?
- vLLM V1 调度器中的“统一 Token 预算”哲学是什么?它是如何将所有请求形态统一量化的?
- 当显存物理 Block 彻底耗尽时,Swapping(换出)与 Recomputation(重算)各自的优缺点是什么?
- Radix Tree(基数树)是如何用于 Prefix Caching 的?空闲块队列为什么用双向链表实现?
9.3 3 道高阶开放式课后思考题(含极限 Corner Case)
- 【极限抖动与惊群效应题】: 在高度激进的超卖配置下,假设系统物理 Block 使用率已达 99.9%。此时若一个长请求触发了抢占被换出到 CPU,空出了 20 个 Block;但这 20 个 Block 瞬间被其他 20 个 Decode 请求瓜分完毕。随后该被抢占请求被重新唤醒试图换回,系统再度发生二次抢占,陷入残酷的“反复换入换出死锁(Thrashing)”。作为底层 Infra 工程师,你将如何设计冷却迟滞时间(Hysteresis)与退避算法来打破这种死锁?
- 【硬件底层瓶颈题】: 从 GPU 硬件微架构的角度看,PagedAttention 读取离散物理块必定会导致跨 Cache Line 的非完全合并访存(Uncoalesced Memory Access)。为什么在实测中,这种非连续访存带来的硬件开销,没有吃掉它节省显存所带来的高吞吐收益?(提示:结合 Decode 阶段的 GEMV 访存模式与 SRAM 内部加载粒度分析)。
- 【架构推演题】: 如果我们将注意力机制从经典的 Softmax Attention 替换为线性注意力(如 Mamba / RWKV / Linear Attention 等无 KV Cache 架构),PagedAttention 与 Continuous Batching 的存在意义是否还成立?在大模型架构可能再次演进的未来,推理系统的调度核心会发生怎样的质变?
10. 📚 参考资料与核心源码/经典论文指引
- [PagedAttention 原著] Kwon, W., et al. (SOSP 2023). “Efficient Memory Management for Large Language Model Serving with PagedAttention.” —— 开启大模型虚拟分页内存革命的奠基论文。
- [Orca 原著] Yu, G. I., et al. (OSDI 2022). “Orca: A Distributed Serving System for Transformer-Based Generative Models.” —— 首次确立 Iteration-level Scheduling 连续批处理范式。
- [Sarathi-Serve 论文] Agrawal, A., et al. (OSDI 2024). “Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve.” —— 提出 Chunked Prefill 削峰填谷理论与实证。
- [SGLang / RadixAttention 论文] Zheng, L., et al. (arXiv 2023). “Efficiently Programming and Serving Large Language Models with SGLang.” —— 深入剖析 Radix Tree 前缀复用与自动树形调度。
- [vLLM 官方源码核心库]:
- 内存分配核心:
vllm/v1/core/kv_cache_manager.py与vllm/v1/core/block_pool.py; - 调度核心:
vllm/v1/core/sched/scheduler.py; - PagedAttention 算子实现:
csrc/attention/attention_kernels.cu。
- 内存分配核心:
附录:Appendix A — 大厂硬核高频面试题与白板推导(Interview Drill)
面试题 1:现场手推 PagedAttention 的显存地址映射公式,说明其相比原生 PyTorch 连续张量的性能开销来自哪里。
🎯 考核考察点
- 考察候选人对 GPU 底层内存对齐、Stride 计算与 CUDA 汇编间址寻址的掌握深度;
- 考察能否客观剖析 PagedAttention 的副作用(没有技术是完全无代价的)。
💡 详细白板推导
步骤一:原生连续张量的地址推导 在原生连续内存中,给定请求索引 、头索引 、Token 序列位置 、维度偏移 : 该地址在编译期或进入 Kernel 前即可算出一维步长,GPU 指令流水线可以使用基址变址寻址(Base + Offset),硬件发射效率极高。 步骤二:PagedAttention 的虚拟间接寻址推导 在 PagedAttention 中,物理内存被切分为块大小为 的 Block:- 计算逻辑块号与块内偏移:
- 查块表拿到物理块编号(引入一次访存开销):
- 计算最终物理地址:
- 额外的查表显存访问(Table Indirection Overhead):Kernel 在读取数据前必须先读取
block_table,虽然其较小通常能命中 L1/L2 Cache,但依然占用了片上寄存器资源; - 控制流发散与访存不合并(Memory Non-coalescing):相邻的逻辑 Token 跨块时可能会跳跃到物理显存完全不相邻的区域,破坏了连续 128 字节的 Cache Line 完美突发读取。 大厂标准结论:PagedAttention 以约 3% ~ 5% 的极微弱 Kernel 算子纯执行延迟开销,换取了 200% ~ 400% 的显存容量释放与并发吞吐跃迁,在系统级收益上是不可思议的大胜。
面试题 2:深入剖析 vLLM 抢占机制:当显存 Block 耗尽时,调度器如何选择被抢占的请求?Swapping 和 Recomputation 各自在什么场景下最优?
🎯 考核考察点
- 考察对工业级分布式推理运行时(Runtime)高负载自愈策略的深度理解;
- 考察软硬件协同的量化权衡思维。
💡 解题标准答案
- 被抢占对象的选择算法(Victim Selection):
- 调度器默认采用 LIFO(后进先出) / 最年轻请求优先(Youngest First) 策略;
- 设计哲学:新进入系统生成的请求,已经耗费的 GPU 算力最少,且其累积生成的 KV 块最少;抢占它所造成的浪费最小,且能迅速释放出急需的 Block 供给正在收尾的老请求。
- 两类抢占恢复手段的适用场景划分:
- Swapping 胜出场景:大模型(如 70B)、长上下文(>2K)、且宿主机配备了 PCIe 5.0 高速总线及充裕 Host RAM。此时重新跑一次大 Prefill 算力成本高不可攀,跨 PCIe 搬运几十毫秒即可完成;
- Recomputation 胜出场景:中小模型(如 7B/8B)、短上下文(<512)、或宿主机 Host 内存已被其他进程压满的场景。此时 GPU 算力充裕,几毫秒内即可重新算完 Prefill,同时完全避免了消耗昂贵的 CPU 内存。
面试题 3:如果将 Block Size 从 16 调大到 128 或调小到 4,系统会发生什么变化?请从硬件访存合并、块表开销、内部碎片三个维度给出权衡分析。
🎯 考核考察点
- 考察对超参数底层物理机理的透视能力,拒绝死记硬背。
💡 详细三维权衡分析
标准落地结论:
工业生产环境绝不可盲目走向极端。Block Size 为 16 或 32 是经过全球开源社区成千上万次实测后确立的黄金中庸解。
🎨 【配图工坊生图 Prompt 暂存区 · 生成配图后可一键整块删除】
提示:本区块仅供创作者生成 Midjourney / DALL-E 3 架构配图使用。图片生成并归档至 assets 目录后,可直接删除本区块,不影响正文章节与目录结构。
蓝图 1:PagedAttention 虚拟分页显存映射全景工坊
- 文件路径:
assets/ringi_33_overview.png - 核心中文标签:
逻辑KV块、块表映射枢纽、物理显存块池、零外部碎片
🇨🇳 中文直接生图指令(推荐直接发给 ChatGPT / DALL-E 3):
🌐 中英双语精准 Prompt(Midjourney / DALL-E 3 专用):
蓝图 2:Continuous Batching 动态迭代级组批流水线工坊
- 文件路径:
assets/ringi_33_continuous_batching.png - 核心中文标签:
动态即时补位、完成立即释放、迭代步级组批、算力无空转
🇨🇳 中文直接生图指令(推荐直接发给 ChatGPT / DALL-E 3):
🌐 中英双语精准 Prompt(Midjourney / DALL-E 3 专用):
蓝图 3:Radix Tree 前缀复用与写时复制(CoW)工坊
- 文件路径:
assets/ringi_33_prefix_caching_radix.png - 核心中文标签:
公共系统前缀、基数树分支、写时复制CoW、秒级TTFT响应
🇨🇳 中文直接生图指令(推荐直接发给 ChatGPT / DALL-E 3):
🌐 中英双语精准 Prompt(Midjourney / DALL-E 3 专用):