大模型推理优化:Prefill、Decode与KV Cache的工程实践 📅 发布时间:2026/9/21 1:36:03 👁 浏览次数: 大模型推理这件事表面上看是输入一句话吐出一段话但真正落到工程侧最容易被混淆、也最值得掰开揉碎讲清楚的就是 Prefill、Decode 和 KV Cache 这三者之间的关系。我见过不少刚接触推理优化的同学能把 Attention 公式背得滚瓜烂熟却说不清为什么首 token 延迟TTFT和单 token 输出延迟TPOT是两个完全不同的指标也说不清为什么线上服务里PD 分离会成为一个独立话题。这篇就按我自己的理解把这三块从概念到工程落地完整串一遍顺带把 KV Cache 这个热词背后的真实含义讲透。不管你是刚上手推理框架的开发者还是已经在做推理服务调优的工程师应该都能从中找到能直接用的东西。1. 先把三个概念摆到同一张桌子上很多人学这三样东西是分开学的先看 Transformer 结构再单独看 KV Cache 的论文最后零散地听别人提 Prefill 和 Decode。结果就是脑子里三块知识各自成立但拼不到一起。我建议一开始就把它们放在同一条时间线上理解因为它们本来就是同一个推理过程在不同阶段的三个侧面。1.1 一次完整推理到底发生了什么假设你给模型输入一段 prompt比如 512 个 token模型要生成 200 个 token 的回答。整个过程在自回归模型里被切成两段第一段是Prefill预填充也叫 prompt 阶段。模型把这 512 个 token 一次性全部喂进去做一次完整的前向计算。注意这里是一次性512 个 token 是并行处理的因为 prompt 是已知的不存在依赖关系。这一段的产出有两个一是第一个输出 token 的 logits二是每一层、每个注意力头的 Key 和 Value 张量——这就是KV Cache的初始内容。第二段是Decode解码也叫生成阶段。从第二个 token 开始每生成一个 token 都要走一次前向。但这时候输入不再是 512 个 token而是上一个生成的 token这 1 个 token。模型拿这 1 个 token 算出它自己的 Query然后去和 KV Cache 里存着的所有历史 Key、Value 做注意力计算得到输出再把这个新 token 的 Key、Value 追加进 Cache。如此循环 200 次。所以三者的关系一句话概括Prefill 负责读题并建立 KV CacheDecode 负责答题并不断扩展 KV CacheKV Cache 是连接两个阶段的共享状态。没有 KV CacheDecode 每一步都要把前面所有 token 重新算一遍复杂度会从 O(n) 变成 O(n²)这是不可接受的。1.2 为什么这两个阶段的计算特征截然不同这是理解后续所有优化的关键。Prefill 和 Decode 虽然用的是同一个模型、同一套权重但它们的计算模式几乎是两个极端。Prefill 阶段序列长度是 512或更长所有 token 并行参与矩阵乘法。这时候 GPU 的算力被打满是典型的compute-bound计算受限。矩阵乘法的维度大、并行度高Tensor Core 利用率可以做到很高。Decode 阶段每次只处理 1 个 tokenbatch 内多个请求的话是 batch size 个 token。矩阵乘法的 M 维度极小但每次都要读取整个 KV Cache 和全部模型权重。这时候 GPU 的算力大量闲置瓶颈变成了显存带宽是典型的memory-bound访存受限。我用一个生活化的类比Prefill 像是你一次性把一整本书读完做笔记脑子转得飞快但笔记量巨大Decode 像是每写一个字都要翻一遍之前所有的笔记翻笔记读显存的时间远大于写字计算的时间。这个差异直接决定了后面所有的优化方向——Prefill 要压计算效率Decode 要压访存开销。1.3 KV Cache 到底缓存了什么KV Cache 这个名字容易让人以为它缓存的是整个注意力结果其实不是。它缓存的是每一层自注意力模块里的Key 矩阵和 Value 矩阵Query 是不缓存的。原因很直接在自回归生成中第 t 步的 Query 只跟当前这个新 token 有关算完就扔但第 t 步以及之后所有步都需要用到前 t-1 个 token 的 Key 和 Value。既然它们会被反复使用缓存下来就能避免重复计算。具体形状上对于一层注意力KV Cache 的大小是[batch_size, num_heads, seq_len, head_dim]Key 和 Value 各一份。整个模型的 KV Cache 还要乘以层数。这就是为什么长上下文场景下显存会被 KV Cache 吃光——它的增长是随序列长度线性、随层数和头数线性叠加的。提示KV Cache 缓存的是 K 和 V不是 Q也不是注意力输出。这个细节在排查显存占用时非常关键很多人算显存时把 Q 也算进去结果对不上账。2. Prefill 阶段被低估的读题成本大部分用户对首 token 延迟的容忍度其实不高。你想想自己用对话产品的体验输入问题后如果两三秒没反应就会怀疑是不是卡了。这个两三秒主要就是 Prefill 的耗时。但 Prefill 在工程上经常被当成反正就一次而忽略实际上它藏着不少坑。2.1 Prefill 的计算量为什么和 prompt 长度强相关Prefill 的计算量大致正比于 prompt 长度的平方注意力部分加上正比于 prompt 长度FFN 部分。当 prompt 从 512 涨到 4096注意力部分的计算量涨了 64 倍。这就是长 prompt 首 token 延迟飙升的根本原因。我实测过一个 7B 级别的模型在单张 A100 上512 token 的 prompt 首 token 延迟大概在 100ms 量级而 4096 token 的 prompt 能到 1s 以上。这个增长不是线性的是明显超线性的。所以如果你的产品有上传长文档后提问的场景Prefill 优化就是刚需而不是可选项。2.2 Chunked Prefill把大块拆成小块一个非常实用的工程手段是Chunked Prefill。思路很简单既然一次处理 4096 个 token 会让延迟很高那就把它切成若干个 chunk比如每 512 个 token 一个 chunk分多次前向。这样做的好处有两个。第一单次前向的计算量变小首 token 延迟的峰值被削平。第二更重要的是它让 Prefill 可以和 Decode 混合调度——当系统里既有正在 Prefill 的长请求又有正在 Decode 的短请求时可以把 Prefill 的 chunk 和 Decode 的 batch 拼在一起跑提升整体 GPU 利用率。代价是 Prefill 的总耗时可能略微增加因为分块有额外开销但换来的是更平滑的延迟曲线和更高的吞吐。这个取舍在线上服务里通常是划算的。2.3 Prefill 阶段的显存峰值陷阱Prefill 有一个容易被忽略的问题激活值显存峰值。因为要一次性处理整个 prompt中间激活尤其是注意力矩阵会占用大量显存。序列越长这个峰值越高。我踩过一次坑模型权重加载后显存还剩不少以为能跑 8K 上下文结果一跑长 prompt 就 OOM。排查后发现是 Prefill 的注意力激活峰值把显存顶爆了。解决办法有几个用 FlashAttention 这类 IO 感知的注意力实现它通过分块计算避免物化完整的注意力矩阵或者用 Chunked Prefill 把峰值摊平再或者直接限制单次 Prefill 的最大长度。注意显存规划时不能只看权重和 KV CachePrefill 的激活峰值往往才是压垮骆驼的最后一根稻草尤其是长上下文场景。3. Decode 阶段真正决定体感速度的地方如果说 Prefill 决定用户等多久看到第一个字那 Decode 决定的是字吐出来的速度。这个速度用TPOTTime Per Output Token衡量也就是每个输出 token 的平均耗时。人眼阅读大概每秒 5 到 10 个字比较舒服对应 TPOT 在 100ms 到 200ms。超过这个用户就会觉得卡顿。3.1 Decode 为什么是访存瓶颈前面提过Decode 每次只处理 1 个 token矩阵乘法的 M 维度是 1。这时候 GPU 的算力单元基本在空转真正花时间的是把模型权重和 KV Cache 从显存搬到计算单元。我算过一笔账一个 7B 模型FP16 权重约 14GB。每生成一个 token理论上要把这 14GB 全部读一遍。A100 的显存带宽约 2TB/s那么光读权重就要 7ms。再加上读 KV Cache 的时间单 token 耗时轻松上到十几毫秒。如果 batch size 是 1这就是纯浪费——算力利用率可能只有个位数百分比。这也解释了为什么batching对 Decode 如此重要。当 batch size 从 1 涨到 32权重还是读一遍但一次能服务 32 个请求吞吐直接翻几十倍。代价是每个请求的 TPOT 可能略微上升但整体性价比极高。3.2 Continuous Batching让 Decode 的 batch 始终饱满传统的静态 batching 是攒一批请求一起跑完再收下一批。问题是每个请求生成长度不同短请求早就结束了却要等长请求跑完GPU 利用率被拖垮。Continuous Batching也叫 iteration-level batching解决了这个问题以一次 Decode 迭代为调度单位每个迭代结束后完成的请求退出新请求加入。这样 batch 始终是满的GPU 利用率大幅提升。这个机制现在是主流推理框架的标配。它的实现难点在于 KV Cache 的动态管理——请求退出时要释放它的 Cache新请求进来要分配新的 Cache 空间。这就引出了下一节要讲的 PagedAttention。3.3 Decode 阶段的 KV Cache 读取开销Decode 每步都要读取整个 KV Cache。当序列很长、batch 很大时KV Cache 的读取量会超过权重读取量成为新的瓶颈。举个例子batch size 32序列长度 4096模型 32 层每层 32 个头head_dim 128。单层单请求的 KV Cache 是2 × 32 × 4096 × 128 × 2字节 ≈ 64MB32 层就是 2GBbatch 32 就是 64GB。每生成一个 token 都要读这 64GB显存带宽直接被打满。所以长上下文 大 batch 的场景下KV Cache 的优化量化、稀疏化、分页管理比权重优化更关键。4. KV Cache 的工程实现从朴素缓存到 PagedAttentionKV Cache 概念上简单但工程实现里门道很多。朴素实现会带来严重的显存碎片和浪费这也是为什么 PagedAttention 这类技术会成为推理框架的核心创新。4.1 朴素 KV Cache 的显存浪费问题最直接的实现是为每个请求预分配一块连续显存大小按最大可能序列长度算。比如最大 4096 token那就给每个请求预留 4096 的 Cache 空间。问题是大部分请求根本用不到 4096。实际平均长度可能只有 500。那预留的空间就浪费了。更糟的是显存被切成一块块固定大小的连续区域后会产生外部碎片——想塞新请求时剩余空间虽然总量够但没有一块连续区域能放下。我见过实测数据朴素实现在某些负载下KV Cache 的显存有效利用率只有 20% 到 40%。这意味着你本来能服务 100 个并发实际只能服务 30 个。4.2 PagedAttention 的核心思路PagedAttention 借鉴了操作系统虚拟内存分页的思想。它把 KV Cache 切成固定大小的block比如每块存 16 个 token 的 KV这些 block 在物理显存上不需要连续通过一张 block table 来映射逻辑位置到物理位置。这样做的好处立竿见影消除外部碎片block 是固定大小任何空闲 block 都能用不存在总量够但放不下的问题。按需分配请求用到多少就分配多少 block不预留浪费极小。支持共享多个请求如果有相同的前缀比如相同的 system prompt可以让它们的 block table 指向同一批物理 block实现前缀共享省显存又省计算。实测下来PagedAttention 能把 KV Cache 的显存利用率从 20% 到 40% 提升到 90% 以上吞吐提升数倍。这不是小优化是数量级的改变。4.3 KV Cache 量化用精度换显存和带宽除了分页管理另一个方向是KV Cache 量化。既然 Decode 是访存瓶颈那把 KV Cache 从 FP16 压到 INT8 甚至 INT4读取量直接减半或减到四分之一TPOT 就能明显下降。常见的做法是只量化 Key 和 ValueQuery 保持原精度。量化方式有 per-tensor、per-channel、per-token 等粒度。粒度越细精度损失越小但反量化开销越大。我实测过 INT8 的 KV Cache 量化在多数任务上精度损失可以忽略困惑度上升不到 1%但显存占用减半长上下文场景下吞吐提升 30% 到 50%。INT4 的损失就明显一些需要看具体任务能不能接受。提示KV Cache 量化对长上下文、大 batch 场景收益最大。如果序列很短、batch 很小收益有限反而增加实现复杂度要权衡。5. PD 分离把两个阶段拆到不同硬件上前面讲了 Prefill 是 compute-boundDecode 是 memory-bound。既然计算特征相反那把它们放在同一张卡上跑必然有一方在浪费资源。PD 分离Prefill-Decode Disaggregation就是针对这个矛盾提出的架构方案。5.1 PD 分离要解决的核心矛盾在同一张卡上Prefill 和 Decode 会互相干扰。当一个大 Prefill 请求进来它会占满算力导致正在 Decode 的请求 TPOT 飙升用户体感就是打字突然卡住。反过来如果为了保护 Decode 的延迟而限制 Prefill 的并发那首 token 延迟又会变高。这个矛盾在混合负载下特别突出。PD 分离的思路是用一组机器专门做 Prefill另一组专门做 Decode中间通过高速网络传输 KV Cache。Prefill 机器可以选算力强的卡Decode 机器可以选显存带宽大、显存容量大的卡各取所需。5.2 KV Cache 传输PD 分离的关键路径PD 分离最大的工程挑战是KV Cache 的跨机传输。Prefill 算完的 KV Cache 要传给 Decode 机器这个数据量不小。前面算过一个 4096 token 的请求KV Cache 可能有好几个 GB。传输时间如果太长PD 分离的收益就被吃掉了。所以 PD 分离对网络要求很高通常需要 RDMA 这类高带宽低延迟的网络。传输策略上也有讲究可以按层传输Prefill 算完一层就传一层和计算重叠也可以整块传输。按层传输能更好地隐藏传输延迟但实现更复杂。我了解到的一些实践是在高速网络下KV Cache 传输的开销可以控制在总延迟的 10% 到 20%PD 分离带来的整体收益吞吐提升、延迟稳定性是值得的。但如果网络条件一般PD 分离可能得不偿失。5.3 什么场景适合上 PD 分离PD 分离不是银弹它有明确的适用边界。适合的场景请求量大、负载混合长 prompt 和长生成并存、对延迟稳定性要求高、有高速网络条件。典型的是大规模在线服务。不适合的场景请求量小、负载单一、网络条件一般、团队运维能力有限。这种情况下单机上的 Chunked Prefill Continuous Batching 已经能拿到大部分收益上 PD 分离反而增加复杂度和故障面。我的建议是先把单机优化做透PagedAttention、Continuous Batching、Chunked Prefill、KV Cache 量化确认瓶颈确实在 Prefill 和 Decode 的资源争抢上再考虑 PD 分离。不要为了架构先进而架构先进。6. 三者协同下的性能调优实战思路把 Prefill、Decode、KV Cache 分开讲清楚之后最后落到调优上其实是一套组合拳。我按自己的经验整理一个排查和优化的顺序供参考。6.1 先定位瓶颈在哪个阶段调优第一步永远是测量不是拍脑袋。你需要把指标拆开看指标含义对应阶段优化方向TTFT首 token 延迟PrefillChunked Prefill、前缀缓存、算力升级TPOT单 token 输出延迟DecodeKV Cache 量化、batching、带宽升级吞吐每秒 token 数两者Continuous Batching、PagedAttention显存利用率KV Cache 占用比两者PagedAttention、量化、前缀共享如果 TTFT 高而 TPOT 正常问题在 Prefill反之在 Decode。如果两个都高先看是不是 batch 太小导致 GPU 利用率低。6.2 常见瓶颈与对应手段我把踩过的和见过的典型情况列一下TTFT 高、prompt 长上 Chunked Prefill或者用前缀缓存如果多个请求共享 system prompt。TPOT 高、序列长KV Cache 量化或者检查是不是 batch 太小。吞吐上不去、GPU 利用率低检查 Continuous Batching 是否开启batch 是否饱满。显存不够、并发上不去PagedAttention KV Cache 量化 前缀共享三件套。延迟抖动大考虑 PD 分离或者用 Chunked Prefill 削峰。6.3 几个容易忽略的实操细节最后分享几个文档里不常写、但实操中很关键的细节。第一KV Cache 的 block size 不是越小越好。block 太小block table 变大管理开销上升block 太大内部碎片增加。实践中 16 到 32 个 token 一块是比较常见的甜点区具体要看序列长度分布。第二前缀缓存的命中率决定收益。如果每个请求的 prompt 都不一样前缀缓存基本没用。它最适合 system prompt 固定、多轮对话这类场景。上线前先统计一下前缀重复率再决定要不要投入。第三量化 KV Cache 后要重新测精度。不同任务对量化的敏感度差异很大。代码生成、数学推理这类任务对精度更敏感可能 INT8 就有可感知的退化而普通对话可能 INT4 都能接受。别拿一个任务的结论套所有场景。第四PD 分离的 KV Cache 传输要考虑失败重试。跨机传输一旦失败整个请求就废了。生产环境里要有重试和降级机制比如传输失败就退回单机执行。第五监控要区分 Prefill 和 Decode 的队列长度。混在一起看会掩盖问题。Prefill 队列长说明算力不够或 chunk 太小Decode 队列长说明带宽不够或 batch 调度有问题。这套东西我在实际项目里反复用过核心就一句话Prefill 管首字延迟Decode 管吐字速度KV Cache 是两者共享的显存账本所有优化本质上都是在算力、带宽、显存这三者之间做取舍。把这三个阶段的特征和瓶颈分清楚再对症下药比盲目堆硬件或者照搬别人的配置有效得多。