尧图精选

AI推理服务性能优化实战:从批处理、显存管理到并发控制

🕒 发布时间:2026/10/1 6:13:59 📁 来源:尧图网络
1. 从能跑到跑得稳AI系统性能工程到底在解决什么很多人做AI项目模型训练完了、推理接口通了、Demo跑起来了就觉得大功告成。但真正上线之后才发现事情远没有那么简单——延迟忽高忽低、吞吐量上不去、GPU利用率长期趴在30%、显存时不时OOM、并发一上来整个服务就像多米诺骨牌一样崩掉。这些问题不是模型本身的问题而是系统性能工程要解决的范畴。AI系统性能工程说白了就是让AI系统在生产环境中跑得稳、跑得快、跑得省的一整套方法论和工程实践。它横跨了模型推理优化、硬件资源调度、服务架构设计、性能监控与调优等多个层面。和传统后端性能优化最大的不同在于AI系统的性能瓶颈往往不在CPU或网络而在GPU计算、显存带宽、数据传输管道这些环节而且模型本身的计算图特征会直接影响整个系统的性能表现。这个系列的前两篇分别聊了性能建模和瓶颈定位的基础思路这一篇重点落在推理服务的性能优化实战上。我会从推理引擎选型、批处理策略、显存管理、并发控制、性能压测这几个维度展开把我在实际项目中踩过的坑和验证过的方案都摊开来讲。不管你是刚接触AI系统部署的工程师还是已经在做推理优化的老手应该都能从中找到一些可以直接用的东西。注意本文讨论的所有优化手段都基于合规的工程实践场景不涉及任何违规内容。2. 推理引擎选型为什么你的模型跑得比别人慢一倍2.1 推理引擎的核心差异在哪里同样一个模型用不同的推理引擎跑性能差距可以到2-5倍甚至更多。这不是夸张是我在实际项目中反复验证过的结论。推理引擎的核心差异主要体现在三个方面计算图优化能力、内存管理策略、硬件适配深度。计算图优化方面好的推理引擎会做算子融合把多个小算子合并成一个大算子减少kernel launch开销、常量折叠把编译期能算的提前算掉、死代码消除等。举个例子一个典型的Transformer推理过程中LayerNorm后面接一个线性层再后面接一个激活函数优秀的引擎会把这三个操作融合成一个kernel减少两次显存读写。别小看这个优化在长序列场景下显存带宽往往是真正的瓶颈。内存管理策略的差异更明显。有些引擎每次推理都重新分配显存有些则维护一个显存池做复用。前者在频繁调用时会产生大量碎片后者虽然初始开销大但长期运行更稳定。我在一个对话系统项目里做过对比同一个7B模型A引擎在QPS 50的时候开始出现显存碎片导致的OOMB引擎用显存池管理跑到QPS 120才碰到真正的显存上限。硬件适配深度这个就不用多说了不同引擎对特定硬件的指令集优化程度差别很大。有些引擎对某些加速卡做了深度的kernel定制换一个硬件平台性能就大打折扣。2.2 主流推理引擎的适用场景对比选推理引擎不是选最好的而是选最合适的。下面这张表是我根据实际项目经验整理的对比供参考引擎优势场景劣势场景上手难度社区活跃度ONNX Runtime跨平台部署、CPU推理超大规模模型低高TensorRT固定shape、极致延迟动态shape频繁变化中高中vLLM大语言模型高吞吐非Transformer架构中高Triton多模型混合部署单模型极致优化中高TorchServePyTorch生态无缝性能天花板较低低中选型的时候有几个关键问题要先问自己模型结构会不会频繁变输入shape是固定的还是动态的延迟优先还是吞吐优先部署环境是单卡还是多卡这些问题的答案基本能帮你缩小到1-2个候选。2.3 引擎切换的隐性成本很多团队在选型时只看benchmark数字忽略了切换成本。我见过一个团队从ONNX Runtime切到TensorRT性能确实提升了40%但花了整整三周做模型转换适配、算子兼容性处理、精度对齐验证。而且后续每次模型更新都要重新走一遍转换流程维护成本很高。所以选型的时候一定要算总账性能收益 × 预期运行时长 - 迁移成本 - 维护成本。如果模型迭代频率很高选一个转换链路短、自动化程度高的引擎可能比选一个性能极致但转换麻烦的引擎更划算。3. 批处理策略吞吐量和延迟之间的跷跷板3.1 静态批处理与动态批处理的本质区别批处理是推理优化的第一把刀。原理很简单GPU是并行计算设备一次处理一个样本和一次处理一批样本计算时间差别不大但吞吐量天差地别。问题在于怎么组批。静态批处理就是固定batch size攒够一批再送进去。实现简单但有两个致命问题一是延迟不可控如果请求稀疏用户可能要等很久才能凑够一批二是资源浪费如果请求量波动大低峰期GPU利用率很低。动态批处理则是根据当前请求队列的情况动态决定每次推理的batch size。请求多的时候batch大请求少的时候batch小甚至单条推理。这样延迟和吞吐量都能兼顾但实现复杂度高不少。我在一个在线翻译服务里做过对比测试静态batch size32的方案P99延迟在请求低谷时反而比高峰期高因为要等凑批。换成动态批处理后P99延迟降低了60%同时吞吐量还提升了15%。3.2 连续批处理大模型推理的破局点对于大语言模型来说传统的批处理有个根本性问题每个请求生成的序列长度不一样如果按最长序列来padding短序列的请求就浪费了大量计算。连续批处理Continuous Batching的思路是不等一整批全部完成而是某个请求生成结束后立刻把新的请求插进来。这个策略对LLM推理的提升非常显著。实测数据一个13B模型用传统批处理GPU利用率大概40-50%换成连续批处理后GPU利用率能到80-90%吞吐量提升2-3倍。实现连续批处理需要引擎层面的支持vLLM和TensorRT-LLM都内置了这个能力。如果你用的是原生PyTorch推理自己实现连续批处理的工作量不小需要管理每个请求的KV Cache、维护调度队列、处理不同序列长度的attention mask建议优先考虑成熟引擎。3.3 批处理参数调优的实操经验调批处理参数的时候有几个经验值可以参考最大batch size不要超过显存的70%容量对应的batch size留30%给峰值波动和KV Cache增长批处理超时窗口一般设5-20ms太短了凑不到批太长了延迟高队列长度上限设一个上限防止请求无限堆积超过就返回503让上游重试提示批处理参数没有万能值一定要用真实流量模式做压测。用均匀流量测出来的最优参数在真实场景下可能完全不是最优的。4. 显存管理OOM不是靠加卡解决的4.1 显存都去哪了推理阶段的显存分布很多人一遇到OOM就想着加卡但其实先搞清楚显存到底被什么占用了。推理阶段的显存开销主要分四块模型权重这是固定开销FP16的7B模型大概占14GBINT8量化后约7GBKV Cache这是动态开销和batch size、序列长度成正比在长序列场景下可能超过模型权重本身中间激活值每层计算的临时结果和batch size、序列长度相关框架开销CUDA context、通信buffer等一般1-2GB我见过一个案例一个团队用7B模型做推理显存24GB的卡batch size开到16就OOM。他们以为是模型太大后来一查发现KV Cache占了8GB多因为他们的应用场景序列长度经常到2048。把KV Cache改成分页管理后同样的显存能跑到batch size 32。4.2 KV Cache优化的几种实用手段KV Cache是大模型推理显存优化的重中之重几种常见手段量化KV Cache把KV Cache从FP16降到INT8显存直接减半精度损失通常在可接受范围内。实测一个13B模型KV Cache量化后在对话场景下BLEU分数只降了0.3但显存省了将近一半。分页管理借鉴操作系统的虚拟内存思路把KV Cache分成固定大小的页按需分配。这样不同请求之间可以共享显存池碎片问题也解决了。vLLM的PagedAttention就是这个思路。前缀共享如果多个请求有相同的system prompt那这部分KV Cache可以共享不用每个请求都存一份。在多轮对话场景下这个优化效果很明显。滑动窗口对于超长序列只保留最近N个token的KV Cache老的丢掉。这个会损失长距离依赖但在很多实际场景下影响不大。4.3 显存碎片那个让你半夜被叫起来的问题显存碎片是个很隐蔽的问题。你的程序刚启动时显存充足跑了一段时间后开始报OOM但nvidia-smi看显存占用并不高。这就是碎片化导致的——大量小的分配和释放把显存切得七零八落虽然总空闲量够但没有一块连续的空间能满足新的分配请求。解决办法有几个一是用显存池预分配一大块显存自己管理避免频繁向CUDA申请释放二是设置PYTORCH_CUDA_ALLOC_CONF环境变量调整分配策略三是定期重启服务简单粗暴但有效。# PyTorch显存分配器配置示例 export PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:512,roundup_power2_divisions:16这个配置的意思是大于512MB的分配请求单独处理避免被切碎分配大小向上取整到2的幂次减少碎片。实测在长时间运行的服务里这个配置能把OOM的发生时间从几小时延长到几天。5. 并发控制与请求调度别让雪崩从你这里开始5.1 并发数不是越大越好新手最容易犯的错误就是把并发数往大了设觉得并发越高吞吐越高。实际上每个推理服务都有一个吞吐量拐点超过这个点之后吞吐量不升反降延迟飙升。原因很简单请求排队等待的时间超过了并行处理节省的时间而且过高的并发会导致显存竞争、上下文切换开销增大。找到这个拐点的办法就是压测。从低并发开始逐步增加观察吞吐量和延迟的变化曲线。当吞吐量增长明显放缓、延迟开始非线性上升的时候那个点就是你的最优并发数。一般来说设成最优值的80%比较安全留一些余量应对突发流量。5.2 请求优先级与队列管理生产环境里的请求不是平等的。有些是实时交互请求延迟敏感有些是后台批处理任务吞吐优先。如果不加区分地混在一起实时请求会被批处理任务拖死。我的做法是在服务前面加一个优先级队列至少分两级高优先级实时请求和低优先级后台任务。调度器优先处理高优先级队列低优先级队列在高优先级为空时才被调度。这样既保证了实时请求的延迟又利用了空闲算力处理后台任务。实现上可以用一个简单的双队列权重轮询也可以用更复杂的优先级调度算法。关键是要有一个饥饿保护机制——低优先级队列不能永远得不到调度否则后台任务永远完不成。5.3 过载保护优雅降级比直接崩溃好当请求量超过系统承载能力时如果不做保护结果就是所有请求都超时用户体验极差。过载保护的核心思路是主动拒绝一部分请求保证剩下的请求能正常服务。具体策略有几种限流超过阈值的请求直接返回429让客户端重试降级高负载时切换到小模型或简化处理流程排队超时请求排队超过等待时间就返回超时我一般会组合使用正常负载下不限流负载到80%时开始对低优先级请求限流到90%时触发降级把部分请求路由到小模型到95%时对新请求直接拒绝。这样层层递进不会一下子崩掉。6. 性能压测你的benchmark可能一直在骗你6.1 压测流量要模拟真实分布很多团队的压测就是用固定长度的输入、均匀的请求间隔跑一遍然后看P99延迟和吞吐量。这种压测结果参考价值有限因为真实流量从来不是均匀的。真实场景的流量特征包括请求到达服从泊松分布或更复杂的模式、输入长度服从某种长尾分布、不同时段的流量差异很大。压测的时候要尽量模拟这些特征否则你优化出来的参数在真实场景下可能完全不work。我一般会从生产环境采样一批真实请求脱敏后做成压测数据集然后用流量回放工具按真实的时间间隔发请求。这样测出来的结果最接近真实表现。6.2 关注正确的指标压测要看哪些指标很多人只看平均延迟和QPS这远远不够。至少要看这些指标含义关注点P50/P95/P99延迟不同分位的响应时间P99反映最差体验吞吐量单位时间处理请求数要看稳定态而非峰值GPU利用率计算单元繁忙程度低于60%说明有优化空间显存占用峰值和稳态峰值决定OOM风险错误率失败请求占比超过0.1%就要排查队列等待时间请求在队列中的耗时反映调度是否合理特别要强调的是P99延迟。平均延迟好看但P99很差的情况太常见了而用户感知到的恰恰是那些慢请求。如果一个服务的P50是50ms但P99是2s那每100个用户里就有1个要等2秒这个体验是很糟糕的。6.3 压测中的常见陷阱压测本身也有很多坑。第一个坑是压测客户端成为瓶颈客户端发请求的速度跟不上服务处理的速度导致测出来的QPS偏低。解决办法是用多台压测机或者用专业的压测工具。第二个坑是预热不充分很多推理引擎在首次推理时会做JIT编译、显存预分配等操作如果不预热直接压测前几百个请求的延迟会异常高拉高整体指标。正确做法是先跑几百个请求预热等指标稳定后再开始正式压测。第三个坑是忽略长尾请求有些请求因为输入特别长或者触发了某种慢路径延迟是普通请求的几十倍。如果压测数据集里没有这类请求就发现不了这个问题。建议在压测数据里故意混入5%的长尾请求观察系统表现。7. 几个真实项目中的性能优化案例7.1 案例一对话服务P99延迟从3s降到800ms这是一个智能客服项目7B模型单卡A100。上线后发现P99延迟高达3s用户体验很差。排查过程首先看GPU利用率发现只有35%说明计算不是瓶颈。然后看请求队列发现队列等待时间很长说明请求堆积。进一步排查发现问题是静态批处理策略导致的——服务配置的是batch size16但实际流量波动很大高峰期请求排队等凑批低峰期GPU闲着。改成动态批处理后P99延迟降到1.2s。然后继续优化发现KV Cache没有做分页管理长对话场景下显存碎片严重偶尔触发OOM导致请求重试。加上PagedAttention后P99进一步降到800ms。7.2 案例二推理成本降低60%的量化实践一个图像分类服务原来用FP32精度推理单次推理成本较高。做了INT8量化后模型大小减少75%推理速度提升2.3倍精度只降了0.5个百分点。但量化不是简单调个API就完事中间踩了不少坑一是校准数据集的选择。校准集要能代表真实数据分布否则量化后的精度损失会很大。我们一开始用随机采样的数据做校准精度掉了3个点后来换成按类别分层采样精度只掉0.5个点。二是某些层的敏感度不同。不是所有层都适合INT8量化有些层对精度很敏感。我们用了混合精度量化敏感层保持FP16其他层INT8这样在精度和性能之间取得了更好的平衡。7.3 案例三多模型共享GPU的资源隔离一个平台需要同时部署多个模型分类、检测、OCR等共享一张GPU。问题是不同模型的负载差异很大一个模型跑满的时候其他模型就饿死了。解决方案是用MPSMulti-Process Service做GPU时间片隔离给每个模型分配固定的GPU时间片配额。同时在上层做一个请求调度器根据各模型的队列情况动态调整请求分发。这样虽然单模型性能略有下降大概5-10%但整体资源利用率从40%提升到了75%。8. 性能监控体系没有度量就没有优化8.1 推理服务需要监控哪些指标性能优化不是一次性的工作而是持续的过程。要持续优化首先要有完善的监控体系。推理服务的监控指标分几个层次硬件层GPU利用率、显存占用、GPU温度、PCIe带宽、NVLink带宽多卡场景。这些指标能帮你判断硬件是否成为瓶颈。引擎层推理延迟分P50/P95/P99、吞吐量、批处理大小分布、KV Cache命中率、队列深度。这些指标反映推理引擎的运行状态。业务层端到端延迟、成功率、超时率、降级触发次数。这些指标直接反映用户体验。8.2 告警阈值怎么设监控没有告警等于没有监控。但告警阈值设不好要么天天误报让人麻木要么漏报导致故障。我的经验是GPU利用率持续5分钟低于30%可能有资源浪费需要排查P99延迟超过SLA的80%预警需要关注显存占用超过90%警告可能要触发OOM错误率超过0.5%严重立即排查队列深度持续增长严重可能即将过载阈值不是设了就完事要根据实际运行情况不断调整。新服务上线初期可以设宽松一点稳定后逐步收紧。8.3 从监控数据中发现优化机会监控数据不仅能用来告警还能用来发现优化机会。比如如果发现GPU利用率长期在40%左右说明计算资源没有充分利用可以考虑增大batch size或者部署更多模型共享GPU。如果发现P99延迟远高于P50说明有长尾请求需要排查是什么原因导致的——是输入特别长是某个算子性能不稳定还是显存碎片导致的GC如果发现KV Cache命中率低说明前缀共享没有生效可以检查一下请求的system prompt是否一致或者考虑优化prompt结构。这些优化机会都藏在监控数据里关键是要养成定期看监控、分析趋势的习惯。9. 写在最后的一些个人体会做AI系统性能工程这几年最大的感受是性能优化没有银弹只有对系统的深入理解加上持续的度量、实验和改进。同一个优化手段在不同场景下效果可能完全不同别人的最优实践照搬过来未必适合你的系统。另外一点体会是性能优化要有全局视角。只盯着推理引擎调参数可能忽略了上游的请求调度只关注GPU利用率可能忽略了端到端延迟。真正有效的优化往往是系统级的——从请求接入、调度、推理到返回每个环节都要看。还有一个经常被忽略的点性能优化要和成本挂钩。把P99从3s优化到800ms很厉害但如果为此增加了3倍的GPU资源那这个优化在商业上未必划算。好的性能工程是在满足SLA的前提下用最少的资源完成任务。最后性能优化是一个持续的过程不是一次性的项目。业务在变、流量在变、模型在变性能特征也会跟着变。建立一套好的监控和压测体系让性能问题能被及时发现和定位比一次性优化到极致更重要。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →