ONNX Runtime 图优化可能减少冗余计算,但不能预先保证任何 AI 模型都更快。需要用同一原图、输入、线程和计时范围比较关闭优化与开启优化的会话,再检查保存的优化图与输出数值;结构变化、结果一致和耗时变化应分别记录。
下面实际运行了一个含 Identity 和常量加法的教学图。环境为 Windows、Python 3.11.15、ONNX 1.23.1、ONNX Runtime 1.28.0、NumPy 2.4.3、CPU EP。权重和算子由人工构造,没有训练过程;没有测试真实业务模型、GPU、跨机器兼容或端到端服务性能。

本次控制什么,比较什么
ORT 图优化文档说明基础优化包含常量折叠和 Identity 消除。ORT_DISABLE_ALL 关闭图优化;ORT_ENABLE_BASIC 启用基础图优化。本例只比较这两个级别,不同时改量化、输入精度或提供程序。
- 同一 original.onnx:输入 float32 [batch,2],输出同形状。
- 同一 CPU EP:会话的 intra/inter 线程都设置为 1。
- 同一数值检查:batch 1、8、64,并与 x+[4,6] 解析结果对照。
- 同一计时输入:batch 64,预热 100 次;五轮每轮 100000 次 session.run,交替测试顺序。
- 计时只含 session.run,不含图生成、会话初始化、HTTP、磁盘和样本准备。
五轮中每轮计算“100000 次总耗时除以 100000”,然后取五个轮均值的中位数。它不是单次请求耗时的 P50,也没有计算 P95;不能把这个口径与服务延迟指标混用。
运行完整图优化实验
新建空练习目录并进入它,执行 Windows PowerShell 安装命令:
py -3.11 -m venv .venv-opt
.\.venv-opt\Scripts\python.exe -m pip install "onnx==1.23.1" "onnxruntime==1.28.0" "numpy==2.4.3"
将下面代码保存为 compare_optimization.py,运行 .\.venv-opt\Scripts\python.exe -X utf8 compare_optimization.py。代码创建专用 original.onnx、optimized-basic.onnx 与 optimization-report.json,重复执行会覆盖这些练习文件。图构建依据ONNX helper,ORT Python API是会话选项、优化文件保存和 run 的依据。
from pathlib import Path
from time import perf_counter_ns
import json
import numpy as np
import onnx
from onnx import TensorProto, helper, numpy_helper
import onnxruntime as ort
graph = helper.make_graph(
[helper.make_node('Identity', ['features'], ['same']),
helper.make_node('Add', ['constant_a', 'constant_b'], ['offset']),
helper.make_node('Add', ['same', 'offset'], ['result'])],
'optimization-demo',
[helper.make_tensor_value_info('features', TensorProto.FLOAT, ['batch', 2])],
[helper.make_tensor_value_info('result', TensorProto.FLOAT, ['batch', 2])],
[numpy_helper.from_array(np.array([1, 2], dtype=np.float32), 'constant_a'),
numpy_helper.from_array(np.array([3, 4], dtype=np.float32), 'constant_b')],
)
model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 17)])
model.ir_version = 10
onnx.checker.check_model(model)
onnx.save(model, 'original.onnx')
def make_session(level, save=None):
options = ort.SessionOptions()
options.intra_op_num_threads = 1
options.inter_op_num_threads = 1
options.graph_optimization_level = level
if save:
options.optimized_model_filepath = save
return ort.InferenceSession('original.onnx', sess_options=options,
providers=['CPUExecutionProvider'])
raw = make_session(ort.GraphOptimizationLevel.ORT_DISABLE_ALL)
optimized = make_session(ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
'optimized-basic.onnx')
optimized_model = onnx.load('optimized-basic.onnx')
onnx.checker.check_model(optimized_model)
restore_options = ort.SessionOptions()
restore_options.intra_op_num_threads = 1
restore_options.inter_op_num_threads = 1
restore_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
restored = ort.InferenceSession('optimized-basic.onnx', sess_options=restore_options,
providers=['CPUExecutionProvider'])
errors = []
for batch in [1, 8, 64]:
x = np.arange(batch * 2, dtype=np.float32).reshape(batch, 2) / 10
a = raw.run(None, {'features': x})[0]
b = optimized.run(None, {'features': x})[0]
c = restored.run(None, {'features': x})[0]
np.testing.assert_allclose(a, b, rtol=1e-6, atol=1e-6)
np.testing.assert_allclose(b, c, rtol=1e-6, atol=1e-6)
np.testing.assert_allclose(b, x + np.array([4, 6], dtype=np.float32))
errors.append(float(np.max(np.abs(a-b))))
x = np.ones((64, 2), dtype=np.float32)
feed = {'features': x}
for session in [raw, optimized]:
for _ in range(100): session.run(None, feed)
timings = {'raw': [], 'basic': []}
runs_per_round = 100_000
for round_index in range(5):
order = [('raw', raw), ('basic', optimized)]
if round_index % 2: order.reverse()
for name, session in order:
started = perf_counter_ns()
for _ in range(runs_per_round): session.run(None, feed)
timings[name].append((perf_counter_ns()-started) / runs_per_round / 1000)
report = {'original_nodes': [n.op_type for n in model.graph.node],
'optimized_nodes': [n.op_type for n in optimized_model.graph.node],
'batch_sizes_checked': [1, 8, 64],
'max_abs_error': max(errors),
'saved_model_reload_output': 'matched',
'runs_per_round': runs_per_round,
'mean_us_per_run_by_round': timings,
'median_of_round_means_us': {k: float(np.median(v)) for k,v in timings.items()},
'timed_scope': 'session.run only; fixed batch64 float32, CPU, 1 thread; initialization excluded'}
Path('optimization-report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(report,ensure_ascii=False,indent=2))
先看结构和数值,再看速度
original_nodes: ['Identity', 'Add', 'Add']
optimized_nodes: ['Add']
batch_sizes_checked: [1, 8, 64]
max_abs_error: 0.0
saved_model_reload_output: matched
这次保存的基础优化图剩一个 Add:Identity 被移除,两个常量的加法提前得到 [4,6]。三个 batch 的原图和优化会话输出最大绝对差为 0;重新加载优化文件的结果也与优化会话一致。它说明本次结构优化和保存副本的验证通过,不代表其他模型可以跳过数值与任务质量检查。
本次耗时记录
以下为这次 CPU 教学实验的实际轮均值,单位微秒;保留各轮而不是挑最好的一个数字。
| 轮次 | 关闭优化 | 基础优化 |
|---|---|---|
| 1 | 6.306 | 5.398 |
| 2 | 6.275 | 5.423 |
| 3 | 7.637 | 7.129 |
| 4 | 8.260 | 7.042 |
| 5 | 8.379 | 6.770 |
| 五轮均值的中位数 | 7.637 | 6.770 |
本次基础优化的记录较低,同时不同轮次的耗时波动明显。本次足以核对结构变化、输出和计时方法,尚不足以断言稳定的加速幅度。图很小,Python 调用开销、系统调度和测量抖动都可能影响差异;不能把该数字推算成真实大模型的提速比例,也不能保证你复跑得到相同数值。
优化图更少,为什么真实模型仍可能没变快
如果原模型已经优化、可优化部分很少、运行开销由数据准备或网络主导,图节点减少不一定明显改变整体延迟。先检查实际优化文件、目标 EP 与数值,再按自己的输入批次和端到端口径测量;没有速度优势时如实保留结果,不因为启用了开关就写“已加速”。
优化后的文件还可能依赖执行提供程序、优化选项和硬件能力。官方文档特别说明离线优化要匹配目标环境;即使本例仅用基础优化,也没有验证跨设备运行。换目标机器或 EP 后,应重新做加载、固定输入输出和性能检查。
图优化与量化是一回事吗?
不是。这里做图结构变换,仍使用 float32 输入和同一数值契约;INT8 量化改变数值表示,并需要另外检查输出与质量。若要做量化,可阅读ONNX 动态量化及输出比较,不能把本篇的零差异复制为量化结论。
读者下一步是先跑教学图并核对节点、三个 batch 的数值和保存副本,再用同样受控口径比较自己已经能运行的模型。任何实际加速结论都应同时给出模型、输入、环境、计时范围和测量记录。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/32911.html