系统工程 · 推理优化
学习舱 / 系统工程 / 推理优化

推理优化:KV Cache → Continuous Batching → PagedAttention

整理中 深度 · 应用 更新 2026-09-22

前置 Attention

推理优化:KV Cache → Continuous Batching → PagedAttention

一句话定位:模型炼成只是开始——把它跑得又快又便宜,靠的是推理服务层的三件套:KV Cache 省计算、Continuous Batching 省算力空转、PagedAttention 省显存碎片。前置:Attention 与 Transformer——键值缓存的 K、V 就是那篇里注意力打分的原料;解码每一步怎么选词(采样)见采样篇,本篇管的是支撑解码的显存与调度。

先看一次生成的账:钱都花在哪了

一个用户发来一句「帮我总结这篇文章」,模型要一个字一个字往外蹦——每个新词都取决于前面所有词,这是解码的逐字流程;服务端同时在接待几十上百个这样的用户。

算力是买断的(显卡就那些),可每个请求的显存开销却随对话变长无限膨胀:显存装不下,再强的卡也只能排队。

显存到底被什么吃掉了?逐字生成的过程中,有没有哪些计算是重复劳动、哪些空间是白白闲置的?

答案藏在一笔三层账里:KV Cache 是「为了不重复计算必须记住的笔记」(省了计算、吃了显存);Continuous Batching 管的是「等待中的算力空转」(省了吞吐);PagedAttention 管的是「为笔记预留却用不上的空间」(省了碎片)。三件套各管一层,合起来把同一个显卡的服务能力翻了几倍。先看全景:

推理服务层的三件套:各管一层账 ① KV Cache 问题:每步重算全部历史 解法:把 K、V 记下来复用 省计算 · 吃显存 ② Continuous Batching 问题:整批等齐,算力空转 解法:逐字级调度随到随进 省空转 · 提吞吐 ③ PagedAttention 问题:预留连续空间,六到八成闲置 解法:切块按需分配 省碎片 · 翻并发 逻辑链:① 让历史计算可复用(否则②的批里全是重复劳动)→ ② 让批永远满员 → ③ 让批员的「笔记」不浪费显存—— 三件缺一:没有①每步全量重算,没有②算力等最长的请求,没有③显存被碎片塞满接不了新请求 实测口径(vLLM 论文):显存浪费从 60~80% 压到 4% 以下,吞吐较当时最优系统提升 2~4 倍 读法:三件套是三层叠加的「节约」——计算层、调度层、显存层;本篇编号 ①②③ 与全文贯通

图:推理优化三件套——KV Cache 省计算、Continuous Batching 省空转、PagedAttention 省碎片;三层叠加把吞吐翻数倍

渐进拆解:三件套各管一层

① KV Cache:用显存换掉重复计算

逐字生成时,每生成一个新词,都要对「前面所有词」算一遍注意力(K 参与打分、V 是被取用的内容,见注意力篇的打分与加权)。如果每步都从头算,1000 词的文章就要算 1000 轮全量注意力——绝大部分是重复劳动:前 999 个词一步都没变过,它们的打分(Q 与 K 的点积)和取用(按权重混合 V)结果都不会变。

KV Cache 的做法是把每层算过的 K、V 直接存进显存,下一步只算新词自己的 K、V 追加进去。这是典型的空间换时间:计算省了一个数量级,代价是显存里多了一块随对话变长而持续膨胀的「笔记」。

这本笔记有多占地?每个 token 每层要存 K 和 V 各一份向量,乘出来是:2(K、V)× 层数 × 键值头数 × 每头维度 × 精度字节数。以 Qwen2-7B(28 层、4 组键值头 ×128 维、bf16)实算是每 token 56 KiB——一百万 token 的上下文要 53.4 GiB,长上下文篇算过同一笔账,那里关心「上下文能拉多长」,这里关心「服务一个用户要压多大显存」。

显存被笔记吃掉之后,出路在两个方向:显存侧把碎片收回来(见③),或者把笔记压薄(本节末尾的量化);调度侧的 Continuous Batching 则让同一张卡多接活。这笔账画成一条线,斜率与终点都算得出:

KV Cache:一块随对话变长持续膨胀的笔记 KV Cache 显存 上下文长度(token) 0 线性增长:每 token 56 KiB 28 层 × 4 组键值头 × 128 维 × 2 字节(bf16) 上下文 100 万 token KV Cache ≈ 53.4 GiB 显存被吃满后的两条出路:碎片收回来(见③)、把笔记压薄(量化)

图:KV Cache 随上下文长度线性膨胀——每 token 56 KiB,100 万 token 约 53.4 GiB;出路是收碎片(③)或压薄笔记(量化)

② Continuous Batching:别让算力等最慢的请求

最早的批处理是「凑一批发」:攒 32 个请求一起算,必须等 32 个全部生成完才能接下一批——可请求的生成长短天差地别,短请求完成后它的算力空转到整批结束。

Orca 系统(OSDI 2022)提出迭代级调度:每生成一个 token 就重新组一次批——完成的请求立刻退出、新请求立刻顶上,批里永远满员。类比餐厅:老式批处理是「一桌 32 人全部吃完才收台」,连续批处理是「谁吃完立刻翻台」——餐厅还是那个餐厅,翻台率天差地别。它省的不是单请求的速度(批里 N 个请求的算子要拼在同一次前向里,单步总比独占慢一点),是整机的吞吐。两种调度画成时间条,差别一眼可见:

Continuous Batching:别让算力等最慢的请求 老式:凑一批发,等齐才收台 连续:每生成一个 token 就重组批 请求 1请求 2请求 3请求 4 空转空转 整批结束才接下一批 请求 1请求 2请求 3请求 4 新请求顶上新请求 批里永远满员 先到的请求顶上的新请求空转的算力 餐厅类比:左图一桌人全吃完才收台,右图谁吃完立刻翻台——省的是整机吞吐,不是单请求速度

图:连续批处理把调度粒度降到每个 token——短请求退出、新请求顶上,批里永远满员,省的是整机吞吐而非单请求速度

③ PagedAttention:给笔记装上虚拟内存

有了②之后,批里随时进出几十个请求,每个请求都拖着一块持续生长的 KV Cache——显存怎么分配给它们?朴素做法是为每个请求预留一段「最大可能长度」的连续空间(下图左侧:预留 2048 token、实际只聊到 200 的例子),剩下的全空着——预留过大的浪费叫内部碎片;而且连续性要求导致旧请求退出后留下的坑,新请求常常填不进去(外部碎片)。

vLLM 系统(2023)的做法直接搬来了操作系统虚拟内存的思想:把 KV Cache 切成固定大小的块,逻辑上连续、物理上离散,按需分配,再用一张块表记录「这个请求的笔记散在哪些块」——每步经块表间接寻址找到自己的块。

效果是显存浪费从预留式的实测 60~80% 压到 4% 以下,同样一块显卡能同时接待的请求数翻了几倍,vLLM 论文的吞吐对照是较当时最优系统提升 2~4 倍(细账见下图与动手一节的实验)。如今 vLLM、TGI、TensorRT-LLM 等主流推理框架全部内建了这套机制。

显存怎么分给几十本「笔记」 预留式:按最大长度圈地 请求 A:预留 2048 槽 | 实用 200 ▓ 请求 B:预留 2048 槽 | 实用 1800 ▓▓▓▓ 浪费 60~80%:预留空槽白占(含内外碎片) 分页式:16 token 一块,按需领 请求 A 实领 13 块(图示 4 块)| 末块半满 块散在显存各处,块表记着顺序 浪费 4% 以下:空位立刻能派给新请求 代价:每步多查一次块表(间接寻址),注意力算子(GPU kernel)要支持按块取 K、V——用一点点计算开销换数倍并发 同一思想在 60 年前的操作系统里叫虚拟内存分页:程序以为自己拿着连续内存,实际被切成页散在各处 vLLM 论文实测:浪费 60~80% → 4% 以下,吞吐较当时最优系统(FasterTransformer / Orca)提升 2~4 倍 读法:黄色小块是「末块半满」的内部碎片——分页唯一剩下的浪费,被 16 token 的块粒度压到忽略不计

图:预留式 vs 分页式——按最大长度圈地浪费六到八成,切块按需领把浪费压到 4% 以下;思想来自操作系统虚拟内存

KV Cache 量化:给笔记瘦身,精度换空间

①与③管「怎么存」,量化管「存多省」:K、V 本来以 bf16/fp16 这类 2 字节精度存储,压到 INT8/FP8 能把笔记体积砍半——显存装得下更多并发,或同并发下装更长上下文。取舍在精度:注意力打分对 K、V 的数值误差敏感,长上下文任务(要在几万个 token 前精准找一句话)比闲聊任务更容易翻车,所以上线前要用自己的真实任务回归测试,不能只看通用榜。三大压缩路线在此各就各位:量化(压数值,即本节)、GQA 等结构性减头(见注意力篇)、MLA 低秩联合压缩(下一篇的主角)。

大白话:KV Cache 是「演算纸不擦,接着往下写」;Continuous Batching 是「餐厅谁吃完谁翻台,不等全桌」;PagedAttention 是「演算纸活页装订,写一页领一页,不用给每人预订一整本」;量化是「用便签纸替活页纸——省地方,但有的字会洇」。

技术定义:KV Cache 指自回归解码中缓存复用各层历史 token 的键、值向量、每步仅计算新增 token 的机制,显存占用 = 2 × 层数 × 键值头数 × 每头维度 × 精度字节 × token 数;Continuous Batching(迭代级调度)在每个解码步动态加入/退出请求;PagedAttention 将 KV Cache 切为固定大小块、以块表维护逻辑-物理映射、按需分配;KV Cache 量化指以 INT8/FP8 等低精度存储 K、V 以减半显存占用。

动手:算 KV 的账,再设计一个碎片率实验

第一步验算 KV 公式(Qwen2-7B 配置,结果与长上下文篇的 53.4 GiB 对上):

layers, kv_heads, head_dim, dtype_bytes = 28, 4, 128, 2   # Qwen2-7B:GQA 4 组 ×128 维,bf16
per_token = 2 * layers * kv_heads * head_dim * dtype_bytes  # 2 = K 与 V 各一份
print(f"每 token: {per_token/1024:.1f} KiB | 1M token: {per_token * 1e6 / 2**30:.1f} GiB")
# 实算输出:每 token: 56.0 KiB | 1M token: 53.4 GiB

第二步回应本篇自测问题的实验:PagedAttention 的收益能不能量化? 用纯 Python 模拟两种分配策略——生成 1000 个真实感请求(长度 50~2048 随机),比较「按最大长度预留」与「16 token 分页」的总浪费:

import random
random.seed(42)
MAX_LEN, BLOCK = 2048, 16
requests = [random.randint(50, MAX_LEN) for _ in range(1000)]
used = sum(requests)

reserved = MAX_LEN * len(requests)                        # 方案 A:每个都按最大长度预留
blocks = sum(-(-r // BLOCK) for r in requests)            # 方案 B:按块数向上取整
paged_alloc = blocks * BLOCK

print(f"预留式浪费率: {1 - used / reserved:.1%}")          # 48.7%
print(f"分页式浪费率: {1 - used / paged_alloc:.1%}")       # 0.7%

实测:预留式浪费 48.7%、分页式 0.7%——两笔账分开算:分页式的总分配量只有预留式的一半出头,同等显存下省出的部分理论上能再多接约 1 倍的并发(分配量之比约 1.9 倍);浪费率本身则降了约 70 倍。两个诚实的注记:

  • 实验模拟的只是「分配策略」这一层,真实系统的浪费还含外部碎片与调度保留——所以 vLLM 论文实测是 60~80%(推断:长度分布更极端、且含多轮对话的历史包袱),本实验的 48.7% 偏保守。
  • 「浪费率降几十倍」不等于「吞吐涨几十倍」——吞吐还受计算瓶颈限制,论文实测的端到端吞吐提升是 2~4 倍。

实验的可复现路径:改 random.seed 与长度分布,观察浪费率如何随分布漂移,这就是「碎片率是负载的函数」的直接体感。

边界与取舍

什么时候不用管这三件套? 走托管 API(各类大模型平台)时,三件套是供应商的事,你要操心的只是账单;小流量自建(每秒请求数个位数、上下文短)时,一个朴素推理脚本也够——三件套解决的是「并发 × 长上下文」同时上来之后的规模问题,规模没到,提前优化是白忙。

三件套的边界与代价。 三样优化各有各的账单:

  • KV Cache 是「必须吃」的显存——它是复用计算的前提,只能瘦身(量化/结构压缩)不能省掉。
  • Continuous Batching 会拉长单个请求的尾延迟(批里别人在算,你要排队),延迟敏感场景要配合优先级调度。
  • PagedAttention 的块表间接寻址有一点计算开销,换来的是显存层的数倍并发。

三者都是服务层的优化,不改变模型本身——把模型本身变小的路线(结构压缩)在下一篇 MLA。

坏味道预警:拿吞吐数字直接比框架。 各家框架报的吞吐提升都依赖负载假设(请求长度分布、并发数、模型大小),换一组负载排序可能颠倒——选型时用自己的真实流量回放测试(思路同RAG 评估的评估集:先拿真实流量小规模试探,再当正式考卷)。

自测

合上文章先自己答,再展开对照:

PagedAttention 的显存利用率收益能设计实验量化吗?

能,且不需要 GPU——分配策略是纯逻辑,可以模拟。实验设计:其一,生成一组真实感请求长度(如 50~2048 随机分布);其二,分别按「预留式」(每请求分配最大长度)与「分页式」(16 token 一块向上取整)计算总分配量;其三,浪费率 = 1 − 实用量 ÷ 分配量。实测(seed=42、1000 请求):预留式 48.7%、分页式 0.7%,浪费率差距约 70 倍;分页式总分配量约为预留式一半,同等显存下并发容量约 1.9 倍。两个校准注记:模拟只覆盖分配策略层,真实系统含外部碎片与多轮对话包袱,vLLM 论文实测浪费 60~80%;浪费率降几十倍不等于吞吐涨几十倍(计算是另一重瓶颈),论文端到端吞吐提升 2~4 倍。实验可加深:改长度分布(更长对话/更重的多轮占比)观察浪费率漂移。

KV Cache 为什么是「必须吃的显存」?一笔账怎么算?

它是空间换时间的前提:自回归解码每步都要对全部历史做注意力,若不缓存 K、V,每步都重算全部历史的键值投影——1000 token 的生成重复计算近千次。缓存后每步只算新 token 的 K、V,代价是显存里一块持续增长的笔记。账目公式:每 token 显存 = 2(K、V)× 层数 × 键值头数 × 每头维度 × 精度字节。Qwen2-7B(28 层 ×4 组 ×128 维 bf16)实算 56 KiB/token,100 万 token 上下文 53.4 GiB——与长上下文篇「键值显存墙」同源。瘦身只能在这里做文章:量化(压数值到 INT8/FP8)、结构性减头(GQA)、低秩压缩(MLA),显存本身省不掉。

Continuous Batching 省的是什么?为什么单请求可能反而略慢?

省的是算力空转。老式批处理凑满一批必须等整批生成完才接新活,短请求完成后占着的算力白白空转;Continuous Batching 把调度粒度降到「每生成一个字重组一次批」,完成的立刻退出、新到的立刻顶上,批里永远满员——省的是整机吞吐(餐厅翻台率),不是单请求速度。单请求可能略慢的原因:批里始终有别人在共享算力,你的每个字要和最多 N−1 个请求的算子拼在同一次前向里,单步延迟比独占时略高。这是吞吐与尾延迟的经典取舍,延迟敏感场景需要叠加优先级调度来补。

参考与延伸

  • Kwon 等《Efficient Memory Management for Large Language Model Serving with PagedAttention》(SOSP 2023):PagedAttention 与 vLLM 的原始论文(60~80%→4% 浪费、2~4 倍吞吐口径出处) — arxiv.org/abs/2309.06180
  • vLLM 官方博客《vLLM: Easy, Fast, and Cheap LLM Serving》(2023-06):分页机制与实测数据的公告版 — vllm.ai/blog/2023-06-20-vllm
  • Yu 等《Orca: A Distributed Serving System for Transformer-Based Generative Models》(OSDI 2022):迭代级调度(Continuous Batching)的提出处 — www.usenix.org/conference/osdi22/presentation/yu
  • vLLM 文档:FP8 KV Cache 的配置与现状(INT8 KV 见 TensorRT-LLM 等框架文档) — docs.vllm.ai

下一站:MLA 的 KV 压缩为什么省显存——本篇说 KV Cache 只能瘦身不能省掉,MLA 是把瘦身做到结构里的极致:把 K、V 联合压成一个低秩潜向量,推理时再「吸收」回来。

本节点大纲与自测题(4 个子点 · 3 问)

大纲

  • KV Cache 原理与显存三层解法(笔记已成体系)
  • Continuous Batching:吞吐的关键
  • PagedAttention:显存碎片化治理
  • KV Cache 量化(INT8 KV 等)与精度取舍

自测——答不上来就是缺口

  • PagedAttention 的显存利用率收益能设计实验量化吗?
  • KV Cache 为什么是「必须吃的显存」?一笔账怎么算?
  • Continuous Batching 省的是什么?为什么单请求可能反而略慢?