【OpenVino CPU模型加速(二)】使用openvino加速推理

这里默认你已经安装好了openvino环境,如果没安装好,请参考 上一篇 。 openvino支持python、C/C++的API调用,这里以python为例,展现如何使用openvino进行推理,以及他的加速效果究竟如何。如果你是刚接触openvino工具包的,强烈建议你参看下如下这份代码,能够很好地说明openvino的用法和加速效果: gist.github.com/helena-inte…

1. 转换模型

不同框架训练出来的模型可能有不同的转化路径,但是殊途同归,道理都是一样的。都是从深度学习模型转化为openvino支持的模型格式。

1.1 Torch模型转为ONNX模型

ONNX是微软和FaceBook提出用来表示深度学习模型的开放格式。所谓开放就是ONNX定义了一组和环境、平台均无关的标准格式,来增强各种AI模型的可交互性。换句话说,无论你使用何种训练框架训练模型(如TensorFlow/PyTorch/PaddlePaddle/MXNET),在训练完毕后你都可以将这些框架的模型统一转换为ONNX这种统一的格式进行存储。注意ONNX不仅存储了神经网络的权重,同时也存储了模型的结构信息以及网络中的每一层输入输出和一些其他的辅助信息。这里以PyTorch框架为例,需要先将模型训练好的ckpt转为onnx格式。

import torch
def build_model(...):
    raise NotImplementedError
def generate_output_names(...):
    raise NotImplementedError
height, width = 800, 800
model = build_model()
model.eval()
output_names = generate_output_names()
dummy_input = torch.autograd.Variables(
    torch.randn(1, 3, height, width)
output_path = './onnx'
torch.onnx.export(
    model,
    dummy_input,
    output_path,
    verbose=True,
    keep_initializers_as_inputs=True,
    opset_version=11,
    output_names=output_names

1.2 验证onnx模型的正确性

生成onnx后,你需要验证onnx模型输出是否正确,可以使用onnxruntime运行模型,查看onnx模型的输出是否和模型输出一致。如果不一致,则onnx输出有问题,可能你输出onnx时多保存了一些计算操作,可以逐个对应节点的输出来找问题。

import cv2
import onnxruntime
onnx_model_path = './onnx/model_best.onnx'
ort_session = onnxruntime.InferenceSession(onnx_model_path)
image = cv2.imread('./test/test.jpg')  # load the test image
# 使用你的算法前处理部分 同时前处理输出必须为NCHW格式的numpy数组
image = preprocess(image)
ort_inputs = {ort_sessions.get_input()[0].name: image}
ort_outputs = ort_session.run(None, ort_inputs)
# 与模型网络输出结果进行比对

1.3 onnx简化

先解释下onnx简化是什么样的操作,这里贴上onnx-simplifier官方给出的样例,如下这样一段代码:

import torch
class JustReshape(torch.nn.Module):
    def __init__(self):
        super(JustReshape, self).__init__()
    def forward(self, x):
        return x.view((x.shape[0], x.shape[1], x.shape[3], x.shape[2]))
net = JustReshape()
model_name = 'just_reshape.onnx'
dummy_input = torch.randn(2, 3, 4, 5)
torch.onnx.export(net, dummy_input, model_name, input_names=['input'], output_names=['output'])

由于这个模型输入维度是固定的,所以我们期望模型是这样的:

但是ONNX只能获得下面的模型:

onnx-simplifier的核心思路就是利用onnxruntime推断一遍ONNX的计算图,然后使用常量输出代替冗余的运算OP就能得到我们期望的模型结构图,它可以简化深度学习模型,进行一定程度的压缩。使用onnx-simplifier过程非常简单,首先安装第三方软件包,之后直接使用包内工具进行转化。

pip install onnx-simplifier
python -m onnxsim input_onnx_model output_onnx_model

1.4 转化为openvino的IR模型格式

openvino实现CPU的加速效果的本质在于两点,一点是openvino里面的模型优化器模块,它会自动调整网络内部拓扑图结构,自动裁剪冗余的部分,另一点是openvino提供的推理引擎库,实现了在Intel硬件上软硬一体的加速效果。

openvino对PyTorch框架支持还不够成熟,所以一般PyTorch模型需要先转为onnx格式,再转为openvino的IR格式。openvino的IR模型主要是两个文件,bin文件存储了网络的权重信息,xml文件存储了网络的结构信息。这里可以使用Netron软件分别可视化打开onnx模型和xml文件,可以发现网络结构拓扑图是有所不同的,这就是openvino模型优化器的结果。

转换IR模型需要使用openvino中的工具(如果不是onnx的模型格式,使用mo.py):

python3 <INSTALL_DIR>/deployment_tools/model_optimizer/mo_onnx.py --input_model INPUT_MODEL --output_dir <OUTPUT_MODEL_DIR> --data_type FP16

其他参数可以参考官方文档中的说明.

1.5 量化模型至低精度INT8

1.4中openvino的mo.py或者mo_onnx.py工具提供了float, FP32, FP16等量化方式选择,却没有INT8量化。在模型部署时,工业界经常将模型量化到INT8精度,牺牲少量的精度来换取推理性能的提升。对于量化模型到INT8,openvino官方文档给出了两种量化方法:一种是DefaultQuantization,一种是更加精确的Precise Accuracy Aware Quantization方法,此处后面有时间再写。

2. OpenVino模型推理

2.1 OpenVino加载onnx模型进行推理

openvino支持onnx模型的加载和推理,有一定明显的加速效果,但是加速效果比IR模型略逊色一些。如下为调用示例:

from openvino.inference_engine import IECore
def preprocess(img_path):
    raise NotImplementedError
def postprocess(*args):
    raise NotImplementedError
ie = IECore()
model_path = './model_best.onnx'
# 加载onnx模型
net = ie.read_network(model=model_path)
model = ie.load_network(network=net, device_name='CPU')
input_blob = next(iter(net.input_info))
# 图像前处理
image = preprocess(img_path)
# 同步(阻塞)方式推理
ort_outs = model.infer(inputs={input_blob: image})
# 后处理
results = postprocess(ort_outs)

注:工程化可以参考OpenVINO™ Model Server

2.2 OpenVino加载IR模型进行推理

加载IR模型,与加载ONNX模型代码相比,read_network方法需要增加一个参数weights,其他代码无需变动。

from openvino.inference_engine import IECore
def preprocess(img_path):
    raise NotImplementedError
def postprocess(*args):
    raise NotImplementedError
ie = IECore()
model_path = './model_best.xml'
weight_path = './model_best.bin'
# 加载onnx模型
net = ie.read_network(model=model_path, weights=weight_path)
model = ie.load_network(network=net, device_name='CPU')
input_blob = next(iter(net.input_info))
# 图像前处理
image = preprocess(img_path)
# 同步(阻塞)方式推理
ort_outs = model.infer(inputs={input_blob: image})
# 后处理
results = postprocess(ort_outs)

3. OpenVino加速测试效果

不同的CPU型号可能加速效果也不尽相同,在Ubuntu 16.04服务器 Intel(R) Xeon(R) CPU E5-2690 v3 @ 2.60GHz条件下串行测试,结果如下表:

推理模块模型格式图片数量平均单张图片推理时间备注
torchckpt41090.758s(baseline) CPU推理
torchscript41090.77sCPU推理
onnxruntimeonnx(simplified)41090.296sCPU推理
openvinoonnx(simplified)41090.1097sCPU推理
openvinoIR41090.0599sCPU推理FP32

由此可见,加速效果是非常明显的。 注:同事在别的环境测试,相比torch只有2.5-3倍的性能提升。加速是确定性的,但是不同的环境可能会产生不同的加速效果。

4. OpenVino加载IR模型进行异步推理

OpenVINO可以设定要使用同步还是异步,先来说明什么是同步跟异步。

  • 同步 synchronous (sync): 程序中一次只能执行完成该任务才能执行下一项任务。
  • 非同步、异步 asynchronous (async): 程序中可以同时执行不同任务,每个任务不需等到其他任务完成,即可执行下一项任务。
  • Python API(官方文档):

  • openvino异步推理,load_network需要设置num_requests参数,表示最多同时接收多少个请求。
  • 应有请求队列对传入请求进行管理,每次传入一组请求。
  • 我写了一段支持同步和异步的推理代码,可以直接复用:

    from openvino.inference_engine import IECore, StatusCode
    class OpenVinoModel:
        def __init__(self, model_path, weight_path, is_async, num_requests=None):
            ie = IECore()
            net = ie.read_network(model=model_path, weights=weight_path)
            if is_async:
                self._model = ie.load_network(network=net, device_name='CPU',
                                              num_requests=num_requests)
            else:
                self._model = is.load_network(network=net, device_name=''CPU)
            self._input_blob = next(iter(net.input_info))
        def _preprocess(self, img_bgr):
            raise NotImplementedError
        def _postprocess(ort_outs):
            raise NotImplementedError
        def sync_inference(self, img_bgr):
            image = self._preprocess(img_bgr)
            ort_outs = self._model.infer(inputs={self._input_blob: image})
            results = self._postprocess(ort_orts)
            return results
        def async_inference(self, input_list):
            """Asynchronous inference"""
            num_inputs = len(input_list)
            meta_list = []
            for i in range(num_inputs):
                meta = self._preprocess(input_list[i])
                meta_list.append(meta)
            for i in range(num_inputs):
                self._model.requests[i].async_infer({self._input_blob: meta_list[i]})
            output_queue = list(range(num_inputs))
            outputs_list = [[] for _ in range(num_inputs)]
            while True:
                for i in output_queue:
                    # Immediately returns a inference status without bloking or interrupting
                    infer_status = self._model.requests[i].wait(0)
                    if infer_status == StatusCode.RESULT_NOT_READY:
                        continue
                    print(f'Infer request {i} returned {infer_status}')
                    if infer_status != StatusCode.OK:
                        return -2
                    # Read infer request results
                    ort_outs = self._model.requests[i].outputs
                    results = self._postprocess(ort_outs)
                    outputs_list[i] = results
                    output_queue.remove(i)
                if not output_queue:
                    break
            return outputs_list
    

    Reference

  • zhuanlan.zhihu.com/p/346511883
  • github.com/daquexian/o…
  • gist.github.com/helena-inte…
  • docs.openvino.ai/latest/open…
  • openvino量化为INT8模型:blog.csdn.net/qq_37541097…
  •