1. 压测结果不可信这件事,远比你想的复杂
做过性能测试的人大概都遇到过这种场景:同一台机器、同一个模型、同一份推理代码,只改了一个开关,两次压测结果却差了十万八千里。更让人抓狂的是,你反复跑几遍,每次数据都不一样,最后连自己都不确定哪个结果才是“真的”。
这不是玄学,这是压测领域最容易被忽视的系统性问题。很多人以为压测就是“跑个脚本、看个QPS”,但实际上,从环境准备到指标采集,从预热策略到结果解读,每一个环节都可能埋着让数据失真的坑。尤其是大模型推理场景,显存分配、批处理调度、KV Cache复用这些机制交织在一起,一个看似无关紧要的开关,可能直接改变整个系统的行为模式。
这篇文章要聊的,就是压测结果为什么不可信、哪些因素在背后捣鬼、以及怎么一步步把可信度拉回来。适合正在做模型推理性能测试的工程师、需要出压测报告的技术负责人,以及任何对“数据说话”这件事有执念的人。不管你是刚接触压测的新手,还是已经踩过几轮坑的老手,下面这些内容应该都能帮你少走一些弯路。
2. 压测可信度的核心影响因素拆解
2.1 为什么“同一模型、同一机器”不等于“同一条件”
很多人下意识觉得,只要模型权重一样、机器配置一样,压测条件就是一样的。这个假设在传统Web服务压测里勉强成立,但在大模型推理场景下,它几乎必然失效。
原因在于,大模型推理的性能不仅取决于“静态配置”,还高度依赖“运行时状态”。举几个最典型的例子:
- 显存碎片化程度:第一次跑和第二次跑,显存分配器面对的内存布局完全不同。即使总显存足够,碎片化也可能导致某次推理触发OOM或者被迫使用更慢的分配路径。
- KV Cache的命中率:如果压测请求之间存在前缀重叠,KV Cache的复用率会显著影响吞吐。而缓存状态是跨请求累积的,第一次跑和第十次跑的缓存命中率可能天差地别。
- GPU频率与温度状态:连续压测会让GPU温度上升,触发降频。第一次跑的Boost频率和第五次跑的稳定频率可能差出10%以上。
- 操作系统页缓存:模型文件第一次加载需要从磁盘读取,第二次可能已经在页缓存里了。这个差异在冷启动场景下能造成数倍的加载时间差距。
所以,“同一模型、同一机器”只是必要条件,不是充分条件。真正决定压测可比性的,是运行时状态的一致性。而那个“只改了一个开关”,恰恰可能打破了这种一致性。
2.2 那个“开关”到底改了什么:从批处理策略说起
标题里说的“只改一个开关”,在实际场景中通常对应以下几类配置:
| 开关类型 | 典型参数 | 影响维度 |
|---|---|---|
| 批处理策略 | max_batch_size、dynamic_batching | 吞吐量、延迟分布 |
| 显存分配 | preallocate、memory_pool | 首token延迟、OOM概率 |
| 计算精度 | fp16/bf16/int8 | 吞吐、显存占用、输出质量 |
| 调度策略 | continuous_batching、iteration_level | 并发能力、尾延迟 |
| 缓存机制 | prefix_caching、kv_cache_reuse | 吞吐、首token延迟 |
这些开关之所以“小”,是因为它们在配置文件里往往只是一行布尔值或者一个数字。但它们对系统行为的影响是非线性的。比如把max_batch_size从8改成16,理论上吞吐应该翻倍,但实际上可能因为显存不足导致部分请求被拒绝,或者因为批处理延迟增加导致尾延迟飙升。
更隐蔽的是,有些开关会改变请求的排队和调度顺序。在压测场景下,请求的到达模式是人为构造的,如果调度策略变了,同样的请求序列可能被完全不同的顺序处理,最终统计出来的P99延迟自然对不上。
2.3 压测工具本身的“隐形偏差”
除了被测系统,压测工具本身也可能引入偏差。常见的坑包括:
- 客户端瓶颈:压测机CPU跑满、网络带宽打满,导致发出的请求速率不稳定。这时候你测到的其实是压测工具的上限,不是被测系统的上限。
- 时间统计精度:不同工具对“延迟”的定义不同。有的从发送请求开始计时,有的从收到第一个字节开始。在流式输出场景下,这个差异能造成数量级的误判。
- 连接复用策略:每次请求新建连接 vs 复用长连接,对服务端的压力完全不同。如果两次压测一个用短连接一个用长连接,结果没有可比性。
- 请求内容差异:输入长度、输出长度、是否流式,这些都会影响推理耗时。如果两次压测的请求分布不同,结果自然不同。
我见过一个典型案例:两次压测用的是同一个脚本,但第二次跑的时候压测机同时在做其他事情,导致请求发送间隔抖动变大。最后测出来的P99延迟比第一次高了40%,排查了半天才发现是压测机自己的问题。
3. 让压测结果可信的实操方法论
3.1 环境隔离与状态重置的标准流程
要让压测结果可信,第一步就是保证每次压测的初始状态一致。下面是我在实际项目中总结的一套流程:
硬件状态检查清单:
- GPU温度降到基线以下(通常待机5-10分钟)
- 确认没有其他进程占用GPU(用nvidia-smi反复确认)
- 检查GPU频率是否锁定(如果支持,建议锁定频率避免动态调频)
- 确认CPU governor设置为performance模式
- 检查内存和显存没有残留占用
软件状态重置清单:
- 重启推理服务进程,确保显存完全释放
- 清理页缓存(echo 3 > /proc/sys/vm/drop_caches)
- 确认模型加载路径一致,避免缓存差异
- 重置压测工具的连接池和统计计数器
- 如果涉及KV Cache,确认缓存已清空
这套流程看起来繁琐,但它是压测可比性的基础。我通常会把它们写成一个脚本,每次压测前自动执行,避免人为遗漏。
注意:清理页缓存需要root权限,在生产环境慎用。如果无法清理,至少要在报告中注明“页缓存状态未重置”,让读者知道这个偏差存在。
3.2 预热策略:跑多少轮才能拿到稳定数据
预热是压测中最容易被敷衍的环节。很多人跑一轮就开始记录数据,结果第一轮的数据明显偏离后续轮次。预热的目的,是让系统进入稳态。
对于大模型推理服务,稳态通常需要满足以下条件:
- GPU频率稳定(不再因为温度变化而降频)
- 显存分配器进入稳定状态(不再频繁向系统申请新内存)
- KV Cache达到稳定命中率(如果请求有前缀重叠)
- 服务端的线程池、连接池达到稳定数量
根据我的经验,预热轮次至少需要3-5轮,每轮持续时间不少于30秒。如果模型较大或者请求模式复杂,可能需要10轮以上。判断是否进入稳态的方法很简单:连续两轮的吞吐量差异小于2%,P99延迟差异小于5%,就可以认为进入稳态了。
这里有个实操技巧:把每轮的统计数据都打出来,画成趋势图。如果看到吞吐量还在爬升或者延迟还在下降,说明还没到稳态,继续跑。如果数据开始上下波动但没有明显趋势,说明已经进入稳态区间,可以开始记录正式数据了。
3.3 指标采集:哪些数据必须记录,哪些是噪音
压测报告里最常见的问题是“指标一大堆,但关键信息缺失”。我建议把指标分成三类:
核心指标(必须记录):
- 吞吐量:requests/s、tokens/s(区分输入和输出)
- 延迟:P50、P90、P99、P999(区分首token延迟和每token延迟)
- 错误率:超时、OOM、拒绝连接等分类统计
- 资源利用率:GPU利用率、显存占用、CPU利用率、网络带宽
辅助指标(建议记录):
- 批处理大小分布:实际运行的batch size直方图
- 队列深度:请求在服务端排队的长度变化
- KV Cache命中率:如果适用
- GPU频率和温度:用于判断是否降频
噪音指标(谨慎解读):
- 平均值:容易被极端值拉偏,参考价值有限
- 单次采样值:波动大,不能代表整体
- 压测工具自身的CPU/内存占用:除非怀疑工具瓶颈,否则不用太关注
采集频率也很关键。对于持续几分钟的压测,建议至少每秒采集一次,这样才能捕捉到瞬态波动。如果采集间隔太长,P99延迟可能被平滑掉,你看到的“稳定”其实是假象。
3.4 结果对比:如何判断差异是真实还是随机
拿到两组压测数据后,怎么判断差异是真实的性能变化还是随机波动?我通常用以下方法:
方法一:重叠区间判断
如果两组数据的P99延迟置信区间有重叠,且重叠部分超过30%,那么差异可能不显著。置信区间可以用bootstrap方法计算,不需要假设分布。
方法二:多轮重复实验
同一配置跑3-5轮,看轮间差异。如果轮间差异和配置间差异相当,说明配置的影响被噪音淹没了。这时候需要增加轮次或者延长每轮时间。
方法三:效应量计算
不要只看相对差异,还要看绝对差异。比如QPS从1000降到990,相对差异1%,但如果你的业务要求是±0.5%,那这个差异就是显著的。效应量要结合业务容忍度来判断。
方法四:控制变量复核
如果发现某个开关导致了显著差异,先别急着下结论。把开关改回去再跑一轮,看差异是否复现。如果复现不了,可能是其他因素在捣鬼。
下面是一个简单的对比表格模板,我通常用它来整理压测结果:
| 指标 | 配置A(开关关) | 配置B(开关开) | 差异 | 是否显著 |
|---|---|---|---|---|
| QPS | 1250 ± 30 | 1180 ± 25 | -5.6% | 是 |
| P99首token延迟 | 85ms ± 5ms | 92ms ± 6ms | +8.2% | 是 |
| P99每token延迟 | 12ms ± 1ms | 12.5ms ± 1.2ms | +4.2% | 否 |
| GPU利用率 | 92% | 89% | -3% | 待定 |
这张表能帮你快速判断哪些指标的变化是可信的,哪些可能只是噪音。
4. 常见问题与排查技巧实录
4.1 压测结果波动大的排查思路
波动大是最常见的问题,排查时按以下顺序逐层排除:
第一层:压测工具侧
- 压测机CPU是否跑满?用top或htop确认
- 网络带宽是否打满?用iftop或nload确认
- 压测工具是否报了连接错误或超时?查看工具日志
- 请求发送速率是否稳定?看工具的输出统计
第二层:服务端侧
- GPU利用率是否稳定?用nvidia-smi -l 1持续观察
- 显存占用是否持续增长?可能是内存泄漏
- 服务端日志是否有OOM或调度异常?
- 批处理大小是否波动很大?
第三层:环境侧
- 是否有其他进程在争抢资源?
- 温度是否导致降频?查看GPU频率历史
- 网络是否有丢包或重传?用netstat或ss查看
我遇到过一次典型的波动问题:压测结果忽高忽低,排查了半天发现是压测机和被测机之间的交换机端口有错误包。换了个端口就稳定了。这种底层问题最容易被忽视,但排查起来也最简单——看网卡统计就知道了。
4.2 首token延迟与吞吐量互相矛盾的解读
这是大模型推理压测中最常见的“矛盾”:开了某个优化开关后,吞吐量上去了,但首token延迟也上去了。这时候怎么判断这个开关该不该开?
关键看业务场景。如果是离线批处理,吞吐量优先,首token延迟高一点无所谓。如果是实时对话,首token延迟直接决定用户体验,吞吐量再高也没用。
但还有一种情况:吞吐量和首token延迟同时变差。这通常意味着开关引入了额外的开销,比如更复杂的调度逻辑或者更频繁的显存分配。这时候需要看P99而不是平均值,因为平均值可能掩盖了尾部的严重退化。
我的一般原则是:先看P99,再看P50,最后看平均值。P99代表最差情况下的用户体验,P50代表典型情况,平均值只用来做粗略对比。
4.3 显存不足导致的“假性能下降”
显存不足不一定会报OOM,它可能以更隐蔽的方式影响性能:
- 显存分配器被迫使用更慢的路径(比如从cudaMalloc变成cudaMallocAsync)
- 部分层被换出到CPU内存,导致推理速度骤降
- 批处理大小被动态调小,吞吐量下降但延迟看起来正常
排查方法:在压测过程中持续监控显存占用和分配次数。如果显存占用接近上限,且分配次数异常增加,基本可以确定是显存压力导致的性能下降。
解决办法:要么减小批处理大小,要么优化显存使用(比如启用KV Cache量化),要么换更大显存的卡。没有银弹,只能根据实际情况取舍。
4.4 压测脚本本身的性能陷阱
压测脚本写不好,测出来的数据全是假的。常见的脚本陷阱包括:
- 同步阻塞:每发一个请求就等响应,导致并发上不去。应该用异步IO或者多线程。
- JSON解析开销:在压测循环里解析响应JSON,CPU开销可能超过被测系统。建议只统计字节数,不解析内容。
- 日志打印:每请求打印一行日志,IO开销巨大。压测时应该关闭或降低日志级别。
- 内存泄漏:脚本本身内存持续增长,跑久了触发GC或者OOM。要定期检查脚本的内存占用。
我通常会在正式压测前,先用小规模请求跑一遍脚本,确认脚本本身的CPU和内存占用在可接受范围内。如果脚本CPU占用超过50%,那压测结果基本不可信。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 结果波动大 | 压测机瓶颈 | 看压测机CPU/网络 | 换压测机或降低速率 |
| 结果波动大 | GPU降频 | 看GPU频率历史 | 锁定频率或加强散热 |
| 吞吐上不去 | 批处理太小 | 看batch size分布 | 调大max_batch_size |
| 首token延迟高 | 显存不足 | 看显存占用 | 减小batch或优化显存 |
| P99异常高 | 请求排队 | 看队列深度 | 增加并发或优化调度 |
| 两次结果不可比 | 状态未重置 | 检查预热和清理流程 | 严格执行重置流程 |
| 错误率突然升高 | 连接池耗尽 | 看服务端连接数 | 调大连接池或降低并发 |
5. 从压测数据到可信结论的最后一公里
5.1 统计显著性与业务显著性的平衡
压测数据有了,差异也看到了,但“差异是否重要”是另一个问题。统计显著性告诉你“差异不是随机产生的”,业务显著性告诉你“差异是否值得关注”。
举个例子:某个开关让P99延迟从100ms降到98ms,统计上显著(因为样本量大),但业务上可能完全不重要(用户感知不到2ms的差异)。反过来,如果P99从100ms升到150ms,即使统计上不显著(样本量小),业务上也可能很严重。
我的做法是:先看业务容忍度,再看统计显著性。如果差异在业务容忍范围内,统计再显著也不值得跟进。如果差异超出业务容忍度,即使统计不显著,也要增加样本量确认。
5.2 压测报告的“可信度声明”
一份负责任的压测报告,应该包含“可信度声明”,明确告诉读者哪些结论是可靠的,哪些存在不确定性。我通常会在报告开头写一段类似这样的话:
本报告数据基于X轮预热后的Y轮正式压测,每轮持续Z秒。环境状态已按标准流程重置。但需要注意:GPU频率未锁定,可能存在±5%的频率波动;页缓存未清理,首次加载可能偏慢。因此,吞吐量数据的置信区间为±3%,延迟数据的置信区间为±8%。跨配置对比时,请优先关注差异超过置信区间的指标。
这段话看起来是“免责声明”,但实际上它大大提升了报告的可信度。因为它展示了你对压测局限性的清醒认识,而不是盲目给出一个“精确”的数字。
5.3 持续压测与回归测试的落地建议
单次压测的可信度问题解决了,但性能回归测试是持续的过程。我的建议是:
- 建立基线:在代码稳定期跑一组基准数据,作为后续对比的参考。
- 自动化流程:把环境重置、预热、压测、数据采集、报告生成全部脚本化,减少人为误差。
- 定期校准:每隔一段时间重跑基线,确认环境没有发生漂移。
- 异常告警:设置性能阈值,超过阈值自动告警,而不是等人去看报告。
这套机制跑起来后,你会发现压测不再是“一次性任务”,而是持续保障性能的手段。每次代码变更后跑一轮回归,几分钟就能知道有没有性能退化,比等到线上出问题再排查高效得多。
5.4 一个真实案例的完整复盘
最后分享一个我亲身经历的案例。某次优化中,我们给推理服务加了一个“连续批处理”开关。第一次压测显示吞吐量提升了30%,团队很兴奋。但第二次压测(换了台机器)显示吞吐量只提升了5%。第三次压测(同一台机器但不同时间)显示吞吐量反而下降了10%。
排查过程:
- 确认三次压测的脚本、请求分布、预热轮次完全一致。
- 检查GPU频率历史,发现第三次压测时GPU温度偏高,频率降了8%。
- 检查显存占用,发现第二次压测时显存碎片化严重,批处理大小被动态调小。
- 检查压测机,发现第一次压测时压测机负载较低,后两次有其他任务在跑。
最终结论:连续批处理开关确实能提升吞吐量,但提升幅度受环境状态影响很大。在理想条件下(GPU满频、显存充足、压测机空闲),提升约25-30%;在一般条件下,提升约10-15%;在恶劣条件下,可能没有提升甚至下降。
这个案例的教训是:压测结果必须附带环境状态说明。脱离环境谈性能数字,就像脱离剂量谈毒性,都是耍流氓。
后来我们把环境状态检查做成了压测流程的强制步骤,每次压测前自动采集GPU频率、显存碎片率、压测机负载等指标,附在报告里。这样即使数据有波动,读者也能判断波动的原因,而不是简单地说“这次结果不可信”。
压测这件事,说到底是在和系统的复杂性打交道。你越想得到一个“干净”的数字,就越需要把那些不干净的变量控制住。控制不住的部分,就诚实地标注出来。可信的压测报告不是没有误差的报告,而是误差被清晰界定和解释的报告。