性能分析工具入门:Linux perf、Nsight Systems、PyTorch Profiler 基础使用

做推理优化,第一件事不是调参数,而是搞清楚——时间到底花在哪了。

我见过太多人上来就改 batch size、换量化精度,折腾半天效果甚微。为什么?因为没有数据支撑。性能分析工具就是你的「听诊器」,能精准定位瓶颈在计算、访存还是通信上。

这一章,我会带你上手三款最常用的工具:Linux perf、Nsight Systems 和 PyTorch Profiler。它们分别覆盖系统级、GPU 级和框架级分析,组合起来基本能解决 90% 的推理延迟问题。

核心观点:不要凭感觉优化,让工具告诉你答案。

性能分析工具知识体系 推理延迟优化目标 Linux perf 系统级 CPU/内存/IO Nsight Systems GPU 级 Kernel/通信 PyTorch Profiler 框架级 Op/Tensor CPU 热点 & 缓存 GPU 利用率 & 带宽 算子耗时 & 内存 通信 & 同步 💡 组合使用:perf 扫系统 → Nsight 看 GPU → Profiler 定算子 三层递进,不漏掉任何瓶颈

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),我一般先看三个东西:

  1. GPU 利用率时间线:有没有大段空白?如果有,说明 CPU 在等 GPU 或者数据没准备好。
  2. Kernel 执行时间:哪个 kernel 最耗时?是不是意料之中的算子?
  3. 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. 三款工具如何配合使用?

说了这么多,你可能想问:到底该用哪个?

我的工作流是这样的:

  1. 第一轮:用 perf stat 快速扫一遍,看 CPU 利用率和缓存命中率。如果 CPU 利用率低(< 50%),大概率瓶颈在 IO 或 GPU 同步。
  2. 第二轮:用 Nsight Systems 看 GPU 时间线,确认 GPU 利用率、Kernel 耗时、Memcpy 占比。
  3. 第三轮:用 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 看算子。三层递进,没有盲区。