图片

黑客松活动介绍

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

OpenVINO™ 介绍

OpenVINO™ 工具套件是一款由英特尔公司开发的支持快速开发视觉、语音识别和自然语言处理应用的工具包。它采用了最新的人工智能神经网络模型,包括卷积神经网络、循环神经网络和注意力机制网络等,以实现高效的计算机视觉和深度学习应用。
OpenVINO™ 的主要功能包括:在边缘侧支持卷积神经网络的推理加速;在英特尔CPU、GPU、FPGA等设备上的混合执行/异构计算执行;通过大量的预训练模型库加速从产品原型到市场化的过程;支持传统的计算机视觉标准库中的操作,如 OpenCV 和 OpenCL 等。

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

(说明:Generated by 文心一言,Prompt:介绍一下OpenVINO™ 工具套件)

环境配置

环境搭建

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

git clone https://github.com/OpenVINOtoolkit/OpenVINO.git

OpenVINO™ 为我们提供了一份编译构建的文档,详情见链接:https://github.com/openvinotoolkit/openvino/blob/master/docs/dev/build.md

我们可以根据自身开发环境的不同查看对应平台的构建文档。我比较推荐使用 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 文件进行分析,提供语法检查、代码跳转等功能

编译

根据构建文档中的步骤,我们初始化 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/ 中能够找到 PaddlePaddle 相关的测试程序:

图片

添加映射

接口对齐

确认编译没有问题后,接下来便是真正的算子映射开发了。我们首先需要查阅文档,将 PaddlePaddle 算子和 OpenVINO™ 算子的功能、输入、属性等对齐,以 silu 为例,我们在 PaddlePaddle 文档中很容易找到其相关文档:图片接下来便是在 OpenVINO™ 的文档中找到 silu 相关的文档,通过搜索发现没有任何结果。此时有两种可能:

  • 在 OpenVINO™ 中,有功能完全相同,但命名不同的算子。
  • OpenVINO™ 中没有该算子的实现,我们需要使用现有的算子进行组合,组合后的算子能够实现相同的功能。

于是我们可以沿着这两种可能去寻找解决方法:方法一:在激活函数中寻找名字不同,功能相同或相似的算子。经过一番查找,我在 PaddlePaddle 的文档中找到了一个计算公式和 silu 一样的激活函数 swish,swish 在 OpenVINO™ 文档中的定义如下图所示,显然当 β=1 时,就能完全对应上了,问题解决。图片方法二:通过现有的组合算子实现。仔细观察 silu 的计算公式,可以发现silu(x) = x * sigmoid(x),我们也可以使用 multiply、sigmoid 两个算子组合实现相同的功能。因为已经存在可以直接映射的算子,我们就没必要将使用该方法了,在此仅提供一个实现思路,如遇到无法直接进行映射的算子,该思路可以作为一个参考。

映射实现

新建映射文件 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 算子的映射。在其他的算子中,输入的变量和属性可能不止一个,我们可以通过查看  PaddlePaddle 源码中的 OpMaker 或 paddle/fluid/operators/compat/op.pbtxt 获取变量名和属性名,以下是 Paddle Silu 算子的变量名和属性名:

图片

注册映射

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

OP_CONVERTER(silu);

{"silu", op::silu},

单测用例构造

创建文件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™ 读取、转换、运行该模型,通过比对计算结果判断测试是否通过。

注册单测

在 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"),

编译并测试

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

./paddle_tests --gtest_filter=*silu*

图片测试完成,没有问题之后,使用 pre-commit  格式化代码,根据OpenVINO™ 贡献指南的要求提PR,等待相应的研发检查即可。https://github.com/openvinotoolkit/openvino/blob/master/CONTRIBUTING.md

总结

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

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

飞桨PaddlePaddle
30 声望34 粉丝

飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件、丰富的工具组件于一体,是中国首个自主研发、功能丰富、开源开放的产业级深度...