ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型推理优化实战:KV Cache、Continuous Batching与投机解码

大模型推理优化实战:KV Cache、Continuous Batching与投机解码 1. 推理优化到底在优化什么从一次线上延迟抖动说起很多人第一次接触推理优化是因为线上服务突然扛不住了。我印象很深的一次某个对话类服务上线两周白天平均响应 800ms 还算能看结果一到晚上高峰期P99 直接飙到 6 秒以上用户开始投诉“打字机卡成 PPT”。排查下来不是模型本身慢而是请求排队、显存碎片、KV Cache 反复分配释放把 GPU 拖垮了。这件事让我彻底明白推理优化不是把模型变小那么简单它是一整套围绕吞吐、延迟、显存、成本四个维度的系统工程。先把概念说清楚。所谓推理优化指的是在模型权重已经训练完成、不再更新的前提下通过调度策略、显存管理、计算图优化、精度压缩、解码算法改进等手段让单位时间内服务更多请求吞吐或者让单个请求更快返回延迟同时把显存和算力成本压下来。它和训练优化的最大区别在于训练追求的是收敛速度和最终精度推理追求的是在可接受的精度损失内把每一分算力榨干。这里有个反直觉的点吞吐和延迟往往是对立的。你把 batch 开大吞吐上去了但单个请求要等更久才能凑齐一个 batch延迟就上去了。所以真正成熟的推理优化从来不是无脑堆参数而是先明确业务目标——是离线批处理吞吐优先还是在线实时对话延迟优先还是两者兼顾比如用 Continuous Batching 动态调度。目标不同优化路径完全不一样。这篇文章适合三类人看一是刚接手推理服务、被延迟和成本折磨的工程师二是想系统理解 KV Cache、Continuous Batching、Speculative Decoding 这些热词背后原理的技术负责人三是准备做推理框架选型、需要一份可落地清单的架构师。我会把每个优化手段的“为什么”讲透再给出可复现的配置和踩坑经验尽量让你看完就能上手调。提示推理优化没有银弹。任何声称“一招提速 10 倍”的方案背后一定有特定的场景假设。先量清楚自己的瓶颈在哪再动手。2. 吞吐与延迟的博弈先搞清楚你的瓶颈在哪2.1 用两个指标定位问题TTFT 和 TPOT在动手优化之前必须先把指标拆细。业界常用两个核心指标来刻画推理体验TTFTTime To First Token首 token 延迟从请求发出到第一个 token 返回的时间。它主要受 prefill 阶段处理输入 prompt影响输入越长TTFT 越大。TPOTTime Per Output Token每 token 输出时间生成阶段每个 token 的平均耗时。它主要受 decode 阶段影响和 batch 大小、显存带宽强相关。这两个指标一拆问题就清楚了。如果 TTFT 高但 TPOT 正常说明瓶颈在 prefill可能是输入太长、batch 里混了超长 prompt或者调度器没有做 prefill/decode 分离。如果 TPOT 高说明 decode 阶段被拖慢通常是 batch 太大导致显存带宽打满或者 KV Cache 管理低效。我一般会先跑一个压测把 TTFT 和 TPOT 分别打出来再对照下面这张表判断方向现象可能瓶颈优先尝试的优化TTFT 高TPOT 正常prefill 计算密集输入截断、prefill/decode 分离、chunked prefillTTFT 正常TPOT 高decode 显存带宽受限减小 batch、KV Cache 量化、投机解码两者都高且随并发线性恶化调度与排队Continuous Batching、请求优先级队列低并发也慢单请求计算或框架开销算子融合、CUDA Graph、精度压缩2.2 为什么“加大 batch”不是万能药很多人第一反应是“并发上不去就加 batch”。但 batch 加大的收益是有上限的。decode 阶段每生成一个 token都要把整个模型的权重从显存读一遍计算量其实很小瓶颈在显存带宽。batch 从 1 加到 8吞吐可能翻好几倍但从 32 加到 64吞吐提升可能只有 10%而延迟却翻倍。这就是所谓的显存带宽墙。更麻烦的是静态 batch 要求所有请求同时开始、同时结束。实际业务里请求长度参差不齐短请求被长请求拖住GPU 利用率被拉低。这就是 Continuous Batching 要解决的核心问题后面会专门讲。2.3 一个可复现的压测方法我习惯用固定长度的合成请求做基线压测排除真实流量波动的干扰。思路是构造不同输入长度比如 128、512、2048 token和不同输出长度64、256的组合逐步提高并发记录 TTFT、TPOT、吞吐和显存占用。# 伪代码合成请求压测框架 import time, statistics def benchmark(client, input_lens, output_lens, concurrency_levels): results [] for conc in concurrency_levels: for il in input_lens: for ol in output_lens: latencies, ttfts [], [] # 并发发起 conc 个请求 for _ in range(conc): t0 time.time() first client.generate(prompt_lenil, max_tokensol, streamTrue) ttfts.append(first_time - t0) latencies.append(total_time - t0) results.append({ concurrency: conc, input_len: il, output_len: ol, ttft_p50: statistics.median(ttfts), latency_p99: sorted(latencies)[int(len(latencies)*0.99)-1], }) return results跑完这组数据你会得到一张“并发-延迟”曲线。曲线开始变陡的拐点就是你当前配置的合理并发上限。超过这个点再压吞吐涨不动延迟却爆炸用户体验直接崩。注意压测一定要用和线上一致的输入输出长度分布。用 128 token 的短请求测出来的漂亮数字放到 4K 长文本场景里会完全失真。3. KV Cache 优化显存里最值得动刀的地方3.1 KV Cache 为什么是显存杀手自回归生成时每生成一个新 token都要用到之前所有 token 的 Key 和 Value 做注意力计算。如果每次都重算计算量会随序列长度平方增长。KV Cache 的思路就是把历史 token 的 K、V 缓存下来新 token 只算自己的然后和缓存拼接。这是推理能跑起来的基础。但代价是显存。KV Cache 的大小可以用这个公式估算KV Cache 显存 2 × batch_size × seq_len × num_layers × num_kv_heads × head_dim × dtype_bytes以一个 7B 级别、32 层、32 个 KV 头、head_dim 128 的模型为例FP16 精度下单条 2048 长度的序列KV Cache 大约是2 × 1 × 2048 × 32 × 32 × 128 × 2 bytes ≈ 1.07 GB一条序列就 1GBbatch 开到 16 就是 17GB还没算模型权重本身。这就是为什么长上下文场景下显存总是不够用。3.2 PagedAttention把 KV Cache 当操作系统内存管vLLM 提出的 PagedAttention 是这几年 KV Cache 管理最重要的思路。它的核心类比是操作系统的虚拟内存分页把 KV Cache 切成固定大小的 block比如 16 个 token 一块不再要求一条序列占用连续显存而是用一张 block table 记录逻辑块到物理块的映射。这样做的好处很直接消除显存碎片不用为每条序列预留最大长度的连续空间按需分配 block。支持共享多个请求如果有相同前缀比如相同的 system prompt可以共享同一批物理 block省显存。便于回收序列结束后 block 立即归还提升显存利用率。实测下来PagedAttention 能把显存利用率从传统方案的 60% 左右提到 90% 以上同样的卡能多跑 2 到 4 倍的并发。这个提升在长上下文场景尤其明显。3.3 KV Cache 量化与驱逐精度和显存的取舍如果显存还是不够可以考虑两条路量化把 KV Cache 从 FP16 压到 INT8 甚至 FP8。INT8 量化通常能把 KV Cache 显存砍一半精度损失在多数任务上可以接受。但要注意KV Cache 量化对长文本任务的影响比短任务大因为误差会随序列累积。我的经验是对话类短任务大胆用 INT8长文档理解类任务先做小规模评测再决定。驱逐/滑动窗口只保留最近 N 个 token 的 KV或者按注意力分数淘汰不重要的 token。这类方法能显著降显存但会损失长距离依赖适合对上下文长度不敏感的场景。用之前一定要确认业务能不能接受“忘记前面内容”。方案显存节省精度影响适用场景PagedAttention30%-50%无通用首选KV Cache INT8约 50%小到中短对话、高并发KV Cache FP8约 50%小支持 FP8 的硬件滑动窗口可控中到大长文本摘要、流式处理提示KV Cache 量化不是免费的。上线前务必用真实业务数据做 A/B别只看困惑度指标。4. Continuous Batching让 GPU 一刻都不闲着4.1 静态 batch 的浪费到底有多大传统静态 batch 的工作方式是攒够一批请求一起送进 GPU等这批全部生成完再处理下一批。问题在于同一批里有的请求生成 20 个 token 就结束了有的要生成 500 个。短请求结束后它占的显存和计算槽位就空着但 GPU 必须等最长的那个请求跑完才能开始下一批。这段时间 GPU 利用率可能只有 30%。我做过一个对比在请求长度方差很大的场景下静态 batch 的 GPU 利用率平均只有 40% 左右而换成 Continuous Batching 后能稳定在 80% 以上。这个差距直接反映在成本和吞吐上。4.2 迭代级调度的核心机制Continuous Batching也叫 iteration-level scheduling的思路是调度粒度从“一批请求”细化到“一个 decode step”。每生成一个 token调度器就检查一次有没有请求完成了完成了就立刻释放它的槽位从等待队列里拉新请求进来。这样 GPU 的每个 decode step 都尽可能塞满不会因为个别长请求而空转。具体流程大致是新请求进入等待队列调度器按策略FCFS、优先级、SJF 等挑选。每个 decode step 前把已完成的序列移出 batch把新序列加入。对 batch 内所有序列并行执行一次前向各自生成下一个 token。重复直到所有序列结束。这里有个关键细节prefill 和 decode 的计算特性完全不同。prefill 是计算密集型decode 是显存带宽密集型。如果把它们混在同一个 batch 里会互相拖累。所以现在主流框架都支持chunked prefill把长 prompt 的 prefill 切成小块和 decode 交错执行避免长输入阻塞整个 batch。4.3 调度策略怎么选FCFS、优先级还是 SJF调度策略直接影响延迟分布。常见的有三种FCFS先来先服务实现简单公平但长请求会拖累后面的短请求P99 延迟容易恶化。优先级调度给不同业务线或用户分级重要请求优先。适合多租户场景但要防止低优先级请求饿死。SJF最短作业优先优先处理预计输出短的请求能显著降低平均延迟。但需要预测输出长度预测不准反而适得其反。我的经验是在线对话用优先级 适度 SJF离线批处理用 FCFS 就够。另外一定要设置请求超时和最大等待时间防止某个请求无限期排队。# 调度配置示例以常见推理框架的参数风格示意 scheduler: policy: priority # 优先级调度 max_batch_size: 32 # 单批最大序列数 max_waiting_time_ms: 200 # 请求最长排队时间 chunked_prefill: true # 开启分块 prefill prefill_chunk_size: 512 # 每块 prefill 的 token 数 enable_preemption: true # 显存不足时抢占低优先级请求注意开启抢占preemption后被抢占的请求需要重新计算或从 KV Cache 恢复会带来额外开销。显存充足时建议关闭避免不必要的重算。5. Speculative Decoding用“草稿”换速度5.1 投机解码的基本原理decode 阶段慢本质是因为自回归只能一个 token 一个 token 地生成每一步都要读一遍模型权重GPU 的并行能力用不上。Speculative Decoding 的思路很巧妙用一个小的草稿模型draft model先快速生成 K 个候选 token再用大模型一次性并行验证这 K 个 token 是否接受。因为验证是并行的一次前向就能确认多个 token相当于把串行生成变成了“批量验证”。关键点在于投机解码不改变输出分布。只要接受/拒绝的判定规则设计正确基于概率比最终结果和直接用大模型生成在数学上是等价的。这是它比“直接用小模型”高明的地方——小模型快但质量差投机解码既快又保质量。5.2 草稿模型怎么选加速比从哪来加速比取决于两个因素草稿模型的接受率和草稿模型相对大模型的速度。理想情况下草稿模型越接近大模型的输出分布接受率越高加速越明显。常见做法有同系列小模型比如用同架构的小尺寸模型做草稿分布接近接受率高。EAGLE / Medusa 类方法不单独训练草稿模型而是在大模型上加预测头直接预测多个后续 token。自投机用模型自身的浅层或量化版本做草稿。实测数据上接受率在 70%-80% 时端到端加速通常能到 1.8 到 2.5 倍。但如果接受率低于 50%加速可能被草稿模型的开销抵消甚至变慢。草稿方案接受率额外显存实现复杂度同系列小模型高中低EAGLE 类预测头高低中量化自投机中低中随机草稿低无低5.3 什么场景不适合投机解码投机解码不是万能的。以下几种情况要谨慎batch 已经很大大 batch 下 decode 本身已经接近计算饱和投机带来的并行验证收益变小草稿模型反而抢算力。输出很短比如只生成 10 个 token投机的前期开销还没摊薄就结束了。显存紧张草稿模型要额外占显存如果本来就在 OOM 边缘加草稿模型可能直接崩。我的建议是先确认 decode 是瓶颈且 batch 不大、输出较长时再上投机解码。上线前一定要对比开启前后的 TTFT 和 TPOT别只看理论加速比。6. 把这些手段串起来一套可落地的调优顺序6.1 从显存到调度再到解码的优先级优化不是把所有手段堆上去而是有先后顺序。我一般按这个顺序推进先上 PagedAttention这是基础设施几乎无精度损失收益大先做。再开 Continuous Batching chunked prefill解决 GPU 空转和长输入阻塞吞吐提升最明显。然后调调度策略根据业务延迟要求选优先级或 SJF设置合理的超时和 batch 上限。显存还不够就量化 KV CacheINT8 优先做好精度评测。最后考虑投机解码在 decode 瓶颈明确、batch 不大的场景下加。这个顺序的逻辑是先做无精度损失、收益确定的事再做有取舍的优化。很多团队一上来就搞量化结果精度掉了、收益还不明显反而走了弯路。6.2 一个真实的调优案例复盘回到开头那个 P99 飙到 6 秒的服务。我们的处理过程是这样的第一步压测定位。发现 TTFT 正常TPOT 随并发快速上升判断是 decode 阶段显存带宽瓶颈 调度排队。第二步换用支持 PagedAttention 和 Continuous Batching 的推理框架。显存利用率从 55% 提到 88%同样并发下 TPOT 下降约 40%。第三步开启 chunked prefill把长 system prompt 的 prefill 切块TTFT 的 P99 从 2.3 秒降到 900ms。第四步调调度策略为优先级 短请求优先P99 整体从 6 秒降到 1.8 秒。第五步对 KV Cache 做 INT8 量化显存再省 45%并发上限翻倍精度用业务集评测损失不到 1%。整个过程没有用投机解码因为当时 batch 已经不小收益有限。最终单卡吞吐提升约 3.2 倍成本直接降了一半多。6.3 上线后必须盯住的几个指标优化上线不是终点。我建议长期监控这几个指标TTFT / TPOT 的 P50、P95、P99看分布不只看均值。GPU 利用率和显存占用利用率长期低于 60% 说明调度还有优化空间。请求排队时长排队时间超过生成时间的服务体验一定差。KV Cache 命中率和碎片率碎片率高说明 block 管理有问题。单位 token 成本这是最终衡量优化效果的商业指标。提示监控要区分 prefill 和 decode 两个阶段。混在一起看永远定位不到真正的瓶颈。7. 几个容易踩的坑和我的实操心得第一个坑是盲目追求大 batch。我见过团队把 batch 开到 128吞吐数字很好看结果 P99 延迟 10 秒以上用户全跑了。batch 大小要跟着延迟 SLA 走不是越大越好。第二个坑是忽略输入长度分布。真实流量里往往有少量超长输入它们会严重拖累整个 batch。解决办法是给超长输入单独排队或者用 chunked prefill 隔离影响。第三个坑是量化后不做业务评测。困惑度perplexity涨一点点不代表业务指标没问题。分类、抽取、代码生成这些任务对量化的敏感度完全不同必须用真实数据测。第四个坑是调度策略一成不变。业务高峰期和低峰期的最优策略可能不一样。我一般会做动态调整高峰期偏向短请求优先保延迟低峰期用 FCFS 保公平。最后一个心得优化前先建立基线优化后做 A/B。没有基线的优化都是玄学。每次只改一个变量记录清楚才能知道到底是哪个手段起了作用。推理优化是个持续迭代的活不是一次配置就一劳永逸。
返回列表