PaddlePaddle-OpenVINO算子映射指南

openlab_4276841a 更新于 1年前

一、前言

1.1 黑客松活动介绍

飞桨黑客马拉松是一项兼具编程乐趣和挑战一项活动。通过该活动,我们能够接触并参与到企业建设的大型开源项目,提升个人编程能力,增强开源社区互动,推动开源生态发展。本期飞桨黑客马拉松由深度学习技术及应用国家工程研究中心主办,飞桨承办,英特尔作为顶级赞助方,为我们带来了OpenVINO算子映射和增加Notebook Demo等任务。算子映射能够将PaddlePaddle模型中的算子映射到OpenVINO中,完成模型在两个框架之间的转换,最终达到在PaddlePaddle训练模型、OpenVINO部署推理的效果,在两个框架之间搭起模型转换的桥梁。

1.2 OpenVINO介绍

OpenVINO Toolkit是一款由英特尔公司开发的支持快速开发视觉、语音识别和自然语言处理应用的工具包。它采用了最新的人工智能神经网络模型,包括卷积神经网络、循环神经网络和注意力机制网络等,以实现高效的计算机视觉和深度学习应用。

OpenVINO Toolkit的主要功能包括:在边缘侧支持卷积神经网络的推理加速;在英特尔CPU、GPU、FPGA等设备上的混合执行/异构计算执行;通过大量的预训练模型库加速从产品原型到市场化的过程;支持传统的计算机视觉标准库中的操作,如OpenCV和OpenCL等。

总之,OpenVINO Toolkit是一个强大的工具包,可以帮助开发者快速构建和优化计算机视觉和深度学习应用,提高应用性能和商业价值。

Generated by 文心一言,Prompt:介绍一下OpenVINO Toolkit


二、环境配置

2.1 环境搭建

首先Fork一份OpenVINO的GitHub仓库,并将其克隆到本地(Fork后,链接中的openvinotoolkit应当替换为自己的用户名,新建分支等git操作在此不再赘述):

git clone https://github.com/openvinotoolkit/openvino.git
OpenVINO为我们提供了一份编译构建的文档,链接,我们可以根据自身开发环境的不同查看对应平台的构建文档。我比较推荐使用Docker创建一个开发容器,这里我选择使用paddlepaddle提供的开发镜像,里面已经内置了常用的编译工具链,开箱即用非常方便。

docker run \
-it \
--gpus=all \
--name=fisher_openvino \
--net=host \
-v `pwd`:`pwd` \
paddlepaddle/paddle:latest-dev-cuda11.7-cudnn8.4-trt8.4-gcc8.2 \
/bin/bash

部分参数解析:

--gpus=all:将物理机上的GPU挂载到容器中,如本机无GPU可不挂载
--name=fisher_openvino:给自己创建的容器起一个好记的名字
--net=host:直接使用主机的网络,方便在容器中使用主机的代理
-v pwd:pwd:将主机当前的路径直接挂载到容器对应路径中,方便clangd插件对compile_commands.json文件进行分析,提供语法检查、代码跳转等功能

2.2 编译OpenVINO

根据构建文档中的步骤,我们初始化git子模块、安装构建依赖工具包、配置编译选项,以下是我的编译选项,可根据个人需要进行修改,其中关键的参数是-DENABLE_TESTS=ON,该参数将打开测试模块的编译,以便后续开发完成后进行本地测试。

cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="{project_base_dir}/build/openvino_dist" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_CLANG_FORMAT=ON \
-DENABLE_MYRIAD=OFF \
-DENABLE_VPU=OFF \
-DENABLE_PYTHON=ON \
-DNGRAPH_PYTHON_BUILD_ENABLE=ON \
-DENABLE_DEBUG_CAPS=ON \
-DENABLE_TESTS=ON \
-DENABLE_INTEL_GPU=OFF \
-DENABLE_WHEEL=ON

编译完成后,在bin/intel64/Release/中能够找到Paddle相关的测试程序:

In [2]
display.Image(b64decode(image1))

<IPython.core.display.Image object>

三、添加映射

3.1 接口对齐

确认编译没有问题后,接下来便是真正的算子映射开发了。

我们首先需要查阅文档,将Paddle算子和OpenVINO算子的功能、输入、属性等对齐,以silu为例,我们在Paddle文档中很容易找到其相关文档:

In [3]
display.Image(b64decode(image2))

<IPython.core.display.Image object>
接下来便是在OpenVINO的文档中找到silu相关的文档,通过搜索发现没有任何结果。此时有两种可能:

在OpenVINO中,有功能完全相同,但命名不同的算子。
OpenVINO中没有该算子的实现,我们需要使用现有的算子进行组合,组合后的算子能够实现相同的功能。
于是我们可以沿着这两种可能去寻找解决方法:

方法一:在激活函数中寻找名字不同,功能相同或相似的算子。经过一番查找,我在Paddle的文档中找到了一个计算公式和silu一样的激活函数swish,swish在OpenVINO文档中的定义如下图所示,显然当β=1时,就能完全对应上了,问题解决。
In [4]
display.Image(b64decode(image3))

<IPython.core.display.Image object>
方法二:通过现有的组合算子实现。仔细观察silu的计算公式,可以发现silu(x) = x * sigmoid(x),我们也可以使用multiply、sigmoid两个算子组合实现相同的功能。因为已经存在可以直接映射的算子,我们就没必要将使用该方法了,在此仅提供一个实现思路,如遇到无法直接进行映射的算子,该思路可以作为一个参考。

3.2 映射实现

新建映射文件src/frontends/paddle/src/op/silu.cpp,将算子映射实现放置在以下命名空间域中:

namespace ov {
namespace frontend {
namespace paddle {
namespace op {
// 算子映射的具体实现
} // namespace op
} // namespace paddle
} // namespace frontend
} // namespace ov

实现算子映射的具体逻辑:

NamedOutputs silu(const NodeContext& node) {
const auto x = node.get_input("X");
return node.default_single_output_mapping(
{std::make_shared<default_opset::Swish>(x)},
{"Out"}
);
}

在以上代码中,我们从计算节点中获取输入变量X,将其作为输入调用Swish,将输出写回到计算节点的Out变量中,完成了silu算子的映射。

在其他的算子中,输入的变量和属性可能不止一个,我们可以通过查看Paddle源码中的OpMaker或paddle/fluid/operators/compat/op.pbtxt获取变量名和属性名,以下是Paddle Silu算子的变量名和属性名:

In [5]
display.Image(b64decode(image4))

<IPython.core.display.Image object>

3.3 注册映射

在src/frontends/paddle/src/op_table.cpp中注册该算子映射,该文件中包含了Paddle到OpenVINO的所有映射,我们依样画葫芦即可。


OP_CONVERTER(silu);

{"silu", op::silu},


3.4 单测用例构造

创建文件src/frontends/paddle/tests/test_models/gen_scripts/generate_silu.py,用于生成silu相关的测试用例,在编写测试用例时,我们需要从以下几个维度去考虑单测用例,尽量做到全覆盖:Shape(静态、动态、1D~4D-Tensor)、数据类型(int32、float32等)、算子属性等。

def silu(name: str, x, data_type, use_static=True):
# 启用Paddle静态图模式
pdpd.enable_static()
with pdpd.static.program_guard(pdpd.static.Program(), pdpd.static.Program()):
if use_static:
# 静态Shape作为输入
node_x = pdpd.static.data(name='input_x', shape=x.shape, dtype=data_type)
else:
# 动态Shape作为输入
node_x = pdpd.static.data(name='input_x', shape=[1, 1, -1, -1], dtype=data_type)
# Paddle silu
out = pdpd.nn.functional.silu(x=node_x, name='silu')
# 使用CPU计算
cpu = pdpd.static.cpu_places(1)
exe = pdpd.static.Executor(cpu[0])
# startup program will call initializer to initialize the parameters.
exe.run(pdpd.static.default_startup_program())
# 执行器执行并保存模型
outs = exe.run(feed={'input_x': x}, fetch_list=[out])
saveModel(name, exe, feedkeys=['input_x'], fetchlist=[out], inputs=[x], outputs=[outs[0]], target_dir=sys.argv[1])
return outs[0]


def main():
x1 = np.random.randn(2,).astype('float32')
silu("silu_static_test1", x1, 'float32', True)
# More static shape test cases
x5 = np.random.randn(1, 1, 32, 32).astype('float32')
silu("silu_dynamic_test1", x5, 'float32', False)
# More dynamic shape test cases


if __name__ == "__main__":
main()

以上脚本将会为每一个测试用例导出并保存一个模型,OpenVINO读取、转换、运行该模型,通过比对计算结果判断测试是否通过。


3.5 注册单测

src/frontends/paddle/tests/op_fuzzy.cpp中注册单测,单测的名称就是在上一步中保存的模型名称。

std::string("silu_static_test1"),
std::string("silu_static_test2"),
std::string("silu_static_test3"),
std::string("silu_static_test4"),
std::string("silu_dynamic_test1"),
std::string("silu_dynamic_test2"),
std::string("silu_dynamic_test3"),
std::string("silu_dynamic_test4"),


四、测试

4.1 编译并测试

重新编译并在bin/intel64/Release/中运行以下测试,可以通过--gtest_filter参数筛选想要运行的单测,我们只增加了silu算子的映射和单测,因此我们只测试这一部分。

./paddle_tests --gtest_filter=*silu*
In [6]
display.Image(b64decode(image5))

<IPython.core.display.Image object>
测试完成,没有问题之后,使用pre-commit格式化代码,根据OpenVINO贡献指南的要求提PR,等待相应的研发检查即可。

五、总结

算子映射任务能够帮助我们了解算子的具体功能,理解模型在不同的AI框架中是如何进行转换的,对于新手来说是一份宝贵的学习经验。以下是我在开发过程中总结出来的一点经验,希望能够帮助到大家:

· 首次编译尽量使用代理,使用代理能够避免一部分网络原因造成的仓库克隆失败等问题
· 多翻翻文档,许多算子的功能是相同或相似的,多看文档也能够认识各种算子的功能,形成基本概念
· 多学习别人的思路、写法,自己想出的组合算子实现可能比较复杂,参考学习别人的思路和写法能够给自己带来一些改进的灵感

0个评论