做大语言模型的可扩展性评估,很多人第一反应是拿压测工具打一轮,看吞吐量多少、延迟多少,然后写个报告就完事。真等你做完几轮,你会发现“可扩展性”这三个字远不是“加机器性能翻倍”那么简单,背后牵扯着显存分配、KV Cache、调度策略、批量推理方式、通信带宽等一系列问题。我最近在一套多卡推理环境上做了一次完整的可扩展性评估,从指标设计到负载模型再到压测实录,中间踩了不少坑,也总结了一些能直接抄作业的方法。这篇就当给同行的一份实践笔记,适合正在做大模型服务选型、容量规划、性能调优的同学参考。
1. 可扩展性评估到底在评什么
1.1 先分清“性能”和“可扩展性”
很多项目把性能测试和可扩展性评估混为一谈,这是第一个要纠正的认知。性能测的是“给定资源下系统能做到多好”,比如单卡情况下每秒能吐多少token,P99延迟是多少。可扩展性测的是“资源增加后能力能不能跟着长”,比如从1张卡扩到2张、4张、8张,吞吐和并发承载能力是线性往上走,还是涨到一定程度就平了、甚至回落。
大语言模型推理服务为什么特别容易出现扩展性不佳的情况?核心原因有三个:显存墙、通信墙、调度墙。显存墙在于模型参数和KV Cache都会吃显存,卡多了缓存可以分散,但每张卡能放多少又受模型并行策略限制;通信墙在于多卡之间要做张量并行、流水线并行,每轮推理都要同步,通信开销会随着卡数增长而放大;调度墙在于推理引擎的调度策略未必能充分利用多卡算力,连续批处理(continuous batching)的窗口设计不合理,就会出现算力空转。
我自己习惯用一个直观指标判断扩展性:扩展效率。假设单实例的吞吐是T1,n个实例的理想吞吐是T1×n,实际总吞吐是Tn,那么扩展效率就是Tn÷(T1×n)。如果4实例扩展效率低于60%,基本可以认为扩展方案有瓶颈,要么是架构选型问题,要么是资源争抢问题,而不是简单地继续加机器就能解决。这个指标在后面的实操部分会反复用到。
1.2 按场景拆解评估目标
可扩展性评估不是一个统一模板,先要搞清楚你的服务跑什么场景,因为场景直接决定评估指标和压测方式。
在线交互场景,比如聊天助手、客服机器人,优先级是延迟,典型指标是TTFT(首Token延迟)和TPOT(每个输出Token的生成时间),同时对尾延迟敏感,P99不能高得太离谱。这种场景下评估可扩展性,重点看的是:并发用户数从100涨到1000,系统能不能保持SLO达标率在99%以上。资源扩展的目标是让每个用户的等待时间不随总负载线性恶化。
离线批量场景,比如批量文档总结、代码分析、离线知识库处理,优先级是吞吐。请求可以排队,单个请求慢一点没关系,关键是单位时间内处理完多少请求、每卡每天能产出多少Token。这种场景下评估可扩展性,重点看的是:从1台机器扩到10台,总吞吐是不是近似线性增长,单请求的平均处理时间有没有因为资源变多反而变差。
还有一种混合场景,线上交互和离线任务共用一套推理集群,这种最麻烦。离线长请求会占住显存和算力,导致在线请求的TTFT剧烈抖动。评估这种场景的可扩展性,就不能只看平均吞吐,还得看资源隔离能力——把在线和离线任务混部时,在线服务质量会不会被离线任务拉垮。如果你遇到的是这种场景,压测设计必须把混合负载考虑进去,否则测出来的数据在真实生产中根本不适用。
1.3 常见的三个评估误区
第一个误区是只测低并发。我见过不少评估报告,并发只测到8、16,看起来吞吐线性上涨,就得出“扩展性很好”的结论。实际上很多推理服务在低并发时表现都不差,因为显存带宽和算力都没到瓶颈。真正考验扩展性的是高并发下的资源竞争,比如并发128、256甚至更高时,KV Cache是否够用、预填充和解码阶段争不争算力。压测设计一定要把并发推到超出预期的峰值水位。
第二个误区是固定输入输出长度。用固定长度请求压测,数据很干净,但和真实业务差距巨大。真实场景里有的请求输入50个token,有的输入5000个token,输出也是长短不一。混合长度请求会直接影响显存复用策略和批处理效率,固定长度测出来的扩展效率会偏高。所以我在负载模型里一定会加入长度分布,这个后面详细说。
第三个误区是忽略预热和冷启动。模型权重加载、CUDA Kernel初始化、显存预留都有一次性开销,特别是首次请求经常会慢得离谱。如果没有预热直接开测,会把正常状态的指标拉偏,甚至怀疑系统有严重问题。评估开始前一定要先跑一段热身流量,让推理引擎的显存分配、调度器、KV Cache池都进入稳定状态,再记数据。
2. 指标口径与负载模型:先定尺子再测数据
2.1 核心指标选型与计算口径
大模型推理评估的指标比较多,但不是每个都要测。我习惯根据评估目标选用一组核心指标,表格里列的是我这次计算使用的口径,大家可以直接参考。
| 指标 | 计算口径 | 用途 |
|---|---|---|
| TTFT | 从请求发起到返回首个Token的时间,包含排队和预填充 | 判断接入体验,在线场景关键 |
| TPOT | 从首个Token到完整序列结束,实际解码期间每个Token的平均生成时间 | 判断生成速度,决定打字感 |
| E2E Latency | 从请求发起到完整返回的总时长 | 综合体验,离线场景更关注 |
| Total Throughput | 单位时间内系统输出的Token总数,包含预填充和解码的全部Token | 判断系统产能 |
| SLO达标率 | 满足给定延迟目标的请求占比,如P95延迟≤2秒 | 判断服务质量是否稳定 |
| 扩展效率 | 实际吞吐增量÷理想吞吐增量 | 判断扩展是否收益正常 |
关于TPOT要提醒一句,不同框架里的口径不完全一样。有些框架把TPOT直接输出为解码阶段每个token的平均耗时,有些则需要你自己算,即(E2E - TTFT)除以输出Token数。不同口径之间差一个首token的预填充时间,在短输出场景下差距很大。做评估报告时务必写清楚口径,否则不同团队之间对不上数,后面做容量规划就会基于错误数据。
分位数的重要性我反复强调过:只看平均值非常危险。一组请求P50延迟1.0秒,P99延迟8秒,均值可能只有1.3秒,看起来挺好,但真实用户遇到8秒延迟时早就流失了。所以我的压测报告里至少包含P50、P90、P95、P99四档分位数,评估SLO时也优先用P95或P99做判定线。
2.2 请求负载怎么设计
负载模型决定评估结果能不能代表生产情况。我这次用了一个比较简单但有效的方案:请求的输入长度按业务历史分布抽样,输出长度也按业务分布抽样,到达模式分两种——固定并发模式和泊松到达模式。
固定并发模式适合测系统容量上限。比如固定200个并发请求,每个请求结束立即发下一个,让系统始终处于满负载状态,测出的是系统在给定并发下的极限能力。泊松到达模式适合模拟真实用户请求的自然到达节奏,平均每秒到达多少个请求,峰谷波动更接近生产环境。我做容量评估时两种都会测:固定并发看极限,泊松到达看稳定性。
具体到请求本身,还需要控制两个东西:输入长度和输出长度。输入长度影响预填充的算力消耗,输出长度影响解码阶段的持续时间和显存占用。如果业务里输入长度是几十到几百token,压测却全用2048token的输入,预填充耗时会被严重放大,TPOT数据也会被带偏。反过来,如果业务需要长输出,压测却只跑短输出,KV Cache的压力就完全没测出来。正确做法是统计线上或模拟数据中的长度分布,按比例生成压测请求。这里没有线上数据的话,可以按经验设定一个偏真实的分布,比如输入20%短文本、60%中文本、20%长文本,并在报告里注明假设条件。
2.3 算清楚扩展效率这笔账
有了单实例基准,就可以设计扩展性对比了。我的做法是:先跑单实例满负载,拿到吞吐T1和时延基线;然后扩展到2实例、4实例、8实例,分别跑同样的负载模型,记录总吞吐Tn和时延曲线。最后画一条“理想线性扩展线”和实际数据曲线,一眼就能看出扩展性是否健康。
这里有个容易忽略的细节:扩展性分为“增加推理实例”和“增加单实例内部卡数”两条路径。增加实例是水平扩展,通过负载均衡分发流量,理论上通信开销小,扩展效率通常较高。增加单实例卡数是垂直扩展,引入张量并行和流水线并行,通信开销明显增幅变大,但单实例处理能力会变强。两者的扩展效率曲线完全不同,必须分开评估。我曾经测过一个方案,水平扩展做到8实例效率还在85%以上,但单实例内部从1卡扩到4卡效率只有60%,原因就是张量并行每增加一张卡,每层Transformer都要做一次全量通信同步,计算量分配和通信比例不匹配。
扩展效率的绝对值没有统一标准,要看你对业务的期待。一般来说,吞吐扩展效率70%以上算可以接受,90%以上算优秀,低于60%就要警惕。当然,如果时延SLO达标才是关键指标,吞吐效率低一些但延迟收益明显,那结论可能也成立。关键是把所有维度的数据摆到一起,算明白成本收益,而不是看到一个数字就下结论。
3. 实操过程:一次完整压测是怎么跑下来的
3.1 部署与监控准备
说完了理论,进入真正做压测的部分。我这次评估的环境是4卡GPU推理机,用某开源大模型部署服务,推理框架开启连续批处理和显存自动预留。第一步不是压测,而是把基础参数定好。
常用参数里最核心的是最大并发序列数(max_num_seqs)和KV Cache显存上限。连续批处理引擎会动态调整批大小,但批太大时单请求等待时间变长,批太小时算力利用率不足。我最初用默认配置跑,发现并发64时TPOT直接飙高,后来把KV Cache预留调大、最大序列数调小,时延曲线才正常。这些参数没有绝对最优,要结合模型大小和业务长度分布来调。
监控方面,建议至少盯四类数据:GPU利用率、显存占用、请求排队长度、正在解码的序列数。压测开始前我会用监控工具记录一个基线,压测中实时观察,压测结束后把时间序列和压测结果对齐。这里给一个很实用的建议:每一轮压测开始前,重启一次推理服务,确保上一轮的显存碎片和缓存状态不污染下一轮数据。对比多轮结果时,千万要保证除被测变量外其他条件一致,否则数据无法横向比较。
3.2 压测步骤全流程
我这次的可扩展性评估完整流程如下:
- 准备3组长度分布不同的压测请求集,分别模拟短文本、中等文本、长文本业务,每组包含相同比例的输入输出长度组合。
- 对单实例启动服务,做一次短时间预热请求,等显存分配稳定、首轮请求完成。
- 依次用16、32、64、128、256并发跑固定并发模式,每轮持续5分钟,收集TTFT、TPOT、吞吐和显存数据。
- 改用泊松到达模式,以平均每秒20、40、80、160个请求的速度跑一轮,持续10分钟,观察时延分布和丢请求情况。
- 扩展到2实例,重复步骤2到4。再用4实例重复,记录所有数据。
- 汇总数据,计算扩展效率,绘制吞吐和时延随并发、实例数的变化曲线。
- 根据瓶颈现象做一两次反向验证实验,比如降低并发观察TPOT是否回落,或者关闭连续批处理对比差异。
压测得到的原始数据量很大,建议直接在整理时只保留关键指标。下面是我这次评估中间一轮的部分结果示例,展示不同并发下单实例的表现,方便说明怎么看数据:
| 并发数 | 总吞吐 (token/s) | TTFT P95 (ms) | TPOT P95 (ms/Token) | GPU利用率 | 显存占用 |
|---|---|---|---|---|---|
| 16 | 320 | 420 | 38 | 76% | 58% |
| 32 | 460 | 590 | 42 | 82% | 63% |
| 64 | 510 | 810 | 55 | 88% | 72% |
| 128 | 480 | 1250 | 73 | 90% | 86% |
| 256 | 430 | 2000 | 95 | 91% | 95% |
这里可以看出一个典型现象:并发从16涨到64时,吞吐持续上涨,但TTFT和TPOT也在涨,说明系统在逼近算力和KV Cache瓶颈。并发超过128后,吞吐不升反降,TTFT大幅恶化,说明请求排队严重,系统过载了。这时加并发没有意义,扩展评估要记录的是“最大有效并发”,也就是还能维持正常服务质量的并发水位,而不是无限压到系统崩溃为止。
扩展到更多实例后,随着总并发路由到不同实例,整体吞吐应该上升。我实测4实例的扩展效率大约78%,没有达到理想的2倍、4倍增长,后来定位发现是负载均衡层的连接数分布不均匀,导致某台实例先饱和。这个案例说明,扩展性好不好不光是推理引擎的事,整个服务链路上的每一个中间层都会影响最终结果。
3.3 从评估结果反推瓶颈
数据测完不是终点,更重要的是解读数据、定位瓶颈。我总结了三类最常见的表现和解法。
第一类:并发升高,TPOT同步恶化,但GPU利用率已经很高。这说明系统已经跑到算力或显存带宽上限。这时扩展通常要减小批大小、限制并发数,或者升级到带宽更高的硬件;如果业务允许,量化模型也能明显改善瓶颈。
第二类:并发升高,吞吐停滞,但GPU利用率不到50%。这种情况典型说明调度或排队有问题。比如max_num_seqs设得过小,请求都卡在队列里,GPU没有数据可算;比如CPU预处理和GPU推理没有重叠,数据搬运成了瓶颈。排查时先看排队序列数是多少,如果队列长期有积压,优先检查调度参数。
第三类:扩展实例后,总吞吐增加不明显,甚至单实例吞吐下降。大概率是负载均衡或通信链路问题。我遇到过某次扩展,4实例总吞吐只有单实例的2.6倍,排查发现负载均衡策略对长连接不友好,大量请求被hash到同一台机器上。后来改成连续请求轮询分发,扩展效率才回到85%以上。
做瓶颈定位时,有个重要原则:一次只变一个变量。很多人压测不达标,随手同时改并发数、改模型量化、改调度参数,结果指标变好了都不知道是哪一步起的作用。我在压测时会把每一项改动单独跑一轮,记录完整数据再对比,宁可多花时间,不能糊里糊涂调参。
4. 常见问题与排查技巧实录
4.1 并发上不去的三个典型原因
压测最常见的问题是“并发一拉高就报错”,或者“请求大量超时”。我遇到的大多数情况,根因不出以下三个。
一是显存OOM或者接近满。KV Cache是按序列数线性增长的,每个序列都有独享的缓存空间,并发数越高,KV Cache占用越大。当显存接近满的时候,推理框架会拒绝新请求,或者频繁驱逐旧项目来腾空间,结果就是请求失败率飙升。排查方法是看监控里的显存曲线,如果显存在高并发时接近100%,先调低max_num_seqs,或者缩小每序列KV Cache的预留上限。
二是调度器频繁抢占。连续批处理框架通常有一种策略:当新请求到达且批已满时,把正在生成的请求暂停,腾位置给新请求做预填充,这个机制叫抢占或交换。抢占发生时,被暂停的请求延迟会显著增加。并发高、新请求持续到达时,抢占可能成为常态,导致整体延迟变得极不可控。排查方式是在日志里看preemption计数,如果抢占频率很高,说明并发配置超过了系统舒适区,应该降低并发或者换用支持优先级队列的策略。
三是CPU侧成为瓶颈。请求预处理、Tokenization、采样、调度决策都消耗CPU。GPU很贵,CPU通常配得不多,高并发时CPU先跑满,GPU反而在等数据。排查方法是看压测机和服务端的CPU利用率,如果服务端CPU利用率长期接近100%,需要升级CPU型号、增加实例数分担CPU负载,或者优化前处理的并行线程数。
这三条排查顺序建议先看显存、再看抢占、最后看CPU,因为显存和抢占问题的定位成本最低,CPU问题往往要折腾配置和架构,放到最后更稳妥。
4.2 时延抖动和长尾问题
另一类高频问题是:压测平均时延看着正常,但P99忽高忽低,甚至某些请求出现数十秒延迟。这类长尾问题在线场景下尤为致命。
我遇到的典型原因有三个。第一是长请求阻塞短请求。连续批处理虽然能把不同请求放在同一个batch里,但输出长度特别长的请求会占据解码槽位很长时间,后续短请求就得排队。缓解办法是在调度器层面做长度感知,或者为短请求单独预留槽位。第二是抢占造成的恢复时间,被抢占的请求需要重新载入KV Cache,这个操作很慢,会让这部分请求变成明显的尾部离群点。第三是监控和日志本身造成干扰,比如大量访问日志同步写盘,磁盘IO瞬时饱和,也会让时延突然升高。
我处理长尾问题的一个经验是:先把请求长度分成短、中、长三类分别统计时延。如果只有长请求有长尾,那是调度和显存问题;如果短请求也有长尾,多半是抢占或系统层面干扰。分桶统计比全量混在一起算P99更能定位问题。
4.3 统计口径造成的坑
最后这一类坑不在系统,在数据统计本身。同样一组压测数据,用不同统计口径,能得出完全相反的结论。
第一个坑是延迟从哪开始算。有的工具TTFT从请求创建时开始计时,包含了连接建立和排队时间;有的只从请求进入服务端开始计时,丢掉了网络和排队。评估SLO时必须统一口径,我建议从客户端发起角度统计端到端TTFT,这样更贴近用户感受,但也要同时记录服务端视角的数据,方便排查两者之差。
第二个坑是时间窗口设置。压测刚开始的前30秒,系统在填充显存和适应负载,数据通常偏慢;压测结束前的最后几秒,调度器可能在排空队列,数据又偏快。如果只用整个4小时压测数据的平均值,这些边缘效应都会被混进去。我会把每轮压测的前30秒和后30秒数据剔除,只用中间稳定段做分析。
第三个坑是失败请求要不要纳入统计。如果压测时发生过载,部分请求超时或报错,这些失败请求是不该计入平均时延的,因为它们没有完整走完流程。但如果不单独记录失败率,只看成功请求的数据,报告会显得过于乐观。正确做法是:报告里同时给出请求成功率和时延分布,失败率过高时,整体结论要打折扣。
我这次做评估有个很深的体会:做可扩展性评估,比数据更重要的是一致性。改动环境参数一定要记录清楚,每次只改一个变量,统计口径写明白,这样数据之间才是可比的。压测不是炫技也不是走流程,而是从资源使用、延迟曲线、吞吐效率里读出一个系统能承担到什么水位。把这个流程跑熟了,后续无论是做容量规划、成本预估,还是判断架构要不要调整,都会轻松很多。