3. 性能分析基础

在开始优化 ONNX 模型之前,必须先建立一套统一的度量体系。没有量化就没有优化——如果无法准确回答“当前模型延迟是多少”“吞吐量是否达标”“内存占用是否合理”,后续的一切调优动作都将失去参照基准。本章聚焦三个核心性能指标(延迟、吞吐量、内存占用)与两类关键分析工具(Profiler、Benchmark),为后续章节的案例调优打下基础。

3.1 三大核心性能指标

ONNX 模型性能分析中,最常被提及的指标是延迟(Latency)、吞吐量(Throughput)和内存占用(Memory Footprint)。三者从不同维度刻画模型运行效率,且往往相互制约。

指标 定义 典型单位 关注场景
延迟 单个推理请求从输入到输出所花费的时间 ms / 次 实时交互、边缘端部署
吞吐量 单位时间内完成的推理请求数量 QPS、FPS 批量离线推理、云端服务
内存占用 模型加载及推理过程中占用的 RAM/VRAM MB / GB 移动端、嵌入式设备

延迟通常关注P50、P95、P99分位数,而非平均值——因为平均值容易被极端值掩盖。吞吐量则需区分单流吞吐与多流并发吞吐,后者更贴近真实服务场景。内存占用需同时关注静态内存(模型权重、图结构)与动态内存(推理过程中的中间张量、临时缓冲区)。

经验法则:延迟与吞吐量并非完全对立。当 batch size 增大时,单次延迟会上升,但单样本平均延迟可能下降,吞吐量则显著提升。分析时应明确固定 batch size 与并发数,否则指标之间不可比。

3.2 分析工具全景:Profiler 与 Benchmark

ONNX Runtime 生态提供了两类互补的工具:Profiler 用于细粒度定位耗时瓶颈,Benchmark 用于宏观测量端到端性能。两者配合使用,才能完成“发现瓶颈 → 定位算子 → 验证优化”的闭环。

ONNX Runtime 性能分析工具分工 ONNX 模型 Profiler(性能剖析器) 逐算子耗时 / 张量形状 / 内存分配 Benchmark(基准测试) 端到端延迟 / 吞吐量 / 并发表现 输出内容 • 每个算子执行时间(CPU/GPU) • 算子输入/输出张量维度 • 内存分配与释放事件 • 图优化前后对比数据 输出内容 • 平均/中位数/尾延迟(P95/P99) • 吞吐量(QPS / FPS) • 不同 batch size 对比 • 多线程/多实例扩展性 使用场景 优化前定位瓶颈算子 使用场景 优化后验证整体收益 推荐流程:Benchmark 初测 → Profiler 定位 → 优化 → Benchmark 复测

3.3 核心工具实操:Profiler 与 Benchmark 使用

3.3.1 Profiler:定位算子级瓶颈

ONNX Runtime 的 Profiler 通过 SessionOptions 启用,可记录每个算子的执行时间、张量形状及内存事件。以下代码展示了如何开启 Profiler 并输出 JSON 格式的 trace 文件:

import onnxruntime as ort
import json

sess_options = ort.SessionOptions()
sess_options.enable_profiling = True  # 开启 Profiler
sess_options.profile_file_prefix = "onnx_profile"  # 输出文件前缀

session = ort.InferenceSession("model.onnx", sess_options)
session.run(None, {"input": input_data})

# 获取 Profiler 输出文件路径
prof_file = session.end_profiling()
print(f"Profiler 输出: {prof_file}")

# 解析 trace 文件,提取耗时 Top 算子
with open(prof_file, "r") as f:
    trace_data = json.load(f)

# 按耗时排序,找出瓶颈算子
events = [e for e in trace_data if e.get("cat") == "Node"]
events.sort(key=lambda x: x.get("dur", 0), reverse=True)
for e in events[:5]:
    print(f"算子: {e['name']}, 耗时: {e['dur']} us")

Profiler 输出的 trace 文件遵循 Chrome Tracing 格式,可直接在 chrome://tracing 或 Perfetto UI 中可视化。重点关注:

  • 耗时占比最高的算子——通常是 Conv、MatMul、Attention 等计算密集型算子。
  • 张量形状异常——例如某个中间张量维度远大于预期,导致不必要的内存拷贝。
  • 内存分配频繁——大量小张量的反复分配/释放会拖慢整体速度。

3.3.2 Benchmark:宏观性能测量

ONNX Runtime 提供了 onnxruntime_benchmark 命令行工具,也支持通过 Python 脚本自定义测试。以下是一个典型的 Benchmark 脚本:

import onnxruntime as ort
import numpy as np
import time
import statistics

session = ort.InferenceSession("model.onnx")
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
input_data = np.random.randn(*input_shape).astype(np.float32)

# Warm-up:预热 10 次,排除首次加载影响
for _ in range(10):
    session.run(None, {input_name: input_data})

# 正式测试:100 次推理
latencies = []
for _ in range(100):
    start = time.perf_counter()
    session.run(None, {input_name: input_data})
    latencies.append((time.perf_counter() - start) * 1000)  # ms

latencies.sort()
p50 = statistics.median(latencies)
p95 = latencies[int(len(latencies) * 0.95)]
p99 = latencies[int(len(latencies) * 0.99)]
avg = statistics.mean(latencies)

print(f"平均延迟: {avg:.2f} ms")
print(f"P50: {p50:.2f} ms | P95: {p95:.2f} ms | P99: {p99:.2f} ms")
print(f"吞吐量: {1000 / avg:.1f} FPS")
关键参数说明:
  • Warm-up 次数:建议至少 10 次,以触发图优化、内存池预分配等初始化逻辑。
  • 测试次数:建议 100 次以上,以获得稳定的统计分布。
  • 线程数设置:通过 sess_options.intra_op_num_threads 控制,默认使用所有物理核心。

3.4 案例:ResNet50 性能基线测量

以 ResNet50 为例,演示如何建立性能基线。假设模型已转换为 ONNX 格式,输入为 [1, 3, 224, 224] 的 RGB 图像。

第一步:Benchmark 初测,获取整体性能基线:

# 命令行方式(使用官方工具)
python -m onnxruntime.tools.benchmark \
  --model resnet50.onnx \
  --input_size 1x3x224x224 \
  --iterations 100 \
  --warmup 10

# 输出示例
# Average latency: 12.34 ms
# P95 latency: 14.56 ms
# Throughput: 81.0 FPS

第二步:Profiler 定位瓶颈,找出耗时最高的算子:

# 开启 Profiler 后,解析 trace 文件得到 Top 算子
# 典型输出:
# Conv_0: 2.1 ms (17.0%)
# Conv_1: 1.8 ms (14.6%)
# BatchNormalization_0: 0.9 ms (7.3%)
# MaxPool_0: 0.6 ms (4.9%)
# ...

第三步:分析结论。若 Conv 算子总耗时占比超过 60%,说明计算密度高,可考虑算子融合或低精度推理;若数据搬运(Transpose、Concat)耗时占比高,则需检查图结构是否合理。

3.5 常见误区与复盘

误区一:只测平均延迟,忽略尾延迟。

平均延迟 10ms 的模型,P99 可能高达 50ms。对于实时推理服务,尾延迟直接影响用户体验。必须同时报告 P50/P95/P99。

误区二:未固定测试环境。

CPU 频率波动、GPU 温度、其他进程干扰都会影响测量结果。建议:

  • 关闭 CPU 动态调频(设置 performance governor)
  • GPU 测试前先预热至稳定温度
  • 重复测试 3 次取最优/中位数
误区三:忽略内存测量。

ONNX Runtime 默认使用内存池复用中间张量,但某些算子(如动态形状的 Resize)仍可能触发额外分配。使用 psutil 或 nvidia-smi 监控推理过程中的内存峰值:

import psutil
import os

# 在推理循环中采样内存
process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / 1024 / 1024  # MB
session.run(None, {input_name: input_data})
mem_after = process.memory_info().rss / 1024 / 1024
print(f"推理内存增量: {mem_after - mem_before:.1f} MB")

复盘总结:性能分析不是一次性的工作,而应贯穿整个优化周期。每次优化操作后,都应重新运行 Benchmark 验证收益,并用 Profiler 确认瓶颈是否转移。建议建立标准化的性能测试脚本,纳入 CI/CD 流程,防止模型更新后性能回退。

实践建议:将本章的测量方法固化为团队内部的性能测试模板。每次模型变更后,自动运行 Benchmark 并对比基线,若延迟或内存增幅超过 5%,则触发告警。这是保障 ONNX 模型在生产环境稳定运行的基础设施。