1. 压测结果不可信的根源拆解
1.1 一个让很多人踩坑的真实场景
同一台机器、同一个模型文件、同一份推理代码,只改了一个开关——比如把批处理大小从1改成8,或者把量化精度从FP16切到INT8——压测出来的吞吐量差了将近一倍。更诡异的是,两次测试的GPU利用率都显示在90%以上,显存占用也差不多,日志里没有任何报错。这时候你拿着两份报告,根本不知道哪份该信。
这个问题在模型推理压测里非常普遍,尤其是做性能基准测试、容量规划、SLA验证的时候。很多人以为“控制变量”就是控制硬件和模型,实际上压测的可信度取决于一整套隐藏变量是否被真正锁定。只改一个开关,恰恰意味着这个开关背后牵连着一连串运行时行为的改变,而这些改变往往不会体现在你监控的那几个指标上。
这篇文章面向的是做过模型推理压测、但发现结果对不上、或者准备做基准测试的工程师。我会把“为什么只改一个开关结果仍不可信”这件事拆开,从压测设计、运行时行为、指标采集、环境干扰四个层面讲清楚,并给出可复现的排查方法和实操建议。
1.2 压测可信度的三个前提条件
在讨论具体原因之前,先把“可信”的定义说清楚。一份可信的压测结果,至少满足三个条件:
- 可复现:同样的配置、同样的输入、同样的机器,重复跑三次,核心指标的波动在可接受范围内(通常要求P99延迟波动小于5%,吞吐量波动小于3%)。
- 可解释:吞吐量、延迟、资源利用率之间的关系能自洽。比如吞吐上去了,延迟应该基本持平或略升,而不是吞吐翻倍但延迟反而降了。
- 可归因:性能变化能对应到具体的配置项或代码路径。如果改了A但性能变了,却找不到A和性能之间的因果链,那这个结果就不能用来做决策。
只改一个开关导致结果不可信,本质上就是这三个条件里至少有一个被破坏了。而破坏它的,往往不是那个开关本身,而是开关背后被连带改变的东西。
1.3 为什么“只改一个开关”反而更危险
控制变量法的逻辑是:只改一个变量,其他不变,那么结果差异就归因于这个变量。这个逻辑在物理实验里成立,因为物理系统的变量是独立的。但在软件系统里,尤其是模型推理这种涉及多层抽象的系统里,变量之间高度耦合。
你改的是批处理大小,但实际被改变的可能包括:内存分配器的行为、CUDA kernel的调度策略、请求队列的排队方式、甚至CPU和GPU之间的数据传输模式。这些连带变化不会出现在你的配置对比表里,但会实实在在影响压测结果。
更危险的是,正因为你“只改了一个开关”,你会下意识地认为其他条件都一样,从而对结果差异给出错误的归因。比如把吞吐差异归因于批处理效率,实际上可能是内存碎片导致的隐性等待。这种错误归因一旦写进容量规划报告,后果比“不知道”更严重。
2. 隐藏变量:那些开关背后被连带改变的东西
2.1 批处理大小改变引发的连锁反应
批处理大小是压测里最常被调整的开关之一。表面上看,它只控制一次前向传播处理多少条请求。但实际上,它至少影响以下五个方面:
内存分配模式。批处理从1变到8,中间激活值的内存需求变成原来的8倍。如果框架用的是动态内存分配,分配器会从“小块频繁分配”模式切换到“大块预留”模式。这两种模式的内存碎片程度、分配延迟、甚至是否触发显存整理,都完全不同。你看到的吞吐提升,可能有一部分来自分配器行为的变化,而不是批处理本身的计算效率。
Kernel调度策略。GPU上的计算是以kernel为单位调度的。批处理大小改变后,每个kernel的grid和block维度都会变。对于小批处理,kernel可能因为并行度不足而无法占满SM;对于大批处理,kernel可能因为寄存器压力或共享内存限制而降低occupancy。这些变化不会体现在“GPU利用率”这个笼统指标里,但会直接影响实际算力利用。
请求排队行为。压测工具通常有请求队列。批处理大小改变后,凑够一个batch所需的时间变了,队列的等待时间分布也跟着变。如果你测的是端到端延迟,这个排队时间会被算进去,导致延迟指标不可比。
CPU-GPU数据传输。批处理越大,单次传输的数据量越大,传输次数越少。PCIe带宽的利用模式从“频繁小包”变成“稀疏大包”,传输效率完全不同。如果压测里包含数据预处理,CPU侧的批处理逻辑也会跟着变。
功耗和频率。大批处理会让GPU更长时间处于高负载状态,触发不同的功耗管理策略。GPU频率可能从boost频率降到base频率,或者相反。这个变化会直接影响计算速度,而且不同批处理下的频率行为可能完全不同。
2.2 量化精度切换带来的精度与速度博弈
把FP16切成INT8,看起来只是数值精度的变化,实际上整个计算图都被改写了。
Kernel实现完全不同。FP16的矩阵乘和INT8的矩阵乘在GPU上用的是不同的指令集和kernel实现。INT8通常用DP4A或Tensor Core的整数路径,FP16用FP16 Tensor Core。两者的吞吐特性、延迟特性、对矩阵形状的敏感度都不一样。你不能用FP16下的最优批处理大小去套INT8。
量化校准的隐性成本。很多框架在第一次推理时会做量化校准或kernel自动调优。这个过程可能耗时几百毫秒到几秒,如果压测没有预热或者预热不充分,这部分成本会被算进延迟指标,导致结果偏高。更隐蔽的是,校准结果可能被缓存,第二次跑的时候就没有这个成本了,导致两次压测的“同一配置”实际上跑的是不同代码路径。
精度损失带来的分支变化。某些模型在INT8下会出现数值溢出或下溢,触发框架的fallback逻辑,把部分算子退回FP16执行。这个fallback是动态的,取决于输入数据的分布。如果两次压测的输入数据不同,fallback的比例就不同,性能自然不可比。
内存带宽压力变化。INT8的权重占用的显存是FP16的一半,理论上内存带宽压力更小。但如果你的模型是计算密集型的,这个优势可能体现不出来;如果是内存密集型的,优势会很明显。问题在于,你很难在压测前判断模型属于哪一类,只能实测。而实测结果又受其他变量干扰。
2.3 线程数与并发度调整的蝴蝶效应
线程数和并发度是另一个常被调整的开关。这两个参数看似独立,实际上和批处理大小、内存分配、调度策略都有耦合。
线程数改变内存访问模式。多线程推理时,每个线程有自己的内存空间或共享内存池。线程数变化会改变内存访问的局部性,进而影响缓存命中率。缓存命中率的变化不会直接体现在吞吐量上,但会通过CPU等待时间影响端到端延迟。
并发度和批处理的交互。并发度决定同时有多少个请求在系统里,批处理决定每次计算处理多少请求。如果并发度小于批处理大小,批处理永远凑不满,实际批处理大小等于并发度。这时候你调整批处理大小,对性能没有任何影响,但你会以为“批处理不敏感”,得出错误结论。
线程调度和CPU亲和性。多线程环境下,操作系统会把线程调度到不同的CPU核心上。如果核心之间的缓存不共享,线程间的数据交换会走内存,延迟大幅增加。线程数改变后,调度模式变了,这个延迟也跟着变。更麻烦的是,这个变化在不同负载下表现不同,低负载时可能看不出来,高负载时才暴露。
2.4 框架版本与编译选项的隐性影响
有时候你确实只改了一个开关,但框架在背后做了不同的事。
JIT编译和算子融合。很多推理框架会对计算图做即时编译和算子融合。融合策略取决于输入形状、批处理大小、甚至内存对齐情况。批处理大小改变后,融合策略可能完全不同,生成的kernel数量和类型都变了。你对比的是两个不同的计算图,而不是同一个图的不同配置。
自动调优缓存。cuDNN、TensorRT这类库有自动调优机制,会针对具体的输入形状和硬件配置搜索最优kernel。调优结果被缓存在本地。如果你第一次跑的时候缓存是空的,第二次跑的时候缓存已经有了,两次的实际执行路径不同。更隐蔽的是,缓存可能因为显存压力被驱逐,导致第三次跑又回到未调优状态。
编译选项的默认值变化。框架版本升级或编译选项改变后,某些默认值可能变了。比如默认的数学精度、默认的内存对齐、默认的并行策略。这些变化不会出现在你的配置对比里,但会影响性能。
3. 指标采集:你看到的可能不是真实发生的
3.1 GPU利用率的欺骗性
GPU利用率是压测报告里最常见的指标,也是最容易被误读的指标。nvidia-smi显示的“GPU利用率”实际上是“在过去一个采样周期内,GPU至少有一个kernel在执行的時間比例”。它不告诉你kernel执行得是否高效,也不告诉你SM的占用率是多少。
一个kernel如果只用了10%的SM,但一直在执行,GPU利用率也会显示100%。反过来,如果kernel执行很快但之间有间隙,利用率可能显示70%,但实际算力利用可能更高。批处理大小改变后,kernel的执行模式和间隙分布都变了,用GPU利用率来对比两次压测,基本没有意义。
更有用的是SM占用率、Tensor Core利用率、内存带宽利用率这些细粒度指标。但这些指标通常需要profiling工具才能拿到,而且profiling本身会引入开销,影响压测结果。
3.2 延迟指标的统计陷阱
延迟指标最常见的陷阱是平均值。平均延迟低不代表用户体验好,因为少数慢请求可能被平均值掩盖。压测报告里应该看P50、P90、P99甚至P999。
但即使看P99,也有陷阱。P99的计算依赖于样本数量。如果两次压测的请求数不同,P99的可比性就存疑。更隐蔽的是,延迟的分布可能不是单峰的。批处理大小改变后,延迟分布可能从单峰变成双峰,P99可能落在两个峰之间的低谷,完全不能代表慢请求的真实延迟。
还有一个陷阱是延迟的测量点。端到端延迟包括网络传输、排队、预处理、推理、后处理。如果你只测了推理部分的延迟,但批处理大小改变了排队时间,端到端延迟的变化可能主要来自排队,而不是推理本身。
3.3 吞吐量指标的口径问题
吞吐量通常用QPS或tokens/s表示。但QPS的口径可能不同:是成功请求的QPS,还是包括失败请求?是客户端视角的QPS,还是服务端视角的QPS?如果压测工具本身成为瓶颈,客户端视角的QPS会低于服务端实际处理能力。
批处理大小改变后,压测工具的发送模式可能变了。小批处理下,压测工具需要更频繁地发送请求,如果工具本身有性能问题,会成为瓶颈。大批处理下,压测工具可能一次发送大量数据,网络带宽成为瓶颈。这两种情况下测到的吞吐量,都不是模型推理的真实吞吐量。
3.4 资源监控的采样频率问题
资源监控的采样频率直接影响你看到的数据。如果采样间隔是1秒,而kernel执行时间是100毫秒,你可能会错过很多细节。批处理大小改变后,kernel执行时间变了,同样的采样频率可能捕捉到完全不同的模式。
更麻烦的是,不同监控工具的采样频率不同。nvidia-smi默认是1秒,dcgm可以到100毫秒,profiling工具可以到微秒级。用不同工具的数据做对比,本身就不严谨。
4. 环境干扰:那些你以为不变的东西
4.1 机器状态的漂移
同一台机器,今天和明天的状态可能完全不同。GPU温度、CPU频率、内存碎片、磁盘缓存、后台进程,这些都在变化。
GPU热节流。GPU在高负载下会发热,温度达到阈值后会自动降频。第一次压测时GPU是冷的,第二次压测时GPU是热的,频率可能差10%以上。这个差异足以让吞吐量结果不可比。解决办法是每次压测前让GPU冷却到相同温度,或者监控频率并记录。
CPU频率调节。现代CPU有复杂的频率调节策略,受负载历史、温度、功耗限制影响。压测前的空闲时间不同,CPU的初始频率状态就不同。批处理大小改变后,CPU负载模式变了,频率调节策略也跟着变。
内存碎片。长时间运行的机器,内存碎片程度会变化。碎片会影响大块内存的分配效率,进而影响批处理相关的内存操作。重启机器可以重置碎片状态,但重启本身也会改变其他状态。
后台进程。操作系统和后台服务会不定期占用CPU、内存、磁盘、网络。这些干扰在压测期间可能恰好发生,导致结果异常。更隐蔽的是,某些后台进程的触发条件和压测负载相关,比如日志轮转、监控上报、缓存刷新。
4.2 网络和存储的波动
如果压测涉及网络请求或磁盘读取,网络和存储的波动会直接影响结果。
网络抖动。即使是在同一台机器上,如果压测工具和服务之间走localhost网络,网络协议栈的处理时间也会波动。批处理大小改变后,网络包的尺寸和频率变了,协议栈的行为也跟着变。
磁盘缓存。如果模型文件或输入数据从磁盘读取,第一次读取和第二次读取的耗时完全不同。第一次是冷读,第二次是热读。如果两次压测的缓存状态不同,结果不可比。解决办法是每次压测前清空缓存,或者确保数据完全在内存中。
NUMA效应。多路CPU的机器上,内存访问延迟取决于CPU和内存的物理位置。如果压测进程被调度到不同的NUMA节点,内存访问延迟会差很多。批处理大小改变后,内存分配模式变了,NUMA效应的影响也跟着变。
4.3 容器和虚拟化层的干扰
如果压测跑在容器或虚拟机里,还有额外的干扰源。
CPU配额和限制。容器可以设置CPU配额,限制进程能使用的CPU时间。批处理大小改变后,CPU使用模式变了,可能触发配额限制,导致性能下降。这个下降不是批处理本身的问题,而是配额机制的问题。
内存限制和OOM。容器可以设置内存限制。大批处理需要更多内存,可能接近限制,触发内存回收或OOM killer。即使没有OOM,内存回收本身也会消耗CPU时间,影响性能。
cgroup调度。cgroup的CPU调度策略和裸机不同,线程的调度延迟可能更大。批处理大小改变后,线程的唤醒频率变了,cgroup调度的影响也跟着变。
虚拟化开销。虚拟机里的系统调用、中断处理、内存访问都有额外开销。这些开销在不同负载下表现不同,批处理大小改变后,开销模式也变了。
5. 实操排查:如何让压测结果可信
5.1 压测前的环境锁定清单
在开始压测之前,把以下状态记录下来并尽量锁定:
| 检查项 | 锁定方法 | 记录内容 |
|---|---|---|
| GPU温度 | 压测前空载冷却至相同温度 | 起始温度、压测中最高温度 |
| GPU频率 | 锁定频率或记录频率曲线 | 核心频率、显存频率 |
| CPU频率 | 锁定为performance模式 | 各核心频率 |
| 内存状态 | 重启机器或记录碎片程度 | 可用内存、大页使用情况 |
| 后台进程 | 关闭非必要服务 | 进程列表、资源占用 |
| 磁盘缓存 | 清空缓存或预热 | 缓存命中率 |
| 网络状态 | 使用本地回环或专用网络 | 延迟、带宽 |
| 容器配额 | 记录并尽量放宽限制 | CPU配额、内存限制 |
这个清单看起来繁琐,但做过几次之后就会形成习惯。关键是每次压测都记录,这样即使有干扰,也能在分析时排除。
5.2 预热策略:跑多少次才能开始记录
预热是压测里最容易被忽视的环节。预热的目的不是让机器“热起来”,而是让所有缓存、调优、分配器状态达到稳定。
预热轮次。通常建议至少预热3轮,每轮跑完整的数据集。第一轮触发JIT编译和自动调优,第二轮让缓存和分配器稳定,第三轮确认指标已经收敛。从第四轮开始记录数据。
预热指标判断。怎么知道预热够了?看核心指标的波动。如果连续两轮的吞吐量差异小于1%,延迟P99差异小于2%,可以认为已经稳定。如果还在波动,继续预热。
预热和正式压测的配置必须一致。预热时用的批处理大小、并发度、输入数据,必须和正式压测完全相同。否则预热的效果无法延续到正式压测。
注意预热本身的开销。预热会消耗时间和资源,如果压测环境是按时间计费的,预热成本需要考虑。但省预热时间导致结果不可信,代价更大。
5.3 指标采集的正确姿势
多粒度采集。不要只看GPU利用率和平均延迟。至少采集以下指标:
- 吞吐量:QPS、tokens/s,区分成功和失败
- 延迟:P50、P90、P99、P999,以及延迟分布直方图
- GPU:SM占用率、Tensor Core利用率、显存带宽利用率、显存占用
- CPU:各核心利用率、上下文切换次数、缓存命中率
- 内存:分配延迟、碎片程度、大页使用
- 功耗:GPU功耗、CPU功耗、整机功耗
采样频率匹配。采样频率至少是kernel执行时间的1/10。如果kernel执行100毫秒,采样间隔不要超过10毫秒。dcgm可以到100毫秒,profiling工具可以到微秒级。根据压测精度要求选择。
记录原始数据。不要只记录汇总后的指标,原始时间序列数据也要保存。这样在分析时可以重新计算,发现汇总时被掩盖的问题。
多次重复。每个配置至少跑3次,取中位数或均值。如果3次差异很大,说明有未控制的变量,需要排查。
5.4 结果对比的正确方法
同配置重复性验证。在对比不同配置之前,先用同一配置跑3次,确认波动范围。如果同配置的波动就超过5%,那不同配置的对比没有意义,先解决波动问题。
交叉验证。不要按顺序跑配置A、B、C,而是按A、B、C、A、B、C的顺序跑多轮。这样可以排除时间相关的干扰,比如温度漂移、后台任务周期。
统计显著性。如果两次结果的差异小于同配置的波动范围,这个差异不显著,不能归因于配置变化。可以用简单的t检验或Mann-Whitney U检验来判断。
归因链验证。如果认为性能变化来自某个开关,尝试找到中间的因果链。比如批处理大小改变导致吞吐变化,中间应该能看到kernel执行时间、SM占用率、内存带宽利用率的变化。如果中间指标没变,那因果链不成立,性能变化可能来自其他因素。
6. 常见问题与排查技巧实录
6.1 为什么两次压测的GPU利用率都是99%,但吞吐差了一倍
这是最经典的问题。GPU利用率99%只说明GPU一直在忙,不说明忙得有效率。可能的情况包括:
- 第一次压测的kernel并行度高,SM占用率高,实际算力利用充分
- 第二次压测的kernel并行度低,SM占用率低,大量时间在等内存
- 两次的kernel类型不同,比如一次是计算密集型,一次是内存密集型
排查方法:用profiling工具看SM占用率、Tensor Core利用率、内存带宽利用率。如果SM占用率低但GPU利用率高,说明kernel并行度不够,需要调整block大小或批处理大小。
6.2 预热后第一次记录的数据要不要算
通常不算。预热后的第一次记录,虽然缓存和调优已经稳定,但可能还有残留的异步操作没有完成。比如GPU上还有上一次的kernel在排队,或者CPU上还有未刷新的日志。建议预热后空转几秒,再开始记录。
如果压测时间很短,比如只跑10秒,那预热后的第一次记录占比很大,不算的话样本太少。这时候可以延长压测时间,或者接受第一次记录的偏差,但在报告中注明。
6.3 批处理大小翻倍,吞吐没翻倍,是哪里出了问题
批处理大小翻倍,理论吞吐应该接近翻倍,实际往往达不到。原因可能有:
- 内存带宽瓶颈。批处理翻倍后,内存访问量也翻倍,如果内存带宽已经饱和,吞吐无法提升。
- 计算单元瓶颈。如果SM已经满载,批处理翻倍只是让每个SM处理更多数据,总吞吐不变。
- 调度开销。大批处理下,kernel启动开销占比降低,但内存分配和同步开销可能增加。
- 功耗限制。大批处理触发功耗限制,GPU降频,抵消了批处理带来的效率提升。
排查方法:看内存带宽利用率、SM占用率、GPU频率。如果内存带宽利用率接近100%,说明是内存瓶颈;如果SM占用率接近100%,说明是计算瓶颈;如果GPU频率下降,说明是功耗瓶颈。
6.4 量化后精度下降,但速度没提升,怎么回事
量化后速度没提升,常见原因:
- 模型是计算密集型的。INT8的优势在内存带宽,如果模型计算量大,内存不是瓶颈,量化帮助有限。
- fallback比例高。部分算子退回FP16执行,抵消了INT8的优势。看日志里有没有fallback记录。
- 量化校准不充分。校准数据集不代表实际输入分布,导致量化误差大,框架频繁触发修正逻辑。
- kernel实现不优。某些框架的INT8 kernel实现不如FP16成熟,实际速度可能更慢。
排查方法:用profiling工具看每个算子的执行时间和精度,找出fallback的算子,检查量化校准数据。
6.5 压测结果波动大,怎么定位干扰源
波动大说明有未控制的变量。按以下顺序排查:
- 温度:记录压测前后的GPU和CPU温度,看是否触发降频。
- 频率:记录压测期间的频率曲线,看是否有异常波动。
- 后台进程:用top或htop看压测期间是否有其他进程占用资源。
- 内存:看内存使用曲线,是否有突然的分配或回收。
- 网络:如果涉及网络,看网络延迟和丢包率。
- 磁盘:看磁盘IO,是否有其他读写操作。
- 容器:如果在容器里,看cgroup的CPU和内存统计,是否有配额触发。
定位到干扰源后,要么消除干扰,要么在分析时排除受干扰的样本。
6.6 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 同配置吞吐波动大 | 温度漂移、后台进程、频率调节 | 记录温度频率曲线、检查后台进程 | 锁定频率、关闭后台进程、冷却 |
| 批处理翻倍吞吐不翻倍 | 内存瓶颈、计算瓶颈、功耗限制 | 看内存带宽、SM占用率、GPU频率 | 调整批处理、优化kernel、放宽功耗 |
| 量化后速度没提升 | fallback、校准不足、kernel不优 | profiling看算子、检查校准数据 | 优化校准、换框架、调整量化策略 |
| 延迟P99异常高 | 排队、GC、内存回收 | 看延迟分布、检查GC日志 | 调整并发、优化内存、减少排队 |
| GPU利用率高但吞吐低 | kernel并行度低、内存等待 | 看SM占用率、内存带宽利用率 | 调整block大小、优化内存访问 |
| 预热后指标仍波动 | 异步操作未完成、缓存未稳定 | 延长预热、空转后记录 | 增加预热轮次、确认稳定后再记录 |
6.7 几个容易被忽视的实操心得
心得一:压测脚本本身可能是瓶颈。我遇到过压测工具用Python写的,GIL导致发送请求的线程成为瓶颈。小批处理下,请求发送频率高,GIL竞争严重,测到的吞吐量远低于服务端实际能力。后来换成多进程发送,吞吐量直接翻倍。所以压测前先确认压测工具本身不是瓶颈。
心得二:日志级别会影响性能。DEBUG级别的日志会大量写磁盘,占用CPU和IO。压测时把日志级别调到WARN或ERROR,减少干扰。但要注意,有些框架在ERROR级别下会跳过某些检查,可能改变代码路径。最好在压测前确认日志级别对性能的影响。
心得三:显存碎片比显存不足更隐蔽。显存足够但碎片严重时,大块分配会失败,框架可能触发显存整理或fallback。这个过程的耗时不会体现在显存占用指标里,但会体现在延迟上。定期重启进程或使用显存池可以缓解。
心得四:CPU和GPU的同步点要关注。很多框架在推理结束后会做同步,确保GPU计算完成。这个同步点的位置和频率会影响延迟测量。如果同步点在计时范围内,测到的延迟包括GPU执行时间;如果在范围外,测到的只是CPU提交时间。批处理大小改变后,同步行为可能变化,导致延迟不可比。
心得五:不要迷信“官方推荐配置”。官方推荐配置通常是在特定硬件和特定模型下调优的,换到你的场景可能不是最优。压测的价值就在于找到适合你场景的配置,而不是照搬别人的参数。
7. 从压测设计层面提升可信度
7.1 压测目标的明确化
在开始压测之前,先明确你要回答什么问题。是找最大吞吐量?还是找满足SLA的最大并发?还是对比两个配置的优劣?不同目标对应不同的压测设计。
找最大吞吐量时,要逐步增加负载直到吞吐不再上升,记录拐点。找SLA最大并发时,要固定延迟阈值,逐步增加并发直到延迟超标。对比配置时,要控制其他变量,只改目标配置,多次重复。
目标不明确会导致压测设计混乱,采集的指标不匹配,结果无法回答实际问题。
7.2 负载模型的真实性
压测的负载模型要尽量接近真实场景。真实场景的请求到达不是均匀的,而是有突发的。真实场景的输入数据分布不是固定的,而是有长尾的。真实场景的请求类型不是单一的,而是混合的。
如果压测用均匀到达、固定输入、单一请求类型,测到的性能可能远高于真实场景。批处理大小改变后,这种偏差可能更大,因为真实场景的批处理凑批行为更复杂。
建议用真实请求的采样作为压测输入,用泊松过程或真实到达曲线作为负载模型。如果做不到,至少在报告中说明负载模型的局限性。
7.3 结果报告的完整性
一份完整的压测报告应该包括:
- 硬件配置:CPU、GPU、内存、磁盘、网络
- 软件配置:框架版本、驱动版本、编译选项、环境变量
- 模型信息:模型结构、参数量、输入输出形状
- 压测配置:批处理大小、并发度、线程数、预热轮次
- 负载模型:请求到达模式、输入数据分布
- 核心指标:吞吐量、延迟分布、资源利用率
- 环境状态:温度、频率、后台进程
- 异常记录:任何偏离预期的现象
- 结论和局限性:结果能回答什么问题,不能回答什么问题
报告越完整,结果的可信度越高,别人复现的可能性越大。
7.4 持续压测和回归检测
压测不是一次性的,而是持续的过程。代码变更、框架升级、硬件更换后,都需要重新压测。建立回归检测机制,定期跑基准测试,对比历史结果,及时发现性能退化。
回归检测的关键是基准的稳定性。基准本身要可复现,否则检测到的“退化”可能是基准波动。建议用同配置多次跑的中位数作为基准,设置合理的告警阈值。
8. 一个完整的排查案例
8.1 问题描述
某次压测中,同一模型、同一机器,把批处理大小从4改成8,吞吐量从1200 tokens/s降到900 tokens/s。GPU利用率两次都是95%以上,显存占用从8GB升到12GB,没有报错。
8.2 排查过程
第一步,确认同配置重复性。用批处理4跑3次,吞吐分别是1190、1210、1200,波动小于2%。用批处理8跑3次,吞吐分别是880、910、900,波动小于3%。同配置稳定,差异显著。
第二步,看GPU频率。批处理4时GPU频率稳定在1.8GHz,批处理8时频率在1.5GHz到1.7GHz之间波动。频率下降是吞吐降低的直接原因。
第三步,查功耗。批处理8时GPU功耗接近功耗墙,触发降频。批处理4时功耗有余量,频率稳定。
第四步,看kernel执行时间。批处理8时单个kernel执行时间是批处理4的1.8倍,不是理论上的2倍。说明kernel效率下降了。
第五步,看SM占用率。批处理8时SM占用率从85%降到70%。原因是批处理8的kernel寄存器压力大,occupancy下降。
8.3 解决方案
方案一:放宽功耗限制。如果散热允许,提高功耗墙,让GPU维持高频。但这不是长久之计,功耗墙是硬件保护机制。
方案二:调整批处理大小。批处理6时吞吐达到峰值1300 tokens/s,频率稳定,SM占用率80%。说明批处理8超过了最优值。
方案三:优化kernel。减少寄存器使用,提高occupancy。这需要改模型代码或换框架,成本较高。
最终选择方案二,把批处理大小定为6,并在报告中说明批处理8的性能下降原因。
8.4 案例启示
这个案例说明,批处理大小不是越大越好。存在一个最优值,超过后性能反而下降。这个最优值取决于模型、硬件、功耗限制。压测的价值就是找到这个最优值,而不是盲目追求大批处理。
同时,这个案例也说明,只看GPU利用率和显存占用是不够的。频率、功耗、SM占用率这些指标才能解释性能变化的原因。
9. 工具链的选择与配置
9.1 压测工具
常用的压测工具包括:
- locust:Python写的,易扩展,适合HTTP接口压测。但GIL可能成为瓶颈,高并发下需要多进程。
- wrk/wrk2:C写的,性能好,适合HTTP压测。但不支持复杂的请求逻辑。
- vegeta:Go写的,支持恒定速率压测,适合找吞吐拐点。
- 自定义脚本:用Python或C++直接调用推理接口,控制最灵活,但需要自己处理并发和统计。
选择工具时考虑:是否支持目标协议、是否能产生足够负载、是否方便采集指标、是否容易复现。
9.2 监控工具
- nvidia-smi:基础GPU监控,采样频率1秒,适合粗粒度观察。
- dcgm:NVIDIA的数据中心GPU监控,采样频率可到100毫秒,指标更丰富。
- nsight systems:NVIDIA的profiling工具,可以看kernel时间线、内存传输、API调用。
- py-spy:Python进程的profiling工具,可以看CPU热点。
- perf:Linux系统级profiling,可以看CPU、内存、调度事件。
监控工具的选择取决于你要回答什么问题。粗粒度观察用nvidia-smi,细粒度分析用nsight systems,CPU问题用perf或py-spy。
9.3 配置管理
压测配置要版本化管理。每次压测的配置、代码版本、环境状态都要记录。推荐用YAML或JSON文件管理配置,用Git管理版本。这样结果可追溯,问题可复现。
配置项包括:模型路径、批处理大小、并发度、线程数、预热轮次、压测时长、输入数据路径、日志级别、环境变量。
10. 写在最后
压测这件事,看起来是技术活,实际上是细心活。同一模型、同一机器、只改一个开关,结果仍可能不可信,根本原因在于软件系统的变量耦合和环境的不确定性。要得到可信的结果,需要在压测设计、环境锁定、指标采集、结果分析每个环节都做到位。
我自己的习惯是,每次压测前花10分钟检查环境,压测后花20分钟分析数据,而不是急着跑下一个配置。这10分钟和20分钟,往往能避免几小时的返工。压测报告里,我会明确写出哪些结论是可信的,哪些是有疑问的,而不是给一个笼统的数字。这样即使结果有偏差,读者也能判断偏差的方向和大小。
最后分享一个小技巧:如果时间允许,用不同的压测工具跑同一配置,对比结果。如果两个工具的结果接近,说明结果可信;如果差异大,说明工具有问题或者环境有干扰。这个交叉验证的方法,帮我发现过好几次压测工具的隐性瓶颈。