1. 把 Prefill 和 Decode 当成两个物种来看是这套笔记第一个想讲明白的事聊到高级调优核心其实就两个字边界。Prefill 阶段和 Decode 阶段在计算形态、访存特性、延迟目标上完全是两个物种却挤在同一套显存和同一个调度器里这就是性能问题最集中的来源。先回忆一下自回归生成的过程。用户输入一段 prompt模型先并行处理整段输入一次性把所有 token 的中间状态计算出来这个阶段叫 Prefill预填充。得到第一个输出 token 后后续的每一个 token 都依赖前序全部 token 的信息只能一个一个生成这个阶段叫 Decode解码。哪怕总输出长度只有 500 tokenDecode 也要循环 500 次其中大部分时间 GPU 算力压根用不满。这里有两个对性能工程极其重要的延迟指标TTFTTime To First Token首 Token 耗时和 TPOTTime Per Output Token每个输出 Token 的耗时。前者由 Prefill 主导决定用户第一反应快不快后者由 Decode 主导决定连续说话时流不流畅。不少团队在做压测时只盯总吞吐tokens/s结果优化完吞吐很好看线上用户却普遍抱怨转圈时间久或者一个字一个字蹦出来就是因为没有把这两个阶段拆开来看。更关键的是计算形态的差异。Prefill 阶段输入是一个长序列模型内层是标准的 GEMM矩阵维度大、计算密度高GPU 的算力可以发挥到相当高的水平Decode 阶段输入只有一个 token同样一层的计算退化成了 GEMV矩阵乘向量访存量远大于计算量GPU 算力被严重浪费性能瓶颈几乎是纯访存瓶颈。换句话说Pre fill 的敌人是算力不够和 batch 不够大Decode 的敌人是显存带宽不够和 KV Cache 太大。两者放一起优化就是本篇文章的整体框架。下面分别深挖。2. Prefill 侧的高级优化分块预填充、独立部署与注意力实现迭代2.1 为什么不建议让一个大 prompt 独自占满整张卡很多人在模拟长上下文场景时发现一个 8k token 的 prompt 进来Prefill 部分就要耗掉几百毫秒甚至几秒期间其他请求的 Decode 全部被卡住。长尾延迟由此产生。解决思路之一就是 Chunked Prefill分块预填充把一个长 prompt 拆成若干 chunk以 chunk 为单位加入调度队列这时候同一条 prompt 的 Prefill 可以被其他请求的 Decode 插空执行。这个方案的好处是消除了长 Prompt 独占算力的局观坏处是 Prefill 被拆碎后计算效率略有下降因为每块之间需要额外保存中间状态KV Cache 也要提前为整个序列预留显存。它适合高并发、请求长度波动大的在线服务对应的批处理大小参数也会因此调大不少。2.2 Prefill 与 Decode 完全分家PD 分离架构的取舍当优化走到一定深度很多团队会把 Prefill 和 Decode 部署到两批独立实例上负责 Prefill 的实例专门吃大 batch 的 GEMM负责 Decode 的实例专心做 GEMV。中间通过一个额外的缓存层KV Cache 传输把 prefill 结果交给 decode 实例。这种方案业内俗称 PD 分离。它的核心收益在于两种阶段可以独立扩缩容。线上如果首 Token 延迟敏感就多配 Prefill 实例如果长对话并发高就多配 Decode 实例。代价则是多一跳网络传输、KV Cache 序列化延迟以及系统复杂度上升。实操中需要特别注意传输吞吐与 KV 压缩的配合否则 Prefill 侧省下的时间会全部浪费在传输上。2.3 注意力实现的代际差异从 FlashAttention 到 FP8Prefill 阶段内部的优化很大程度落到 Attention 实现上。早期推理框架的 Attention 要先把完整的 Q、K、V 矩阵存在显存里再计算 softmax显存和带宽消耗极大。后来出现的 FlashAttention 类实现通过分块计算和在线 softmax把本来 O(n²) 的显存占用降为 O(n)同时大幅减少 HBM 访存具体原理简要概括就是不必等完整矩阵先后台分块算部分 softmax再用重缩放修正全局结果。到了 FP8 时代Prefill 阶段的 GEMM 可以用 8 位浮点累加获得更高吞吐但 Attention 的 softmax 部分保持较高精度整个过程中 KV 缓存也还可以保留 FP8 或 BF16。这里我必须提醒FP8 的精度下降因模型和任务而异不能一刀切。我在某个代码生成模型上试过全层 FP8质量问题在短回复上完全看不出来一跑长尾知识问答就明显掉准确率后来只对 Attention 部分做 FP8其他层保持 BF16 才平衡好速度与质量。3. KV Cache 的显存几何公式、量化与页式管理3.1 一条公式算清 KV Cache 占用进入调优之前先把 KV Cache 显存占用这条基础公式记牢KV Cache 大小 ≈ 2K 和 V × 层数 × 注意力头数 × 每头维度 × 序列长度 × 每个元素字节数假设某 7B 级别模型一共有 28 层、40 个注意力头、每头 128 维每个元素占 2 字节BF16一条 4096 token 的序列大约需要2 × 28 × 40 × 128 × 4096 × 2 字节 ≈ 2.35 GB。这还只是单条序列。实际服务中要同时处理几十条序列KV Cache 的增速非常恐怖。这就是为什么很多人在单卡上跑 7B 模型只要并发请求一多立刻出现 OOM因为参数只占 14GB 上下可 KV Cache 每秒钟都在滚雪球一样变大。因此性能工程的一个核心思路就是提前给 KV Cache 划定上限而不是让它无节制增长。3.2 GQA 为什么能凭空砍掉一截缓存近几年不少新模型把 MHA多头注意力改成了 GQA分组查询注意力。不要小看这个结构变动它直接让 KV 头数降到 Query 头数的几分之一KV Cache 体积随之一同缩小。举例来说假设同一层原本有 32 个 Q 头和 32 个 KV 头改成 GQA 后 KV 头降到 8 个KV Cache 直接变成原来的四分之一。在同样的显存预算下并发批处理大小能提上去单位时间吞吐自然翻倍。也就是说在做 KV Cache 调优时模型结构的选型比后端参数调整来得更根本。3.3 KV Cache 量化8bit、4bit 与精度损失的拉锯显存不够量化来凑KV Cache 同样可以量化。业界当前常见的三种档位BF162 字节/元素、FP8 或 INT81 字节/元素、INT4半字节/元素。我的实践经验是FP8/INT8 对大部分任务几乎无损INT4 则明显有质量回落尤其长上下文场景模型对 KV 精度的敏感度会放大。如果显存极度紧张可以只量化 K 或只量化 V或者量化后又把一部分重要 token 的 KV 保持高精度。这类混合策略需要配合具体模型做验证。另外要注意量化 KV Cache 虽然省显存但增加了量化与反量化的计算开销如果实现不好TPOT 反而变慢。选型时建议用/usr/bin/time这类工具实测阶段耗时而不是只看显存数字。3.4 PagedAttention 与显存仓库管理显存管理的第二步是索引式存储。传统方式是用一块连续显存装完一整条序列的 KV遇到变长请求时内部碎片极多。PagedAttention 类方案把一个序列的 KV 切成多个固定大小的 page通过页表把物理上不连续的显存块拼成逻辑上连续的序列。这块内容对于性能调优的意义在于它决定了你能在同样显存容量里装多少并发请求。页表开得快、分配策略合理并发吞吐就能上去。实操时把显存使用率、页碎片率和 KV Cache 命中率同时监控起来注意这三个数字往往互相牵制找到一个合适的折中区间。4. 一次典型调优全程设备、显存核算与实验矩阵4.1 设定场景单机单卡到多机部署的起步我以一个非常常见的起步配置为例单机 8 卡单卡显存 80GB要服务一个 7B 规模对话模型。量化参数为 BF16权重约占 14GB。如果为了更高吞吐把 batch 并发调到 64 条序列每条平均上下文长度 2k那么 KV Cache 需求的粗略估算如下单条 2k 序列的 KV Cache ≈ 2 × 28 层 × 8 个 KV 头假设 GQA× 128 维 × 2048 × 2 字节 ≈ 1.17 GB。64 条并发需要大约 75GB已经逼近单卡 80GB 上限再加上模型权重和激活值必然放不下。这时候就必须引入量化 KV、扩大 batch 的显存上限或者说调整并发策略。4.2 显存预算表给参数、激活和 KV 各划一条线把显存管理抽象成一张预算表是生产环境落地的关键方法。一般建议将显存按如下比例划分模型权重约 30%——这部分几乎固定激活值与临时张量约 20%——随上下文长度变化峰值出现在 Prefill 阶段KV Cache 约 45%——这部分最灵活可量化、可淘汰、可压缩剩余 5% 作为安全余量。表中每个数字都要根据实测调整而不是拍脑袋。你可以在框架日志里看到各个模块的实际峰值占用对照预算表分析哪一块超了、哪一块没用到调优目标也随之清晰。4.3 实验矩阵一次只动一个变量我在调参时习惯做一个实验矩阵确保每一轮只改一个变量。比如第一轮测不同 KV Cache 上限35GB、40GB、45GB分别对吞吐和 TPOT 的影响第二轮测量化档位BF16/INT8/INT4对质量与速度的双重影响第三轮测并行配置的影响是否开 PD 分离、Prefill 实例几台、Decode 实例几台。表格记录每个配置下的 TTFT、TPOT、吞吐、OOM 次数、质量测试分然后收敛到综合最优。一个实际跑出来的例子非真实项目名大概是40GB KV 预算配 INT8 量化TPOT 从 38ms 降到 27ms吞吐提升 32%改成 INT4 后吞吐提升 48%但长上下文质量分下降 3 个点最终取舍下来还是守住了 INT8。4.4 从实验室到线上加压、回落、再压测调参跑通后不要直接上线建议按这样的节奏先在离线环境用两到三倍的线上峰值请求压测观察显存溢出次数和延迟分位数如果能撑住再把部分生产流量灰度导入灰度期间对比 TTFT、TPOT 是否劣化以及有无意外 OOM 导致的请求重算。整个过程重点关注幸存率新增 KV Cache 上限后系统在极限并发下请求成功比例有没有下降。这套流程看着保守但能筛掉九成调参后立刻上线爆显存的意外。5. 系统级联动前缀缓存、推测解码与调度器策略5.1 前缀缓存让重复请求根本不走 Prefill线上大量请求通常共享同一段系统提示词或长文档不同用户只是在后面追加问题。PKM前缀缓存 / 语义缓存就是抓住这一特征把公共前缀的 KV 结果直接复用这样新请求的 Prefill 部分彻底消除TTFT 从几百毫秒降到几十毫秒。效果取决于命中率。如果系统提示词每次都不一样缓存命中率自然低。实际收效最好的场景是固定系统前置 个性化后续问题比如客服机器人、代码补全中的文件前缀。调优时重点关注命中率统计不要盲信框架默认开启的缓存。另外要注意缓存的容量上限和淘汰策略KV 缓存长期不淘汰的话显存会被垃圾数据慢慢撑爆。5.2 推测解码如何改变 Prefill/Decode 的压力结构推测解码Speculative Decoding用一个小模型先草拟多个 token再由大模型一次验证以此把 Decode 阶段的串行步数压缩为更少次数的前向计算。它表面上只影响 Decode实际上对 KV Cache 和 Prefill 也有很大扰动草稿模型生成的 KV 是否放入主模型缓存、验错后如何回滚都会影响显存峰值和调度复杂度。我的建议是推测解码要作为整体系统的一部分评估而不是只看单步延迟收益。实测中它对短回复场景收益最大长文档生成场景因为草稿模型准确率下滑收益会被验错概率吃掉。5.3 调度策略长短请求如何在队列里和平共处有些请求 prompt 很短、输出很长有些则 prompt 很长、输出很短。调度器如果按队列顺序走短请求很可能会被前面的长输出卡死于是需要更细的优先级策略。进阶做法是区分Pre fill 阶段是否必须立即做短 prompt 的 Prefill 可以在解码间隙穿插完成长 prompt 的 Prefill 尽量放在负载低谷期。这里有一个我在实测中遇到的反直觉结论把过多资源预留给 Prefill 中的大请求不一定更优相反大请求的 Prefill 切片后穿插进 decode 间隙系统整体尾延迟反而更稳定。这在很大程度上取决于业务场景不能一概而论。5.4 过载保护与回退预案再稳的调优也会遇到突发流量。集群上的各实例必须有一套过载保护机制比如当 KV Cache 使用率达到一定阈值时拒绝新请求或把新请求引导到其他实例当显存接近上限时提前对旧请求做抢占或把 KV 缓存落盘。这里要重点测试抢占后重新计算的开销是否可接受因为被抢占请求如果是长上下文重算代价会非常大。研究 KV Cache 不够用、请求过多时部分的替代方案是 CPU offload把一部分 KV 数据搬到主存用时再回传。这个方法能救急但回传速度会成为新瓶颈。我通常只把它定位成最后保险不作为常规手段。6. 生产环境里摸爬滚打后最想告诉你的事6.1 显存碎片和活跃序列的动态平衡PagedAttention 虽然把物理碎片管理得很好但也不是没有坑。我发现高并发下的页表分配和释放过于频繁会导致分配器本身出现软碎片——空闲 page 总量充足却因为分布不连续而无法满足大请求。解决办法就是控制单次分配 page 的大小和数量并定期做显存整理。此处不要迷信某个框架默认值要实际跑一段时间看看你的服务是不是属于快速分配和释放的类型。6.2 多卡张量并行与 KV Cache 的重复放大很多人想当然认为张量并行后 KV Cache 占用减半但实际上 Attention 头被切分到多张卡每张卡分别缓存自己负责的那部分头整体的 KV Cache 总量和单卡配置并不完全同步反而可能因为重复缓存而出现更多占用。实践中要注意部署时给每张卡留出足够的 KV Cache 余量并检查多卡之间的负载是否均衡而不是只看整机显存总和。6.3 指标采集要以阶段为单位而不是只看整体线上监控建议把 TTFT 和 TPOT 分开记录并把 Prefill 阶段与 Decode 阶段的耗时分别统计。只盯平均生成速度容易掩盖 Prefill 长尾。这一点在长上下文服务里尤其明显原本 Prefill 慢的请求如果恰好排在了一批解码请求后面整体耗时会雪上加霜。加上阶段切分的指标后你才能一眼看出瓶颈到底在预填充还是解码。6.4 关于最大 KV 预算的常识与反常识不是每个部署都需要把 KV Cache 预算开到最大。我踩过的坑是为了压榨硬件把 KV 预算调到接近显存上限结果激活值一有波动就 OOM触发流程里最重的重算。因为 KV 缓存可以量化、可以淘汰但激活值是立即要用、不能省的。所以宁可给 KV 预留 80%也别让它突破安全线系统稳定性优先于峰值吞吐。这套学习笔记写到这里本质上体现了性能工程四个字的分量它不是单一参数的事而是把模型结构、推理框架、显存管理、调度策略串起来一起做的系统工程。练好这套拆解方法比照着教程抄一个调优参数有用十倍。
阅读完成 · 觉得有帮助?