[VSINPU]Split/Pad and some element-wise OPs support (#22916)

### Description
-Add split/pad/neg/not/ceil/round/min/max op support
-Fix conv2d op default pads value issue
-Add VSINPU EP to support python bindings


### Motivation and Context
-New OPs support for VSINPU EP

---------

Signed-off-by: Kee <xuke537@hotmail.com>
This commit is contained in:
Kee 2024-12-03 05:57:30 +08:00 committed by GitHub
parent e8bf46a70e
commit 8c52fa3924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 417 additions and 2 deletions

View file

@ -170,6 +170,7 @@ target_link_libraries(onnxruntime_pybind11_state PRIVATE
onnxruntime_session
${onnxruntime_libs}
${PROVIDERS_NNAPI}
${PROVIDERS_VSINPU}
${PROVIDERS_XNNPACK}
${PROVIDERS_COREML}
${PROVIDERS_RKNPU}
@ -1018,4 +1019,13 @@ if (onnxruntime_USE_QNN)
endif()
endif()
if (onnxruntime_USE_VSINPU)
add_custom_command(
TARGET onnxruntime_pybind11_state POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:onnxruntime_providers_vsinpu>
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/capi/
)
endif()
endif()

View file

@ -112,7 +112,7 @@ class ConvOpBuilder : public BaseOpBuilder {
}
}
} else {
auto pads = helper.Get("pads", std::vector<uint32_t>{0U, 0U});
auto pads = helper.Get("pads", std::vector<uint32_t>{0U, 0U, 0U, 0U});
if (group != 1 && group != weight_tensor->GetShape()[OChannel_idx]) {
if (is_1d_conv) {
op = graph_ep->GetGraph()

View file

@ -65,6 +65,12 @@ ELEMENTWISE_OP_BUILDER(Floor, Floor);
ELEMENTWISE_OP_BUILDER(Log, Log);
ELEMENTWISE_OP_BUILDER(Sin, Sin);
ELEMENTWISE_OP_BUILDER(HardSwish, HardSwish);
ELEMENTWISE_OP_BUILDER(Neg, Neg);
ELEMENTWISE_OP_BUILDER(Not, LogicalNot);
ELEMENTWISE_OP_BUILDER(Ceil, Ceil);
ELEMENTWISE_OP_BUILDER(Round, Round);
ELEMENTWISE_OP_BUILDER(Min, Minimum);
ELEMENTWISE_OP_BUILDER(Max, Maximum);
class PowOpBuilder : public BaseOpBuilder {
bool IsOpSupported(const onnxruntime::GraphViewer& graph_viewer,

View file

@ -0,0 +1,191 @@
/****************************************************************************
*
* Copyright (c) 2024 Vivante Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*****************************************************************************/
#pragma once
#include <memory>
#include <vector>
#include <utility>
#include <limits>
#include <algorithm>
#include "core/optimizer/initializer.h"
#include "core/providers/vsinpu/builders/impl/base_op_builder.h"
#include "core/providers/common.h"
#include "core/providers/shared/utils/utils.h"
namespace onnxruntime {
namespace vsi {
namespace npu {
typedef tim::vx::ops::PadV2::pad_mode_type PadMode;
class PadOpBuilder : public BaseOpBuilder {
public:
int GetMinSupportedOpSet(const NodeUnit& /* node_unit */) const override { return 11; }
bool IsOpSupported(const onnxruntime::GraphViewer& graph_viewer,
const Node* node) const override {
NodeAttrHelper helper(*node);
const auto mode = helper.Get("mode", "constant");
auto input_defs = node->InputDefs();
size_t num_inputs = input_defs.size();
auto input_shape = vsi::npu::util::GetTensorShape(*input_defs[0]);
int32_t rank = input_shape.NumDimensions();
const auto& initializers = graph_viewer.GetAllInitializedTensors();
if (mode == "wrap") {
LOGS_DEFAULT(WARNING) << "`wrap` mode Pad is not currently supported for now.";
return false;
}
if (mode == "constant") {
if (num_inputs > 2 && input_defs[2]->Exists()) {
// only support if `constant_value` input is a constant initializer
if (!Contains(initializers, input_defs[2]->Name())) {
LOGS_DEFAULT(WARNING) << "constant_value must be a constant initializer.";
return false;
}
}
}
// only support if `pads` input is known and does not contain negative values
{
const auto* pads_initializer = graph_viewer.GetConstantInitializer(input_defs[1]->Name());
if (!pads_initializer) {
LOGS_DEFAULT(WARNING) << "pads must be a constant initializer";
return false;
}
Initializer unpacked_tensor(*pads_initializer);
auto tensor_data = unpacked_tensor.DataAsSpan<int64_t>();
for (size_t i = 0; i < unpacked_tensor.size(); i++) {
if (tensor_data[i] < 0) {
LOGS_DEFAULT(WARNING) << "Negative pad value is not supported: pads["
<< i << "] = " << tensor_data[i];
return false;
}
}
}
return true;
}
bool HasSupportedInputOutputsImpl(const InitializedTensorSet& initializers,
const NodeUnit& node_unit) const override {
for (size_t i = 0; i < node_unit.Inputs().size(); ++i) {
const auto& iodef = node_unit.Inputs()[i];
if (0 == i) {
if (!util::IsTypeSupported(&iodef.node_arg) ||
(*iodef.node_arg.Type() == "tensor(int64)") ||
(*iodef.node_arg.Type() == "tensor(bool)")) {
LOGS_DEFAULT(WARNING) << "Unspport tensor data type:" << *iodef.node_arg.Type();
return false;
}
} else if (1 == i) {
if (!Contains(initializers, iodef.node_arg.Name())) {
LOGS_DEFAULT(WARNING) << "pads must be a constant initializer.";
return false;
}
} else if (2 == i) {
if (iodef.node_arg.Exists() && !Contains(initializers, iodef.node_arg.Name())) {
LOGS_DEFAULT(WARNING) << "constant_value must be a constant initializer.";
return false;
}
} else if (i == 3) {
if (!Contains(initializers, iodef.node_arg.Name())) {
LOGS_DEFAULT(WARNING) << "axes must be a constant initializer..";
return false;
}
}
}
return true;
}
bool HandleBuildOp(vsi::npu::GraphEP* graph_ep,
std::vector<std::shared_ptr<tim::vx::Tensor>>& inputs,
std::vector<std::shared_ptr<tim::vx::Tensor>>& outputs,
const NodeUnit& node_unit) override {
LOGS_DEFAULT(VERBOSE) << "Creating Pad Op.";
NodeAttrHelper helper(node_unit);
const auto mode = helper.Get("mode", "constant");
auto input_defs = node_unit.Inputs();
PadMode pad_mode = PadMode::PAD_MODE_CONSTANT;
float const_val = 0.0f;
std::vector<int64_t> axes_tensor_data;
int32_t input_rank = inputs[0]->GetShape().size();
if (mode == "constant") {
pad_mode = PadMode::PAD_MODE_CONSTANT;
} else if (mode == "reflect") {
pad_mode = PadMode::PAD_MODE_REFLECT;
} else if (mode == "edge") {
pad_mode = PadMode::PAD_MODE_EDGE;
} else {
LOGS_DEFAULT(WARNING) << "`wrap` mode Pad is not currently supported for now.";
return false;
}
// `pads` input
std::vector<int64_t> onnx_pads(inputs[1]->GetSpec().GetElementNum());
inputs[1]->CopyDataFromTensor(onnx_pads.data());
// `constant_value` input
if (inputs.size() > 2 && pad_mode == PadMode::PAD_MODE_CONSTANT) {
if (input_defs[2].node_arg.Exists()) {
inputs[2]->CopyDataFromTensor(&const_val);
}
}
// `axes` input
if (inputs.size() > 3) {
// optional input axes is provided, use axes initializer data
std::vector<int64_t> axes_tensor(inputs[3]->GetSpec().GetElementNum());
inputs[3]->CopyDataFromTensor(axes_tensor.data());
std::transform(
axes_tensor.begin(), axes_tensor.end(), std::back_inserter(axes_tensor_data),
[input_rank](int64_t axis) { return HandleNegativeAxis(axis, input_rank); });
} else {
// if not provided, make a default axes as [0, 1, ..., input_rank - 1]
std::vector<int64_t> default_axes(input_rank);
std::iota(std::begin(default_axes), std::end(default_axes), 0);
axes_tensor_data = std::move(default_axes);
}
int64_t num_axes = axes_tensor_data.size();
std::vector<uint32_t> front_size(input_rank, 0);
std::vector<uint32_t> back_size(input_rank, 0);
int64_t axes_index = 0;
for (int64_t axes : axes_tensor_data) {
front_size[axes] = onnx_pads[axes_index];
back_size[axes] = onnx_pads[axes_index + num_axes];
axes_index++;
}
std::reverse(front_size.begin(), front_size.end());
std::reverse(back_size.begin(), back_size.end());
auto op = graph_ep->GetGraph()->CreateOperation<tim::vx::ops::PadV2>(
front_size, back_size, const_val, pad_mode);
op->BindInput(inputs[0]).BindOutputs(outputs);
graph_ep->GetOps().push_back(std::move(op));
return true;
}
};
} // namespace npu
} // namespace vsi
} // namespace onnxruntime

View file

@ -0,0 +1,190 @@
/****************************************************************************
*
* Copyright (c) 2024 Vivante Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*****************************************************************************/
#pragma once
#include <memory>
#include <vector>
#include <utility>
#include <limits>
#include <algorithm>
#include "core/optimizer/initializer.h"
#include "core/providers/vsinpu/builders/impl/base_op_builder.h"
#include "core/providers/common.h"
#include "core/providers/shared/utils/utils.h"
namespace onnxruntime {
namespace vsi {
namespace npu {
class SplitOpBuilder : public BaseOpBuilder {
public:
bool IsOpSupported(const onnxruntime::GraphViewer& graph_viewer,
const Node* node) const override {
NodeAttrHelper helper(*node);
auto axis = helper.Get("axis", 0);
auto input_defs = node->InputDefs();
size_t num_inputs = input_defs.size();
size_t num_outputs = node->OutputDefs().size();
auto input_shape = vsi::npu::util::GetTensorShape(*input_defs[0]);
int32_t rank = input_shape.NumDimensions();
std::vector<int64_t> splits_list;
bool split_provided = false;
if (axis >= rank || axis < -rank) {
LOGS_DEFAULT(WARNING) << "Axis is invalid in Split. Axis(" << axis
<< ") is out of rank[" << -rank << "," << rank - 1 << "]";
return false;
}
axis = HandleNegativeAxis(axis, rank);
const auto split_dims_at_axis = input_shape.GetDims()[axis];
if (num_inputs > 1 && input_defs[1]->Exists()) {
// if optional input `split` is provided
const auto* splits = graph_viewer.GetConstantInitializer(input_defs[1]->Name());
if (!splits) {
LOGS_DEFAULT(WARNING) << "Optional input 'split' must be a constant initializer if provided.";
return false;
}
Initializer unpacked_tensor(*splits);
auto split_sizes_ = unpacked_tensor.DataAsSpan<int64_t>();
splits_list.assign(split_sizes_.begin(), split_sizes_.end());
split_provided = true;
}
if (num_inputs == 1) {
// opset1,2,11 split as attribute
if (helper.HasAttr("split")) {
auto split_sizes_ = *helper.GetInt64s("split");
splits_list.assign(split_sizes_.begin(), split_sizes_.end());
split_provided = true;
} else if (node->SinceVersion() >= 18) {
const auto outputs_count = helper.GetInt64("num_outputs");
if (!outputs_count.has_value()) {
LOGS_DEFAULT(WARNING) << "No 'num_outputs' provided. For split 18+, num_outputs is a required attribute.";
return false;
}
if (outputs_count.value() != static_cast<int32_t>(num_outputs) ||
outputs_count.value() > split_dims_at_axis) {
LOGS_DEFAULT(WARNING) << "Invalid num_outputs provided.\n. The value should be smaller or equal to the size "
"of dimension being split. num_outputs: "
<< outputs_count.value();
return false;
}
}
}
if (!split_provided) {
// populate split sizes based on num_outputs so existing code can be utilized
int32_t size = narrow<int32_t>(std::ceil(float(split_dims_at_axis) / num_outputs));
int32_t remainder = split_dims_at_axis % size;
std::vector<int64_t> split_sizes_(num_outputs, size);
if (remainder) {
split_sizes_.back() = remainder;
}
splits_list.assign(split_sizes_.begin(), split_sizes_.end());
}
uint32_t sum_of_splits = std::accumulate(splits_list.begin(), splits_list.end(), SafeInt<uint32_t>(0));
if (sum_of_splits != split_dims_at_axis) {
LOGS_DEFAULT(WARNING) << "Sum of the 'split' input values must equal to the dim value at 'axis' specified. "
<< "dim value at 'axis' specified: "
<< split_dims_at_axis
<< ", sum of 'split' input values: "
<< sum_of_splits;
return false;
}
if (!std::all_of(splits_list.begin(), splits_list.end(), [](int64_t value) { return value >= 0; })) {
LOGS_DEFAULT(WARNING) << "Invalid value in 'split' attribute. All values must be > 0";
return false;
}
auto average_split = sum_of_splits / num_outputs;
if (!std::all_of(splits_list.begin(), splits_list.end(), [average_split](int64_t value) { return value == average_split; })) {
// TO DO, remove this check after driver supports it.
LOGS_DEFAULT(WARNING) << "Uneven splits are not currently supported for now.";
return false;
}
return true;
}
bool HasSupportedInputOutputsImpl(const InitializedTensorSet& initializers,
const NodeUnit& node_unit) const override {
for (size_t i = 0; i < node_unit.Inputs().size(); ++i) {
const auto& iodef = node_unit.Inputs()[i];
if (0 == i) {
if (!util::IsTypeSupported(&iodef.node_arg) ||
(*iodef.node_arg.Type() == "tensor(int64)") ||
(*iodef.node_arg.Type() == "tensor(bool)")) {
LOGS_DEFAULT(WARNING) << "Unsupport tensor data type:" << *iodef.node_arg.Type();
return false;
}
} else if (!Contains(initializers, iodef.node_arg.Name())) {
LOGS_DEFAULT(WARNING) << "Optional input 'split' must be a constant initializer if provided.";
return false;
}
}
return true;
}
bool HandleBuildOp(vsi::npu::GraphEP* graph_ep,
std::vector<std::shared_ptr<tim::vx::Tensor>>& inputs,
std::vector<std::shared_ptr<tim::vx::Tensor>>& outputs,
const NodeUnit& node_unit) override {
LOGS_DEFAULT(VERBOSE) << "Creating Split Op.";
NodeAttrHelper helper(node_unit);
auto axis = helper.Get("axis", 0);
axis = util::ReverseAxis(axis, inputs[0]->GetShape().size());
const auto split_dims_at_axis = inputs[0]->GetShape()[axis];
auto num_outputs = outputs.size();
// transform splite vector to timvx slice
std::vector<int64_t> onnx_split;
if (inputs.size() > 1) {
std::vector<int64_t> split_sizes_(inputs[1]->GetSpec().GetElementNum());
inputs[1]->CopyDataFromTensor(split_sizes_.data());
onnx_split.assign(split_sizes_.begin(), split_sizes_.end());
}
if (inputs.size() == 1) {
if (helper.HasAttr("split")) {
auto split_sizes_ = *helper.GetInt64s("split");
onnx_split.assign(split_sizes_.begin(), split_sizes_.end());
}
if (node_unit.SinceVersion() >= 18 || !helper.HasAttr("split")) {
// populate split sizes based on num_outputs so existing code can be utilized
int32_t size = narrow<int32_t>(std::ceil(float(split_dims_at_axis) / num_outputs));
int32_t remainder = split_dims_at_axis % size;
std::vector<int64_t> split_sizes_(num_outputs, size);
if (remainder) {
split_sizes_.back() = remainder;
}
onnx_split.assign(split_sizes_.begin(), split_sizes_.end());
}
}
std::vector<uint32_t> slices(onnx_split.begin(), onnx_split.end());
std::reverse(slices.begin(), slices.end());
auto op = graph_ep->GetGraph()->CreateOperation<tim::vx::ops::Split>(
axis, slices);
op->BindInput(inputs[0]).BindOutputs(outputs);
graph_ep->GetOps().push_back(std::move(op));
return true;
}
};
} // namespace npu
} // namespace vsi
} // namespace onnxruntime

View file

@ -53,6 +53,8 @@
#include "impl/cast_op_builder.h"
#include "impl/dropout_op_builder.h"
#include "impl/slice_op_builder.h"
#include "impl/split_op_builder.h"
#include "impl/pad_op_builder.h"
namespace onnxruntime {
namespace vsi {
namespace npu {
@ -110,7 +112,15 @@ static const std::map<std::string, createIOpBuildItemFunc> reg = {
REGISTER_OP_BUILDER("Resize", ResizeOpBuilder),
REGISTER_OP_BUILDER("Cast", CastOpBuilder),
REGISTER_OP_BUILDER("Dropout", DropoutOpBuilder),
REGISTER_OP_BUILDER("Slice", SliceOpBuilder)
REGISTER_OP_BUILDER("Slice", SliceOpBuilder),
REGISTER_OP_BUILDER("Split", SplitOpBuilder),
REGISTER_OP_BUILDER("Neg", NegOpBuilder),
REGISTER_OP_BUILDER("Not", NotOpBuilder),
REGISTER_OP_BUILDER("Ceil", CeilOpBuilder),
REGISTER_OP_BUILDER("Round", RoundOpBuilder),
REGISTER_OP_BUILDER("Min", MinOpBuilder),
REGISTER_OP_BUILDER("Max", MaxOpBuilder),
REGISTER_OP_BUILDER("Pad", PadOpBuilder)
#undef REGISTER_OP_BUILDER
};

View file

@ -69,6 +69,9 @@ void addGlobalSchemaFunctions(pybind11::module& m) {
#ifdef USE_NNAPI
onnxruntime::NnapiProviderFactoryCreator::Create(0, std::optional<std::string>()),
#endif
#ifdef USE_VSINPU
onnxruntime::VSINPUProviderFactoryCreator::Create(),
#endif
#ifdef USE_RKNPU
onnxruntime::RknpuProviderFactoryCreator::Create(),
#endif

View file

@ -1180,6 +1180,10 @@ std::unique_ptr<IExecutionProvider> CreateExecutionProviderInstance(
const auto partitioning_stop_ops_list = session_options.config_options.GetConfigEntry(
kOrtSessionOptionsConfigNnapiEpPartitioningStopOps);
return onnxruntime::NnapiProviderFactoryCreator::Create(0, partitioning_stop_ops_list)->CreateProvider();
#endif
} else if (type == kVSINPUExecutionProvider) {
#ifdef USE_VSINPU
return onnxruntime::VSINPUProviderFactoryCreator::Create()->CreateProvider();
#endif
} else if (type == kRknpuExecutionProvider) {
#ifdef USE_RKNPU

View file

@ -440,6 +440,7 @@ std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_ArmNN(
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_DML(int device_id);
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_Nnapi(
uint32_t flags, const optional<std::string>& partitioning_stop_ops_list);
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_VSINPU();
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_Rknpu();
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory_CoreML(uint32_t flags);
constexpr const char* kDefaultExecutionProviderEntry = "GetProvider";