Skip to content

Commit 7558609

Browse files
wejoncyskottmckaygithub-actions[bot]
authored andcommitted
[CoreML ] ML Program more operators support [3/N] (microsoft#22710)
### Description - Erf - Round - Max - ReduceMax - ReduceMean - ReduceSum - Unsqueeze - Squeeze - Softmax ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. --> --------- Co-authored-by: Scott McKay <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 6d514d0 commit 7558609

File tree

15 files changed

+521
-160
lines changed

15 files changed

+521
-160
lines changed

onnxruntime/core/providers/coreml/builders/impl/base_op_builder.cc

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ using namespace CoreML::Specification;
1313
namespace onnxruntime {
1414
namespace coreml {
1515

16-
// Once all ops are supportted FP16, we can remove it. Before that, we keep a set of ops to
17-
// filter suppported ones.
18-
static std::set<std::string> Float16Ops = {
19-
"Add", "ArgMax", "AveragePool", "BatchNormalization", "Cast", "Clip", "Concat", "Conv", "ConvTranspose",
20-
"DepthToSpace", "Div", "Gelu", "Gemm", "GlobalAveragePool", "GlobalMaxPool", "GridSample", "GroupNormalization",
21-
"InstanceNormalization", "LayerNormalization", "LeakyRelu", "MatMul", "MaxPool", "Mul", "PRelu", "Pow",
22-
"Reciprocal", "Relu", "Reshape", "Resize", "Sigmoid", "Slice", "Split", "Sqrt", "Sub", "Tanh", "Transpose"};
23-
2416
namespace {
2517
// TODO, move this to shared_library
2618
bool HasExternalInitializer(const InitializedTensorSet& initializers, const Node& node,
@@ -64,20 +56,27 @@ bool BaseOpBuilder::IsOpSupported(const Node& node, const OpBuilderInputParams&
6456
}
6557

6658
if (!HasSupportedOpSet(node, logger)) {
59+
LOGS(logger, VERBOSE) << "Operator [" << node.OpType() << "] does not support this opset";
6760
return false;
6861
}
6962

7063
if (!HasSupportedInputs(node, input_params, logger)) {
64+
LOGS(logger, VERBOSE) << "Operator [" << node.OpType() << "] has unsupported inputs";
7165
return false;
7266
}
7367

7468
// We do not support external initializers for now
7569
const auto& initializers = input_params.graph_viewer.GetAllInitializedTensors();
7670
if (HasExternalInitializer(initializers, node, logger)) {
71+
LOGS(logger, VERBOSE) << "Operator [" << node.OpType() << "] has external initializers";
7772
return false;
7873
}
7974

80-
return IsOpSupportedImpl(node, input_params, logger);
75+
if (!IsOpSupportedImpl(node, input_params, logger)) {
76+
LOGS(logger, VERBOSE) << "Operator [" << node.OpType() << "] is not supported by the impl";
77+
return false;
78+
}
79+
return true;
8180
}
8281

8382
bool BaseOpBuilder::HasSupportedInputs(const Node& node, const OpBuilderInputParams& input_params,
@@ -114,13 +113,10 @@ bool BaseOpBuilder::IsInputDtypeSupport(const Node& node, size_t idx,
114113
return true;
115114
}
116115

117-
// only support MLProgram for FP16
118-
#if defined(COREML_ENABLE_MLPROGRAM)
119-
if (input_params.create_mlprogram && input_type == ONNX_NAMESPACE::TensorProto_DataType_FLOAT16 &&
120-
Float16Ops.count(node.OpType())) {
116+
// only MLProgram support FP16
117+
if (input_params.create_mlprogram && input_type == ONNX_NAMESPACE::TensorProto_DataType_FLOAT16) {
121118
return true;
122119
}
123-
#endif
124120

125121
LOGS(logger, VERBOSE) << "[" << node.OpType() << "] Input type: [" << input_type << "] is not currently supported";
126122
return false;

onnxruntime/core/providers/coreml/builders/impl/binary_op_builder.cc

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "core/providers/coreml/builders/helper.h"
77
#include "core/providers/coreml/builders/impl/base_op_builder.h"
88
#include "core/providers/coreml/builders/impl/builder_utils.h"
9+
#include "core/providers/coreml/shape_utils.h"
910
#include "core/providers/coreml/builders/model_builder.h"
1011
#include "core/providers/coreml/builders/op_builder_factory.h"
1112
#include "core/providers/shared/utils/utils.h"
@@ -55,6 +56,64 @@ bool CheckIfBothInputShapesMatch(const Node& node, const logging::Logger& logger
5556
}
5657
} // namespace
5758

59+
#if defined(COREML_ENABLE_MLPROGRAM)
60+
static std::vector<int64_t> InferOutputShape(const std::vector<int64_t>& a, const std::vector<int64_t>& b) {
61+
std::vector<int64_t> output_shape;
62+
int64_t i_a = 0, j_b = 0;
63+
if (a.size() >= b.size()) {
64+
output_shape = a;
65+
j_b -= a.size() - b.size();
66+
} else {
67+
output_shape = b;
68+
i_a -= b.size() - a.size();
69+
}
70+
71+
for (size_t i = 0; i < output_shape.size(); i++, i_a++, j_b++) {
72+
const int64_t a_dim = (i_a >= 0) ? a[i_a] : 1;
73+
const int64_t b_dim = (j_b >= 0) ? b[j_b] : 1;
74+
if (a_dim == -1 || b_dim == -1) {
75+
output_shape[i] = -1;
76+
} else {
77+
output_shape[i] = std::max(a_dim, b_dim);
78+
}
79+
}
80+
return output_shape;
81+
}
82+
83+
// Add variadic inputs to the model builder
84+
// in onnx spec, some node allows variadic inputs, such as max(x, y, z, ...)
85+
// while in coreml, maximum op only allows two inputs maximum(x, y)
86+
// the conversion is doing the following:
87+
// max(x, y, z, ...) -> max(max(x, y), z, ...)
88+
static void AddVariadicInputs(std::unique_ptr<CoreML::Specification::MILSpec::Operation>* op,
89+
ModelBuilder& model_builder,
90+
const Node& node,
91+
const logging::Logger& logger) {
92+
using namespace CoreML::Specification::MILSpec;
93+
const auto& input_defs(node.InputDefs());
94+
std::string_view layer_input_name_x = model_builder.GetUniqueName(node, "variadic");
95+
auto input_dtype = input_defs[0]->TypeAsProto()->tensor_type().elem_type();
96+
const int32_t elem_type = static_cast<int32_t>(input_dtype);
97+
std::vector<int64_t> x0_shape, x1_shape;
98+
GetShape(*input_defs[0], x0_shape, logger);
99+
GetShape(*input_defs[1], x1_shape, logger);
100+
x0_shape = InferOutputShape(x0_shape, x1_shape);
101+
std::unique_ptr<Operation> op_prev = std::move(*op);
102+
for (size_t i = 2; i < input_defs.size(); i++) {
103+
AddIntermediateOperationOutput(*op_prev, layer_input_name_x, elem_type, x0_shape);
104+
std::unique_ptr<Operation> op_cur = model_builder.CreateOperation(node, op_prev->type());
105+
AddOperationInput(*op_cur, "x", layer_input_name_x);
106+
AddOperationInput(*op_cur, "y", input_defs[i]->Name());
107+
model_builder.AddOperation(std::move(op_prev));
108+
op_prev = std::move(op_cur);
109+
layer_input_name_x = model_builder.GetUniqueName(node, "variadic");
110+
GetShape(*input_defs[i], x1_shape, logger);
111+
x0_shape = InferOutputShape(x0_shape, x1_shape);
112+
}
113+
*op = std::move(op_prev);
114+
}
115+
#endif
116+
58117
Status BinaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
59118
const logging::Logger& logger) const {
60119
const auto& op_type(node.OpType());
@@ -70,6 +129,8 @@ Status BinaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const
70129
coreml_op_type = "add";
71130
} else if (op_type == "Mul") {
72131
coreml_op_type = "mul";
132+
} else if (op_type == "Max") {
133+
coreml_op_type = "maximum";
73134
} else if (op_type == "Sub") {
74135
coreml_op_type = "sub";
75136
} else if (op_type == "Div") {
@@ -86,8 +147,11 @@ Status BinaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const
86147
std::unique_ptr<Operation> op = model_builder.CreateOperation(node, coreml_op_type);
87148
AddOperationInput(*op, "x", input_defs[0]->Name());
88149
AddOperationInput(*op, "y", input_defs[1]->Name());
150+
if (input_defs.size() > 2) {
151+
// "max" node may have variadic inputs
152+
AddVariadicInputs(&op, model_builder, node, logger);
153+
}
89154
AddOperationOutput(*op, *node.OutputDefs()[0]);
90-
91155
model_builder.AddOperation(std::move(op));
92156
} else
93157
#endif // defined (COREML_ENABLE_MLPROGRAM)
@@ -157,6 +221,10 @@ bool BinaryOpBuilder::HasSupportedInputsImpl(const Node& node, const OpBuilderIn
157221
return false;
158222
}
159223

224+
if (node.OpType() == "Max" && !input_params.create_mlprogram) {
225+
return false;
226+
}
227+
160228
return true;
161229
}
162230

onnxruntime/core/providers/coreml/builders/impl/clip_op_builder.cc

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,26 +98,24 @@ Status ClipOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
9898
const bool min_max_attribs = node.SinceVersion() < 11;
9999
std::string_view min_name;
100100
if (input_dtype == ONNX_NAMESPACE::TensorProto_DataType_FLOAT) {
101-
min_name = min_max_attribs ? model_builder.AddScalarConstant(clip_op.type(), "min", min)
102-
: node.InputDefs()[1]->Name();
101+
min_name = (min_max_attribs || !has_min) ? model_builder.AddScalarConstant(clip_op.type(), "min", min)
102+
: node.InputDefs()[1]->Name();
103103
} else {
104-
min_name = min_max_attribs ? model_builder.AddScalarConstant(clip_op.type(), "min", MLFloat16(min))
105-
: node.InputDefs()[1]->Name();
104+
min_name = (min_max_attribs || !has_min) ? model_builder.AddScalarConstant(clip_op.type(), "min", MLFloat16(min))
105+
: node.InputDefs()[1]->Name();
106106
}
107107

108108
AddOperationInput(clip_op, "alpha", min_name);
109109

110-
if (has_max) {
111-
std::string_view max_name;
112-
if (input_dtype == ONNX_NAMESPACE::TensorProto_DataType_FLOAT) {
113-
max_name = min_max_attribs ? model_builder.AddScalarConstant(clip_op.type(), "max", max)
114-
: node.InputDefs()[2]->Name();
115-
} else {
116-
max_name = min_max_attribs ? model_builder.AddScalarConstant(clip_op.type(), "max", MLFloat16(max))
117-
: node.InputDefs()[2]->Name();
118-
}
119-
AddOperationInput(clip_op, "beta", max_name);
110+
std::string_view max_name;
111+
if (input_dtype == ONNX_NAMESPACE::TensorProto_DataType_FLOAT) {
112+
max_name = (min_max_attribs || !has_max) ? model_builder.AddScalarConstant(clip_op.type(), "max", max)
113+
: node.InputDefs()[2]->Name();
114+
} else {
115+
max_name = (min_max_attribs || !has_max) ? model_builder.AddScalarConstant(clip_op.type(), "max", MLFloat16(max))
116+
: node.InputDefs()[2]->Name();
120117
}
118+
AddOperationInput(clip_op, "beta", max_name);
121119
}
122120
}
123121

@@ -200,7 +198,9 @@ Status ClipOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
200198
bool ClipOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
201199
const logging::Logger& logger) const {
202200
float min, max;
203-
return GetClipMinMax(input_params.graph_viewer, node, min, max, logger);
201+
bool ret = GetClipMinMax(input_params.graph_viewer, node, min, max, logger);
202+
// what does it mean if min == max?
203+
return ret && (min != max);
204204
}
205205

206206
void CreateClipOpBuilder(const std::string& op_type, OpBuilderRegistrations& op_registrations) {

onnxruntime/core/providers/coreml/builders/impl/reduction_op_builder.cc

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
#include "core/providers/common.h"
66
#include "core/providers/coreml/builders/helper.h"
77
#include "core/providers/coreml/builders/impl/base_op_builder.h"
8+
#include "core/providers/coreml/builders/impl/builder_utils.h"
89
#include "core/providers/coreml/builders/model_builder.h"
910
#include "core/providers/coreml/builders/op_builder_factory.h"
1011
#include "core/providers/shared/utils/utils.h"
1112

13+
#ifdef __APPLE__
14+
#include <TargetConditionals.h>
15+
#endif
16+
1217
namespace onnxruntime {
1318
namespace coreml {
1419

@@ -20,6 +25,7 @@ class ReductionOpBuilder : public BaseOpBuilder {
2025

2126
bool IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
2227
const logging::Logger& logger) const override;
28+
bool SupportsMLProgram() const override { return true; }
2329
};
2430

2531
namespace {
@@ -48,13 +54,12 @@ Status ReductionOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, co
4854
const logging::Logger& /* logger */) const {
4955
const auto& op_type(node.OpType());
5056
const auto& input_defs(node.InputDefs());
51-
const auto& initializers(model_builder.GetInitializerTensors());
5257

5358
std::vector<int64_t> axes;
5459

5560
NodeAttrHelper helper(node);
5661
if (input_defs.size() > 1 && input_defs[1]->Exists()) {
57-
auto& axes_tensor = *initializers.at(input_defs[1]->Name());
62+
auto& axes_tensor = *model_builder.GetConstantInitializer(input_defs[1]->Name());
5863
Initializer axes_initializer(axes_tensor);
5964
int64_t* data = axes_initializer.data<int64_t>();
6065
int64_t size = axes_initializer.size();
@@ -66,28 +71,77 @@ Status ReductionOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, co
6671

6772
const bool keepdims = helper.Get("keepdims", 1) != 0;
6873
const bool noop_with_empty_axes = helper.Get("noop_with_empty_axes", 0) != 0;
74+
#if defined(COREML_ENABLE_MLPROGRAM)
75+
if (model_builder.CreateMLProgram()) {
76+
using namespace CoreML::Specification::MILSpec;
77+
78+
std::string_view coreml_op_type;
79+
if (noop_with_empty_axes && axes.size() == 0) {
80+
coreml_op_type = "identity";
81+
} else if (op_type == "ReduceSum") {
82+
coreml_op_type = "reduce_sum";
83+
} else if (op_type == "ReduceMean") {
84+
coreml_op_type = "reduce_mean";
85+
} else if (op_type == "ReduceMax") {
86+
coreml_op_type = "reduce_max";
87+
} else if (op_type == "ReduceMin") {
88+
coreml_op_type = "reduce_min";
89+
} else if (op_type == "ReduceProd") {
90+
coreml_op_type = "reduce_prod";
91+
} else {
92+
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
93+
"ReductionOpBuilder::AddToModelBuilderImpl, unexpected op: ", op_type);
94+
}
95+
std::unique_ptr<Operation> op = model_builder.CreateOperation(node, coreml_op_type);
96+
AddOperationInput(*op, "x", input_defs[0]->Name());
97+
if (coreml_op_type != "identity") {
98+
if (axes.size() > 0) {
99+
AddOperationInput(*op, "axes", model_builder.AddConstant(op->type(), "axes", axes));
100+
}
101+
AddOperationInput(*op, "keep_dims", model_builder.AddScalarConstant(op->type(), "keep_dims", keepdims));
102+
}
103+
AddOperationOutput(*op, *node.OutputDefs()[0]);
104+
105+
model_builder.AddOperation(std::move(op));
106+
} else
107+
#endif // (COREML_ENABLE_MLPROGRAM)
108+
{
109+
std::unique_ptr<COREML_SPEC::NeuralNetworkLayer> layer = model_builder.CreateNNLayer(node);
110+
111+
if (op_type == "ReduceSum") {
112+
AddReductionParams(layer->mutable_reducesum(), axes, keepdims, noop_with_empty_axes);
113+
} else if (op_type == "ReduceMean") {
114+
AddReductionParams(layer->mutable_reducemean(), axes, keepdims, noop_with_empty_axes);
115+
} else {
116+
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
117+
"ReductionOpBuilder::AddToModelBuilderImpl, unknown op: ", op_type);
118+
}
69119

70-
std::unique_ptr<COREML_SPEC::NeuralNetworkLayer> layer = model_builder.CreateNNLayer(node);
120+
*layer->mutable_input()->Add() = node.InputDefs()[0]->Name();
121+
*layer->mutable_output()->Add() = node.OutputDefs()[0]->Name();
71122

72-
if (op_type == "ReduceSum") {
73-
AddReductionParams(layer->mutable_reducesum(), axes, keepdims, noop_with_empty_axes);
74-
} else if (op_type == "ReduceMean") {
75-
AddReductionParams(layer->mutable_reducemean(), axes, keepdims, noop_with_empty_axes);
76-
} else {
77-
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
78-
"ReductionOpBuilder::AddToModelBuilderImpl, unknown op: ", op_type);
123+
model_builder.AddLayer(std::move(layer));
79124
}
80-
81-
*layer->mutable_input()->Add() = node.InputDefs()[0]->Name();
82-
*layer->mutable_output()->Add() = node.OutputDefs()[0]->Name();
83-
84-
model_builder.AddLayer(std::move(layer));
85125
return Status::OK();
86126
}
87127

88128
bool ReductionOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
89129
const logging::Logger& logger) const {
90130
const auto& input_defs = node.InputDefs();
131+
if (!input_params.create_mlprogram &&
132+
(node.OpType() == "ReduceMax" || node.OpType() == "ReduceMin" || node.OpType() == "ReduceProd")) {
133+
return false;
134+
}
135+
136+
#if defined(TARGET_OS_IOS) && defined(TARGET_CPU_X86_64)
137+
// to pass https://dev.azure.com/onnxruntime/onnxruntime/_build/results?buildId=1563483&view=logs&j=f7cc61a9-cc70-56e7-b06c-4668ca17e426
138+
// ReductionOpTest.ReduceSum_half_bert
139+
int32_t input_type;
140+
GetType(*input_defs[0], input_type, logger);
141+
if (node.OpType() == "ReduceSum" && input_type == ONNX_NAMESPACE::TensorProto_DataType_FLOAT16) {
142+
return false;
143+
}
144+
#endif
91145

92146
NodeAttrHelper helper(node);
93147

@@ -99,18 +153,16 @@ bool ReductionOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInpu
99153
if (input_defs.size() > 1 && input_defs[1]->Exists()) {
100154
// 'axes' is optional input in new opsets
101155
const auto& axes_name = input_defs[1]->Name();
102-
const auto& initializers = input_params.graph_viewer.GetAllInitializedTensors();
103-
if (!Contains(initializers, axes_name)) {
156+
const auto* axes = input_params.graph_viewer.GetConstantInitializer(axes_name);
157+
if (!axes) {
104158
LOGS(logger, VERBOSE) << "Axes of reduction must be a constant initializer";
105159
return false;
106160
}
107161

108-
empty_axes = initializers.at(axes_name)->int64_data_size() == 0;
162+
empty_axes = axes->int64_data_size() == 0;
109163
}
110-
111-
if (empty_axes && noop_with_empty_axes) {
112-
// TODO: When we add ML Program support we should enable this as it makes the node an Identity op
113-
LOGS(logger, VERBOSE) << "CoreML doesn't support noop on empty axes for reduction layers" << std::endl;
164+
if (empty_axes && noop_with_empty_axes && !input_params.create_mlprogram) {
165+
LOGS(logger, VERBOSE) << "NeuralNetwork doesn't support noop on empty axes for reduction layers";
114166
return false;
115167
}
116168

0 commit comments

Comments
 (0)