利用 onnxruntime 库同时推理多个模型的效率研究
1. 背景
需求:针对视频形式的数据输入,对每一帧图像,有多个神经网络模型需要进行推理并获得预测结果。如何让整个推理过程更加高效,尝试了几种不同的方案。
硬件:单显卡主机。
2. 方案
由于存在多个模型需要推理,但模型之间没有相互依赖关系,因此很容易想到通过并行的方式来提高运行效率。
对比了如下几种方案的结果,包括:
- 串行
- 线程
- 进程
- 协程
3. 实现
3.1 整体流程
配置了 4 个体量相近的模型。
为了屏蔽读取和解码的时间消耗对最终结果的影响,提前读取视频并准备输入。
统计每个单独模型执行推理的累积时间,以及整体的运行时间。
import asyncio
from time import time
def main():
frames = load_video()
weights = load_weights()
print('串行:')
one_by_one(weights, frames)
print('多线程:')
multit_thread(weights, frames)
print('多进程:')
multi_process(weights, frames)
print('协程:')
asyncio.run(coroutine(weights, frames))
3.2 串行
读取到当前帧数据后,所有模型依次运行。
def one_by_one(weights, frames):
sessions = [init_session(weight) for weight in weights]
costs = [[] for _ in range(len(weights))]
since_infer = time()
for frame in frames:
for session in sessions:
since = time()
_ = session.run('output', {'input': frame})
cost = time() - since
costs[idx].append(cost)
print([sum(cost) for cost in costs])
print("infer:", time() - since_infer)
return
3.3 多线程
为每一个模型分配一个线程。
from threading import Thread
def multit_thread(weights, frames):
sessions = [init_session(weight) for weight in weights]
threads = []
since_infer = time()
for session in sessions:
thread = Thread(target=run_session_thread, args=(session, frames))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print("infer:", time() - since_infer)
return
def run_session_thread(session, frames):
costs = []
for frame in frames:
since = time()
_ = session.run('output', {'input': frame})
costs.append(time() - since)
print(sum(costs))
return
3.4 多进程
为每一个模型分配一个进程。
由于 session 不能在进程间传递,因此需要在每个进程的内部单独初始化。如果数据较多,这部分初始化的时间消耗基本可以忽略不急。
from multiprocessing import Manager, Process
def multi_process(weights, frames):
inputs = Manager().list(frames)
processes = []
since_infer = time()
for weight in weights:
process = Process(target=run_session_process, args=(weight, inputs))
process.start()
processes.append(process)
for process in processes:
process.join()
print("infer:", time() - since_infer)
return
def run_session_process(weight, frames):
session = init_session(weight)
costs = []
for frame in frames:
since = time()
_ = session.run('output', {'input': frame})
costs.append(time() - since)
print(sum(costs))
return
3.5 协程
为每一个模型分配一个协程。
async def coroutine(weights, frames):
sessions = [init_session(weight) for weight in weights]
since_infer = time()
tasks = [
asyncio.create_task(run_session_coroutine(session, frames))
for session in sessions
]
for task in tasks:
await task
print("infer:", time() - since_all)
return
async def run_session_coroutine(session, frames):
costs = []
for frame in frames:
since = time()
_ = session.run('output', {'input': frame})
costs.append(time() - since)
print(sum(costs))
return
3.6 其他辅助函数
import cv2
import numpy as np
import onnxruntime as ort
def init_session(weight):
provider = "CUDAExecutionProvider"
session = ort.InferenceSession(weight, providers=[provider])
return session
def load_video():
# 为了减少读视频的时间,复制相同的图片组成batch
vcap = cv2.VideoCapture('path_to_video')
count = 1000
batch_size = 4
frames = []
for _ in range(count):
_, frame = vcap.read()
frame = cv2.resize(frame, (256, 256)).transpose((2, 0, 1))
frame = np.stack([frame] * batch_size, axis=0)
frames.append(frame.astype(np.float32))
return frames
def load_weights():
return ['path_to_weights_0',
'path_to_weights_1',
'path_to_weights_2',
'path_to_weights_3',]
4. 结果及分析
4.1 执行结果
以batch_size=4
共运行 1000 帧数据,推理结果如下:
方案 | 串行 | 线程 | 进程 | 协程 |
---|---|---|---|---|
单模型累积时间/s | 7.9/5.3/5.2/5.2 | 13.5/13.5/15.6/15.7 | 13.5/13.8/13.7/13.6 | 6.5/5.2/5.3/5.3 |
总时间/s | 23.7 | 15.8 | 30.1 | 22.5 |
显存占用/MB | 1280 | 1416 | 3375 | 1280 |
平均 GPU-Util | 约 60% | 约 85% | 约 70% | 约 55% |
- 在这个场景下,多线程是综合效率最高的方式(时间最短、显存占用合理、GPU 利用率最高);
- 串行作为最基础的方案,总时间就是每个模型执行时间之和;
- 多进程的方式,单模型的累积时间与多线程类似,但是总时间有明显增加,且极大增加了显存占用;
- 用协程的方式,总结果看,与串行模式本质上是一样的。
4.2 结果分析
4.2.1 关于线程方案
为什么多线程相比串行可以提高运行效率?
- 基本的判断是,
session.run()
函数运行时,既有 CPU 执行的部分,又有 GPU 执行的部分; - 如果是串行方案,则 CPU 运行时,GPU 会等待,反之亦然;
- 当换用多线程方案后,当一个线程从 CPU 执行切换到 GPU 执行后,会继续执行另一个线程的 CPU 部分,并等待 GPU 返回结果。
4.2.2 关于进程方案
为什么多进程反而降低了运行效率?
- 基本的判断是,整体执行的瓶颈并不在 CPU 的运算部分,而是在于 GPU 上模型前向推理的计算部分;
- 因此,用多个进程并没有充分利用系统资源,多个 CPU 核心会争夺同一个 GPU 的计算资源,并增加了调度消耗。
4.2.3 关于协程方案
为什么看起来协程与串行的效果一样?
协程方案在执行过程中,从表现上来看:
- 单个模型的累积时间是逐步
print
出来的,间隔大致等于每个模型的累积时间(而线程和进程方案中,几乎是同时输出 4 个模型的累积时间,说明是同时运行结束); - 显存占用是逐步增加的,最后达到与串行方案一致。
可能的原因:
- CPU 和 GPU 的任务切换,可能无法触发协程的切换,导致最终的效果是,一个模型完成了所有数据的推理后,再进行下一个模型的推理。
使用协程的必要性:
- 从线程改为协程,是为了进一步降低线程切换的消耗;
- 在这个场景下,需要同时执行推理的模型数量一般不会太多,建立同样数量的线程,系统资源的消耗是可控的;
- 因此,没有使用协程的必要性。
关于协程的使用,也是现学,有可能因为使用方法不当而得出以上的结论。如有错误,欢迎指正。
利用 onnxruntime 库同时推理多个模型的效率研究的更多相关文章
- 人脸检测及识别python实现系列(5)——利用keras库训练人脸识别模型
人脸检测及识别python实现系列(5)——利用keras库训练人脸识别模型 经过前面稍显罗嗦的准备工作,现在,我们终于可以尝试训练我们自己的卷积神经网络模型了.CNN擅长图像处理,keras库的te ...
- Python 3 利用 Dlib 19.7 和 sklearn机器学习模型 实现人脸微笑检测
0.引言 利用机器学习的方法训练微笑检测模型,给一张人脸照片,判断是否微笑: 使用的数据集中69张没笑脸,65张有笑脸,训练结果识别精度在95%附近: 效果: 图1 示例效果 工程利用pytho ...
- 利用 TFLearn 快速搭建经典深度学习模型
利用 TFLearn 快速搭建经典深度学习模型 使用 TensorFlow 一个最大的好处是可以用各种运算符(Ops)灵活构建计算图,同时可以支持自定义运算符(见本公众号早期文章<Tenso ...
- 【模型推理】Tengine 模型转换及量化
欢迎关注我的公众号 [极智视界],回复001获取Google编程规范 O_o >_< o_O O_o ~_~ o_O 本文介绍一下 Tengine 模型转换 ...
- c# 利用动态库DllImport("kernel32")读写ini文件(提供Dmo下载)
c# 利用动态库DllImport("kernel32")读写ini文件 自从读了设计模式,真的会改变一个程序员的习惯.我觉得嘛,经验也可以从一个人的习惯看得出来,看他的代码编写习 ...
- php学习笔记:利用gd库生成图片,并实现随机验证码
说明:一些基本的代码我都进行了注释,这里实现的验证码位数.需要用的字符串都可以再设置.有我的注释,大家应该很容易能看得懂. 基本思路: 1.用mt_rand()随机生成数字确定需要获取的字符串,对字符 ...
- CocoaPods的安装及使用/利用开源库Diplomat实现分享及第三方登录/git的使用
<<史上最简洁版本>> 1.gem sources -l查看 当前的源 //1.1 sudo -i..以下都是以管理员的身份来操作的 2.gem sources --remov ...
- python利用selenium库识别点触验证码
利用selenium库和超级鹰识别点触验证码(学习于静谧大大的书,想自己整理一下思路) 一.超级鹰注册:超级鹰入口 1.首先注册一个超级鹰账号,然后在超级鹰免费测试地方可以关注公众号,领取1000积分 ...
- 利用OpenSSL库对Socket传输进行安全加密(RSA+AES)
轉自:http://blog.chinaunix.net/uid-9543173-id-3921143.html 利用OpenSSL库对Socket传输进行安全加密(RSA+AES) 1. 利用RSA ...
随机推荐
- thinkpad笔记本选型
ThinkPad分为了几大系列,低端的有L系列.E系列,比较高端的有T系列.X系列及P系列,这些系列中质量比较稳定属于商务办公系列,中端有针对商务或者是娱乐的R系列.A系列和S系列.具体介绍如下: 1 ...
- 华为交换机配置ACL详细步骤
ACL 介绍 #2000-2999普通ACL,根据源IP过滤 #3000-3999高级ACL,根据源目的端口和源目的地址等过滤 #4000-4999二层ACL,根据源目的MAC等过滤 配置举例: 拒绝 ...
- 『现学现忘』Docker基础 — 22、使用Docker安装Nginx
目录 步骤1:搜索镜像 步骤2:下载Nginx镜像 步骤3:运行Nginx镜像 步骤4:进行本机测试 步骤5:进入容器内操作 步骤6:测试外网访问容器 步骤1:搜索镜像 使用docker search ...
- 写博客的技巧整理——基于Markdown
我们需要掌握各种技巧,这样才能在写博客时游刃有余,以下内容觉得不错就点个赞吧 文章目录 1.目录与目录跳转 目录一(示例用勿点) 目录二(示例用勿点) 目录三(示例用勿点) 2.文字与图片 3.引用 ...
- Arduino UNO开发板、Arduino CNC Shield V3.0扩展板、A4988驱动板、grbl固件使用教程
前言 CNC Shield V3.0可用作雕刻机,3D打印机等的驱动扩展板,板上一共有4路步进电机驱动模块的插槽,可驱动4路不进电机,而每一路步进电机都只需要2个IO口,也就是说,6个IO口就可以很好 ...
- Spring核心思想:IOC(控制反转)、DI(依赖注入)和AOP(面向切面编程)
Spring有三大核心思想,分别是控制反转(IOC,Inversion Of Controller),依赖注入(DI,Dependency Injection)和面向切面编程(AOP,Aspect O ...
- 《Java多线程编程核心技术》知识梳理
<Java多线程编程核心技术> @author ergwang https://www.cnblogs.com/ergwang/ 文章末尾附pdf和png下载链接 第1章 Java多线程技 ...
- Spring Boot 中的监视器是什么?
Spring boot actuator 是 spring 启动框架中的重要功能之一.Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态.有几个指标必须在生产环境中进行检 ...
- java-规约-OOP
public class OOP { /** * 避免通过一个类的对象引用访问此类的静态变量或者静态方法 * 直接通过类名去访问 */ // 错误使用例子: public static void ma ...
- java-設計模式-單例模式
單例模式 一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点. 一个类只有一个实例,且该类能自行创建这个实例的一种模式. 簡單的對比就是: 例如,Windows 中 ...