代码贡献 | 为 OpenVINO™ 支持 Paddle 2.5

点击蓝字

关注我们,让开发变得更有趣

作者 | 卢畅 英特尔 OpenVINO™ 工具套件领航者联盟成员,PPDE

排版 | 李擎

6d5d00fb33637f9ca5e9d0eeb95f6ce8.png

OpenVINO™..♩~ ♫. ♪..

前言

我是飞桨黑客马拉松第五期 OpenVINO™ 赛题获奖者——为 OpenVINO™ 添加了对 Paddle 2.5 的支持。在此记录下来贡献的过程,希望有更多的同学可以参与到 OpenVINO™ 的社区建设当中来。我在贡献代码的过程中,也遇到了一些问题,在此,非常感谢英特尔的技术老师们非常耐心地指导我,帮助我解决了问题!

那么,接下来就让我们正式进入正题!

7741395c7a7a871818c2e663d202c9e0.gif

介绍

1. OpenVINO™ 是什么

OpenVINO™ 是英特尔推出的一款深度学习推理框架,它可以将训练好的模型转换为 OpenVINO™ 支持的 IR 格式,从而可以在 OpenVINO™ 的推理引擎上进行推理。

OpenVINO™ 支持多种深度学习框架,包括 Paddle、TensorFlow、PyTorch 等。

2. 任务说明

在这个任务完成之前,OpenVINO™ 只支持 Paddle 2.4 的版本,由于 Paddle 2.5 的一些接口变动,OpenVINO™ 无法直接支持 Paddle 2.5。同时,由于 Paddle 2.4 版本并不支持 Python3.11,因此 OpenVINO™ 默认关闭了对 Paddle 的支持,需要手动开启,在手动开启后,又会遇到无法编译出 Paddle 相关单侧的问题。

本任务的目标是为 OpenVINO™ 添加对 Paddle 2.5 的支持,并确保 OpenVINO™ 可以正常编译出 Paddle 相关单侧且线上CI均可通过。

5880ff7219e0c01b503dfa197beecf50.gif

开发过程

1. 问题分析

在任务开始之前,OpenVINO™ 开启对 Paddle 的支持后主要会遇到两个问题:

  • API名称变动导致的编译报错,如:paddle.fluid.layers.elementwise_add -> paddle.add

  • Op 行为变化导致的输出结果不一致,如:paddle.argmax 新增了 0-d tensor 的支持,但是 OpenVINO™ 中的 Op 并没有对应的修改

针对上面这两个问题,主要的解决方案如下:

  1. 将老 API 与新 API 名称映射

  2. 修改名称/属性变动的 API

  3. 修复因 Op 行为变动导致的单侧报错

2. 将老 API 与新 API 名称映射

由于 Paddle 2.5 版本在 API 层面发生了较大的变化,因此需要将老 API 与新 API 名称进行映射,这样 OpenVINO™ 中的代码就可以使用新 API 名称,从而解决 API 名称变动导致的编译报错问题。该问题可参考 Paddle 官网的 API 映射表(链接:https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/model_convert/convert_from_older_versions/paddle_api_mapping_cn.html#paddle-1-8-paddle-2-0-api)。

为了兼容老版本的 API,OpenVINO™ 中的代码需要同时支持新 API 与老 API,因此需要在 generate_xxx.py 中进行相应修改。

with paddle.static.program_guard(paddle.static.Program(), paddle.static.Program()):
        node_x = paddle.static.data(name='x', shape=x.shape, dtype=x.dtype)
        node_i = paddle.full(shape=[1], fill_value=0, dtype='int64', name='i')
        if paddle.__version__ >= '2.0.0':
            node_i = paddle.add(node_i, node_x)
        else:
            paddle.fluid.layers.nn.elementwise_add(node_i, node_x)
        node_ten = paddle.full(shape=[1], fill_value=10, dtype='int64', name='ten')

代码中的 paddle.fluid.layers.nn.elementwise_add 就是老版本的 API,而 paddle.add 就是新版本的 API。

3. 修改名称/属性变动的 API

对于部分 API 接口,老版本与新版本的名称或属性发生了变化,因此需要给 OpenVINO™ 中的代码进行相应的修改。比如 paddle.fluid.layers.relu6(x, threshold=6.0, name=None) 和 paddle.nn.functional.relu6(x, name=None) 的属性发生了变化。

可以看到,paddle.fluid.dygraph.relu6 中的 threshold 属性在新版本中被删除了。

这种情况下需要确认 Python 源码中是否修改底层 C++ 源码,如果是修改了 C++ 源码,那么需要在 OpenVINO™ 的op源码中进行相应的修改。如果没有修改 C++ 源码,那么只需要对应修改 Python 源码即可。

一般情况下,底层 C++ 源码不会修改,Python 层一般是修改属性的名称,修改属性的默认值,删除某个属性等。

比如新版本 relu6 在 Paddle 的 Python 端的实现如下:

def relu6(x, name=None):
    threshold = 6.0
    if in_dynamic_or_pir_mode():
        return _C_ops.relu6(x)


    check_variable_and_dtype(
        x, 'x', ['float16', 'uint16', 'float32', 'float64'], 'relu6'
    )
    helper = LayerHelper('relu6', **locals())
    out = helper.create_variable_for_type_inference(x.dtype)
    helper.append_op(
        type='relu6',
        inputs={'X': x},
        outputs={'Out': out},
        attrs={'threshold': threshold},
    )
    return out

通过实现代码可以看到,新版本的 relu6 在 Python 端并没有修改 C++ 源码,只是删除了 threshold 属性,在调用 C++ 源码时,将 threshold 属性设置为了默认值 6.0。

因此,对于这种情况,只需要修改 OpenVINO™ 中的 Python 单侧代码即可,不需要修改 C++ 源码。OpenVINO™ 在进行模型转化的时候是对底层 op 进行转化,因此只要 Paddle 没有修改底层 Op 的行为,那么 OpenVINO™ 就不需要修改 Op 相关的代码。

4. 修复因 Op 行为变动导致的单侧报错

在 Paddle 2.5 版本中,部分 Op 的行为发生了变化,导致 OpenVINO™ 中的单侧报错。比如 paddle.argmax 新增了 0-d tensor 的支持,但是 OpenVINO™ 中的 Op 并没有对应的修改。想要修复这种问题,需要结合单侧报错的具体情况进行相应的修改。

在介绍如何修复单侧报错之前,先介绍一下 OpenVINO™ 的算子支持机制。

4.1 OpenVINO™ 算子支持机制

接下来我们先看一下 OpenVINO™ 中的算子支持机制。

通过 Paddle 官方提供的 Topk_v2 样例进行说明:

// Copyright (C) 2018-2021 Intel Corporation
// SPDX-License-Identifier: Apache-2.0


#include "default_opset.hpp"
#include "openvino/frontend/paddle/node_context.hpp"


namespace ov {
namespace frontend {
namespace paddle {
namespace op {
NamedOutputs top_k_v2(const NodeContext& node) {
    auto x = node.get_input("X");
    Output<Node> k_expected_node;
    if (node.has_input("K")) {
        auto k_variable = node.get_input("K");
        auto k_var_node = std::make_shared<default_opset::Convert>(k_variable, element::i32);
        k_expected_node = std::make_shared<default_opset::Squeeze>(k_var_node);
    } else {
        const auto k_expected = node.get_attribute<int>("k", 1);
        k_expected_node = default_opset::Constant::create(element::i32, {}, {k_expected});
    }


    auto axis = node.get_attribute<int32_t>("axis", -1);
    bool sorted = node.get_attribute<bool>("sorted", true);
    bool largest = node.get_attribute<bool>("largest", true);


    std::string sort_type = sorted ? "value" : "none";
    std::string mode = largest ? "max" : "min";


    auto node_topk = std::make_shared<default_opset::TopK>(x, k_expected_node, axis, mode, sort_type);


    NamedOutputs named_outputs;
    named_outputs["Out"] = OutputVector{node_topk->output(0)};
    named_outputs["Indices"] = OutputVector{node_topk->output(1)};


    return named_outputs;
}
}  // namespace op
}  // namespace paddle
}  // namespace frontend
}  // namespace ov

在 OpenVINO™ 中,一般来说每个算子都是一个单独的文件,比如 Topk_v2 算子对应的文件就是 topk_v2.cpp。在这个文件中,我们可以看到 top_k_v2 函数,这个函数就是 OpenVINO™ 中的 Topk_v2 算子的实现。

在这个函数中,我们可以看到 auto x = node.get_input("X");,这个函数就是获取输入的 Tensor,auto node_topk = std::make_shared(x, k_expected_node, axis, mode, sort_type); 这个函数就是创建 Topk_v2 算子,named_outputs["Out"] = OutputVector{node_topk->output(0)}; 这个函数就是获取输出的 Tensor。

每个Op 都可以映射为一个图结构,数据根据图结构在不同的计算节点之间流通和计算,而Node便定义了图结构中的数据节点,通过实现每一个Node,便可以通过组合实现更多的算子。

Op 转换的代码需要写在 src/frontends/paddle/src/op/ 目录下,并在 src/frontends/paddle/src/op_table.cpp 中进行注册。

单测代码需要写在 src/core/tests/frontend/paddle/test_models/gen_scripts 目录中,并在 src/core/tests/frontend/paddle/op_fuzzy.cpp 中进行注册。

4.2 修复因 Op 行为变动导致的单侧报错

下面以 paddle.argmax 为例,介绍如何修复因 Op 行为变动导致的单侧报错。

修复此类问题一般只能见招拆招,需要结合单侧报错的具体情况进行相应的修改。比如 paddle.argmax 新增了 0-d tensor 的支持,但是 OpenVINO™ 中的 Op 并没有对应的修改。因此,我们需要在 OpenVINO™ 中的 Op 中添加对 0-d tensor 的支持。经过对代码的分析我们可以发现,OpenVINO™ 中该 Op 是通过 std::make_shared(node_reshape, k, axis, "max", "index", index_element_type); 实现的,但是 TopK 并没有对 0-d tensor 进行支持。我们可以判断 output_size 是否为 0,如果为 0,那么就组合一个 Slice 节点返回即可。以下是修改后的代码:

NamedOutputs argmax(const NodeContext& node) {
    auto data = node.get_input("X");
    bool flatten = node.get_attribute<bool>("flatten");
    const element::Type& index_element_type = element::i64;
    const Output<ov::Node> k = ov::opset6::Constant::create(ov::element::i64, {}, {1});


    if (!flatten) {
        auto axis = node.get_attribute<int64_t>("axis");
        const auto axis_to_remove = ov::opset6::Constant::create(element::u64, Shape{}, {axis});
        auto node_topk = std::make_shared<ov::opset6::TopK>(data, k, axis, "max", "index", index_element_type);
        const auto reshaped_indices = std::make_shared<ov::opset6::Squeeze>(node_topk->output(1), axis_to_remove);
        return node.default_single_output_mapping(
            {std::make_shared<ov::opset6::Convert>(reshaped_indices, element::i64)},
            {"Out"});
    } else {
        int64_t axis = 0;
        const Output<ov::Node> reshape_flatten = ov::opset6::Constant::create(ov::element::i64, {1}, {-1});
        auto node_reshape = std::make_shared<ov::opset6::Reshape>(data, reshape_flatten, true);
        auto node_topk = std::make_shared<ov::opset6::TopK>(node_reshape, k, axis, "max", "index", index_element_type);
        const auto output_info = node.get_output_port_infos("Out");
        // 获取输出的维度
        size_t output_size = output_info[0].second.size();
        // 如果输出的维度为0,那么就组合一个Slice节点返回
        if (output_size == 0) {
            auto out = std::make_shared<ov::opset6::Squeeze>(node_topk->output(1));
            return node.default_single_output_mapping({std::make_shared<ov::opset6::Convert>(out, element::i64)},
                                                      {"Out"});
        } else {
            return node.default_single_output_mapping(
                {std::make_shared<ov::opset6::Convert>(node_topk->output(1), element::i64)},
                {"Out"});
        }
    }
}

除了 argmax 之外,还有一些 Op 也需要进行相应的修改:

  • p_norm

  • reduce_ops

  • matmul_v2

  • elementwise_floordiv

具体的修改可以参考 PR (链接:https://github.com/openvinotoolkit/openvino/pull/20161)。

d35ba47a6d5e9e2c03755aef8ae8e4f4.gif

总结

这次的黑客松活动,让我对 OpenVINO™ 有了更深入的了解。

OpenVINO™ 的工程师们非常热心,对于社区的问题都会非常耐心的解答。我也是第一次在 PR 页面有 144 次的 Conversation。

整个 PR 的周期大概是 3 个月,期间经历了很多次的修改,最终才能够被合并。在这次的活动中,我也学到了很多知识,比如 OpenVINO™ 的算子支持机制,Op 的单侧测试等。

希望有更多的同学可以参与到 OpenVINO™ 的社区建设当中来,为 OpenVINO™ 的发展及开源社区的建设贡献自己的力量!

OpenVINO™

--END--

你也许想了解(点击蓝字查看)⬇️➡️ OpenVINO™ 2023.2 发布:让生成式 AI 在实际场景中更易用➡️ 开发者实战 | 基于 OpenVINO™ 和 LangChain 构建 RAG 问答系统➡️ 开发者实战 | 如何利用低比特量化技术进一步提升大模型推理性能➡️ 开发者实战 | 介绍OpenVINO™ 2023.1:在边缘端赋能生成式AI➡️ 基于 ChatGLM2 和 OpenVINO™ 打造中文聊天助手➡️ 基于 Llama2 和 OpenVINO™ 打造聊天机器人➡️ OpenVINO™ DevCon 2023重磅回归!英特尔以创新产品激发开发者无限潜能➡️ 5周年更新 | OpenVINO™  2023.0,让AI部署和加速更容易➡️ OpenVINO™5周年重头戏!2023.0版本持续升级AI部署和加速性能➡️ OpenVINO™2023.0实战 | 在 LabVIEW 中部署 YOLOv8 目标检测模型➡️ 开发者实战系列资源包来啦!➡️ 以AI作画,祝她节日快乐;简单三步,OpenVINO™ 助你轻松体验AIGC
➡️ 还不知道如何用OpenVINO™作画?点击了解教程。
扫描下方二维码立即体验 
OpenVINO™ 工具套件 2023.2

点击 阅读原文 立即体验OpenVINO 2023.2

962b30a9a5742261202e5e6084408517.png

文章这么精彩,你有没有“在看”?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值