做LLM服务部署的人,迟早都会撞上同一个问题:单发请求的时候模型响应又快又稳,一旦并发上来,整个服务的表现就开始变得不可捉摸——有时候是吞吐上不去,有时候是首字时延忽高忽低,有时候干脆直接OOM崩溃。很多人第一反应是“加GPU”或者“把并发参数调大”,但如果不先把瓶颈定位清楚,加卡和调参往往都只是在瞎猜。这篇文章把我这些年实际部署和排查LLM并发访问性能问题用到的完整方法论拆开讲,从指标定义、分层定位、压测实操到常见故障速查,给出一套可以直接复用的分析流程。内容不挑框架,不挑具体硬件,核心思路是通用的,适合正在做LLM服务落地、模型推理优化或者平台稳定性保障的工程师参考。
1. 先搞清楚:并发场景下LLM服务到底慢在哪一层
1.1 一个请求从进来到返回,经过哪些环节
LLM服务的调用链路比普通Web服务长得多,也复杂得多。一个请求从客户端发出到最终返回完整回答,至少要经过这几个环节:
- 网关和负载均衡层:接收外部请求、做路由、限流、鉴权。这一层通常是普通的网络服务,单机扛几千上万个并发连接都没问题,但在高吞吐的LLM场景下,网关的转发延迟和排队策略反而会成为第一个瓶颈。
- 推理框架的调度层:这是LLM服务特有的部分。框架收到请求后,要决定把它放在哪个worker上、什么时候放进Batch、排队能容忍多久,都由这一层负责。调度策略的好坏直接决定并发上限。
- 模型推理引擎:真正执行模型计算的地方。一个请求的生命周期会拆成两个阶段——预填充(prefill)和逐Token生成(decode),两个阶段的计算特征完全不同,瓶颈表现也完全不同。
- 硬件资源层:GPU扛着绝大部分计算,CPU负责数据处理、结果采样以及一部分与GPU通信的杂活,内存和显存分别决定能放多少请求状态和KV Cache。
很多并发性能问题,表面上看是“GPU不够快”,实际上瓶颈可能藏在第一层的排队策略、第二层的调度逻辑、或者第四层的显存分配上。这也是我一直强调的:分析LLM并发瓶颈,要按层次拆,不要一上来就被GPU利用率数字牵着鼻子走。
1.2 并发放大瓶颈的三种典型路径
并发本身不会制造问题,它只会把原本存在的小问题放大成系统级故障。从实践来看,并发访问下的性能劣化基本沿着三条路径发生。
路径一是排队效应。服务端处理能力是固定的,并发一旦超过处理上限,请求就开始排队。排队带来的不只是等待时间变长,更麻烦的是尾时延爆炸——只要有几个请求特别长,整个系统的TP99和TP99.9都会变得很难看。很多时候并发上不去并不是计算能力不够,而是队列管理策略不合理,比如用了严格FIFO又没有超时熔断,一个大请求把后续所有小请求都堵死。
路径二是资源争抢。GPU的计算单元和显存带宽是共享的,多个请求同时在跑,必然要抢这些资源。LLM的decode阶段本质上是显存带宽密集型操作,几个长请求一起生成Token时,带宽会被迅速打满,这时候再增加并发,每个请求的生成速度反而会一起掉下来。这种“越加越慢”的现象,会让很多人在指标图前怀疑人生。
路径三是框架和系统层的额外开销。Python推理框架里的锁竞争、请求级的内存拷贝、CPU和GPU之间的同步等待、日志和指标采集,这些都是在并发上去之后会被显著放大的开销。我见过一个案例,压测到30并发时GPU利用率只有40%,但CPU已经跑到120%满载,最后发现瓶颈居然在框架里大量的JSON序列化和正则匹配上。这种问题加多少卡都没用,得先处理代码层开销。
这三条路径往往同时存在,所以分析时要学会用组合指标去拆,而不是只盯着一两个监控图下结论。下面先定指标,再讲分层定位的实操方法。
2. 指标先行:分析性能瓶颈前必须定的核心指标
2.1 吞吐与时延:LLM场景的指标口径
分析并发性能,最忌讳的就是没有统一指标就开始调参。先说清楚LLM场景里几个必须定义的指标。
吞吐(TPS)指系统每秒完成的请求数。很多人以为TPS等于并发数除以单请求耗时,在普通Web服务里这个公式近似成立,但在LLM服务里不成立——因为一个请求不是一次性算完的,它会持续占用GPU资源几十秒甚至几分钟。所以LLM服务更常用的口径是“每块GPU每秒生成的Token数”和“同时处理的请求数”,前者反映系统的实际产出能力,后者反映资源占用情况。这两个指标比单纯的请求级TPS更能说明系统有没有被有效利用。
时延要拆成两个来看。TTFT(首Token时延)是从请求发出到收到第一个Token的时间,它主要由prefill阶段和排队延迟决定,用户感知最直接。TPOT(每Token生成时间)是相邻两个Token之间的间隔,它决定首Token之后的生成速度,也就是用户看到的“打字机”节奏。单请求时TTFT可能只要200毫秒,并发到50之后变成3秒,这个变化趋势比绝对值更值得关注。
我给团队定的基线经验是:先压出单请求时的TTFT和TPOT作为理论基准,然后以两者不劣化超过50%为界限,找到系统能承受的最大并发度。这个50%是经验值,不同模型和硬件可以自己调整,但一定要有一个量化标准,否则你没法判断到底有没有劣化、劣化了多少。
2.2 并发度、队列深度与饱和度
除了吞吐和时延,还有三个指标容易被忽视:并发度、队列深度、饱和度。
并发度指“同时在处理中”的请求数。注意它和QPS不是一回事:QPS是每秒进来的请求数,并发度是系统里正在跑的请求数。对LLM这种长耗时服务,一个请求占着GPU资源几十秒,所以并发度的含义比QPS重要得多。系统里同时跑10个请求和100个请求,GPU压力和调度难度完全不同。
队列深度指排着队还没有开始处理的请求数。这个指标是排队效应最直接的晴雨表。队列深度持续增长且没有回落趋势,说明系统的处理能力已经到顶,再压下去只会徒增等待时间。
饱和度指关键资源被用到的程度,可以是GPU的计算单元占用率、显存带宽占比,也可以是CPU某个核心的占用率。饱和度达到80%以上通常要警惕,达到95%以上基本可以断定该资源就是当前瓶颈。我一直跟团队说:饱和度要找“第一满”的资源,因为系统里第一个被占满的资源往往就是短板。
2.3 指标采集方案与压测基线
指标定了,怎么采也有讲究。单机压测阶段建议至少采集四个层面的数据:
- 系统层:CPU、内存、磁盘IO、网络连接数,用系统自带的监控命令就能看,注意区分空闲和等待两种状态。
- GPU层:显存占用、计算单元利用率、显存带宽利用率、温度功耗,用GPU厂商配套的监控工具看。这里有个坑——显存占用高不代表GPU在工作,很多框架会把缓存占满,但计算单元利用率可能只有个位数。
- 框架层:排队请求数、正在处理的请求数、TTFT和TPOT的分位分布。这些通常要打开推理框架自带的监控接口才能拿到。
- 应用层:业务自身的耗时日志、超时率、重试率。别小看这层,很多时候问题已经从服务端转移到了客户端,比如客户端超时设置太短导致大量重试,把服务端活活压垮。
压测基线建立也很重要。我一般先跑一个低并发基线(比如1到2个并发),得到单请求的最佳时延作为上限参考;然后按2、5、10、20、50、100逐步加压,每个档位稳定跑3到5分钟,记录稳态指标而不是毛刺峰值。压测脚本里的请求内容也要模拟真实场景:混合不同输入长度、混合短问答和长文生成,因为纯短请求和纯长请求压出来的结果差异非常大。
2.4 组合指标判别表:快速定位瓶颈方向
单个指标说明不了问题,指标组合才能指向具体瓶颈。我把常见的组合和对应结论整理成一张表,排查时可以直接对照:
| 现象组合 | 典型结论 | 优先排查方向 |
|---|---|---|
| GPU利用率高 + 显存占用高 + TTFT正常 | 计算能力接近上限 | 模型量化、精简解码逻辑、控制并发度 |
| GPU利用率低 + 显存占用高 + TPOT慢 | 可能被缓存或调度卡住 | 检查KV Cache管理、批处理策略 |
| GPU利用率低 + CPU利用率高 | 瓶颈在预处理或框架开销 | 优化数据处理、关掉多余日志 |
| TTFT暴涨 + 队列深度持续增长 | 处理速度跟不上请求速度 | 降低目标并发、优化prefill批次 |
| TPOT正常 + 整体吞吐上不去 | 调度层没把请求喂饱 | 检查批大小上限、调度策略 |
| 显存没满但OOM | 显存碎片化或预留不足 | 开启显存整理、调整缓存上限 |
这张表不绝对,但能帮你在拿到一堆监控图之后快速圈定排查范围,少走弯路。接下来要做的,就是沿着分层思路把瓶颈精确定位到某一个环节。
3. 分层拆解:从客户端到GPU的瓶颈定位法
3.1 入口网关与请求排队:最容易忽略的第一道关卡
很多人排查LLM性能瓶颈时直接盯着GPU,结果在网关层栽了跟头。我印象很深的一次,压测时发现并发一高TTFT就开始猛涨,GPU利用率却不到30%,查了一圈发现是负载均衡策略出了问题:新进来的请求被轮询发到多个worker上,但每个worker自身的队列已经堆积了很多请求,新请求只能排在队尾等着。
网关层的排查重点有三个:一是负载均衡策略是不是按worker的实际负载来分发,而不是简单的轮询;二是队列是不是无界队列,无界队列在大流量下会无限堆积,最终拖垮整个服务;三是限流和熔断有没有生效,没有限流的话,上游突发流量会直接击穿服务。解决办法也比较直接:把负载均衡改成按队列深度加权分发,给队列设置上限,队列满了就直接返回过载响应,让客户端走重试策略。
这里还要提醒一句:网关层的连接数、线程池大小、超时时间都是配套的,调了负载均衡策略但没调超时时间,可能会导致客户端等不到结果就重试,反而放大流量。
3.2 框架调度与批处理策略:连续批处理为什么是核心
LLM推理框架的调度层,最关键的机制是批处理策略,尤其是连续批处理(continuous batching)。传统做法是等一批请求全部结束再一起释放,这种静态批处理在并发场景下效率很低——一批里只要有一个长请求,其他短请求都得陪着等。连续批处理的核心思路是:批次不固定,只要某个请求生成完了就把它移出批次,马上补一个新请求进来。这样GPU的计算单元始终有活干,吞吐能提升不少。
如果框架的批处理机制没生效,你会发现一个很典型的症状:GPU计算单元利用率有波动,但吞吐一直上不去,而且请求越长,整体吞吐越低。排查时要确认三件事:框架版本是否开启了连续批处理;批大小上限是否足够大(很多框架默认批上限偏保守);请求长度是否差异极大导致批次碎片化。批大小上限设置我要多说一句:不是越大越好。批大小增大确实能提高GPU利用率,但也会让KV Cache占用快速增长,而且一旦某个请求特别长,它会拖住整个批次,反而把TPOT拉长。
调度策略里的另一个关键点是prefill和decode是否混跑。有些框架会把prefill和decode分开处理,因为两者计算特征差异很大:prefill是计算密集,decode是带宽密集。分离调度能避免两类请求互相干扰,但实现复杂度高。如果框架支持这个选项,值得开起来对比压测一下,很多时候能显著改善长尾时延。
3.3 推理引擎与算子热点:prefill和decode要分开看
到了推理引擎这一层,就必须要理解prefill和decode的本质差异。prefill阶段要把用户的整个输入一次性喂给模型,计算量很大,但对延迟的影响是线性的;decode阶段则是每次都只生成一个Token,需要反复读取模型参数,计算量不大,但受显存带宽限制严重。
用GPU厂商提供的性能分析工具抓算子热点时,你会发现:prefill阶段的热点集中在矩阵乘法算子,bound在计算单元上;decode阶段的耗时大头反而在线性层的小矩阵运算上,因为每次计算的数据量很小,但需要频繁访问显存中的参数量和中间量。拿到算子级别的耗时分布之后,优化方向就清楚了:prefill快就走量化或者精简模型结构,decode慢就走算子融合、减少中间张量读写、或者用更高效的解码实现。
还有一个很容易忽略的点:采样和后处理算子。有些框架在每次decode之后要做logits处理、采样、解码标记等操作,这些在单请求时看不出来,并发高了之后如果采样逻辑写得低效,CPU端会成为瓶颈。用性能分析器如果抓到很多时间花在采样相关算子,可以考虑换更高效的采样实现或者把采样移到GPU上做。
3.4 显存与KV Cache:并发越高越紧张的一块资源
LLM并发服务里,显存管理是重中之重,而大头就在KV Cache。KV Cache是用来缓存历史Token的注意力键值对的,它的大小和请求的上下文长度、并发度直接相关。直观理解就是:每个请求在生成过程中都要把已经看过的内容记下来,并发请求越多、上下文越长,占用的显存就越大。
显存层面的瓶颈有两个常见表现。一是KV Cache上限设得太小,导致框架频繁把历史上下文丢掉或者强制截断,模型回答质量下降不说,还会触发重新计算,性能雪崩。二是显存碎片化严重:多个请求不断进出,KV Cache的内存块被拆得七零八落,明明剩余总量够,但就是分不出连续的大块来,于是报OOM。解决碎片化问题的思路是采用分页式缓存管理(把KV Cache切成固定大小的块,按需分配),或者定期做显存整理、重启回收。
排查显存瓶颈时,不要只看“显存占用率”这一张图,要看框架暴露的缓存命中率、缓存重算率、以及请求级的显存分配日志。缓存重算率一旦超过5%,基本可以断定KV Cache管理有问题,需要调缓存策略或者换框架版本。另外,长上下文请求对显存的放大效应也要提前评估,别拿平均长度去预估容量,要拿P95甚至P99的长度算,否则并发稍高就会被长请求打爆显存。
3.5 CPU-GPU协作与通信开销:多卡场景更容易踩坑
当模型被切到多块GPU上做并行推理时,通信开销会成为新的瓶颈来源。GPU之间的数据交换走高速互连,每传输一次张量都有固定的延迟和带宽成本。并发上来之后,多个请求同时触发通信,互连带宽会被迅速打满,各卡的计算单元反而在空转等待数据。
排查多卡通信瓶颈,主要看两个数据:单卡在执行计算等待通信的时间占比,以及互连链路的带宽占用率。如果算力等待通信的时间占比超过30%,就要考虑减少通信次数——比如把需要频繁交换的层合并成更大的通信块,或者调整并行切分方式。这里有个代价权衡:并行度切得越细,单卡显存压力越小,但通信开销越大。实际部署中,并行度不是越高越好,要拿同一条压测曲线对比不同并行度下的吞吐和时延,找到交叉点。对于7B级别模型,很多时候单卡就能跑,强行切多卡反而得不偿失;只有模型大到单卡放不下或者单卡算力不足时,多卡并行才有意义。
4. 实操记录:一次典型的并发性能劣化排查过程
4.1 压测环境与脚本准备
光讲方法论有点虚,我拿一个实际发生过的排查案例来走一遍完整流程。模拟项目X的背景是:某个内部知识库问答服务接入了LLM,单块数据中心级GPU部署,模型是7B规模的对话模型,推理框架用的是某开源推理框架,最大上下文长度设置为8K。业务方抱怨:白天高峰期经常出现“转圈很久才出第一个字”,偶尔直接返回错误。
第一步先搭压测环境。压测工具选用开源压测工具,脚本里模拟了三种请求:短问答(输入50个字,输出200个字)、中等问答(输入500个字,输出500个字)、长文总结(输入2000个字,输出800个字),比例按真实流量设为3:4:3。并发梯度设为1、5、10、20、30、50,每个档位稳定压5分钟,记录指标。
压测之前我特意做了件小事:把框架的详细访问日志打开,记录每次请求的到达时间、排队开始时间、prefill开始时间、decode开始时间和结束时间。实践证明这个举动价值巨大,后面几乎所有排查结论都是从这些时间点里算出来的。
4.2 第一轮:QPS上不去,先看是不是GPU真的满了
低并发基线先跑出来的数据:并发1时TTFT约180毫秒,TPOT约35毫秒,也就是每秒大约28个Token,这个数字符合7B模型在这张卡上的正常水平。并发加到5,一切正常,TTFT略涨到220毫秒,吞吐在涨。并发加到10,奇怪的事情出现了:QPS几乎不再增长,稳定在每秒钟完成3到4个请求左右,TPOT从35毫秒涨到了48毫秒,但GPU计算单元利用率只有55%左右。
这就出现了我之前说的“GPU没满但性能上不去”的典型情况。按组合指标判别表,方向锁定在框架调度层。进一步看框架日志里的时间点,发现请求从到达开始到真正进入批次,平均等待了1.8秒,排队时间比prefill时间还长。原因很快浮出水面:框架默认的最大批大小是32,但decode阶段的批次实际上一直没有填满,因为请求长度差异大,很多短请求早就结束,批次里面剩下的都是长请求,新请求补充不进来。这是我第一轮得到的结论:瓶颈在连续批处理的填充效率,不在GPU算力。
4.3 第二轮:提高并发后显存OOM,问题在KV Cache
把批大小上限从32调到64,同时打开框架的连续批处理开关,重新压测。这次QPS确实涨了,从3.5到了一定水平,但并发加到20的时候直接出现了显存OOM,整个worker重启,线上服务断了十几秒。
查看显存监控记录,发现进程退出前显存占用率只有75%,不是满了才崩的,而是分配KV Cache时找不到连续的大块显存。这就是典型的显存碎片化问题。解决方法是调整框架的缓存策略,把KV Cache改成按固定块分配,同时限制单个请求的最大上下文长度,避免个别长请求一次性申请超大块显存。调完之后,并发20下OOM消失,显存使用稳定在85%左右。
这轮排查的教训是:OOM不一定等于显存总量不够,很可能是分配策略的问题。遇到OOM先别急着加显存,先看框架日志里申请的大小和位置,再决定是调整缓存策略、限制上下文长度,还是真需要升级硬件。
4.4 第三轮:单请求正常但并发后TTFT抖动剧烈
OOM解决之后继续加压,并发到30时出现了新的症状:TTFT的中位数还算稳定,但TP99从300毫秒直接飙到5秒以上,而且抖动剧烈,忽快忽慢。GPU计算单元利用率还是没到80%,看起来不像算力瓶颈。
把请求时间点日志拉出来按阶段聚合,发现抖动集中在prefill阶段:当一批请求里同时进来几个长文总结请求时,prefill批次的计算量瞬间暴涨,把后续请求的prefill都堵住了。推理框架默认把同一时间到达的请求放进一个批次做prefill,如果这批请求都是长输入,就产生了“长请求堵短请求”的问题。
解决办法是开启框架的prefill和decode分离调度选项:prefill请求攒够一定数量才启动,避免一个大prefill批次卡住所有decode请求;同时设置prefill批次的输入token总量上限,超过就拆分。调整之后,并发30下TTFT的TP99从5秒降回到1.2秒,抖动明显改善。
4.5 定位结论与优化落地
把三轮排查的结论汇总下来:第一轮发现连续批处理填充低效,第二轮发现KV Cache碎片化,第三轮发现prefill和decode互相干扰。这三个问题都不是“GPU不够快”,而是框架调度和显存管理的配置问题。最终优化方案总共四项:
- 打开连续批处理,批大小上限调到64。
- KV Cache改为块级分配并设置上限,限制单请求最大上下文。
- 开启prefill/decode分离调度,限制prefill批次的总输入量。
- 网关层加队列上限和超时熔断,防止上游突发流量把框架压垮。
优化后重新压测:并发50时QPS比初始版本提升了约2.6倍,TP99时延从5秒下降到1.4秒,GPU计算单元利用率从55%提升到接近80%。整个过程没有换硬件,没有改模型,纯靠定位分析就把性能挖出一大截。这个案例可以说明一件事:LLM并发性能问题的根因,绝大多数时候不在模型本身,而在模型周围的调度和资源管理。
5. 高频问题与排查技巧速查
5.1 并发上去了吞吐反而下降
这是LLM服务最经典的反直觉现象。核心原因通常是decode阶段的显存带宽被打满,所有请求同时在读模型参数,带宽分不过来,每个请求的生成速度都下降。排查时先看GPU带宽利用率,再看TPOT是不是随并发同步上涨。如果是带宽问题,方向是降低并发上限、用量化模型减小每次读取的数据量、或者优化算子减少重复读参。
也有可能是批处理策略导致的:静态批处理下批次里一旦有长请求,整个批次释放慢,新请求迟迟进不来,宏观表现就是并发越高吞吐反而下降。这时候要果断确认连续批处理是否真的在生效。
5.2 显存没满但报OOM
显存总量剩余很多却报OOM,九成是碎片化问题,一成是框架内存池管理Bug。解决问题的第一步永远是看框架日志:OOM时申请的内存块多大、最大可用连续块多大。如果是碎片化,就改用块级缓存分配方式,或者定时做显存整理。另外,大批量KV Cache申请和释放是碎片产生的根源,限制单请求上下文长度能在源头减少超大的连续申请。
5.3 Token生成速度远低于预期
单个请求TPOT明显慢于理论值时,先别怀疑GPU型号不行。按经验顺序排查:是不是模型量化位宽太高导致解码效率低;是不是采样阶段做了太多CPU和GPU之间的同步;是不是框架在decode阶段插了额外的安全检查或日志操作;是不是其他请求在共享GPU带宽,用GPU监控工具确认一下即可。从实践看,TPOT异常多数是同步和调度问题,并非真正的算力问题。
5.4 时延分布长尾严重
TP99和TP50差距悬殊,说明存在少数请求被拖住的“堵车”现象。最常驻的三类原因:一类是长输入请求占据了prefill批次,后续短请求被迫等待;一类是请求排队位置靠后,赶上系统微突发,排队时间过长;还有一类是个别慢请求触发了超时重试,重试流量又叠加进系统。定位方法就是分阶段统计时延分布,看漂移出长尾的到底是排队、prefill还是decode阶段,然后对症下药。
5.5 多卡扩展后性能不升反降
多卡并行不是免费的午餐。模型切分粒度太细、通信次数太多、或者互连带宽本身不够,都会导致每加一张卡,通信开销吃掉的收益比加卡带来的算力增量还多。排查时重点看通信时间占比和互连链路带宽占用率。我见过不少案例是模型不大但强行把并行度拉满,结果性能比单卡还差。这里的经验是:模型能在单卡放下且算力够用,就不要切多卡;必须多卡时,对比不同并行度下的实际压测数据,再做决定。
5.6 排查工具清单与日常监控建议
最后整理一份我常用的排查工具清单,按用途分类列一下:
- 系统层监控:操作系统自带性能监控命令,重点看CPU的user、system、wait三类时间占比。
- GPU层监控:GPU厂商监控工具,重点采集计算单元利用率、显存占用、带宽利用率、温度功耗。
- 进程级分析:系统自带的进程追踪工具,排查CPU绑定和上下文切换开销。
- 算子级分析:GPU厂商性能分析套件和推理框架自带的性能分析器,生成算子耗时火焰图。
- 框架指标接口:推理框架自带的监控接口,采集排队数、处理中请求数、TTFT/TPOT分布、KV Cache使用率。
日常监控我建议至少保留这五块看板:QPS和并发度、TTFT和TPOT的中位数/TP99、排队深度、GPU利用率与带宽利用率、KV Cache和显存使用率。不要贪多,五块看板配合告警阈值,足够覆盖90%的并发性能问题。告警阈值可以参考前文说的基线方法:以单请求最优值的1.5倍作为告警线,持续超过3分钟就触发排查。
结尾
说实话,分析LLM并发访问性能瓶颈这件事,真正难的不是用什么高级工具,而是有没有一套清晰的拆解思路。我踩过不少坑之后最大的体会是:永远先量化再优化,先分层再定位,先解决调度和显存问题再考虑换硬件。很多团队一遇到并发性能差就申请加卡,结果加完发现GPU利用率还是上不去,钱白花了。如果你现在正被类似问题困扰,建议先花半天时间把框架访问日志打开、把各项指标压测基线跑出来,再对照这篇文章的排查流程走一遍。最后再分享一个小技巧:每次压测之后把结论写成一段话存档,标上“现象-根因-改动-效果”,积累一段时间之后,你会发现自己判断瓶颈的速度越来越快,很多问题看一眼监控图就知道出在哪一层了。