[CoreML EP] Implement Unary & Reduce operators (#15532)

### Description

This change is a follow-up to #15327. It adds Unary operators (Sqrt,
Reciprocal) and Reduce operators (ReduceSum, ReduceMean). I've tried to
follow existing patterns in the code :-)


### Motivation and Context

This reduces fragmentation across EPs when using CoreML on macOS,
thereby speeding up execution.

---------

Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
This commit is contained in:
Shukant Pal 2023-05-24 04:16:59 -04:00 committed by GitHub
parent 954ea6604a
commit f316bc57c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 2 deletions

View file

@ -0,0 +1,127 @@
// Copyright (c) Shukant Pal.
// Licensed under the MIT License.
#include "core/providers/common.h"
#include "core/providers/shared/utils/utils.h"
#ifdef __APPLE__
#include "core/providers/coreml/builders/model_builder.h"
#endif
#include "core/providers/coreml/builders/helper.h"
#include "core/providers/coreml/builders/op_builder_factory.h"
#include "core/optimizer/initializer.h"
#include "base_op_builder.h"
namespace onnxruntime {
namespace coreml {
class ReductionOpBuilder : public BaseOpBuilder {
#ifdef __APPLE__
public:
void AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) const override;
private:
Status AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& logger) const override;
#endif
private:
bool IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
const logging::Logger& logger) const override;
};
#ifdef __APPLE__
namespace {
template <typename T>
void AddReductionParams(T* params, const std::vector<int64_t>& axes, bool keepdims, bool noop_with_empty_axes) {
params->set_keepdims(keepdims);
for (auto& axis : axes)
params->add_axes(axis);
if (axes.size() == 0 && !noop_with_empty_axes)
params->set_reduceall(true);
}
} // namespace
void ReductionOpBuilder::AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) const {
const auto& input_defs(node.InputDefs());
// We have already embedded the axes into the CoreML layer.
// No need to copy them later to reduce memory consumption.
if (input_defs.size() > 1)
model_builder.AddInitializerToSkip(input_defs[1]->Name());
}
Status ReductionOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& /* logger */) const {
const auto& op_type(node.OpType());
const auto& input_defs(node.InputDefs());
const auto& initializers(model_builder.GetInitializerTensors());
std::vector<int64_t> axes;
NodeAttrHelper helper(node);
if (input_defs.size() > 1 && input_defs[1]->Exists()) {
auto& axes_tensor = *initializers.at(input_defs[1]->Name());
Initializer axes_initializer(axes_tensor);
int64_t* data = axes_initializer.data<int64_t>();
int64_t size = axes_initializer.size();
axes = std::vector<int64_t>(data, data + size);
} else if (helper.HasAttr("axes")) {
axes = helper.Get("axes", std::vector<int64_t>{});
}
const bool keepdims = helper.Get("keepdims", 1) != 0;
const bool noop_with_empty_axes = helper.Get("noop_with_empty_axes", 0) != 0;
std::unique_ptr<COREML_SPEC::NeuralNetworkLayer> layer = CreateNNLayer(model_builder, node);
if (op_type == "ReduceSum") {
AddReductionParams(layer->mutable_reducesum(), axes, keepdims, noop_with_empty_axes);
} else if (op_type == "ReduceMean") {
AddReductionParams(layer->mutable_reducemean(), axes, keepdims, noop_with_empty_axes);
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"ReductionOpBuilder::AddToModelBuilderImpl, unknown op: ", op_type);
}
*layer->mutable_input()->Add() = node.InputDefs()[0]->Name();
*layer->mutable_output()->Add() = node.OutputDefs()[0]->Name();
model_builder.AddLayer(std::move(layer));
return Status::OK();
}
#endif
bool ReductionOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
const logging::Logger& logger) const {
const auto& input_defs = node.InputDefs();
if (input_defs.size() > 1 && input_defs[1]->Exists()) {
const auto& axes_name = input_defs[1]->Name();
const auto& initializers = input_params.graph_viewer.GetAllInitializedTensors();
if (!Contains(initializers, axes_name)) {
LOGS(logger, VERBOSE) << "Axes of reduction must be a constant initializer";
return false;
}
NodeAttrHelper helper(node);
if (initializers.at(axes_name)->int64_data_size() == 0 && helper.Get("noop_with_empty_axes", 0) != 0) {
LOGS(logger, VERBOSE) << "CoreML doesn't support noop on empty axes for reduction layers" << std::endl;
return false;
}
}
return true;
}
void CreateReductionOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations) {
op_registrations.builders.push_back(std::make_unique<ReductionOpBuilder>());
op_registrations.op_builder_map.emplace(op_type, op_registrations.builders.back().get());
}
} // namespace coreml
} // namespace onnxruntime

View file

@ -0,0 +1,58 @@
// Copyright (c) Shukant Pal.
// Licensed under the MIT License.
#include "core/providers/common.h"
#ifdef __APPLE__
#include "core/providers/coreml/builders/model_builder.h"
#endif
#include "core/providers/coreml/builders/helper.h"
#include "core/providers/coreml/builders/op_builder_factory.h"
#include "base_op_builder.h"
namespace onnxruntime {
namespace coreml {
class UnaryOpBuilder : public BaseOpBuilder {
private:
#ifdef __APPLE__
Status AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& logger) const override;
#endif
};
#ifdef __APPLE__
Status UnaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& /* logger */) const {
const auto& op_type(node.OpType());
const auto& input_defs(node.InputDefs());
std::unique_ptr<COREML_SPEC::NeuralNetworkLayer> layer = CreateNNLayer(model_builder, node);
if (op_type == "Sqrt") {
layer->mutable_unary()->set_type(COREML_SPEC::UnaryFunctionLayerParams::SQRT);
} else if (op_type == "Reciprocal") {
layer->mutable_unary()->set_type(COREML_SPEC::UnaryFunctionLayerParams::INVERSE);
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
"UnaryOpBuilder::AddToModelBuilderImpl, unknown op: ", op_type);
}
*layer->mutable_input()->Add() = input_defs[0]->Name();
*layer->mutable_output()->Add() = node.OutputDefs()[0]->Name();
model_builder.AddLayer(std::move(layer));
return Status::OK();
}
#endif
// Operator support related
void CreateUnaryOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations) {
op_registrations.builders.push_back(std::make_unique<UnaryOpBuilder>());
op_registrations.op_builder_map.emplace(op_type, op_registrations.builders.back().get());
}
} // namespace coreml
} // namespace onnxruntime

View file

@ -99,6 +99,17 @@ static OpBuilderRegistrations CreateOpBuilderRegistrations() {
CreatePadOpBuilder("Pad", op_registrations);
}
{ // Unary
CreateUnaryOpBuilder("Sqrt", op_registrations);
CreateUnaryOpBuilder("Reciprocal", op_registrations);
}
{ // Reduction
// ReduceMean is used in layer normalization which seems to be problematic in Python tests.
CreateReductionOpBuilder("ReduceMean", op_registrations);
CreateReductionOpBuilder("ReduceSum", op_registrations);
}
return op_registrations;
}

View file

@ -35,6 +35,8 @@ void CreateCastOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_
void CreateFlattenOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations);
void CreateLRNOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations);
void CreatePadOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations);
void CreateUnaryOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations);
void CreateReductionOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations);
} // namespace coreml
} // namespace onnxruntime

View file

@ -1547,6 +1547,40 @@ TEST(ReductionOpTest, ReduceMean_int32) {
test.Run();
}
TEST(ReductionOpTest, ReduceMean_axes_input) {
OpTester test("ReduceMean", 18, onnxruntime::kOnnxDomain);
test.AddAttribute("keepdims", (int64_t)1);
test.AddInput<float>("data", {3, 2, 2},
{1, 2,
3, 4,
5, 6,
7, 8,
9, 10,
11, 12});
test.AddInput<int64_t>("axes", {2}, std::vector<int64_t>{0, 2}, true);
test.AddOutput<float>("reduced", {1, 2, 1}, {5.5, 7.5});
// TODO: DNNL, TensorRT, and OpenVINO dont support "axes" input in opset 18, re-enable after
test.Run(OpTester::ExpectResult::kExpectSuccess, "",
{kDnnlExecutionProvider, kTensorrtExecutionProvider, kOpenVINOExecutionProvider, kDmlExecutionProvider});
}
TEST(ReductionOpTest, ReduceMean_do_not_keepdims_axes_input_initializer) {
OpTester test("ReduceMean", 18, onnxruntime::kOnnxDomain);
test.AddAttribute("keepdims", (int64_t)0);
test.AddInput<float>("data", {1, 2, 2},
{1.0f, 2.0f,
3.0f, 4.0f});
test.AddInput<int64_t>("axes", {1}, std::vector<int64_t>{1}, true);
test.AddOutput<float>("reduced", {1, 2}, {2.0f, 3.0f});
// TODO: DNNL, TensorRT, and OpenVINO dont support "axes" input in opset 18, re-enable after
test.Run(OpTester::ExpectResult::kExpectSuccess, "",
{kDnnlExecutionProvider, kTensorrtExecutionProvider, kOpenVINOExecutionProvider, kDmlExecutionProvider});
}
TEST(ReductionOpTest, ReduceMean0DTensor) {
OpTester test("ReduceMean");
test.AddInput<float>("data", {}, {2});
@ -4838,7 +4872,9 @@ TEST(ReductionOpTest, ReduceSum_RK_parallel) {
}
}
test.AddOutput<float>("reduced", {32}, expected);
test.Run();
// CoreML does not provide 1e-5 precision here (it's off by 1e-4)
test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kCoreMLExecutionProvider});
}
TEST(ReductionOpTest, ReduceSum_RK_keepdims) {

View file

@ -25,11 +25,14 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution
|ai.onnx:Pad|Only constant mode and last two dim padding is supported.<br/>Input pads and constant_value should be constant.<br/>If provided, axes should be constant.|
|ai.onnx:Pow|Only supports cases when both inputs are fp32.|
|ai.onnx:PRelu|Input slope should be constant.<br/>Input slope should either have shape [C, 1, 1] or have 1 element.|
|ai.onnx:Reciprocal||
|ai.onnx.ReduceSum||
|ai.onnx:Relu||
|ai.onnx:Reshape||
|ai.onnx:Resize||
|ai.onnx:Sigmoid||
|ai.onnx:Squeeze||
|ai.onnx:Sqrt||
|ai.onnx:Sub||
|ai.onnx:Tanh||
|ai.onnx:Transpose||
|ai.onnx:Transpose||