1. 大模型推理的性能账本为什么你的服务又慢又贵搞大模型推理服务的人迟早都会撞上同一堵墙模型明明跑起来了单条请求的延迟看着还行可一旦并发上来吞吐量死活上不去GPU 利用率像心电图一样忽高忽低显存却先一步爆了。你加卡、换更贵的型号、调 batch size折腾一圈发现收益远没有想象中大。问题往往不在模型本身而在于你没搞清楚推理这件事到底在拼什么。把大模型推理拆开看其实只有两个阶段Prefill预填充和Decode解码。这两个阶段的性能瓶颈完全不同——Prefill 阶段是计算密集型拼的是算力Decode 阶段是访存密集型拼的是显存带宽。而贯穿两个阶段、决定你能塞下多少并发、能跑多长上下文的是KV 缓存它直接决定了显存代价。这三件事构成了 LLM 推理性能的核心框架理解它们之间的取舍关系比盲目堆硬件有用得多。这篇文章适合正在做推理服务部署的工程师、做模型落地的算法同学以及任何被吞吐上不去、显存不够用折磨过的人。我会把 Prefill 和 Decode 的算力/带宽账算清楚把 KV 缓存的显存开销拆到字节级别再给出实际部署中真正管用的优化手段和踩坑经验。看完你至少能回答三个问题我的瓶颈到底在哪、该往哪个方向优化、以及为什么有些优化看起来很美但实际没用。2. Prefill 与 Decode两个阶段两套完全不同的性能逻辑2.1 一次推理请求到底经历了什么当你把一段 prompt 丢给模型推理过程被切成两段。第一段是 Prefill模型把整段输入 token 一次性喂进去为每个 token 计算出 Key 和 Value 向量存进 KV 缓存同时算出第一个输出 token。第二段是 Decode从第一个输出 token 开始模型每次只生成一个 token每生成一个就要把前面所有 token 的 KV 缓存读一遍再算出下一个。这个切分不是人为规定的而是由自回归生成的本质决定的。Prefill 能并行处理整段输入是因为输入 token 之间没有依赖关系可以一次性算完Decode 必须串行是因为第 N 个输出 token 依赖第 N-1 个没法并行。理解这个区别是理解一切性能问题的起点。Prefill 像是一次性搬一整车货Decode 像是每次只搬一件但要把整个仓库翻一遍。两者的资源消耗模式截然不同。2.2 Prefill 为什么拼算力Prefill 阶段处理的是长度为 L 的输入序列模型要对这 L 个 token 做完整的注意力计算和前馈网络计算。注意力的计算量大致是 O(L²·d)其中 d 是隐藏维度前馈网络是 O(L·d²)。当输入很长时注意力那部分的平方项会迅速主导计算量。关键在于Prefill 阶段有大量可以并行的矩阵乘法GPU 的 Tensor Core 能被充分喂饱。这时候 GPU 的计算单元是满负荷运转的显存带宽反而成了次要因素——因为数据复用率高算一次能用到很多次。所以 Prefill 的性能天花板由GPU 的浮点算力FLOPS决定尤其是 FP16/BF16 的算力。实测下来Prefill 阶段 GPU 的计算利用率MFUModel FLOPs Utilization通常能跑到 40% 到 60%好的实现能到 70% 以上。这个数字说明算力确实被用起来了瓶颈在算力本身。2.3 Decode 为什么拼带宽Decode 阶段每次只处理一个新 token但要把 KV 缓存里所有历史 token 的 Key 和 Value 读出来参与注意力计算。假设上下文长度是 L隐藏维度是 d层数是 N那么每生成一个 token需要读取的 KV 缓存数据量是 2·L·d·N·2 字节FP16 下每个数 2 字节K 和 V 各一份。这个读取量随上下文线性增长而计算量却很小——只有一次矩阵向量乘法。结果就是计算单元大部分时间在等数据从显存搬过来。这就是典型的访存密集型memory-bound场景。用一个生活类比Prefill 像是让一群工人同时搬一堆砖人多力量大Decode 像是让一个工人每次只搬一块砖但他每搬一块都要跑回仓库最里面取跑腿的时间远大于搬砖的时间。这时候你加再多工人算力也没用得把仓库搬到工人手边提高带宽或者减少跑腿次数优化缓存。实测中 Decode 阶段的计算利用率经常只有个位数百分比GPU 大部分时间在等显存。所以 Decode 的性能天花板由显存带宽Memory Bandwidth决定。2.4 两个阶段的性能特征对比维度Prefill 阶段Decode 阶段计算模式矩阵-矩阵乘法为主矩阵-向量乘法为主瓶颈类型计算密集型访存密集型决定性能的硬件浮点算力FLOPS显存带宽GB/s典型计算利用率40%~70%5%~15%可并行性高整段输入并行低逐 token 串行对 batch 的敏感度中等高batch 能摊薄权重读取优化方向算子融合、量化、并行KV 缓存优化、批处理、投机解码这张表是整篇文章的骨架。你遇到的每一个性能问题先判断它落在哪个阶段再对照这张表找方向基本不会跑偏。3. KV 缓存决定显存代价的那个隐形大户3.1 KV 缓存到底占多少显存KV 缓存的显存占用公式很直接KV 缓存大小 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × batch_size × 数据类型字节数其中 2 代表 K 和 V 各一份。以一个 70 亿参数、32 层、32 个注意力头、头维度 128 的模型为例FP16 精度下每个 token 的 KV 缓存是2 × 32 × 32 × 128 × 2 字节 524288 字节 ≈ 0.5 MB看起来不多但注意这是每个 token。如果上下文长度是 8192单个请求就是 4 GB如果 batch_size 是 16直接飙到 64 GB。而模型权重本身 FP16 下才 14 GB 左右。也就是说KV 缓存很容易超过模型权重成为显存第一大户。这就是为什么很多人发现模型能加载进去但一跑长上下文就 OOM。不是模型太大是 KV 缓存把显存吃光了。3.2 为什么 KV 缓存不能省有人会问既然 KV 缓存这么占地方为什么不每次重新算答案是计算代价太高。Decode 阶段每生成一个 token如果不缓存就要把前面所有 token 的 K 和 V 重新算一遍计算量是 O(L²)而缓存后只需要 O(L) 的读取。在长上下文场景下重算的时间成本是缓存的几十倍甚至上百倍。所以 KV 缓存是用显存换时间的经典 trade-off。问题不在于要不要缓存而在于怎么把缓存做得更省、更高效。3.3 显存带宽如何成为 Decode 的硬约束回到 Decode 阶段。每生成一个 token需要读取的 KV 缓存是 0.5 MB以上面模型为例。如果显存带宽是 2 TB/s那么理论上每秒能读取 400 万次这样的数据块也就是每秒能生成 400 万个 token这个数字显然太乐观了因为还要算上模型权重的读取。实际上Decode 阶段每生成一个 token除了读 KV 缓存还要读一遍模型权重因为每次只算一个 token权重没法复用。70 亿参数 FP16 是 14 GB每生成一个 token 就要把这 14 GB 读一遍。在 2 TB/s 带宽下光读权重就要 7 毫秒也就是单请求每秒最多生成约 140 个 token。这个计算揭示了一个关键事实Decode 阶段的单请求速度被显存带宽死死卡住。要提升吞吐只能靠 batch——多个请求共享同一次权重读取。batch_size 从 1 加到 16权重读取的摊销成本降到 1/16吞吐能提升十几倍。但 batch 一大KV 缓存又成倍增长显存又不够了。这就是推理服务里最核心的矛盾。4. 从理论到落地推理服务的优化实操4.1 先定位瓶颈你的服务卡在哪优化之前必须先测量。我见过太多人上来就调参数结果优化了个寂寞。定位瓶颈的方法很简单第一步看 Prefill 和 Decode 的耗时占比。如果 Prefill 占大头说明输入长、算力不够往算力方向优化如果 Decode 占大头说明输出长、带宽不够往缓存和批处理方向优化。第二步看 GPU 利用率曲线。Prefill 阶段利用率高、Decode 阶段利用率低是正常的如果两个阶段利用率都低说明有调度或框架层面的问题。第三步看显存占用构成。把模型权重、KV 缓存、激活值分开统计。如果 KV 缓存占比超过 50%说明显存主要被缓存吃了要优先做缓存优化。提示不要只看平均延迟要看 P99 延迟。推理服务的用户体验由长尾决定平均值会骗人。4.2 Prefill 优化把算力榨干Prefill 的优化核心是提高计算利用率让 Tensor Core 别闲着。算子融合是最直接的手段。把 LayerNorm、矩阵乘、激活函数这些相邻算子合并成一个 kernel减少 kernel 启动开销和中间结果的显存读写。FlashAttention 就是典型代表它把注意力计算融合成一个 kernel避免了中间注意力矩阵的显存读写在长序列上提速明显。量化是另一个大杀器。把权重和激活从 FP16 降到 INT8 甚至 INT4算力需求直接减半或降到四分之一。但要注意量化对 Prefill 的收益不如对 Decode 明显因为 Prefill 本来就是算力瓶颈量化主要省的是显存和带宽。序列并行适合超长输入。当输入长度超过单卡能处理的范围把序列切分到多张卡上并行计算每张卡只处理一部分 token 的注意力。代价是卡间通信开销适合输入特别长的场景。4.3 Decode 优化和带宽做朋友Decode 的优化核心是减少显存读取量、提高读取效率。连续批处理Continuous Batching是必选项。传统批处理要等一个 batch 里所有请求都生成完才能接下一批GPU 在等待时是空闲的。连续批处理让每个请求独立进出一个请求生成完立刻换下一个进来GPU 始终有活干。实测下来连续批处理能把吞吐提升 2 到 5 倍是性价比最高的优化。PagedAttention解决的是 KV 缓存碎片问题。传统实现里 KV 缓存要预分配连续显存长度按最大可能值算浪费严重。PagedAttention 把缓存切成固定大小的块按需分配显存利用率能从 20%~40% 提升到 90% 以上。这意味着同样的显存能塞下更多并发请求。投机解码Speculative Decoding用一个小模型先猜几个 token大模型再一次性验证。如果猜对了就省下了大模型的多次前向。这个方法的收益取决于小模型的命中率命中率高时能提速 2 到 3 倍命中率低时反而拖慢。适合输出内容比较确定的场景比如代码生成、结构化输出。量化 KV 缓存是直接省显存的手段。把 KV 缓存从 FP16 降到 INT8显存占用直接减半带宽需求也减半。代价是精度损失实测在多数任务上 INT8 KV 缓存的质量损失可以接受但要注意校准。4.4 关键参数的计算与选择部署时几个参数必须算清楚不能拍脑袋。最大 batch size由显存决定。可用显存 总显存 - 模型权重 - 激活值预留。假设总显存 80 GB模型权重 14 GB激活预留 10 GB剩 56 GB 给 KV 缓存。如果每个请求平均上下文 4096每 token 0.5 MB单请求缓存 2 GB那么最大 batch 约 28。但这是理论上限实际要留 20% 余量取 22 左右。最大上下文长度要和 batch size 权衡。上下文翻倍单请求缓存翻倍能支持的 batch 就减半。如果业务上大部分请求上下文很短可以设置动态上限短请求多塞、长请求少塞。GPU 显存带宽是选卡的硬指标。做 Decode 密集型服务优先选带宽高的卡而不是算力高的卡。同样价格下带宽翻倍带来的 Decode 吞吐提升往往比算力翻倍更明显。参数计算依据建议取值最大 batch size(可用显存 - 权重 - 激活) / 单请求缓存理论上限的 80%最大上下文业务 P95 上下文长度略高于 P95留余量KV 缓存精度质量要求与显存权衡FP16 优先紧张时 INT8块大小PagedAttention显存碎片与调度开销权衡16 或 325. 常见问题与排查技巧实录5.1 吞吐上不去先查这三处第一处batch 是不是太小。很多人为了降延迟把 batch 设得很小结果 GPU 利用率上不去吞吐自然低。Decode 阶段 batch 从 1 加到 8吞吐通常能翻好几倍延迟增加却很有限。先试着把 batch 调大看吞吐和延迟的变化曲线找到拐点。第二处KV 缓存是不是碎片化。如果显存明明还有但新请求就是进不来多半是缓存碎片。用 PagedAttention 或者检查缓存分配策略看是不是预分配了过大的连续块。第三处调度是不是串行。检查请求队列的处理逻辑是不是一个请求处理完才接下一个。连续批处理没开的话GPU 会有大量空闲等待。5.2 长上下文为什么特别慢长上下文慢在两个地方。Prefill 阶段注意力计算量随长度平方增长输入从 2K 到 8KPrefill 时间可能涨 10 倍以上。Decode 阶段每生成一个 token 要读的 KV 缓存随长度线性增长8K 上下文的读取量是 2K 的 4 倍。应对方法Prefill 用 FlashAttention 类的高效注意力实现Decode 用 KV 缓存量化或滑动窗口注意力只保留最近 N 个 token 的缓存。滑动窗口会损失长距离依赖适合对远期上下文不敏感的任务。5.3 显存明明够却 OOM 的排查这种情况通常是显存碎片或者峰值超限。排查步骤用显存分析工具看峰值占用而不是平均值。Prefill 阶段的激活值峰值可能远高于 Decode。检查是否有预分配的大块显存没释放。看 KV 缓存的实际占用和理论值是否吻合不吻合说明有泄漏或碎片。临时把 batch 降到 1看是否还 OOM排除并发因素。注意PyTorch 的缓存分配器会保留已释放的显存torch.cuda.memory_allocated和torch.cuda.memory_reserved要分开看前者是实际使用后者是保留量。5.4 量化后质量下降怎么办量化 KV 缓存或权重后质量下降通常是校准没做好。几个经验权重量化用 GPTQ 或 AWQ比朴素的 round-to-nearest 好很多。KV 缓存量化要按通道校准不同通道的数值范围差异很大。先量化权重质量可接受再量化 KV 缓存逐步来。保留关键层比如第一层和最后一层的高精度其余层量化质量损失更小。5.5 常见问题速查表现象可能原因排查方向吞吐低、GPU 利用率低batch 太小、调度串行调大 batch、开连续批处理长上下文 OOMKV 缓存超限量化缓存、限制上下文、PagedAttentionDecode 慢带宽瓶颈、缓存读取量大缓存量化、滑动窗口、换高带宽卡Prefill 慢算力瓶颈、注意力实现低效FlashAttention、量化、序列并行显存够却 OOM碎片、峰值超限显存分析、PagedAttention、降 batch量化后质量降校准不足换量化方法、保留关键层精度6. 我踩过的坑和几条实在建议做推理服务这几年有几个坑印象特别深。有一次线上服务吞吐突然掉了一半查了半天发现是某个请求带了超长上下文把 KV 缓存撑爆触发了缓存驱逐其他请求的缓存被反复重建。后来加了上下文长度限制和优先级调度才稳住。这件事让我明白推理服务的稳定性不只取决于平均情况更取决于最坏情况。还有一次为了省显存把 KV 缓存量化到 INT4结果生成质量肉眼可见地下降尤其是需要长距离推理的任务。后来改成 INT8 加关键层保留 FP16质量回来了显存也只多了不到 20%。量化不是越狠越好要找到质量和成本的平衡点。几条实在建议第一先测量再优化别凭感觉调参第二连续批处理和 PagedAttention 是必选项不开这两个其他优化都是事倍功半第三选卡时 Decode 密集型服务优先看带宽别只看算力第四长上下文场景一定要做缓存量化或滑动窗口否则显存迟早爆第五压测要用真实流量分布别用均匀分布的假数据长尾请求才是真正的考验。这个领域变化很快新的注意力实现、新的量化方法、新的调度策略层出不穷。但底层那套账——Prefill 拼算力、Decode 拼带宽、KV 缓存决定显存——短期内不会变。把这套账算清楚新东西出来你也能快速判断它值不值得上。