3. 性能基准测试与评估:CPU基线性能测试、GPU加速比计算、Profiling工具使用、瓶颈分析

说实话,性能基准测试这件事,我见过太多团队栽跟头了。

有人拿个单batch跑一遍,就宣称“GPU比CPU快100倍”。有人用PyTorch默认配置跑ONNX,然后说“ONNX Runtime太慢”。这些我都遇到过。说白了,没有规范的基线测试,后面的优化全是空中楼阁。

这一章,我们就来聊聊怎么科学地做性能评估。我会把我在几个量产项目中踩过的坑、总结的经验,都摊开来讲。

3.1 CPU基线性能测试

做迁移的第一步,不是急着上GPU。而是先把CPU上的性能摸清楚。我个人习惯,至少跑三组数据:单线程、多线程、以及实际部署场景下的吞吐量。

3.4.1 PyTorch CPU基准测试

PyTorch在CPU上跑推理,有几个关键点要注意。嗯,这里我直接给代码模板,你们拿去就能用。

import torch
import time
import numpy as np

# 设置CPU线程数,我建议先测单线程
torch.set_num_threads(1)

model = torch.load('model.pth', map_location='cpu')
model.eval()

# 构造输入,batch_size=1 和 batch_size=32 都要测
dummy_input = torch.randn(1, 3, 224, 224)

# 预热,这个步骤很多人会忘
for _ in range(10):
    _ = model(dummy_input)

# 正式测试
num_iter = 100
start = time.time()
for _ in range(num_iter):
    _ = model(dummy_input)
end = time.time()

avg_latency = (end - start) / num_iter * 1000  # 转成毫秒
print(f'CPU单线程延迟: {avg_latency:.2f} ms')
我的经验:预热这一步千万别省。我见过有人没预热,第一次推理包含了模型加载和JIT编译的时间,结果测出来的延迟比实际高了3倍。你想想看,这数据要是拿去汇报,后面全得重来。

3.4.2 ONNX Runtime CPU基准测试

ONNX Runtime在CPU上的优化,其实比PyTorch原生要好一些。特别是对于静态图,ORT的图优化能省不少事。

import onnxruntime as ort
import time
import numpy as np

# 创建session,我习惯用CPUExecutionProvider
session = ort.InferenceSession('model.onnx',
    providers=['CPUExecutionProvider'])

input_name = session.get_inputs()[0].name
dummy_input = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 预热
for _ in range(10):
    _ = session.run(None, {input_name: dummy_input})

# 正式测试
num_iter = 100
start = time.time()
for _ in range(num_iter):
    _ = session.run(None, {input_name: dummy_input})
end = time.time()

avg_latency = (end - start) / num_iter * 1000
print(f'ONNX Runtime CPU延迟: {avg_latency:.2f} ms')
注意:ONNX Runtime的session创建本身就很耗时。我曾经在线上服务里每来一个请求就创建一个session,结果延迟直接爆炸。正确的做法是:session复用,只创建一次。

3.2 GPU加速比计算

加速比,说白了就是CPU耗时除以GPU耗时。但这里有个坑:你拿什么CPU和什么GPU比?

我建议这样:

  • 单卡 vs 单核CPU:看纯计算加速比,适合评估算法优化效果
  • 单卡 vs 多核CPU:看实际部署加速比,适合评估硬件选型

加速比计算公式很简单:

加速比 = CPU平均延迟 / GPU平均延迟

# 举个例子
cpu_latency = 45.2  # ms
gpu_latency = 2.3   # ms
speedup = cpu_latency / gpu_latency
print(f'加速比: {speedup:.1f}x')
关键点:加速比不是越高越好。我见过一个模型,加速比达到了80x,但仔细一看,CPU延迟是800ms,GPU延迟是10ms。这种模型在CPU上根本没法用,加速比再高也没意义。真正要看的,是绝对延迟是否满足业务需求

3.3 Profiling工具使用

光看加速比不够,你得知道时间花在哪了。Profiling工具就是干这个的。

3.3.1 Nsight Systems

Nsight Systems是我最常用的工具。它能看CPU和GPU的时间线,一眼就能看出哪里在等待、哪里在计算。

基本用法:

# 命令行启动
nsys profile -o output_profile python inference.py

# 或者用GUI界面,更直观
# 我习惯先跑一次命令行,拿到profile文件,再用GUI打开分析

看什么?我一般关注这几个指标:

  • GPU利用率:如果低于80%,说明数据加载或预处理在拖后腿
  • CPU-GPU数据传输:看memcpy的时间,如果占比超过10%,就要考虑异步传输了
  • Kernel启动开销:小模型经常被这个拖慢,可以考虑kernel fusion
避坑指南:我曾经在一个项目里,Nsight Systems显示GPU利用率只有30%。我查了半天,发现是数据加载用了同步方式,CPU在等数据,GPU在等CPU。改成异步DataLoader后,利用率直接飙到85%。你看,工具不会骗你,但你要会看。

3.3.2 nvprof(老牌工具)

nvprof虽然被Nsight Systems取代了,但有些场景还是好用。比如你想快速看每个kernel的耗时,nvprof更直接。

# 基本用法
nvprof --print-gpu-trace python inference.py

# 只看最耗时的几个kernel
nvprof --print-gpu-trace --metrics gpu__time_duration.sum python inference.py

输出示例:

Kernel名称 耗时(us) 占比
conv2d_forward 450.2 35%
matmul_kernel 320.1 25%
relu_forward 45.3 3.5%

3.4 瓶颈分析

有了Profiling数据,下一步就是找瓶颈。我总结了一个四步分析法:

  1. 看整体:CPU和GPU的时间线,有没有明显的空闲期
  2. 看传输:Host到Device的数据传输是不是瓶颈
  3. 看计算:哪个kernel最耗时,能不能优化或融合
  4. 看调度:有没有不必要的同步点

举个例子,我最近优化一个模型:

  • Nsight Systems显示,GPU利用率只有60%
  • 进一步看,发现每次推理前都有一次memcpy,耗时2ms
  • 而模型推理本身才5ms,传输占了将近30%
  • 解决方案:用pinned memory + 异步传输,把传输时间降到0.5ms

优化后,GPU利用率到了85%,整体延迟从7ms降到了5.5ms。

核心思路:瓶颈分析不是找最慢的kernel,而是找最拖累整体流程的环节。有时候一个看起来不慢的memcpy,因为阻塞了后续操作,反而成了最大的瓶颈。

好了,这一章的内容就这些。记住:没有基准测试的优化都是耍流氓。下一章我们会聊量化技术的基础,但在此之前,我建议你先把手头的模型跑一遍完整的Profiling,把数据留好。后面优化完了,对比起来才有说服力。


专注资料整理