资讯动态

LLM推理优化实战:显存、KV Cache与解码性能瓶颈拆解

发布时间:2026/9/10 4:27:34 来源:尧图企业网站定制
做 LLM 推理优化的时间长了你会发现一个特别拧巴的现象模型参数看着没多大但跑起来显存说爆就爆单看一次推理延迟好像还能忍一上并发吞吐立刻拉胯。网上聊推理优化的文章不少但大多停留在KV Cache 要省着用上 FP16 量化这种结论层面真正把背后的原理讲透、让读者能自己判断该往哪个方向优化的内容并不多。这篇文章想做的就是这个事——从 LLM 推理的底层机制出发把算力、显存、访存、调度这些核心约束逐层拆开再用我实际跑过的案例和数据告诉你每一步优化到底在解决什么问题以及为什么某些方案在这个场景有效、换个场景就翻车。内容包括解码过程的本质瓶颈、KV Cache 的显存公式与量化取舍、注意力机制的计算模式与 Flash Attention 为什么快、批量调度与投机解码的数学逻辑、以及我自己整理的一套排查流程和指标观测方法。适合正在做推理服务部署、想优化响应速度或降低显存占用的工程师也适合刚接触 LLM 应用开发、想搞明白为什么本地跑个 7B 模型都这么慢的学习者。读完你至少能回答三个问题显存被谁吃了时间花在哪了优化手段各自的代价是什么1. 核心瓶颈拆解LLM 推理的时间与显存都去哪了1.1 自回归解码一切优化都要服从的硬约束要理解 LLM 推理优化先得接受一个底层事实大语言模型的推理本质上是串行的自回归过程。模型每次只生成一个 token生成下一个 token 时要依赖前面已经生成的所有 token。所以生成 100 个 token就要调用 100 次模型前向计算这 100 次没法并行至少现在主流架构下没法跨 token 并行只能一次一次来。这就是为什么 LLM 推理的延迟和显存压力都绕不开一个词——顺序依赖。有一个很直观的生活类比查字典写句子。你每写下一个字之前要先看看已经写好的整句话是什么再决定这个字怎么写。你不能跳过前一个字直接写后一个字因为你后一个字的内容取决于前面的语义。自回归解码就是这个过程只不过看已经写好的句子这一步在计算上代价极高——每一次都要把历史 token 的隐藏状态重新读一遍、算一遍交互。这个特性直接决定了优化逻辑的边界你没法靠单纯堆显卡并行度来压单请求延迟因为第 N 个 token 的计算必须在第 N-1 个 token 算出之后才能开始。所以从架构层面所有加速手段都只能做两件事让单次前向计算更快或者让一次前向计算同时服务更多请求。从工程视角来看自回归还带来了第二个隐藏问题访存瓶颈。当前主流 GPU 做矩阵乘法的速度非常快但模型权重要从显存取到计算单元里这个搬运过程是有带宽上限的。当你 batch size 很小的时候实际计算量很小但权重搬运还是要花同样多的时间这就导致计算单元大量空闲整体表现为跑一个请求很慢。而增大 batch size 后权重只需要搬运一次可以被多个请求复用计算资源的利用率一下就上去了。这是后面所有批处理优化的根本出发点。1.2 一次推理的完整时间构成prefill 与 decode 的差异要分析耗时得把一次完整的推理拆成两段prefill 阶段和decode 阶段。prefill 阶段是处理用户输入的那一下。比如你输入了 100 个 token 的 prompt模型需要把这 100 个 token 并行处理一次性生成每个 token 对应的隐状态并计算出第一个输出 token。这阶段的特点是数据量大、计算密度高AI 加速卡擅长干这活所以实践中 prefill 的吞吐很高但耗时依然可观——prompt 越长这个阶段越慢。decode 阶段是逐 token 生成后续内容的过程。每个 token 生成时需要把之前所有 token 对应的 KV Cache 读出来参与注意力计算。这个阶段的特点是计算量小、访存量线性增长GPU 的算力根本喂不饱瓶颈几乎完全卡在显存带宽上。我曾经拿一张消费级显卡做过粗略测试7B 模型 FP16 权重占 14GB单次 decode 的计算量不到 1 TFLOPs但需要从显存读取的权重和数据加起来超过 14GB。这张卡的算力可以做到 30 TFLOPs 以上但显存带宽只有 600GB/s 左右一算就知道——就算算力翻了四倍decode 的速度也几乎不变因为时间全花在搬数据上了。这个差异直接决定了优化策略的分野prefill 阶段要优化计算效率比如用更长的 prompt 批处理decode 阶段要优化访存效率比如把权重和 KV Cache 做量化压缩减小搬运量。一套方案想同时适配两个阶段往往就得做取舍。1.3 显存的四笔账权重、KV Cache、激活值、运行时开销很多人以为显存压力主要来自模型权重实际跑起来才发现KV Cache 才是那个隐形吞内存的大户。一块显存的去向主要有四块模型权重7B 模型 FP16 格式就是 14GB13B 就是 26GB这是显存占用的基石但它是静态的好算也好预估。KV Cache每个请求在生成过程中都会不断累积这个值是动态的随并发数和序列长度线性增长是最难控制的部分。激活值前向计算过程中产生的中间张量prefill 阶段因为要处理长序列激活值会非常大decode 阶段因为 batch size 一般较小激活值相对可控。运行时开销CUDA context、框架缓存、内存碎片等看起来不多但多个服务叠加后也能吃掉几百 MB 到几个 GB。我实际部署时的经验是权重只能算地基KV Cache 才是决定你能开多大并发、跑多长上下文的天花板。很多场景下 2K 上下文够用但如果业务要支持 32K 甚至更长KV Cache 会以线性速度吃掉显存这比权重翻倍要可怕得多。所以业界才有那么多 KV Cache 压缩、量化、offload 的方案出来。2. KV Cache 的账本显存公式、量化与精度代价2.1 KV Cache 到底是怎么算出来的KV Cache 的全称是 Key-Value Cache它缓存的是注意力机制中每个历史 token 经过投影矩阵计算出的 Key 向量和 Value 向量。为什么要缓存因为生成新 token 时注意力计算需要当前 token 的 Query 和所有历史 token 的 Key、Value 做点积。如果不缓存就得把历史 token 重新往前传一遍计算量直接翻 N 倍N 是序列长度这在工程上完全不可接受。KV Cache 的显存占用有个经典公式显存字节数 2K 和 V 两份 × num_layers层数 × num_heads头数 × head_dim头维度 × seq_len序列长度 × batch_size批量大小 × bytes_per_element每个元素字节数以 FP16 为例7B 模型通常是 32 层、32 个头、head_dim 128算下来每 token 的 KV Cache 占用大约是 32×32×128×2×2 524,288 字节即 0.5MB。单个请求生成 2000 tokenKV Cache 就要吃 1GB 显存如果要并发 16 个请求光 KV Cache 就是 16GB。这个数字放出来你就明白为什么本地跑大模型动不动显存 OOM了。权重 14GB 还能用一张 24GB 的卡装下但并发一开、上下文一长KV Cache 才是压垮显存的最后一根稻草。2.2 量化 KV Cache 的收益与代价既然 KV Cache 占大头那把它从 FP16 压到 INT8 甚至 INT4不就省了 50% 到 75% 的内存逻辑没问题但代价要算清楚。KV Cache 量化的核心难点在于注意力分数对量化误差非常敏感。Key 和 Value 不同位置的值分布差异很大尤其长文本场景下可能存在极端值简单按全局 scale 量化会出现个别 token 的注意力计算严重失真进而影响生成质量和稳定性。这也是为什么 KV Cache 量化不能像权重量化那样直接压缩就完事通常需要做 per-channel 或者 per-head 的 scale 校准。从我实际跑过的对比实验看INT8 KV Cache 在多数任务上质量损失很小BLEU/准确率下降通常在一个点以内工程上可以直接默认开启。INT4 KV Cache 就得分场景了短文本、生成任务里表现还行但长文本或者需要精确数值运算的场景比如数学推理偶尔会出现明显劣化需要结合具体业务评估。另外还要注意量化会引入额外的反量化开销decode 阶段本来带宽就吃紧压缩省下的搬运时间和反量子操作增加的计算时间之间需要实测才能判断哪边更划算。我自己的建议是在带宽严重吃紧的消费级显卡上优先上 INT8 KV Cache如果显存实在不够再考虑 INT4同时做好长文本场景的回归测试。服务端有条件的话还可以试试 NVIDIA 的 TransformerEngine 里提供的 FP8 方案它在保证一定精度的前提下压缩比更高。2.3 显存不够时offload 和重计算这类曲线救国方案显存实在不够怎么办很多框架提供了 offload 方案——把部分权重或者 KV Cache 挪到 CPU 内存计算时再临时搬回显存。这个方案的代价非常直接CPU 与 GPU 之间的 PCIe 带宽通常只有几十 GB/s比显存带宽慢了近一个数量级频繁搬运会严重拖慢速度。我试过把 7B 模型的 KV Cache 做 CPU offload短文本下延迟还能接受但上下文一长整体吞吐直接掉了四成以上。所以 offload 只适合保运行而非保性能的场景比如本地演示或者显存极有限的环境。还有一种激活重计算activation checkpointing方案在前向计算时丢掉部分中间激活值反向传播时再重新算一遍。这种方式在训练场景很常见推理场景用得少——因为推理没有反向传播激活值重算只会白白增加计算量。不过如果框架里提供了某些中间状态的按需重算机制也可以用来节省峰值显存具体收益得看实现。说到底显存优化的本质是在容量和速度之间做权衡只有把账算清楚才能找到最优解。3. 注意力机制优化Flash Attention 与 PagedAttention 的底层逻辑3.1 标准注意力的显存瓶颈为什么长上下文就 OOM聊 KV Cache 的时候偏重容量维度注意力计算的另一个痛点是中间矩阵的峰值显存。标准 attention 计算要先算 Q 和 K 的转置乘积得到注意力分数矩阵这个矩阵的形状是 [batch_size, num_heads, seq_len, seq_len]。seq_len 一长这个中间结果会爆炸性增长。举个例子batch_size 1、16 个头、seq_len 4096注意力分数矩阵的大小是 16×4096×4096×2 字节算下来大约 512MB。看起来还能忍但 seq_len 翻到 8192这个值直接变 2GB而且它只是前向计算里众多中间张量中的一个。换句话说即便权重和 KV Cache 都放得下长上下文场景下标准注意力的中间矩阵仍然可能把显存打穿。这就是为什么会有 Kernel 融合和分块计算的思路——不把完整矩阵算出来而是把一个大的计算拆成多个小块边算边用用完即弃峰值显存就能大幅下降。3.2 Flash Attention 的工作方式与真实收益Flash Attention 的核心思想是分块计算 Kernel 融合。它把 Q、K、V 都切成小块每次只加载一个块到片上 SRAM 里做计算算完立即更新输出然后丢弃中间矩阵。因为不需要把完整的注意力分数矩阵写回显存省掉了一次大矩阵的写入和读取所以不仅节省了显存还大幅减少了访存量。在长序列场景下Flash Attention 的优势可以用数据说话。我实际测过 seq_len 从 2K 涨到 8K 的对比标准 attention 的显存占用是线性上涨然后突然崩掉OOM而 Flash Attention 的显存增长曲线要平缓得多8K 长度下显存占用大约是标准实现的四分之一到五分之一。速度方面FLOPS 高的情况下 Flash Attention 没有明显优势但在 decode 这类访存密集场景时间能缩短 20%~40%。不过要提醒一点Flash Attention 有多种实现版本不同 GPU 架构适配度不同。比如 A100/H100 上用 cuDNN 或 CUTLASS 的 Flash Attention 内核优化很到位但消费级卡的某些自定义实现未必有收益甚至可能因为 kernel 兼容性问题导致速度下降。实操时建议先小规模 benchmark 再决定是否全套开启。另外 Flash Attention 默认通常只支持 FP16/BF16如果你跑的是 FP32 模型需要先做精度转换。3.3 服务框架里的显存管理PagedAttention 的启发PagedAttention 这个思路最早来自 vLLM 项目核心想法是借鉴操作系统虚拟内存的分页机制把 KV Cache 分成固定大小的块block不要求物理上连续通过块表来管理映射。你可能会问KV Cache 不就是一段连续张量吗为什么需要这种分页关键在于内存碎片和预分配浪费。如果每个请求都预分配最大长度的 KV Cache那么实际生成较短的请求就会浪费大量显存。而如果不预分配连续内存空间又容易因为请求不断增长而频繁搬移造成碎片和停顿。PagedAttention 用分页的方式按需分配只给实际用到的块分配物理内存同时还能通过块共享机制支持并行采样多个输出分支共享同一个前缀的 KV Cache内存利用率大幅提升。vLLM 官方数据里PagedAttention 在多请求场景下能将显存利用率提升到接近 90%而传统方式往往只有 60%~70%。这在高并发服务里就是实打实的吞吐差异。现在很多框架如 TensorRT-LLM、SGLang都沿用了类似思路做推理服务选型时是否支持 PagedAttention 或等效方案应该作为一条硬性标准来看。4. 解码加速技术投机解码、并行采样与批处理调度4.1 投机解码用小模型猜答案大模型做验证来换速度自回归解码是串行的——生成第 N1 个 token 必须等第 N 个 token 算完。即使单次前向计算已经优化得很快N 次串行的时间仍然压不下来。投机解码Speculative Decoding换了个思路用一个更小的草稿模型draft model先快速猜出接下来 γ 个 token再用大模型一次前向并行验证这些 token 是否正确。验证结果是整块接受的如果小模型连续猜对了大模型一次计算就相当于一次生成了多个 token串行次数直接减少。这个方案听起来很美但实际效果完全取决于一个小模型猜测的准确率。我跑过一组测试用 1B 模型草稿、13B 模型验证短句生成场景加速比大约 1.5~2 倍但涉及专业术语密集的领域文本时草稿模型频繁猜错验证不通过就得回滚重新生成加速比掉到 1.2 倍甚至更低。所以投机解码不是免费的午餐它的开销在于要同时加载两个模型显存占用会增大收益上限取决于草稿模型与目标模型的默契程度。从工程角度讲投机解码更适合对延迟敏感、生成长度较长、且业务数据与草稿模型训练分布比较接近的场景。如果业务 prompt 内容复杂多变建议先用代表性数据集测一下收益再决定是否部署。4.2 连续批处理动态调度比固定等待高效得多传统的批处理策略是静态的攒够一定数量的请求然后一起跑所有请求同一批次开始、同一批次结束。这么做的问题很明显——如果请求长短不一短请求必须等长请求跑完才能释放资源设备利用率完全看最长请求的脸色。而连续批处理Continuous Batching的核心改进是每个请求的 prefill 和 decode 阶段不再绑定在同一批次里只要某个请求生成完了立刻归还资源新请求马上补位。这个机制让 GPU 始终处于能算就不过夜的状态。用我做过的一个对比实验说明相同请求量下连续批处理的服务吞吐比静态批处理高 1.8~2.3 倍尤其在高并发、长短请求混合的场景差距更明显。这在生产环境几乎已经是默认标配了vLLM、TensorRT-LLM 等主流框架都原生支持。不过连续批处理也带来一个新问题不同请求的 KV Cache 生命周期不一致显存分配与释放频率就会很高。这正好呼应前面提到的 PagedAttention——动态批处理配合分页显存管理才是完整的调度闭环。只做连续批处理但没有好的显存管理器实际收益会大打折扣。4.3 并行采样与 beam search 的取舍要多样还是准除了加速很多业务还需要在推理时做多次采样或 beam search。这里有个误解值得澄清多次采样看起来是同一个模型跑了多次但如果用并行采样并且有前缀共享机制相同 prompt 的处理是可以复用 KV Cache 的成本远低于独立跑多次。vLLM 的并行采样就是通过块共享实现前缀复用这也是 PagedAttention 带来的隐藏收益之一。beam search 则是另一套逻辑——同一时刻保留多个候选序列不断扩展并剪枝计算量和显存占用都更高但在需要最优结果的场景比如翻译、摘要不可替代。实践中建议先确认业务是否真的需要 beam search如果只是内容创作、开放对话这类任务温度采样配合 top-p 就足够没必要为了稳定盲目上 beam search 牺牲吞吐。4.4 选型手记vLLM / TensorRT-LLM / SGLang 的取舍思路框架选型是推理优化落地时绕不开的话题。我基于实际踩坑经验做个简单对照vLLM生态成熟、上手最快PagedAttention 和连续批处理开箱即用社区文档丰富适合绝大多数生产场景。缺点是自定义算子深度调优空间不如 TensorRT-LLM极限性能可能差一些。TensorRT-LLMNVIDIA 官方出品针对自家 GPU 做了深度算子级优化量化支持也最全面适合追求极致吞吐和延迟、且 GPU 型号较单一的场景。缺点是需要把模型转成 TRT engine编译时间长调试链路偏复杂。SGLang主打 RadixAttention可以在请求间复用公共前缀的 KV Cache对多轮对话、few-shot 这类前缀高度重合的场景非常友好。适合 Prompt 复用率高的业务形态。llama.cpp单机 CPU/消费级 GPU 场景的老牌选择量化格式GGUF生态完善在低配环境里优化得相当到位适合本地部署和边缘设备。部署简单但要小心第三方绑定的加速实现不同 fork 之间性能差异可能很大。我的建议是能查清并预测业务形态之前先用 vLLM 贴着模型跑一版基线根据 profiling 结果再考虑是否往 TensorRT-LLM 迁移。不要一上来就追最强性能没有基线数据做基准优化方向的判断会非常主观。5. 实战验证从基线到优化落地的完整流程5.1 搭一套可复现的 benchmark指标与数据采集任何优化动作之前第一步都是把基线和目标定出来。我一般会记录这些指标TTFTTime To First Token从请求发起到返回第一个 token 的时间决定用户首字感知。TPOTTime Per Output Token/ITLInter-Token Latency生成每个 token 的平均耗时决定打字机速度。吞吐量tokens/s单位时间所有请求生成的 token 总数衡量服务整体产能。显存占用曲线峰值显存、KV Cache 用量、是否有 OOM。测的时候要格外注意并发和输入长度的组合。很多优化在单请求下表现平平并发一上来差异才明显也有优化在短文本上高效长文本下反而退化。所以我会建一个混合 worklaod 的压测集30% 短 prompt 50% 中等长度 20% 长 prompt每种并发下记录 P50、P95 和 P99 延迟。没有百分位延迟数据优化很容易做出平均快了但长尾更糟的伪优化。5.2 一步一验配置改动与效果对照的实操记录我拿一个实际项目举例。目标是部署 13B 模型在高并发场景下的服务具体配置过程和数据如下第一版直接上 vLLM 默认配置模型 FP16KV Cache 默认 FP16。压测结果是并发 8、输入 512 token 时P99 TTFT 1.4 秒P99 TPOT 约 90ms显存峰值 28GB已经接近 32GB 卡的极限。问题在于并发上到 12 之后显存直接不够OOM 频繁出现。第二步开启 KV Cache INT8 量化代码只改了一行配置参数。显存峰值降到约 23GBTPOT 反而从 90ms 降到 78ms。为什么量化后反而更快因为 decode 阶段是访存瓶颈KV Cache 数据量减半意味着每次前向计算要搬运的字节数减半访存时间直接缩短。这是理论上预期的效果测出来的数字完全印证了。第三步把调度策略从默认的 prefill 优先改成连续批处理 动态 chunk 大小并开启前缀缓存。这一步对高并发帮助最大并发 16 时P99 TPOT 稳定在 95ms 附近吞吐从原来的 180 tokens/s 提升到了 420 tokens/s。注意这步之后显存也没涨太多因为分页管理和前缀缓存把重复计算省掉了。第四步尝试投机解码挂了个 1B 草稿模型。短文本场景加速比约 1.6 倍但显存峰值多了约 2GB而且长 prompt 场景收益衰减到 1.2 倍。考虑到业务长文本比例不低最后没有在生产开启。这组实验最大的价值不是哪一步参数最优而是展示了一种排查路径显存紧张先看 KV Cache速度瓶颈在 decode 阶段优先考虑访存优化再往上就是框架层面的调度策略调优。5.3 别被平均数骗了延迟分布与显存碎片排查最后说一个很容易被忽视的坑——性能测试不能只看平均值必须看百分位分布和显存分配曲线。特别是显存碎片问题在高并发长稳运行后特别常见服务刚启动时一切正常跑了几小时后开始偶发 OOM但看监控峰值显存又没超限。这种往往是 KV Cache 块反复分配释放后内存碎片化PagedAttention 这类方案虽然缓解了碎片问题但不同框架的实现程度不一样。我会用两个工具辅助排查一是框架自带的 metrics 接口看 KV Cache 使用率和块命中率vLLM 有内置的 /metrics 输出二是 PyTorch 的显存快照memory snapshot抓全量分配图看是否存在大量小碎片块。如果确认是碎片问题可以调大块大小比如从 16 token 调到 32 token或者定期重启节点来缓解——但长期方案还是得升级到支持碎片整理的框架版本。6. 附推理优化常见坑点速查下面是我在多次部署和调优中积累的常见问题与解决思路整理成表格方便对照排查现象根因方向排查与处理建议并发一高就 OOMKV Cache 占满显存开启 KV Cache INT8 量化、限制单请求最大生成长度、控制最大并发数生成速度很慢但算力没跑满decode 阶段访存瓶颈降低权重精度INT8/FP8、开启 Flash Attention、检查是否启用了连续批处理长 prompt 首字延迟高prefill 阶段计算量大开启 chunked prefill、检查注意力实现是否为 FlashAttention短请求被长请求拖累调度策略不当启用连续批处理、按请求长度分队列或设置超时长稳运行后偶发 OOM显存碎片化调整 KV Cache 块大小、定期重启节点、检查框架版本更新说明投机解码开了反而更慢草稿模型命中率过低换更大的草稿模型、针对业务数据微调草稿模型、或停用投机解码模型输出质量波动量化精度损失对比 INT8/INT4 在代表性任务集上的评估指标、适当保留部分层为高精度坑点里最容易忽略的是不同优化之间的相互干扰。比如你开了投机解码又开了 INT4 量化草稿模型和目标模型之间的验证一致性会受量化误差影响命中率可能进一步下降叠加后的性能未必是两者收益之和。每次只改一个变量、做对照实验是推理优化里性价比最高的方法。最后再分享一个我自己的操作习惯每次调参后先跑一个快速 smoke test 确认服务可用再跑全量压测压测数据落盘留档对比避免下次凭感觉说好像快了。优化这个东西没有数据支撑的判断都容易变成玄学。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价