
搞大模型部署的老朋友应该都见过这个场景一张 24GB 的消费级显卡跑 7B 模型权重本身没问题但只要把并行推理的请求数提上来或者上下文一拉长显存立马爆掉。很多人第一反应是“模型太大了”其实不全对。真正吃掉显存的大头往往不是权重而是推理过程中动态生长的 KV Cache。KV Cache 是自回归解码的“产物”也是显存治理绕不过去的一块硬骨头。PagedAttention 则是目前业界最出色的一种解法源自 vLLM 的设计思路用类似操作系统分页的方式把 KV Cache 打散重排既提升显存利用率又明显拉高推理吞吐。这篇文章会从零讲清楚两者到底是什么、显存怎么算、实测怎么调以及我在部署过程中踩过的一系列坑。1. 先从“为什么慢”说起自回归解码与显存瓶颈1.1 一个 token 一个 token 的代价大模型生成文本的方式很多人误以为是一次性把整段话“想”出来实际上不是。主流 decoder-only 模型用的是自回归解码每生成一个 token都要把已经生成的整个序列重新过一遍网络然后预测下一个 token。换句话说第 100 个 token 不是单独算出来的而是把前 99 个 token 的上下文再次完整计算后得到的。这里就出现了一个巨大的浪费。假设你只输入一个“你好”模型生成“世界”两个字生成“世”的时候模型要把“你”“好”“世”三个 token 都参与注意力计算生成“界”的时候又要把“你”“好”“世”“界”四个 token 重新算一遍。越到后面重复计算越严重。这还不光是算力浪费更大的问题在于每一层的 Key 和 Value 矩阵都要同时存在显存里上下文越长中间结果就越多。我经常用一个类比这就像你每次翻书都要从第一页开始读才能找到最后一页的答案。过程里每一页都摊在桌面上桌面上堆满了越来越多、但其实你不会再加翻的旧纸页。KV Cache 的发明就是把这些旧纸页放到抽屉里下次用的时候直接抽出来不摊在桌面上也不重新找。1.2 KV Cache 解决了什么没解决什么KV Cache 的核心逻辑很简单既然旧 token 的 Key 和 Value 在每次解码时都会被重复用到不如把它们缓存下来。这样每生成一个新 token只需要为当前 token 计算新的 Key 和 Value然后拿着缓存去算注意力权重。计算量从“重新跑整个历史”降到“只跑当前的 token”生成速度自然上来了。但问题随之而来缓存也是要占显存的。你可以把 KV Cache 理解成一个不断膨胀的“热插拔内存块”它随着请求数量、上下文长度、模型层数和注意力头数一起增长。更头疼的是不同请求的上下文长度完全不一样有的用户问一句就完事有的用户粘着你聊 2000 个 token显存分配如果做不好就会出现大量浪费甚至 OOM。所以 KV Cache 解决的是“重复计算”的问题而怎么把这块缓存管得高效、装得下更多并发请求就是 PagedAttention 要解决的问题。前者减少算力浪费后者减少显存浪费两者叠加才是推理加速和显存治理的完整故事。1.3 预填充与解码阶段的显存画像大模型推理通常分两个阶段理解这两个阶段对排查显存问题很有帮助。预填充阶段就是用户把完整 prompt 一次性扔给模型模型并行计算所有输入 token 的 Key 和 Value生成第一份 KV Cache。这个阶段计算量大但因为都是矩阵并行显存压力主要来自输入长度本身。解码阶段就是逐 token 生成答案的过程。此时模型计算量小了很多却要反复读取已经缓存的历史 KV。计算虽然轻但显存带宽成了瓶颈——每次生成一个 token都要从显存里把前面的缓存捞出来用。所以你经常会看到一种“怪现象”GPU 利用率看着不高但请求就是慢吞吞的其实瓶颈不在算力而在显存带宽和 KV Cache 的读写效率。对比维度预填充阶段解码阶段计算特征高并行、大矩阵乘法低并行、逐 token 串行显存变化KV Cache 初始建立峰值高KV Cache 持续增长主要瓶颈显存容量、输入长度显存带宽、缓存读取优化重点限制输入长度、合理批大小缓存复用、块式管理预填充阶段把缓存“造”出来解码阶段把缓存“喂”给模型两个阶段都绕不开显存治理。KV Cache 的大小直接影响你能跑多长的上下文、能并行处理多少请求这也是下面要定量展开的内容。2. KV Cache 是怎么算出来的原理、公式与实际估算2.1 缓存的是什么Key 和 Value 从哪来要理解 KV Cache得先弄清楚 Transformer 注意力机制里 Key 和 Value 是什么。在注意力计算中每个 token 会被转换成三个向量Query、Key、Value。Query 用来提问“我要找什么”Key 用来回答“我是什么”Value 则是“我携带的内容”。通过 Query 和所有 Key 做内积得到注意力权重再拿权重对 Value 做加权求和得到最终输出。新 token 生成时它的 Query 需要和历史所有 token 的 Key 做匹配。如果不缓存历史 token 的 Key 和 Value 就要重新计算一遍代价极高。KV Cache 做的就是这一件事把之前每个 token 经过注意力层投影出来的 Key 和 Value 保存下来。新 token 进来后只计算自己的 Key 和 Value让它的 Query 去和缓存中的所有 Key 做匹配再取出对应的 Value 做加权。特别提醒一下KV Cache 不是缓存模型的权重也不缓存完整的中间层输出。它只缓存注意力层里的 Key 和 Value而且是“历史 token”的那一份。这也是为什么它和模型参数大小没有直接关系更多取决于层数、注意力头数、上下文长度和并发请求数。2.2 缓存大小公式用数学看懂显存增量KV Cache 的大小可以估算为每 token 的 KV 字节数 2 × 层数 × Key/Value 头数 × 每个头的维度 × 每个元素占用字节数其中最前面那个“2”表示 Key 和 Value 两份。再乘以上下文长度和请求批大小就是总缓存大小总 KV Cache 大小 每 token 的 KV 字节数 × 序列长度 × 批大小用生活化的方式理解每个 token 就像一页纸每层网络都要给这页纸的 Key 和 Value 各拍一张照片。纸越多拍的照片越多层数越深照片越多并发请求越多照片套数越多。最终堆起来的相册就是显存里的 KV Cache。公式里的“Key/Value 头数”值得特别说明。传统多头注意力 MHA 里Key/Value 头数和 Query 头数相等KV Cache 很大。后来业界发现 Value 和 Key 可以共享部分头于是出现了分组查询注意力 GQA用较少的 KV 头覆盖多组 Query 头把 KV Cache 显著压小。这也是很多新一代模型号称“长上下文友好”的背后原因之一。2.3 一个 7B 模型的估算案例我们拿典型的 7B 模型来算一笔账。假设层数 32隐藏层维度 4096注意力头数 32每个头维度 128使用 fp16 精度。每 token 的 Key 和 Value 显存为2 × 32 层 × 32 个 KV 头 × 128 维度 × 2 字节 524288 字节 512KB也就是说模型每生成一个 tokenKV Cache 要增加 512KB。如果上下文长度为 1024单个请求的 KV 就是 512MB8 个并发请求就是 4GB。这还没算权重和中间激活值。如果你把上下文拉到 4096单个请求的 KV 就变成 2GB8 个请求直接到 16GB。一张 24GB 的显卡跑 7B 模型权重大约要占 14GBfp16剩下 10GB 根本塞不下 8 个 4K 上下文的请求。实际部署时大部分 OOM 都是这么来的。模型规模上下文长度并发请求数KV Cache 估算部署结论7B32 层10248约 4GB24GB 显卡可跑7B32 层40968约 16GB24GB 显卡很悬7B32 层204832约 32GB需要 40GB 以上7B32 层GQA 减半 KV 头40968约 8GB相对宽裕这里给的数字是估算值实际部署还要考虑内存页开销、显存预留、中间激活。但方向非常明确KV Cache 随上下文长度和并发请求数线性增长不做治理的话长上下文高并发就是显存灾难。2.4 组查询注意力、量化等方法如何影响 KV Cache既然 KV Cache 那么大业界自然想了不少办法压缩。第一个方向是架构层面使用 GQA 或者多查询注意力 MQA。GQA 让多个 Query 头共用一组 Key/Value 头KV Cache 可以变成原来的 1/2、1/4 甚至更少。比如 32 个 Query 头只用 8 个 KV 头KV Cache 直接缩到四分之一。这也是我挑模型时很看重的一个点同样的上下文长度和并发数GQA 模型的显存压力会小很多。第二个方向是 KV Cache 量化。正常 fp16 要 2 字节量化成 int8 就只要 1 字节4-bit 量化只要 0.5 字节。和权重量化类似KV Cache 量化也会引入一定精度损失需要在实际业务里做效果对比。如果你做的是开放域问答可能感知不明显但如果做需要精确抽取关系、日期、数字的任务就要谨慎一点。第三个方向是缓存策略优化。比如根据场景主动截断历史 token、滑动窗口只保留最近 N 个 token 的 KV或者对频繁使用的系统提示词做前缀共享。这些策略不改变 KV Cache 的“单 token 单价”但能减少实际缓存的总量。3. PagedAttention像操作系统管理内存一样管理缓存3.1 传统 KV Cache 分配里的“碎片”和“浪费”在没有 PagedAttention 之前大部分推理框架处理 KV Cache 的方式是“预分配一大块连续内存”。假设最大上下文长度是 4096每个请求都会预留 4096 个 token 的 KV 空间。可实际请求往往只有几百 token 长预留的空间大部分时间都是空着的。这种做法的浪费是双重的。一是内部碎片系统按最大长度预留短请求用不满剩下一大截白白占着二是外部碎片不同请求先后结束内存被割成很多不连续的小块新的长请求找不到连续大块哪怕显存总量还很充足也无法分配。你可以想象一个停车场规定每辆车必须停一个“长车位”不管你是自行车还是卡车。自行车占一个长车位空间浪费卡车来了想停两三个连续长车位又常常找不到连续空位。传统 KV Cache 管理差不多就是这样。3.2 PagedAttention 的块式切分与逻辑物理映射PagedAttention 的思路脱胎于操作系统虚拟内存的分页机制。它把 KV Cache 切成固定大小的块每块能装固定数量 token 的 Key 和 Value例如 16 个 token 一块。模型逻辑上是连续的 KV 序列但物理上这些块可以散落在显存任意位置通过一张“块表”记录逻辑块和物理块之间的映射。这样一个请求生成前 16 个 token 时只分配第一块生成到第 17 个 token才分配第二块。不需要一上来就按最大长度把空间全占了。短请求只用少量块长请求按需增长内部浪费被压到极低。外部碎片也不再是问题。因为块的大小固定块和块之间不需要物理连续空闲块可以来自显存任意角落。只要显存里还有足够多的空闲块新的请求就能继续跑这一点在长上下文并发下尤其重要。还有一个很聪明的副产品如果多个请求共享同一段前缀提示词比如系统提示词、few-shot 示例它们的逻辑块可以映射到同一批物理块上。这样前面一大段 KV Cache 只需存一份多人共享极大降低重复缓存的开销。这个特性在一些服务压力大的场景里非常实用。3.3 为什么吞吐量能明显提升PagedAttention 本身并不直接改变单条推理链路的延迟它优化的是“显存能放下多少并发请求”。显存利用率上去了同一个 GPU 上能同时跑的请求数量就多了吞吐量自然跟着涨。再加上 vLLM 配合的连续批处理机制请求不再像传统批处理那样等一批全部结束后才能统一退出。某个请求一旦生成完新请求立刻可以插入它的空位参与调度。两条机制叠加大概可以用更少的显存服务更多的并发请求。我实测的感受是在长上下文、多请求压力下吞吐量比简单预分配缓存方案的提升非常明显而且显存占用曲线稳定很多不像之前那样动不动“阶梯式暴涨”。如果你只用单请求、短上下文做开发调试可能感受不到差距但一上生产PagedAttention 的价值就出来了。4. 实战在 vLLM 里部署并调优 KV Cache / PagedAttention4.1 部署一个可直接接入 OpenAI 接口的推理服务PagedAttention 最经典的工程实现来自 vLLM。它提供了 OpenAI 兼容的服务接口部署好后可以直接用openaiSDK 调用对我们这些习惯了标准接口的开发者非常友好。先安装 vLLM建议用独立的 Python 环境避免依赖冲突。pip install vllm然后启动一个本地模型服务。假如你的模型权重已经放在/data/models/llama-2-7b-chat-hf可以这样启python -m vllm.entrypoints.openai.api_server \ --model /data/models/llama-2-7b-chat-hf \ --served-model-name my-llama \ --gpu-memory-utilization 0.90 \ --max-model-len 4096 \ --block-size 16 \ --max-num-seqs 128启动日志里会显示 GPU KV Cache 的分配情况这是判断 PagedAttention 是否正常工作的第一步。模型装好后用标准接口测一下。curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: my-llama, messages: [{role: user, content: 用一句话介绍 KV Cache}], max_tokens: 128 }能正常返回说明服务起来了。实际使用时建议先用短并发测试再逐步加长上下文和请求数避免一开始就压爆显存。4.2 显存利用率、max-model-len、block-size 怎么配vLLM 里几个参数直接决定 KV Cache 的分配策略我一个个说。--gpu-memory-utilization是给推理服务划定的显存使用上限。比如 0.90 表示最多用 90% 显存剩下 10% 留给 CUDA context、中间计算和其他开销。这个值不能设太满尤其你还要同时跑别的进程或工具时留一点余量能避免很多诡异问题。--max-model-len是模型允许的最大上下文长度它决定了 KV Cache 最多能预留多少空间。这个值不是越大越好很多人想让模型支持长上下文直接设置成 32768结果 24GB 显卡连几个并发请求都跑不动。长上下文是花真金白银买的每增长一截KV Cache 都要跟着涨。前期如果不是业务硬需求我建议先保守一点比如 4096 或 8192跑通了再往上调。--block-size是 PagedAttention 的块大小单位是 token。默认 16一般来说是兼顾内部碎片和块表开销的平衡点。块太大内部碎片增加短请求浪费更多块太小块表变长寻址和元数据开销变大。如果你的业务以短请求为主可以试着用更小的块如果长上下文多默认 16 通常就够用。--max-num-seqs控制同时参与调度的请求数量。它不直接限制 KV Cache 总量但会影响批处理窗口。设置太大显存可能跟不上设置太小吞吐又上不去需要结合显存剩余量和上下文长度来调。4.3 怎么确认 PagedAttention 真的在生效我遇到过有人把 vLLM 拉起来却不确定 PagedAttention 到底有没有在管 KV Cache。这里分享几个我自己常用的验证思路。第一个是看启动日志。vLLM 启动时通常会打印类似于 “GPU KV cache size” 的信息它会告诉你当前显存里分配了多少给 KV Cache。如果你能明显看到 KV Cache 只占显存的一部分而不是每个请求固定预分配一整段那说明 PagedAttention 已经在工作。第二个是看显存曲线。用nvidia-smi持续监控显存占用正常情况下的显存占用应该是“随请求数波动”的而不是“一次性飙升到固定值”。如果批量请求进来显存增长平稳请求结束显存快速回落说明块级分配在起作用。第三个可以直接看 vLLM 暴露的监控指标。vLLM 服务通常提供/metrics端点能看到当前正在运行的请求数、等待队列长度等。把这些数据接到 Prometheus 或 Grafana 里长期观察比我手动敲命令强得多。压测时如果并发从 8 提升到 64显存增长明显低于“线性预分配”预期说明 PagedAttention 的收益体现出来了。4.4 结合连续批处理压测的体感数据没有一份压测数据讲吞吐提升都是空的。我用一个 7B 模型、单张 24GB 显卡试过业务场景偏多轮客服对话平均输入 500 token输出 150 token 左右。传统预分配缓存方案下并发 16 个请求已经比较勉强显存峰值经常顶到 23GB而且后排队的请求延迟明显增大。切到 vLLM 并开启 PagedAttention 后同样显存下能稳定跑到并发 64 个左右吞吐量有明显提升。更重要的是长尾延迟比之前稳不会出现“哪个长请求占着大坑、后面一堆短请求跟着陪跑”的局面。需要提醒的是压测时不要只用固定长度的输入那测不出真实效果。最好混合短、中、长三种请求模拟真实流量。真实流量里的请求长度差异越大PagedAttention 的节省效果就越明显。如果所有请求长度都一样长那它的优势会被掩盖一部分。5. 常见问题与排坑经验5.1 总是 OOM是不是 KV Cache 太大了这是我最常被问的问题。看到CUDA out of memory第一反应就是换更大的显卡但多数时候是 KV Cache 配置过于激进。先按顺序排查确认--max-model-len是否设得过大确认--gpu-memory-utilization是否给其他进程留了空间确认--max-num-seqs是否超出了显存能承受的范围。接着用前面给的公式粗算 KV Cache看它和可用显存是否匹配。如果模型本身支持 GQA尽量用支持 GQA 的版本如果 kv cache 量化能满足效果就打开。这些手段做完还 OOM再考虑换卡也不迟。我自己的原则是先治配置再治量化最后才加预算。5.2 并发一高就慢缓存命中率低有时候 OOM 倒没发生但并发一高速度掉得很厉害。这时要意识到PagedAttention 解决的是“显存里能放多少缓存”但显存带宽是另一层限制。并发请求增多每个请求都要不断读取自己的 KV Cache带宽很快被打满。排查思路是看显存带宽利用率和 GPU 计算利用率之间的差距。如果计算利用率还很低带宽已经满了说明瓶颈在 KV Cache 的读取上。可以尝试减少同时并行存取的请求数、延长批处理窗口或者把模型转换为支持 GQA 的版本。还有一个容易忽略的点检查是否存在大量重复的前缀请求有的话建议实现前缀共享或缓存命中层让相同前缀只存一次。5.3 本地部署、微调后推理KV Cache 有哪些隐藏坑本地部署时很多人用的工具不一定默认支持 PagedAttention。有些框架为了兼容性仍旧采用预分配连续显存的方案。你在本地小并发调试时感受不到差别以为没问题但真正上线时才发现吞吐上不去。微调后推理也有坑。微调会改变模型权重但 KV Cache 的机制不会变。如果你从某个开源模型微调出来又把上下文长度从 2048 提升到 4096KV Cache 占用会成倍增长但很多框架不会自动帮你控制。部署前一定重新评估显存容量别拿微调前的配置硬套。另外不要过度迷信“最大上下文长度”。模型声称支持 32K 上下文不代表一张小卡能在高并发下吃到 32K。上下文长度和并发数是一个跷跷板想让模型支持更长上下文就要接受并发数降低。这是显存治理绕不过去的成本。5.4 长上下文任务下的显存治理建议如果你真的要做长上下文、高并发的业务我建议把“显存治理”这件事提前到模型选型阶段。从一开始就选择 KV Cache 占用更小的架构比如 GQA 系列模型后期能省很多事。另外结构上把系统提示词、few-shot 示例固定放在 prompt 前面方便利用前缀共享。不要让每个请求都带一份完全不同的超长背景材料否则 KV Cache 压力会非常大。可以做一层外部缓存或摘要压缩把不常用的历史知识放到向量数据库里只在必要时拼进上下文。毕竟 KV Cache 是按每个 prompt 里的 token 计算的你少放一个不必要的历史 token就等于少存一份 KV。最后分享一个我自己的习惯每次上线前都会用脚本对推理服务做一次“显存压测”把请求长度分布、并发数、吞吐、显存峰值全部记录下来。不要凭感觉调参。KV Cache 和 PagedAttention 的收益最终都要落到这些数字上。你积累的压测样本越多后续调优就越有底气也越能提前发现那些“平时没事、一上线就爆”的隐藏问题。