为OpenVINO添加对Paddle 2.5的支持

描述

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

1. 前言

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

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

2. 介绍

2.1OpenVINO 是什么?

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

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

2.2任务说明

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

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

3. 开发过程

3.1问题分析

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

1API名称变动导致的编译报错,如:

paddle.fluid.layers.elementwise_add -> paddle.add

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

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

1将老 API 与新 API 名称映射

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

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

3.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.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 相关的代码。

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

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

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

3.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 k_expected_node;
    if (node.has_input("K")) {
        auto k_variable = node.get_input("K");
        auto k_var_node = std::make_shared(k_variable, element::i32);
        k_expected_node = std::make_shared(k_var_node);
    } else {
        const auto k_expected = node.get_attribute("k", 1);
        k_expected_node = default_opset::i32, {}, {k_expected});
    }


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


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


    auto node_topk = std::make_shared(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 中进行注册。

3.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("flatten");
    const element::Type& index_element_type = element::i64;
    const Output k = ov::i64, {}, {1});


    if (!flatten) {
        auto axis = node.get_attribute("axis");
        const auto axis_to_remove = ov::u64, Shape{}, {axis});
        auto node_topk = std::make_shared(data, k, axis, "max", "index", index_element_type);
        const auto reshaped_indices = std::make_shared(node_topk->output(1), axis_to_remove);
        return node.default_single_output_mapping(
            {std::make_shared(reshaped_indices, element::i64)},
            {"Out"});
    } else {
        int64_t axis = 0;
        const Output reshape_flatten = ov::i64, {1}, {-1});
        auto node_reshape = std::make_shared(data, reshape_flatten, true);
        auto node_topk = std::make_shared(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(node_topk->output(1));
            return node.default_single_output_mapping({std::make_shared(out, element::i64)},
                                                      {"Out"});
        } else {
            return node.default_single_output_mapping(
                {std::make_shared(node_topk->output(1), element::i64)},
                {"Out"});
        }
    }
}

 

 

左滑查看更多

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

• p_norm

• reduce_ops

• matmul_v2

• elementwise_floordiv

具体的修改可以参考 PR 

4. 总结

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

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

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

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

审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分