大模型推理调优实战:从指标拆解到关键参数配置
做推理服务的人一定经历过这种局面模型在测试环境怎么压都稳一上生产并发稍微上来就超时或者本地单条请求响应快得飞起线上看着GPU利用率不到一半吞吐却死活上不去。更恼火的是网上到处是零散的经验贴看完还是一头雾水。这篇文章把大模型推理调优的思路串成一条主线先弄懂衡量服务的指标和模型执行时的性能逻辑再按故障分类排查最后把关键调优参数一个个讲透。适合正在负责推理服务上线、优化线上延迟和吞吐的工程师也适合想搞明白推理瓶颈到底在哪的算法同学。1. 先搞清楚在优化什么推理核心指标拆解1.1 延迟类指标TTFT和TPOT必须分开看大模型推理的延迟和普通接口不一样它不是一锤子买卖。用户发一个请求服务端要先把整个prompt喂给模型算一遍然后一个token一个token地往外吐。于是延迟被拆成两个关键数字。TTFTTime To First Token首token延迟。从请求发送到收到第一个字的间隔直观对应“用户感觉服务有没有响应”。TTFT超过两秒体验就会明显变差。TTFT主要由prefill阶段决定也就是模型把整段输入并行计算一遍的过程。TPOTTime Per Output Token每个输出token的耗时对应输出阶段的流畅度。如果TPOT超过100ms用户会觉得打字速度慢得让人难受。实测想达到“顺畅阅读”的感觉TPOT一般要压到50ms以内也就是每秒至少能吐20个token。除了这两个还有一个端到端延迟E2E Latency指整条生成链路从请求进来到完整回答落地的总耗时。业务侧超时判断基本看它但定位问题时要把E2E拆回TTFT加上TPOT乘以生成长度。这里有个容易踩的坑用E2E延迟对比不同配置结果会被生成长度干扰。两个配置一个输出200字、一个输出800字E2E自然差一大截但这不代表服务性能差。正确做法是分开记录TTFT和TPOT并用固定长度输出做基准。1.2 吞吐类指标QPS与token/s不是一回事吞吐指标经常被混着报但意义完全不同。QPS是每秒完成的请求数适合衡量短输出、高并发的交互场景。token/s是每秒生成的token总数适合衡量长文本、流式输出的生产场景。还有一个每车吞吐比如每秒每GPU能生成的token数用来横向对比机器规模。服务端吞吐有个关键特征总token/s约等于单请求TPOT的倒数乘以并发批大小。在prefill能力足够的前提下并发数翻倍通常会让总吞吐明显上升这一点后面调优会反复用到。1.3 为什么延迟与吞吐必须放在同一张表里权衡单独看任何一边都是耍流氓。把batch size拉得很大吞吐可以冲得很高但每个请求排队和计算的时间全被拉长TTFT和TPOT一起恶化反过来把并发压到1单请求延迟漂亮但GPU算力大部分时间在空转。实际项目里一个合理的做法是给三个指标设阈值TTFT不超过2秒、TPOT不超过60ms、总吞吐达到预期业务量然后再去调参。所有调优动作本质是在这三个约束里找平衡点而不是追求单一数字。2. 推理执行模型与显存分配逻辑2.1 Prefill和Decode是完全不同的两类计算模型生成一段回答底层是两段截然不同的计算阶段。Prefill阶段模型拿到整个输入序列把所有token并行计算一个请求可以迅速算出第一轮所有Key和Value。它的特点是计算密集、时间紧凑。Decode阶段模型每次只计算一个token然后把这个token追加到序列末尾通过缓存技术避免重算历史逐字生成。它的特点是访存密集、串行执行。同样是生成100个tokenprefill可能只占零点几秒decode却要几十毫秒乘一百次。这也是为什么线上很多服务平均延迟看着还行输出阶段一样长到让人等得不耐烦。注意很多优化手段都围绕“两段式”展开。比如连续批处理就是想办法把请求的prefill和decode交错到一起跑前缀缓存目的则是砍掉重复prefill的时间。2.2 KV Cache到底吃掉多少显存一次手算KV Cache是解码阶段的核心机制。每次生成一个token模型都要算新的Key和Value向量并把历史上所有token的Key和Value缓存下来避免每次都从头重算。KV Cache的大小随序列长度线性增长随并发序列数线性增长这就是长上下文和大并发吃掉显存的根本原因。可以快速估算假设某个70B参数模型有80层、每组8个KV头、head维度128用BF16存储那么每个token每层需要 2×8×128×2字节4KB80层就是320KB。一个用户如果带了8000个token的上下文这一个请求就占掉约2.5GB10个并发用户就是25GB。算完这个数字再去看显存大部分OOM的根因就一目了然了。多卡并行时权重按张量并行切分到各卡KV Cache也按层切分但预算思路不变每张卡都要同时容纳权重切片、KV Cache、激活和运行时buffer。2.3 完整显存账单权重、KV、激活、运行时buffer很多刚开始调优的同学下意识以为显存就是模型权重占用的空间只盯着权重文件大小结果一上线就爆。实际显存账单有四块模型权重BF16下大约每10亿参数占2GB70B就是140GB左右这是基础盘。KV Cache和上下文长度、并发数正相关上面算过随序列总长增长很快。激活值prefill阶段中间计算结果瞬时占用很高长prompt尤其明显。框架运行时缓存CUDA context、显存碎片、临时buffer通常占几GB到十几GB。所以算显存预算时这四样都要留空间。我的习惯是先大致算一遍再额外留10%到15%作为安全余量绝不在生产环境里把显存利用参数顶到极限。2.4 为什么OOM容易出现在解码阶段把整个过程拉长看prefill阶段虽然激活值高但KV Cache还没累积起来decode阶段KV Cache随产出token数量一路涨叠加并发序列越来越多很容易在生成到一半时撞上显存上限。所以线上看到“单个请求没问题并发一多就报显存错误”先往KV Cache和并发序列数上想往往不需要动权重。3. 故障排查方法论先量化再定位后修改3.1 排障第一步把性能指标完整接出来排查性能问题最容易犯的错就是还没量化就开始猜。我见过有人因为一次线上偶发超时立刻把量化等级从FP16换成INT4结果问题完全没解决反而多了精度损失。正确流程是先拿到证据再改配置。先搭一套轻量级的可观测体系把TTFT、TPOT、prefill时长、decode时长、排队等待时长、显存占用峰值、GPU利用率全部记录成直方图指标。没有这些前置数据后续所有判断都靠猜。给一个最基础的实时监控命令示例# 实时查看GPU利用率和显存变化 nvidia-smi dmon -s pucmet -d 1 -c 60也可以把指标输出到时序数据库等故障波峰过去后拉出对应时间段的曲线。这一步通常能筛掉八成的问题。3.2 常见故障症状与第一怀疑点对照抛一张典型的故障现象定位表现象第一怀疑点定位方法单请求正常并发后OOMKV Cache容量不足看并发数、输入长度、显存曲线TTFT异常高prefill被长输入或重复前缀拖慢看prompt长度分布、prefill耗时TPOT整体慢模型过大、单卡算力不足对比不同量化、换更大GPUTPOT波动大并发波动、排队挤压看排队时长和in-flight请求数输出质量诡异采样参数被改、量化损失复测固定样例、检查采样配置3.3 一次典型TTFT恶化的完整推演举一个复合例子。某个业务上线了问答服务现象是白天高峰期TTFT逐步升到三秒以上TPOT却保持正常。第一次看到这个组合可以先把“TPOT正常”排掉说明单次decode算力没退化重点转向prefill和排队。看指标发现prefill耗时确实涨了但瓶颈不在单条请求而在于高峰期并发请求的大batch把一堆prefill任务堆在一起执行每条都被拖慢。再往下查发现大量重复的固定system prompt每条请求都白算一遍相同的几千token。对症下药就三步给固定前缀做缓存砍掉大量重复prefill把超长prompt截断或做长度限制调度上让prefill请求优先处理。改完之后TTFT从三秒降到六百毫秒。这个例子想说明的是排查不是靠经验玄学而是靠“指标分层—定位瓶颈—针对性改动”的顺序。4. 性能调优的关键参数与实操配置4.1 连续批处理与并发上限的关系传统批处理方式是等一批请求凑齐了再推理批大小固定天然导致排队延迟。主流推理框架基本都改用了连续批处理一个请求decode结束就立刻移出新请求的prefill可以插到当前batch的空隙里GPU始终在密集计算。调这个能力最直接的参数是并发序列上限也叫max_num_seqs或max_batch_size。调大它吞吐会涨但每个请求分到的算力变少TPOT变差调小延迟稳定但吞吐上限低。一个可操作的策略是从单请求基准出发逐步倍增上限每次记录TTFT、TPOT和总吞吐直到TPOT越过阈值再退回上一档。参考配置片段{ max_num_seqs: 16, gpu_memory_utilization: 0.85, max_model_len: 4096, enable_prefix_caching: true }max_num_seqs从16试到64吞吐和TPOT的变化通常会呈一条清晰的曲线选中间最稳的点。4.2 显存利用率与KV Cache预算的配置逻辑被问得最多的就是显存利用率参数到底设多少合适。设低了KV Cache容量不够高并发长上下文直接OOM设高了权重和激活空间被压缩同样可能OOM。我的经验是分两步走。第一步先按权重大小、KV Cache估算值、激活峰值算出总预算看显存还剩多少。第二步把显存利用率参数从0.8起步跑一组极限压力测试尽量长文本、尽量多并发然后逐步递增到0.9左右。如果压测中出现KV Cache不足的报错优先降低并发上限而不是继续压榨显存比例。注意显存利用率是“给框架分配显存的上限”不是“KV Cache独占比例”。调到0.99看起来能最大化缓存实际上留给其他模块的余量几乎为零生产环境非常容易被临时峰值击穿。4.3 量化选型的分场景思路量化减少显存占用、提高计算效率同时也在损失精度。选型时要区分两类场景。对延迟敏感且用户能接受轻微输出变化的在线助手类场景INT8是一个稳妥的中间选项显存降低约一半推理效率提升精度损失通常辨析不出来。对显存极其紧张、模型又很大的场景INT4级别的低比特量化能把大模型压进更小显存但要注意有的层对量化特别敏感输出质量和稳定性都可能波动。有没有更优解有。可以只对部分敏感层做高精度保留用混合精度方案。这类方案配置复杂除非已经明确精度问题由量化触发否则不建议一上来就搞。选型的判断标准也很直接换完之后固定跑同一组线上代表样本对比关键字段是否一致、生成分布是否偏移。只盯benchmark数字不看业务结果是选型中最容易犯的错。4.4 前缀缓存最容易被忽略的加速手段很多应用的prompt由固定模板和用户输入拼接而成。如果是同一个固定前缀模型在prefill阶段会在一模一样的位置算出一模一样的KV Cache。前缀缓存就是让这些固定部分只算一次后续请求直接复用。它对多轮对话场景尤其有效因为历史对话内容会不断拼接进prompt重复计算量非常大。开启后固定前缀命中时TTFT往往能下降一个量级。代价是前缀缓存需要额外显存存放已缓存的KV且不同输入的前缀不一致时命中率低。线上设计系统提示词时把固定部分放在最前面、内容改动尽量少能大幅提高命中率。4.5 投机解码的收益与适用条件投机解码的思路是先用一个小模型快速猜出接下来几个token再让大模型一次验证多个token。如果猜得对一次decode能“白赚”几个token的产出。这个技术对TPOT的优化效果非常明显但稳定依赖两个条件草稿模型足够快且猜中的概率足够高。实测下来在代码生成、结构化JSON输出、固定格式文本这类低熵场景接受率很高收益很大但在开放式创意写作、随机性很强的对话里收益就没那么明显有时还会因为猜测失败浪费算力。所以不要看别人说投机解码好就无脑开先拿自己的线上prompt做一轮小规模A/B测试再决定是否全局开启。5. 线上踩坑实录与问题速查5.1 问题、根因、解法一览表把典型线上问题浓缩成一张速查表问题现象大概率根因解决方向并发一高就OOMKV Cache预算不足或并发上限过大调低并发上限、限制输入长度、优化KV Cache预留TTFT高企固定前缀反复计算 / prefill与decode争抢算力开启前缀缓存、调度优先prefill、限制超长promptTPOT波动明显请求大量到达导致批大小抖动设置稳定并发上限、启用平滑排队、压测确定容量输出内容突然变差量化损失 / 采样参数被改动回滚参数、对比基准样本、局部高精度量化生成停不下来缺少长度兜底或终止条件失效服务端设max_tokens、增加终止符检测5.2 三个反复出现的坑第一个坑显存利用率参数拍脑袋填0.95。结果权重加载时直接报显存不足或者一压测就崩。正确做法是先算清权重和激活的底线空间再往KV Cache方向去加而不是一上来就顶满。第二个坑只看平均延迟不看p95和p99。某个配置平均延迟甚至更低了但p99翻了一倍这在线上等于把一部分用户直接推出门外。任何调优结论都必须同时看尾延迟尤其服务端会同时服务多种类型请求时。第三个坑只顾GPU算力忘了数据预处理和tokenizer开销。大并发时系统会先把大量prompt转成token、做长度校验、拼装prompt模板这些CPU进程如果不做分离或限流会成为隐蔽的排队瓶颈。我调过的某次“GPU利用率只有30%”的问题根因就是CPU侧tokenizer线程数不够。5.3 攻坚排障时我常用的老办法如果配置已经调到看起来都合理的状态但性能还不满足要求我会回到一个老做法构造一条明确的复现请求固定它的输入和输出长度在不同配置下反复跑。真到排障攻坚期问题几乎都能在这样“可变条件最少”的实验里暴露出来比东看一眼西猜一下高效得多。这套方法对算法和工程同学都适用核心就是把变量锁住一次只动一个。做推理调优这么久我最大的感受是大部分性能和故障问题都不是玄学而是指标没拆清、资源没算透、变更没有对照导致的。每次上线前固定跑一遍同一批基准样本把TTFT、TPOT和p95保存下来任何改动都拿它做对照。这套流程看着笨但真到线上出问题的时候它能帮你省下整整一晚上的排查时间。之后再遇到推理卡顿先对照速查表过一遍再回到参数区手动调一轮你会发现很多问题都没那么邪乎。