ONNX Runtime Python 推理性能优化:8 个低延迟工程实践

简介: 深度学习推理慢?未必是模型问题。本文揭示8大ONNX Runtime工程优化技巧:合理选择执行提供器、精准控制线程、规避内存拷贝、固定Shape分桶、启用图优化、CPU量化加速、预热与微批处理、向量化前后处理。不改模型也能显著提升性能,低延迟落地关键在于细节调优。

在深度学习落地过程中,有一个常见的误区:一旦推理速度不达标,大家的第一反应往往是拿着模型开到,比如:做剪枝、搞蒸馏、甚至牺牲精度换小模型。

实际上生产环境中的 Python 推理链路隐藏着巨大的“工程红利”。很多时候你的模型本身并不慢,慢的是低效的数据搬运、混乱的线程争用以及不合理的 Runtime 默认配置。在不改变模型精度的情况下,仅靠ONNX Runtime (ORT) 的工程特性,往往就能从现有技术栈中“抠”出惊人的性能提升。

以下是 8 个经过实战验证的低延迟优化策略,专治各种“莫名其妙的慢”。

1、 明确指定 Execution Provider 及其顺序

ORT 会严格按照你传入的

providers

列表顺序进行尝试。把最快的放在第一位,并且尽量避免它静默回退(Fallback)到 CPU。如果不显式指定,ORT 有时候会“犹豫”,这都会消耗时间。

 import onnxruntime as ort  

providers = [  
    ("TensorrtExecutionProvider", {"trt_fp16_enable": True}),  # if supported  
    "CUDAExecutionProvider",  
    "CPUExecutionProvider",  
]  

sess = ort.InferenceSession("model.onnx", providers=providers)  
print(sess.get_providers())  # verify what you actually got

Fallback 是有成本的,如果环境里有 TensorRT 就优先用,没有就降级到 CUDA,最后才是 CPU。把这个路径写死。另外在边缘设备上,OpenVINO 或者 CoreML 的性能通常吊打普通 CPU 推理;如果是 Windows 平台带集显DirectML 也是个容易被忽视的加速选项。

2.、像做手术一样控制线程数(不要超配)

线程配置有两个核心参数:intra-op(算子内并行)和 inter-op(算子间并行)。这两个参数的设置必须参考机器的物理核心数以及你的负载特性。

 import os, multiprocessing as mp, onnxruntime as ort  

cores = mp.cpu_count() // 2 or 1  # conservative default  
so = ort.SessionOptions()  
so.intra_op_num_threads = cores  
so.inter_op_num_threads = 1  # start low for consistent latency  
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  

sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"])

默认的线程策略经常会跟 NumPy、BLAS 库甚至你的 Web Server 抢占资源,导致严重的线程争用和长尾延迟。建议把

inter_op

设为 1(通常能获得更稳定的延迟),然后遍历测试

intra_op

(从 1 到物理核数),盯着 p50p95 指标找最佳平衡点,不要光看平均速度。

3、使用 IO Binding 规避内存拷贝(GPU 必选项)

如果在 GPU 上跑推理,却每次

run()

都把张量从 Device 拷回 Host再拷回 Device,利用 IO Binding 将输入/输出直接绑定在显存上,复用这块内存。

 import onnxruntime as ort  
import numpy as np  

sess = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider"])  
io = sess.io_binding()  

# Example: preallocate on device via OrtValue (CUDA)  
import onnxruntime as ort  
x = np.random.rand(1, 3, 224, 224).astype(np.float32)  
x_ort = ort.OrtValue.ortvalue_from_numpy(x, device_type="cuda", device_id=0)  

io.bind_input(name=sess.get_inputs()[0].name, device_type="cuda", device_id=0, element_type=np.float32, shape=x.shape, buffer_ptr=x_ort.data_ptr())  
io.bind_output(name=sess.get_outputs()[0].name, device_type="cuda", device_id=0)  

sess.run_with_iobinding(io)  
y_ort = io.get_outputs()[0]  # still on device

这对于高频请求特别重要,哪怕单次拷贝只耗费几毫秒,累积起来也是巨大的开销,所以让热数据留在它该在的地方。

4、锁定 Shape 或采用分桶策略

动态 Shape 看起来很灵活,但它会阻碍 ORT 进行激进的算子融合和 Kernel 优选。在导出 ONNX 时能固定 Shape 就尽量固定

如果业务场景确实需要变长输入,可以采用分桶(Bucketing)策略:

 # pseudo: choose session by input shape  
 def get_session_for_shape(h, w):  
     if h <= 256 and w <= 256: return sess_256  
     if h <= 384 and w <= 384: return sess_384  
     return sess_fallback

比如在视觉任务中,把输入限定在 224、256、384 这几档,创建对应的 Session。哪怕只分两三个桶,性能表现也比完全动态 Shape 强得多。

5、开启全图优化并验证

这一步很简单但容易被忽略。开启

ORT_ENABLE_ALL

,让 ORT 帮你做算子融合、常量折叠和内存规划。

 import onnxruntime as ort  

so = ort.SessionOptions()  
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
# optional: serialize the optimized model for inspection  
so.optimized_model_filepath = "model.optimized.onnx"  

sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"])

更少的算子意味着更少的 Kernel Launch 开销和内存带宽压力。建议导出一个

optimized_model_filepath

,用 Netron 打开看看,确认 Conv+BN+ReLU 这种经典组合是不是真的被融合成一个节点了,如果没融那就是优化链路上有问题。

6、CPU 推理?直接上量化

如果只能用 CPU,INT8 量化或者动态量化是提速神器。配合 CPU 的向量指令集能极大减少矩阵乘法的开销。

 from onnxruntime.quantization import quantize_dynamic, QuantType  

quantize_dynamic(  
    model_input="model.onnx",  
    model_output="model.int8.onnx",  
    weight_type=QuantType.QInt8,   # try QInt8 or QUInt8  
    extra_options={"MatMulConstBOnly": True}  
)

然后加载量化后的模型:

 import onnxruntime as ort  
 sess = ort.InferenceSession("model.int8.onnx", providers=["CPUExecutionProvider"])

对于 Transformer 类模型,动态量化通常能带来 1.5 到 3 倍的加速且精度损失很小。不过需要先在真实数据上验证,如果精度掉得厉害尝试 Per-channel 量化或者只量化计算最密集的算子。

7、预热、复用与 Micro-Batching

InferenceSession

的初始化开销很大,属于重资源对象。务必全局只创建一次,并且需要启动后先跑几次 Dummy Data 做预热,把 Kernel Cache 和内存池填好。

 # app startup  
 sess=ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider"])  
 dummy= {sess.get_inputs()[0].name: np.zeros((1, 3, 224, 224), np.float32)}  
 for_inrange(3):  
     sess.run(None, dummy)  # warms kernels, caches, memory arenas

如果是高并发场景不要一个个请求单独跑,攒一个 Micro-batch(比如 2 到 8 个样本)一起送进去,能显著提高 GPU 利用率(Occupancy)。

 definfer_batch(batch):  
     inputs=np.stack(batch, axis=0).astype(np.float32, copy=False)  
     returnsess.run(None, {sess.get_inputs()[0].name: inputs})[0]

调整 Batch Size 的时候,盯着 p95 延迟吞吐量看,找到那个甜点。

8、优化前后处理:拒绝 Python 循环

很多时候大家抱怨模型慢,其实瓶颈在预处理和后处理。Python 的

for

循环处理像素或 logits 是绝对的性能杀手。所以保持数组内存连续,避免不必要的

astype

转换尽量全部向量化。

 import numpy as np  

# Bad: repeated copies/conversions  
# x = np.array(img).astype(np.float32)  # realloc every time  

# Better: reuse buffers and normalize in-place  
buf = np.empty((1, 3, 224, 224), dtype=np.float32)  

def preprocess(img, out=buf):  
    # assume img is already CHW float32 normalized upstream  
    np.copyto(out, img, casting="no")   # no implicit cast  
    return out  

# Post-process with NumPy ops, not Python loops  
def topk(logits, k=5):  
    idx = np.argpartition(logits, -k, axis=1)[:, -k:]  
    vals = np.take_along_axis(logits, idx, axis=1)  
    order = np.argsort(-vals, axis=1)  
    return np.take_along_axis(idx, order, axis=1), np.take_along_axis(vals, order, axis=1)

几个多余的

.astype()

就能吃掉好几毫秒,这点在低延迟场景下非常致命。

基准测试模板

这是一个简单的 Benchmarking 脚本,改改就能用,别靠感觉优化要用数据来进行对比:

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

def bench(sess, x, iters=100, warmup=5):  
    name = sess.get_inputs()[0].name  
    for _ in range(warmup):  
        sess.run(None, {name: x})  
    times = []  
    for _ in range(iters):  
        t0 = time.perf_counter()  
        sess.run(None, {name: x})  
        times.append((time.perf_counter() - t0) * 1e3)  
    return {  
        "p50_ms": stats.median(times),  
        "p95_ms": sorted(times)[int(0.95 * len(times)) - 1],  
        "min_ms": min(times),  
        "max_ms": max(times)  
    }  

# Example usage  
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]  
so = ort.SessionOptions(); so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
sess = ort.InferenceSession("model.onnx", sess_options=so, providers=providers)  

x = np.random.rand(1, 3, 224, 224).astype(np.float32)  
print(bench(sess, x))

总结

做低延迟推理没有什么黑科技,全是细节。选对 Provider,别乱开线程,减少内存拷贝,固定 Shape,激进地做图融合,最后把 Python 代码洗干净。哪怕只落实其中两三点,性能提升也是肉眼可见的。

https://avoid.overfit.cn/post/aa489c6b429641b9b1a1a3e4a3e4ce1d

作者:Modexa

目录
相关文章
|
机器学习/深度学习 缓存 PyTorch
PyTorch 2.0 推理速度测试:与 TensorRT 、ONNX Runtime 进行对比
PyTorch 2.0 于 2022 年 12 月上旬在 NeurIPS 2022 上发布,它新增的 torch.compile 组件引起了广泛关注,因为该组件声称比 PyTorch 的先前版本带来更大的计算速度提升。
1463 0
|
6月前
|
数据挖掘 数据库 索引
RAG检索模型选型:Bi-Encoder、Cross-Encoder、SPLADE与ColBERT的技术对比
本文解析RAG系统中Bi-Encoder、Cross-Encoder、SPLADE与ColBERT的核心机制,探讨如何平衡高召回与高精准。通过多阶段架构组合稀疏与稠密检索,实现高效准确的语义搜索。
537 3
RAG检索模型选型:Bi-Encoder、Cross-Encoder、SPLADE与ColBERT的技术对比
|
5月前
|
前端开发 机器人 iOS开发
深入OpenClaw网关:架构、网络模型与运行机制全解析
OpenClaw 不仅仅是一个聊天机器人工具,它是一套完整的、长期运行的AI Agent基础设施。其核心Gateway网关扮演着整个系统的“大脑”,统一管理所有即时通信渠道的连接,并协调Pi智能体、移动节点与应用之间的复杂交互。理解其进程模型与网络架构,是确保高权限智能体稳定、可控运行的关键。
|
8月前
|
机器学习/深度学习 并行计算 PyTorch
PyTorch 分布式训练底层原理与 DDP 实战指南
深度学习模型规模激增,如Llama 3.1达4050亿参数,单卡训练需数百年。并行计算通过多GPU协同解决此问题。本文详解PyTorch的分布式数据并行(DDP),涵盖原理、通信机制与代码实战,助你高效实现多卡训练。
1543 5
PyTorch 分布式训练底层原理与 DDP 实战指南
|
机器学习/深度学习 并行计算 PyTorch
ONNX 优化技巧:加速模型推理
【8月更文第27天】ONNX (Open Neural Network Exchange) 是一个开放格式,用于表示机器学习模型,使模型能够在多种框架之间进行转换。ONNX Runtime (ORT) 是一个高效的推理引擎,旨在加速模型的部署。本文将介绍如何使用 ONNX Runtime 和相关工具来优化模型的推理速度和资源消耗。
8151 4
|
存储 机器学习/深度学习 缓存
vLLM 核心技术 PagedAttention 原理详解
本文系统梳理了 vLLM 核心技术 PagedAttention 的设计理念与实现机制。文章从 KV Cache 在推理中的关键作用与内存管理挑战切入,介绍了 vLLM 在请求调度、分布式执行及 GPU kernel 优化等方面的核心改进。PagedAttention 通过分页机制与动态映射,有效提升了显存利用率,使 vLLM 在保持低延迟的同时显著提升了吞吐能力。
8827 22
vLLM 核心技术 PagedAttention 原理详解
|
人工智能 监控 算法
3D-Speaker:阿里通义开源的多模态说话人识别项目,支持说话人识别、语种识别、多模态识别、说话人重叠检测和日志记录
3D-Speaker是阿里巴巴通义实验室推出的多模态说话人识别开源项目,结合声学、语义和视觉信息,提供高精度的说话人识别和语种识别功能。项目包含工业级模型、训练和推理代码,以及大规模多设备、多距离、多方言的数据集,适用于多种应用场景。
4955 18
3D-Speaker:阿里通义开源的多模态说话人识别项目,支持说话人识别、语种识别、多模态识别、说话人重叠检测和日志记录
|
人工智能 测试技术 API
飞桨x昇腾生态适配方案:11_静态OM推理
昇腾AI推理工具`ais_bench`基于ACL开发,支持命令行快速推理与性能测试(吞吐率、时延等),并提供相关API。用户需下载适配环境的`aclruntime`和`ais_bench`的whl包后通过pip安装。设置环境变量后,可通过多种场景使用工具:纯推理(默认输入全0数据)、调试模式(获取详细参数与耗时信息)、文件/文件夹输入(指定Numpy文件或目录)、以及多Device并行推理。例如,BERT模型需按顺序传入三个文件夹对应其输入参数。工具输出包括吞吐率、耗时等关键指标,助力高效模型性能评估。
2163 0
|
编解码 人工智能 JSON
飞桨x昇腾生态适配方案:10_ONNX转OM
本章节主要介绍如何将ONNX模型转化为昇腾AI处理器支持的OM模型,并进行离线推理。通过昇腾张量编译器(ATC),可实现静态OM、动态BatchSize、动态分辨率、动态维度及动态shape等多种模型转换。文中详细说明了ATC工具的使用方法、参数配置、命令格式以及常见问题解决方法,同时提供了具体示例和可视化工具辅助输入参数确认,帮助用户高效完成模型转换与部署。
3733 0