一、问题阐述:
深度学习pytorch框架在实际部署中由于其对环境和资源要求太高以及python语言的限制性,导致其部署存在重重困难,在这里介绍一下onnxruntime(GPU)的部署流程。
二、onnruntime介绍
ONNXRuntime是微软推出的一款推理框架,用户可以非常便利的用其运行一个onnx模型。ONNXRuntime支持多种运行后端包括CPU,GPU,TensorRT,DML等。可以说ONNXRuntime是对ONNX模型最原生的支持。虽然大家用ONNX时更多的是作为一个中间表示,从pytorch转到onnx后直接喂到TensorRT或MNN等各种后端框架了= =,但这并不能否认ONNXRuntime是一款非常优秀的推理框架(微软出品,必属精品)。
而且由于其自身只包含推理功能(1.2版本,最新的ONNXRuntime甚至已经可以训练,可见微软在其上面的野心还是有的),对比主流框架源码看起来没有那么复杂难懂,通过阅读其源码可以非常清晰的理解深度学习框架的一些核心功能原理(op注册,内存管理,运行逻辑等)。
三、打包部署比较
1.不使用GPU
部署方式 | 资源大小 | 运行速度 | 精度 |
pytorch | 1G | 0.05s/img | 90% |
onnxruntime | 80M | 0.01s/img | 89% |
2.使用GPU
部署方式 | 资源大小 | 运行速度 | 精度 |
pytorch | 7G | 0.01s/img | 90% |
onnxruntime | 600M | 0.005s/img | 89% |
这里强调一下:pytorch打包可以在任意显卡下运行,onnxruntime如果在任意显卡下使用后面介绍
四、onnxruntime安装
1.pip install onnx
2.pip install onnxruntime
3.pip install onnxruntime-gpu(针对gpu,不使用gpu此步骤省略)
五、pytorch转onnxruntime
import os
import onnx
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
example = torch.randn(1,3,320,320) # 1,3,320,320保持与pytorch大小一致
model = Unet() # 用户自己的网络模型
model.load_state_dict(torch.load(r"'.....pth")) # 加载训练好的模型
model = model.to(device) # 模型放到cuda或者cpu上
torch.onnx.export(model,example,r".....onnx",opset_version=11) # 导出模型,注意opset_version参数
model_onnx = onnx.load(r".....onnx") # onnx加载保存的onnx模型
onnx.checker.check_model(model_onnx) # 检查模型是否有问题
print(onnx.helper.printable_graph(model_onnx.gfraph)) #打印onnx网络
六、onnxruntime推理
使用onnxruntime-gpu和onnxruntime都是import onnxruntime
import os
import onnx
import onnxruntime
import cv2
import numpy as np
# 注意:onnx是不支持sigmoid算子的,这里自己定义一个函数
def onnx_sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
# 创建一个InferenceSession的实例,并将模型的地址传递给该实例
sess = onnxruntime.InferenceSession('BiSeNet.onnx')
# 调用实例sess的润方法进行推理
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
# step1: 数据预处理
in_size = 512
norm_mean = (0.5, 0.5, 0.5) # 比imagenet的mean效果好
norm_std = (0.5, 0.5, 0.5)
transform = A.Compose([
A.Resize(width=in_size, height=in_size),
A.Normalize(norm_mean, norm_std),])
path_img = "test.jpg"
img_bgr = cv2.imdecode(np.fromfile(path_img), cv2.IMREAD_UNCHANGED)
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 注意 opencv默认bgr,需要转换为rgb
img_rgb = cv2.resize(img_rgb, (in_size, in_size))
img_rgb = img_rgb.astype(np.float32)/255.0
img_rgb = (img_rgb - norm_mean) / norm_std
transformed = transform(image=img_rgb, mask=img_rgb) # 预处理
img_rgb = transformed['image'] # 取出图片
img_in = np.transpose(img_rgb, (2, 0, 1)).astype(np.float32)
img_in = np.expand_dims(img_in, axis=0)
# step2: onnx推理
outputs = sess.run([output_name], {input_name: img_in})
print("process over! ")
# step3: 推理后处理
pre_label = np.squeeze(outputs[0])
pre_label = onnx_sigmoid(pre_label)
# step4:后处理显示图像
background = np.zeros_like(img_bgr, dtype=np.uint8)
background[:] = 255
alpha_bgr = pre_label
alpha_bgr = cv2.cvtColor(alpha_bgr, cv2.COLOR_GRAY2BGR)
h, w, c = img_bgr.shape
alpha_bgr = cv2.resize(alpha_bgr, (w, h))
cv2.imshow("alpha_bgr ",alpha_bgr )
cv2.watiKey(0)
注意onnxruntime网络输入的是numpy格式
七、Pyinstaller打包onnxruntime
1.pyinstaller安装
pip install pyinstaller
2.pyinstaller打包
pyinstaller -D -w main.py
-h | 该模块的help信息 |
-F | 只生成一个exe可执行文件 |
-D | 生成一个目录(包含多个文件),执行速度更快 |
-w | 运行exe时,不显示命令行窗口 |
-i | 可执行文件的icon图标路径 |
-distpatj | 可执行文件保存的路径 |
-n | 可执行文件的名字 |
执行完上述命令会在生成一个对应的main.spec文件(打包用)
3.pyinstaller spec文件打包参数
下面为生成的spec文件,下面介绍相关参数
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['Display.py'],# 此列表存放项目设计的所有python脚本文件
pathex=['E:\\MyCode\\', 'E:\\MyCode\\Display'],# 此列表为项目绝对路径
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Display',# 打包程序的名字
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True,# 打包后的可执行文件双击运行时屏幕会出现一个cmd窗口,不影响原程序运行
icon='D:\\hahah.ico'# 如果想要修改程序图标,使用在EXE()中加入 icon='绝对路径',一定要注意设置程序图标是.ico格式文件(16*16)
)
变量 | 含义 |
a | Analysis类的实例,要求传入各种脚本用于分析程序的导入和依赖。a中内容主要包括以下四部分:scripts,即可以在命令行中输入的Python脚本;pure,程序代码文件中的纯Python模块,包括程序的代码文件本身;binaries,程序代码文件中需要的非Python模块,包括–add-binary参数指定的内容;datas,非二进制文件,包括–add-data参数指定的内容。 |
pyz | PYZ的实例,是一个.pyz文件,包含了所有pure中的所有Python模块。 |
exe | EXE类的实例,这个类是用来处理Analysis和PYZ的结果的,也是用来生成最后的exe可执行程序。 |
coll | COLLECT类的实例,用于创建输出目录。在-F模式下,是没有COLLECT实例的,并且所有的脚本、模块和二进制文件都包含在了最终生成的exe文件中。 |
block_cipher | 加密密钥(防止反编译) |
以上内容中修改比较多的是a、exe的内容,coll和pyz基本没有遇到需要修改的情况。
a.scripts | 也是第一个参数,它是一个脚本列表,可以传入多个py脚本,效果与命令行中指定多py文件相同,即py文件不止一个时,比如“pyinstaller xxx1.py xxx2.py”,pyinstaller会依次分析并执行,并把第一个py名称作为spec和dist文件下的文件夹和程序的名称 |
a.pathex | 默认有一个spec的目录,当我们的一些模块不在这个路径下,记得把用到的模块的路径添加到这个list变量里。同命令“-p DIR/–paths DIR” |
a.datas | 作用是将本地文件打包时拷贝到目标路径下,可以是资源文件。datas是一个元素为元组的列表,每个元组有两个元素,都必须是字符串类型,元组的第一个元素为数据文件或文件夹,元组的第二个元素为运行时这些文件或文件夹的位置。例如:datas=[(’./src/a.txt’, ‘./dst’)],表示打包时将"./src/a.txt"文件添加(copy)到相对于exe目录下的dst目录中。注意:打包之后的exe运行时缺少的dll一般在这添加,也可自行拷贝到exe下 |
a.binaries | 添加二进制文件,也是一个列表,定义方式与datas参数一样。没具体使用过。同命令“–add-binary”。 |
a.hiddenimports | 指定脚本中需要隐式导入的模块,比如在__import__、imp.find_module()、exec、eval等语句中导入的模块,这些模块PyInstaller是找不到的,ImportError,需要手动指定导入,这个选项可以使用多次。在运行exe时提示缺少Moudle name,在这里添加。 |
a.hookspath | 指定额外hook文件(可以是py文件)的查找路径,这些文件的作用是在PyInstaller运行时改变一些Python或者其他库原有的函数或者变量的执行逻辑(并不会改变这些库本身的代码),以便能顺利的打包完成,这个选项可以使用多次。 |
a.runtime_hooks | 指定自定义的运行时hook文件路径(可以是py文件),在打好包的exe程序中,在运行这个exe程序时,指定的hook文件会在所有代码和模块之前运行,包括main文件,以满足一些运行环境的特殊要求,这个选项可以使用多次。 |
a.excludes | 指定可以被忽略的可选的模块或包,因为某些模块只是PyInstaller根据自身的逻辑去查找的,这些模块对于exe程序本身并没有用到,但是在日志中还是会提示“module not found”,这种日志可以不用管,或者使用这个参数选项来指定不用导入,这个选项可以使用多次 |
exe参数console | 设置是否显示命令行窗口,同命令-w/-c。 |
exe参数icon | 设置程序图标,默认spec是没有的,需要手动添加,参数值就是图片路径的字符串。同命令“命令-i/–icon”。 |
八、Pyinstaller打包onnxruntime遇到的问题
提示找不到onnxruntime_providers_shared.dll 文件
修改spec文件,把这个dll文件加进去。(或者直接找到该dll,拷贝到exe下)
a = Analysis(['main.py'],
pathex=[],
binaries=[],
datas=[('D:\\anaconda3\\envs\\xffpackage\\Lib\\site-packages\\onnxruntime\\capi\\onnxruntime_providers_shared.dll', 'onnxruntime\\capi')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
之后再pyinstaller main.spec
提示找不到CUDA
onnxruntime-gpu需要严格安照官方提供的文档与cuda、cudnn匹配
onnxruntime官网 打包好的onnxruntime在cpu下可以正常运行,在GPU版本下无法运行
1.首先按照官网文档检查onnxruntime、cuda、cudnn三个是否匹配
2.打包时候不能使用-F打包成一个单独的exe,应该使用-D打包成一个包含环境的文件目录
pyinstaller -D main.py
3.打包完之后,在Anaconda3下把onnxruntime以及onnxruntime_gpu-1.11.1.dist-info两个文件拷贝到exe目录下,直接覆盖即可
4.打包完之后在自己电脑上可以运行,在他人电脑下提示cuda不能加载
此问题是由于他人电脑上的cuda和cudnn与打包的exe的onnxruntime不匹配。总不能再重新按照他人电脑环境重新打包吧,那这样打包部署的意义就失去了。
问题解决:在我们自己打包的电脑上,按照onnxruntime官网上提示的文档内把需要的cuda和cudnn环境打包进来。
上图中的绿色标记为需要的环境,在打包的电脑下进行查找这些文件,然后拷贝到exe下
其中,cublas64_11.dll和cublasLt64_11.dll以及cudart64_110.dll、cufft64_10.dll在下面文件夹下
其余dll在下面文件夹下
最后,再运行exe大,大功告成!!!