性能分析工具入门:Linux perf、Nsight Systems、PyTorch Profiler 基础使用
做推理优化,第一件事不是调参数,而是搞清楚——时间到底花在哪了。
我见过太多人上来就改 batch size、换量化精度,折腾半天效果甚微。为什么?因为没有数据支撑。性能分析工具就是你的「听诊器」,能精准定位瓶颈在计算、访存还是通信上。
这一章,我会带你上手三款最常用的工具:Linux perf、Nsight Systems 和 PyTorch Profiler。它们分别覆盖系统级、GPU 级和框架级分析,组合起来基本能解决 90% 的推理延迟问题。
核心观点:不要凭感觉优化,让工具告诉你答案。
1. Linux perf:系统级性能剖析
perf 是 Linux 内核自带的性能分析工具,轻量、无侵入。我习惯用它做第一轮排查——先看看 CPU 到底在忙什么。
1.1 安装与基本用法
# Ubuntu/Debian
sudo apt install linux-tools-common linux-tools-$(uname -r)
# CentOS/RHEL
sudo yum install perf
# 基本采样:统计 CPU 周期和指令数
perf stat ./your_inference_binary
# 采样并生成报告
perf record -g ./your_inference_binary
perf report
我个人最常用的是 perf stat,几秒钟就能看到关键指标:
| 指标 | 含义 | 关注点 |
|---|---|---|
| cycles | CPU 时钟周期数 | 越高说明计算越密集 |
| instructions | 执行的指令数 | 与 cycles 对比看 IPC |
| cache-misses | 缓存未命中次数 | 过高说明内存访问模式差 |
| branch-misses | 分支预测失败 | 影响流水线效率 |
小技巧:如果 instructions/cycle(IPC)低于 1.0,说明 CPU 在等数据。这时候优化方向应该是内存布局,而不是减少计算量。
1.2 实战:定位 CPU 热点
有一次我优化一个 ONNX Runtime 的推理程序,发现延迟总比预期高 30%。直觉告诉我可能是数据预处理拖了后腿,但不确定具体在哪。
# 采样 10 秒,记录调用栈
perf record -g -F 99 -- ./run_inference.sh
perf report --stdio -g graph
结果出来,排第一的热点函数是 cv::resize,占了 42% 的 CPU 时间。嗯,原来图像缩放用了双线性插值,换成最近邻插值后延迟直接降了 25%。
注意:perf 需要 root 权限才能访问硬件计数器。如果没权限,可以用 perf stat --user 只统计用户态。
2. Nsight Systems:GPU 端到端可视化
当推理跑在 GPU 上时,perf 就看不全了。这时候需要 Nsight Systems——NVIDIA 官方提供的 GPU 性能分析工具。
说实话,我第一次用 Nsight Systems 时被震撼到了。它能精确到每个 CUDA kernel 的启动时间、执行时间、显存传输,甚至 CPU 端的 launch 开销。
2.1 快速上手
# 命令行模式(无 GUI)
nsys profile -o output_profile -t cuda,nvtx,osrt ./your_inference.py
# 生成可视报告
nsys stats output_profile.qdrep
关键参数说明:
-t cuda,nvtx,osrt:追踪 CUDA API、NVTX 标记和系统运行时-o output_profile:输出文件名前缀--stats=true:自动输出统计摘要
2.2 读报告的核心指标
打开生成的 .qdrep 文件(用 Nsight Systems GUI),我一般先看三个东西:
- GPU 利用率时间线:有没有大段空白?如果有,说明 CPU 在等 GPU 或者数据没准备好。
- Kernel 执行时间:哪个 kernel 最耗时?是不是意料之中的算子?
- Memcpy 大小与耗时:Host-to-Device 和 Device-to-Host 传输是否频繁?
经验之谈:我见过一个项目,推理延迟 60% 花在 CPU 到 GPU 的数据拷贝上。优化方案很简单——把预处理移到 GPU 上用 CUDA 核函数做,延迟直接砍半。
2.3 避坑指南
我曾经踩过一个坑:Nsight Systems 默认会追踪所有 CUDA API 调用,如果推理脚本里调用了大量小算子,生成的报告会非常大(几个 GB),打开和分析都很慢。
解决办法:用 --cuda-memory-usage=false 关闭详细内存追踪,或者只追踪特定 NVTX 范围。
# 只追踪标记区域
nsys profile -t cuda,nvtx -c nvtx --capture-range=custom_range ./your_inference.py
3. PyTorch Profiler:框架级算子分析
如果你用 PyTorch 做推理,PyTorch Profiler 是最直接的武器。它能告诉你每个 torch 算子的耗时、显存分配、以及调用栈。
3.1 基础用法
import torch
import torch.profiler as profiler
# 定义一个简单的推理函数
def run_inference(model, input_tensor):
with torch.no_grad():
for _ in range(100):
output = model(input_tensor)
return output
# 使用 profiler
with profiler.profile(
activities=[
profiler.ProfilerActivity.CPU,
profiler.ProfilerActivity.CUDA,
],
record_shapes=True,
with_stack=True,
) as prof:
result = run_inference(model, input_tensor)
# 打印排序后的结果
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=20))
3.2 输出解读
输出表格长这样(简化版):
| Name | Self CPU % | Self CPU | CUDA Total | # Calls |
|---|---|---|---|---|
| aten::mm | 12.5% | 3.2ms | 45.6ms | 128 |
| aten::softmax | 8.3% | 2.1ms | 28.3ms | 64 |
| cudaMemcpyAsync | 0.5% | 0.1ms | 15.2ms | 32 |
重点关注 CUDA Total 列,它表示该算子在 GPU 上的总耗时。如果某个算子占比异常高,比如 softmax 占了 30%,那就要考虑是不是可以替换成更快的实现。
我的习惯:先用 sort_by="cuda_time_total" 看 GPU 热点,再用 sort_by="self_cpu_time_total" 看 CPU 预处理热点。双管齐下,不遗漏。
3.3 进阶:火焰图与追踪
# 导出为 Chrome Trace 格式
prof.export_chrome_trace("trace.json")
# 然后在 Chrome 浏览器打开 chrome://tracing,加载这个文件
# 你会看到每个算子的起止时间、GPU 流、CPU 线程关系
火焰图特别适合排查「CPU 在等 GPU」的情况。如果 CPU 时间线上有大段空白,而 GPU 在忙,说明 CPU 侧有同步等待——这时候可以尝试用 torch.cuda.stream 做异步处理。
注意:Profiler 本身有开销,尤其是开启 record_shapes=True 和 with_stack=True 时。生产环境建议只采样少量 iteration,或者用 schedule 参数控制采样窗口。
4. 三款工具如何配合使用?
说了这么多,你可能想问:到底该用哪个?
我的工作流是这样的:
- 第一轮:用
perf stat快速扫一遍,看 CPU 利用率和缓存命中率。如果 CPU 利用率低(< 50%),大概率瓶颈在 IO 或 GPU 同步。 - 第二轮:用 Nsight Systems 看 GPU 时间线,确认 GPU 利用率、Kernel 耗时、Memcpy 占比。
- 第三轮:用 PyTorch Profiler 定位具体算子,找到需要替换或融合的热点。
举个例子,我之前优化一个 BERT 推理服务:
- perf 发现 CPU 利用率只有 30%,cache-miss 很高
- Nsight 显示 GPU 利用率 95%,但每次推理前有 8ms 的 CPU 预处理
- Profiler 定位到
tokenizer和attention mask生成占了 6ms
最终方案:把 tokenizer 换成 C++ 实现,attention mask 预计算好缓存起来。延迟从 25ms 降到 18ms,效果立竿见影。
总结一句话:perf 看系统、Nsight 看 GPU、Profiler 看算子。三层递进,没有盲区。