ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型推理性能调优实践:破解显存爆、并发低与延迟高

大模型推理性能调优实践:破解显存爆、并发低与延迟高

谁在做大模型落地的时候没被“推理慢、显存爆、并发低”这三座大山压过?尤其是当模型从实验环境的单卡跑到生产环境的容器编排集群里,环境复杂度和变量数量一下子涨了一个量级。这篇主要围绕大模型实际部署中绕不开的瓶颈,聊聊我从模拟项目X里总结出来的性能调优手段、踩坑记录和一些比较反直觉的结论,希望能给正在做推理服务落地的朋友一个参考。

1. 为什么跑demo很流畅,上生产环境却频繁超时和显存溢出

先说一个非常典型的现象:我们用同一套大模型权重,本机单卡调试时首Token延迟只有几百毫秒,吞吐量看起来也够用。结果一部署到生产容器里,并发请求稍微涨一点就频繁超时,GPU显存也经常报溢出。排查到最后,问题往往根本不在模型本身,而在部署架构和框架参数的默认值上。

大模型的推理服务不是“启动一个进程等输入输出”那么简单。一个标准的生产级推理链路至少包含:请求接入层、上下文调度层、分批推理层、KV Cache管理、动态显存分配、结果返回链路。任何一个环节的默认配置跟实际负载不匹配,都会引发雪崩。

我在模拟项目X里做过一个压力测试:单路请求时,P99延迟大概120ms,一切正常。把并发拉到8路之后,P99直接跳到2秒多,显存也开始跳动。当时第一反应是模型权重太大,但看实际情况,光是KV Cache的预分配策略不合理就浪费了大量显存,导致实际可用的推理批大小急剧缩小。

所以,做生产级调优的第一步是把“模型能不能跑”和“服务能不能扛”这两件事分开看。前者是模型本身的计算量问题,后者是框架的调度、显存策略和并发控制问题。下面几个部分分别从不同层面把我在实践中踩过的坑和分析过程展开讲。

2. 推理性能瓶颈拆解:计算密集、访存密集与调度开销

推理慢这件事,先要搞清楚到底慢在哪个环节。我习惯把一个请求的完整生命周期拆成四个阶段:Prefill(预填充)、Decode(逐个Token生成)、Tokenization(文本切分与还原)、以及网络传输与调度等待。不同阶段的瓶颈特征完全不同,调优手段也不一样。

2.1 Prefill阶段的计算密集特征与优化空间

Prefill阶段是把用户输入的整段Prompt一次性送入模型,做并行计算,计算量大但是次数少。这个阶段是典型的计算密集(Compute-Bound)场景,瓶颈主要在于GPU的算力利用率和Attention计算的效率。

如果发现单请求的首次Token延迟很高,问题往往出现在这个阶段。常见的优化手段包括使用FlashAttention这类融合算子,减少显存读写次数。我在实践中发现,仅仅是切换Attention内核实现,在长Prompt场景下首Token延迟就能下降30%以上。

另外,Prefill阶段还可以做“长度分桶”:把长度相近的请求放在同一批里,减少Padding造成的算力浪费。在模拟项目X里,这种做法让GPU的利用率提升了大概15个百分点。

2.2 Decode阶段的访存瓶颈与KV Cache的影响

Decode阶段是逐个Token往下推理,每一步都得把模型权重和KV Cache重新读一遍,所以这阶段往往不是算力不够,而是访存带宽成了天花板。GPU的显存带宽再高,也架不住每一步都要全量读一遍权重。

这个阶段最容易被忽略的调优点有两个。一个是KV Cache的量化压缩,把缓存从FP16压到INT8甚至INT4,显存占用显著降低,可以腾出空间提高并发批大小。另一个是采用连续批处理(Continuous Batching)机制,让不同请求在同一批里各自推进、动态进出,而不是等整批全部做完再统一收尾。

我在某跨平台系统的多次压测里的主要数据是:启用Continuous Batching后,在相同显存限制下,吞吐量提升了近2倍。这里面的原理很简单——普通批处理的瓶颈是“木桶效应”,最慢的请求拖住整批的进度,而连续批处理让每个请求自己决定何时完成。

2.3 调度开销:容器网络的隐藏时延

调度开销往往是最隐蔽的。容器化部署时,请求要经过网关、服务发现、负载均衡、进程间通信好几跳。每跳增加几毫秒,叠加起来就是几十毫秒的额外延迟。这个损耗在小规模并发时完全不明显,但一旦服务网格配置复杂,问题立刻被放大。

我们在排查一次P99延迟异常飙升的时候,最终定位到的是负载均衡策略的会话保持问题——某些请求被路由到了慢节点,导致尾延迟失控。换成基于最少连接数的均衡策略,并配合服务实例CPU水位做自适应剔除后,长尾问题基本消除了。

在优化模型计算之前,先把调度链路的时延数据摸清楚。因为如果服务框架本身有严重的请求排队或路由问题,模型算得再快也感知不到。

3. 显存优化的四个层次:从权重、缓存到碎片整理

显存溢出可以说是大模型部署翻车的头号原因。核心矛盾在于显存是有限的,但模型权重、KV Cache、中间激活值、临时计算缓冲区都需要占用空间。优化显存应该分层来做,我按实践中的优先级排序分成了四个层次。

第一层是模型权重本身的体积。对开源权重做量化(比如从BF16降到INT8或INT4),能最直接地缩减基础占用。但量化后模型质量会略有损失,需要权衡。我在模拟项目X里用INT8量化后,显存占用减少了约40%,质量损失在评测集上微乎其微。

第二层是KV Cache的管理。默认框架配置通常不会自动调整Cache预留大小,要么预留多了浪费,要么预留少了导致缓存溢出。正确做法是根据模型层数、头数、序列长度上限先算出一个理论最大占用,然后再结合线上请求长度的分布去动态伸缩。在实际项目中,我们用的是动态显存分配策略,框架会根据负载自动调整缓存上限,留出余量给突发流量。

第三层是中间激活值的释放策略。部分框架在反向传播时才需要保存激活值,推理时完全可以边算边释放。检查一下框架的显存复用设置,有时只是打开一个开关就能省下几百MB到数GB不等。

第四层是显存碎片整理。长时间运行的推理服务,随着不同长度请求进出,显存碎片化会越来越严重。遇到“还有显存但分配不出来”的时候,大概率是碎片问题。重启服务只能临时缓解,根本办法是用支持显存池化或碎片整理的推理引擎,或者在业务低峰期做定期滚动重启。

我自己排过的一个典型案例是:某服务运行三天后,显存总量还剩不少,但分配一个稍大的连续块就报OOM。后来排查确认是长时间运行导致的内存碎片化,换用带页面缓存机制的推理引擎之后,连续运行一周都保持稳定。

优化层次作用对象核心收益风险点
第一层模型权重直接降低基础显存占用量化损失
第二层KV Cache动态适配长序列缓存预留不准
第三层中间激活值减少峰值占用需要框架支持
第四层显存碎片保障长期稳定运行可能需要更换引擎

4. 并发控制策略:盲目调大批大小是灾难的开始

很多人的直觉是“并发不够就把批大小调大”,这个直觉在传统深度学习训练里成立,在生成式推理中却很容易翻车。因为Decode阶段是逐个出Token的,批越大,单个Token的计算延迟可能略微增加,而一个批次里所有请求都会被这个延迟拖住。

所以并行度不是越高越好,关键目标是“在延迟SLA达标前提下最大化吞吐”。我采用的是先定延迟阈值,再用二分法找最大并发数的方式。比如先把P95延迟红线定在800ms,然后从4路并发开始,按2的指数往上推,一旦超出红线就回退到上一次的值,然后再细调。

另外,还要区分“请求并发”和“批内并行”两个概念。请求并发是指服务能同时处理的请求数量,批内并行是指一个批次里有多少请求共享一次前向计算。普通同步批处理中两者接近相等,但在支持连续批处理的框架中,可以同时处理“数百个请求的上下文,但每步只计算其中一部分活跃请求”。

实际压测中我们发现,把最大批大小从默认的32调到64,吞吐是涨了,但P99延迟直接翻倍,部分请求超时。原因就是批内请求的序列长度参差不齐,短的早就该结束了,却还得等长的生成完。这其实就是“排队拥堵”在显存空间维度的镜像。

最终的调优结果是一个动态策略:根据当前队列长度和服务器的计算水位,动态调整单步批大小。队列积压多时把批大小调大换取吞吐,队列空时把批大小调小保证延迟。这个策略上线后,吞吐和延迟两个指标都有改善,算是“鱼与熊掌兼得”的实现方式。

5. 部署链路中的环境适配与稳定性:CPU分配、线程数与存储IO

上述优化都建立在环境稳定可用的前提下。不少项目在容器编排平台里跑推理服务时栽跟头,其实是环境适配没做好。这里有三块很容易忽视但影响巨大的点。

5.1 CPU核数与线程配置对GPU利用率的影响

大模型推理虽然主力计算在GPU,但数据预处理、Tokenization、采样、结果返回这些环节都依赖CPU。如果CPU核数配置太少,或者线程池大小没跟着调,就会出现“GPU在等CPU喂数据”的情况,显存利用率看着不高,GPU却是半空闲的。

我检查过一些性能异常的服务,发现CPU使用率长期打满,而GPU利用率只有40%多。调大线程池并增加预处理并发之后,GPU利用率迅速上到90%以上。经验值是:推理服务的CPU核数至少按可用GPU个数的8-16倍来配,具体看Tokenization和采样部分的复杂度。

5.2 存储IO与模型加载时间

另一个容易踩的坑是模型存储和启动速度。大规模模型权重动辄几十GB,从远程存储拉取所需时间可能够重启好几次业务。我们的做法是把模型权重预先缓存到节点本地磁盘,每次启动直接读本地文件,避免反复走远程存储。

实测下来,从远程文件存储加载一个70亿参数模型要将近10分钟,而本地磁盘加载只需要1分多钟。这对弹性伸缩策略影响很大——如果扩容一个副本要等10分钟,那流量突增时基本已经雪崩了。

5.3 推理服务优雅退出和请求排空

最后是稳定性问题。容器平台在滚动更新或缩容时会直接杀进程,如果正在处理的请求还没生成完就被强制终止,用户感知就是“回复突然中断”。必须实现优雅退出机制:收到终止信号后停止接收新请求,但等已处理中的请求全部完成或超时后再真正退出。

这个点在压测中容易被忽略,生产环境必然遇到。我建议把优雅退出超时设置成比模型最长生成时间略长,配合探针的健康检查逻辑,让平台在杀进程之前确认服务已经排空。

6. 稳定性观测与调优闭环:指标收集、压测方法和常见误判

有了前面的调优基础,最后还要把整个系统放在可观测性框架下持续看护。没有数据就没有判断依据,没有压测就没有容量概念。这块讲三个层面:指标选型、压测设计、以及常见的误判陷阱。

6.1 指标采集:不要只看GPU利用率

GPU利用率不是越高越好。在生成式推理里,高利用率有时候意味着批大小设置过大,单个请求延迟严重。我通常会同时观测以下几组指标:GPU计算利用率、显存带宽利用率、KV Cache占用率、请求排队长度、以及P50/P95/P99延迟分布。

这几组指标合在一起才能看到系统全貌。比如排队长度超过某个水位,说明请求接入速度大于处理速度;显存带宽利用率打满而计算利用率不高,则说明访存是瓶颈,需要做量化或权重压缩。

6.2 压测方法:从单请求到逐渐加压的完整流程

压测不能只测一个固定并发数。我推荐的流程是:先单请求跑一轮看基础延迟,然后按2倍并发递增直到延迟突破SLA阈值,记录临界并发数;接着回到阈值内继续跑30分钟以上,观察指标是否稳定,有没有显存缓慢增长或延迟逐渐劣化的情况。

这种压测能同时暴露两类问题:短期容量问题和长期稳定性问题(比如Cache泄露、碎片累积、线程池用尽)。很多服务是“短压正常、长压现形”的——跑5分钟一切正常,跑50分钟后性能断崖,一部分原因指向定时任务占用资源冲突,还有一部分指向资源释放不完整。

6.3 常见误判:延迟升高不一定是算力不够

最后一个常见误判是把所有延迟升高都归结为模型算力不足。我曾遇到一次P99延迟飙到3秒,仔细一查,发现是日志组件的同步写入阻塞了推理线程;还有一次是网络插件在新版本中的流量绕行导致额外时延。所以,在做任何重量级优化之前,先花时间把调用链路的每一跳时延数据都监控起来,往往能少走很多弯路。

在容器场景里部署大模型,本质上是三个维度的协同:算法优化带来的计算效率提升、资源管理层带来的效率保障、以及业务层并发策略的精细调节。没有一个维度的优化能单独解决所有问题。先把环境各项指标摸清,再把显存策略和管理策略逐层调通,最后用稳定的压测方法去验证。这个过程做完之后,不仅模型推理稳定了,整个团队的排障效率也会上一个台阶。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进