Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoreML: ML Program Slice #21433

Merged
merged 4 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions onnxruntime/core/providers/coreml/builders/impl/builder_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ void SetTensorTypeInfo(MILSpec::TensorType& tensor_type, MILSpec::DataType data_
void SetTensorTypeInfo(MILSpec::TensorType& tensor_type, MILSpec::DataType data_type,
const ONNX_NAMESPACE::TensorShapeProto* shape, bool convert_scalar = false) {
tensor_type.set_datatype(data_type);

if (shape) {
auto rank = shape->dim_size();
if (convert_scalar && rank == 0) {
Expand Down Expand Up @@ -313,16 +314,19 @@ void AddOperationInput(MILSpec::Operation& op, std::string_view input_name, std:
(*op.mutable_inputs())[input_name] = std::move(arg);
}

void AddOperationOutput(COREML_SPEC::MILSpec::Operation& op, const NodeArg& output) {
void AddOperationOutput(COREML_SPEC::MILSpec::Operation& op, const NodeArg& output,
std::optional<int32_t> override_element_type) {
auto& outputs = *op.mutable_outputs();
auto& output_arg = *outputs.Add();
output_arg.set_name(output.Name());

MILSpec::ValueType& value = *output_arg.mutable_type();
MILSpec::TensorType& tensor_type = *value.mutable_tensortype();

SetTensorTypeInfo(tensor_type, OnnxDataTypeToMILSpec(output.TypeAsProto()->tensor_type().elem_type()),
output.Shape(), /*convert_scalar*/ true);
auto elem_type = override_element_type ? *override_element_type
: output.TypeAsProto()->tensor_type().elem_type();

SetTensorTypeInfo(tensor_type, OnnxDataTypeToMILSpec(elem_type), output.Shape(), /*convert_scalar*/ true);
}

void AddPadTypeAndPads(COREML_SPEC::MILSpec::Operation& op, ModelBuilder& model_builder, std::string_view op_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ void AddOperationInput(COREML_SPEC::MILSpec::Operation& op,
/// </summary>
/// <param name="op">Operation to update.</param>
/// <param name="output">NodeArg with details of output to add.</param>
void AddOperationOutput(COREML_SPEC::MILSpec::Operation& op, const NodeArg& output);
/// <param name="override_element_type">
/// Override the element type. Only set to handle cases where we believe the data at runtime will be int32 but
/// the original ONNX node has type int64.
/// </param>
void AddOperationOutput(COREML_SPEC::MILSpec::Operation& op, const NodeArg& output,
std::optional<int32_t> override_element_type = std::nullopt);

/// <summary>
/// Add pad_type and pad values.
Expand Down
130 changes: 98 additions & 32 deletions onnxruntime/core/providers/coreml/builders/impl/slice_op_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "core/optimizer/initializer.h"
#include "core/providers/coreml/builders/helper.h"
#include "core/providers/coreml/builders/impl/base_op_builder.h"
#include "core/providers/coreml/builders/impl/builder_utils.h"
#include "core/providers/coreml/builders/model_builder.h"
#include "core/providers/coreml/builders/op_builder_factory.h"
#include "core/providers/coreml/shape_utils.h"
Expand All @@ -28,12 +29,14 @@ class SliceOpBuilder : public BaseOpBuilder {

bool IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& builder_params,
const logging::Logger& logger) const override;

bool SupportsMLProgram() const override { return true; }
};

namespace {
Status PrepareSliceComputeMetadataFromConstantInitializers(const Node& slice_node,
const GraphViewer& graph_viewer,
SliceOp::PrepareForComputeMetadata& compute_metadata) {
Status PrepareSliceComputeMetadata(const Node& slice_node,
const GraphViewer& graph_viewer,
SliceOp::PrepareForComputeMetadata& compute_metadata) {
// TODO largely copied from nnapi::SliceOpBuilder::AddToModelBuilderImpl. put it somewhere where it can be reused?

const auto input_defs = slice_node.InputDefs();
Expand Down Expand Up @@ -114,55 +117,113 @@ void SliceOpBuilder::AddInitializersToSkip(ModelBuilder& model_builder, const No

Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& logger) const {
const auto& input_defs = node.InputDefs();
const auto& output_defs = node.OutputDefs();

std::vector<int64_t> data_shape;
ORT_RETURN_IF_NOT(GetStaticShape(*node.InputDefs()[0], data_shape, logger), "Failed to get input shape.");
auto rank = data_shape.size();

SliceOp::PrepareForComputeMetadata compute_metadata{data_shape};
ORT_RETURN_IF_ERROR(PrepareSliceComputeMetadataFromConstantInitializers(node, model_builder.GetGraphViewer(),
compute_metadata));
ORT_RETURN_IF_ERROR(PrepareSliceComputeMetadata(node, model_builder.GetGraphViewer(), compute_metadata));

#if defined(COREML_ENABLE_MLPROGRAM)
if (model_builder.CreateMLProgram()) {
using namespace CoreML::Specification::MILSpec; // NOLINT
// https://apple.github.io/coremltools/source/coremltools.converters.mil.mil.ops.defs.html#coremltools.converters.mil.mil.ops.defs.iOS15.tensor_transformation.slice_by_index

const InlinedVector<bool> begin_mask_values(rank, false);
InlinedVector<bool> end_mask_values(rank, false);

// Special case - stepping backwards up to and including the first index in the dimension.
// In ONNX Slice, we use end <= -(rank + 1) to represent this. In CoreML, setting endids like that doesn't work,
// so use endmasks to specify the rest of the dimension instead.
for (size_t i = 0; i < rank; ++i) {
if (compute_metadata.steps_[i] < 0 && compute_metadata.ends_[i] == -1) {
end_mask_values[i] = true;
}
}

auto layer = model_builder.CreateNNLayer(node);
*layer->mutable_input()->Add() = node.InputDefs()[0]->Name();
*layer->mutable_output()->Add() = node.OutputDefs()[0]->Name();
auto* slice_static = layer->mutable_slicestatic();
// Only int32 and float are supported by CoreML slice_by_index.
// We convert any int64 model input to int32 when running the CoreML model for the partition.
// Any other integer data created at runtime is the output from CoreML operations, and should int32 not int64.
// Based on that, we assume that the actual input when running will be int32, so we override the output data
// type to reflect this.
// If we were to leave it as TensorProto_DataType_INT64 the CoreML model would be invalid.
std::optional<int32_t> output_datatype;

for (size_t i = 0; i < compute_metadata.starts_.size(); ++i) {
const auto step = compute_metadata.steps_[i],
start = compute_metadata.starts_[i],
end = compute_metadata.ends_[i];
int32_t input_type;
ORT_RETURN_IF_NOT(GetType(*node.InputDefs()[0], input_type, logger), "Failed to get input type");

slice_static->add_beginids(start);
slice_static->add_beginmasks(false);
if (input_type == ONNX_NAMESPACE::TensorProto_DataType_INT64) {
output_datatype = ONNX_NAMESPACE::TensorProto_DataType_INT32;
}

if (step < 0 && end == -1) {
// Special case - stepping backwards up to and including the first index in the dimension.
// In ONNX Slice, we use end <= -(rank + 1) to represent this. In CoreML, setting endids like that doesn't work,
// so use endmasks to specify the rest of the dimension instead.
slice_static->add_endids(-1); // ignored
slice_static->add_endmasks(true);
} else {
slice_static->add_endids(end);
slice_static->add_endmasks(false);
auto op = model_builder.CreateOperation(node, "slice_by_index");

auto begin = model_builder.AddConstant(op->type(), "begin", AsSpan(compute_metadata.starts_));
auto end = model_builder.AddConstant(op->type(), "end", AsSpan(compute_metadata.ends_));
auto stride = model_builder.AddConstant(op->type(), "stride", AsSpan(compute_metadata.steps_));
auto begin_mask = model_builder.AddConstant(op->type(), "begin_mask", AsSpan(begin_mask_values));
auto end_mask = model_builder.AddConstant(op->type(), "end_mask", AsSpan(end_mask_values));

AddOperationInput(*op, "x", input_defs[0]->Name());
AddOperationInput(*op, "begin", begin);
AddOperationInput(*op, "end", end);
AddOperationInput(*op, "stride", stride);
AddOperationInput(*op, "begin_mask", begin_mask);
AddOperationInput(*op, "end_mask", end_mask);

AddOperationOutput(*op, *output_defs[0], output_datatype);

model_builder.AddOperation(std::move(op));

} else // NOLINT
#endif // defined(COREML_ENABLE_MLPROGRAM)
{
auto layer = model_builder.CreateNNLayer(node);
*layer->mutable_input()->Add() = input_defs[0]->Name();
*layer->mutable_output()->Add() = output_defs[0]->Name();
auto* slice_static = layer->mutable_slicestatic();

for (size_t i = 0; i < rank; ++i) {
const auto step = compute_metadata.steps_[i],
start = compute_metadata.starts_[i],
end = compute_metadata.ends_[i];

slice_static->add_beginids(start);
slice_static->add_beginmasks(false);

if (step < 0 && end == -1) {
// Special case - stepping backwards up to and including the first index in the dimension.
// In ONNX Slice, we use end <= -(rank + 1) to represent this. In CoreML, setting endids like that doesn't work,
// so use endmasks to specify the rest of the dimension instead.
slice_static->add_endids(-1); // ignored
slice_static->add_endmasks(true);
} else {
slice_static->add_endids(end);
slice_static->add_endmasks(false);
}

slice_static->add_strides(step);
}

slice_static->add_strides(step);
model_builder.AddLayer(std::move(layer));
}

model_builder.AddLayer(std::move(layer));
return Status::OK();
}

bool SliceOpBuilder::HasSupportedInputsImpl(const Node& node, const OpBuilderInputParams& /*input_params*/,
const logging::Logger& logger) const {
int32_t input_type;
if (!GetType(*node.InputDefs()[0], input_type, logger))
if (!GetType(*node.InputDefs()[0], input_type, logger)) {
return false;
}

if (input_type != ONNX_NAMESPACE::TensorProto_DataType_FLOAT &&
input_type != ONNX_NAMESPACE::TensorProto_DataType_INT64) {
LOGS(logger, VERBOSE) << "[" << node.OpType()
<< "] Input type: [" << input_type
<< "] is not supported for now";
LOGS(logger, VERBOSE) << "[" << node.OpType() << "] Input type: [" << input_type << "] is not supported";
return false;
}

Expand Down Expand Up @@ -197,9 +258,14 @@ bool SliceOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputPar
}

SliceOp::PrepareForComputeMetadata compute_metadata{data_shape};
ORT_THROW_IF_ERROR(PrepareSliceComputeMetadataFromConstantInitializers(node, builder_params.graph_viewer,
compute_metadata));
auto status = PrepareSliceComputeMetadata(node, builder_params.graph_viewer, compute_metadata);
if (status != Status::OK()) {
LOGS(logger, VERBOSE) << "PrepareSliceComputeMetadata failed:" << status.ErrorMessage();
return false;
}

if (!ValidateSliceComputeMetadataForCoreML(compute_metadata, logger)) {
// error logged in ValidateSliceComputeMetadataForCoreML
return false;
}

Expand Down
7 changes: 7 additions & 0 deletions onnxruntime/core/providers/coreml/builders/model_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,13 @@ Status ModelBuilder::RegisterModelInputOutput(const NodeArg& node_arg, bool is_i
if (is_input) {
// the model inputs need to be wired up as args to the 'main' function.
auto tensor_value_type = CreateNamedTensorValueType(node_arg, /*convert_scalar*/ true);

// we need to convert int64 to int32 here as well
if (data_type == ONNX_NAMESPACE::TensorProto_DataType_INT64) {
tensor_value_type.mutable_type()->mutable_tensortype()->set_datatype(
OnnxDataTypeToMILSpec(ONNX_NAMESPACE::TensorProto_DataType_INT32));
}

tensor_value_type.set_name(name);

mlprogram_main_fn_->mutable_inputs()->Add(std::move(tensor_value_type));
Expand Down
7 changes: 7 additions & 0 deletions onnxruntime/core/providers/coreml/builders/model_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ class ModelBuilder {
return AddConstant(op_type, value_type, AsSpan(value), shape);
}

// helper to convert a span of non-const data to const
template <typename T>
std::string_view AddConstant(std::string_view op_type, std::string_view value_type, gsl::span<T> value,
std::optional<gsl::span<const int64_t>> shape = std::nullopt) {
return AddConstant(op_type, value_type, gsl::span<const T>(value), shape);
}

/// <summary>
/// Add a scalar value as a 'const' operation. See AddConstant for details.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion onnxruntime/test/providers/cpu/tensor/slice_op.test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ void RunSliceTest(const std::vector<int64_t>& input_dims,

run_test(false);

// NNAPI EP requires the starts/ends/axes/steps be initializers
// EPs like NNAPI and CoreML require the starts/ends/axes/steps be initializers
run_test(true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution
|ai.onnx:Relu||
|ai.onnx:Reshape||
|ai.onnx:Resize|See [resize_op_builder.cc](https://github.com/microsoft/onnxruntime/blob/main/onnxruntime/core/providers/coreml/builders/impl/resize_op_builder.cc) implementation. There are too many permutations to describe the valid combinations.|
|ai.onnx.Slice|starts/ends/axes/steps must be constant initializers.|
|ai.onnx:Sub||
|ai.onnx:Sigmoid||
|ai:onnx:Tanh||
Expand Down
Loading