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

Add CoreML ML Program Resize #21370

Merged
merged 11 commits into from
Jul 19, 2024
18 changes: 6 additions & 12 deletions onnxruntime/core/providers/coreml/builders/helper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ bool IsNodeSupported(const Node& node, const OpBuilderInputParams& input_params,
}
}

bool IsInputSupported(const Node& node, const NodeArg& input,
const OpBuilderInputParams& input_params, const logging::Logger& logger) {
bool IsInputSupported(const Node& node, const NodeArg& input, const OpBuilderInputParams& input_params,
const logging::Logger& logger, bool allow_empty_input) {
if (!input.Exists()) {
// optional input that is not provided
return true;
Expand Down Expand Up @@ -84,16 +84,10 @@ bool IsInputSupported(const Node& node, const NodeArg& input,
return false;
}

if (dim == 0) {
if (node.OpType() == "Resize" && &input == node.InputDefs()[1]) {
skottmckay marked this conversation as resolved.
Show resolved Hide resolved
// one special case. Resize 'roi' input was originally a required input but is rarely used.
// ROI is not supported in the CoreML implementation so we will ignore the value, but is often added
// (at least in the unit tests) as an initializer with shape {0}.
} else {
LOGS(logger, WARNING) << "CoreML does not support shapes with dimension values of 0. Input:" << input_name
<< ", shape: " << Shape2String(shape);
return false;
}
if (dim == 0 && !allow_empty_input) {
LOGS(logger, WARNING) << "CoreML does not support shapes with dimension values of 0. Input:" << input_name
<< ", shape: " << Shape2String(shape);
return false;
}
}

Expand Down
3 changes: 2 additions & 1 deletion onnxruntime/core/providers/coreml/builders/helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ OpBuilderInputParams MakeOpBuilderParams(const GraphViewer& graph_viewer,
const IOpBuilder* GetOpBuilder(const Node& node);

bool IsInputSupported(const Node& node, const NodeArg& node_arg, const OpBuilderInputParams& input_params,
const logging::Logger& logger);
const logging::Logger& logger,
bool allow_empty_input = false);

bool IsNodeSupported(const Node& node, const OpBuilderInputParams& input_params, const logging::Logger& logger);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bool BaseOpBuilder::IsOpSupported(const Node& node, const OpBuilderInputParams&
bool BaseOpBuilder::HasSupportedInputs(const Node& node, const OpBuilderInputParams& input_params,
const logging::Logger& logger) const {
for (const auto* input : node.InputDefs()) {
if (!IsInputSupported(node, *input, input_params, logger)) {
if (!IsInputSupported(node, *input, input_params, logger, allow_empty_tensor_as_input_)) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
void AddInitializersToSkip(ModelBuilder& /*model_builder*/, const Node& /*node*/) const override {}

protected:
BaseOpBuilder(bool allow_empty_tensor_as_input = false)

Check warning on line 31 in onnxruntime/core/providers/coreml/builders/impl/base_op_builder.h

View workflow job for this annotation

GitHub Actions / Lint C++

[cpplint] reported by reviewdog 🐶 Constructors callable with one argument should be marked explicit. [runtime/explicit] [5] Raw Output: onnxruntime/core/providers/coreml/builders/impl/base_op_builder.h:31: Constructors callable with one argument should be marked explicit. [runtime/explicit] [5]
: allow_empty_tensor_as_input_(allow_empty_tensor_as_input) {
}

// currently we only support float
static bool IsInputFloat(const Node& node, size_t idx, const OpBuilderInputParams& input_params,
const logging::Logger& logger);
Expand All @@ -50,6 +54,8 @@

virtual Status AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& logger) const = 0;

const bool allow_empty_tensor_as_input_; // some operators can handle ignoring an empty tensor as input
};

} // namespace coreml
Expand Down
610 changes: 458 additions & 152 deletions onnxruntime/core/providers/coreml/builders/impl/resize_op_builder.cc

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions onnxruntime/core/providers/coreml/builders/model_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,17 @@ class ModelBuilder {
// so we don't do a copy of the original initializer into the model.
void AddInitializerToSkip(const std::string& tensor_name);

// There are some input which will not be used, add it to a list which will not
// be added to CoreML model, since CoreML does not like input unused
/// <summary>
/// Skip a non-initializer value, that is not used in the CoreML model, but was an input to a supported node.
///
/// This is for a rare edge case where a value is an input to a node but is empty/unused, as the
/// CoreML model requires all model inputs to be consumed.
/// </summary>
/// <remarks>
/// The only known use case for this currently is Resize, and that is largely due to how the unit tests are
/// setup rather than something you'd expect to see in a real model.
/// See ResizeOpBuilder::AddInitializersToSkip for more details.
/// </remarks>
void AddInputToSkip(const std::string& input_name);

const std::string& GetUniqueName(const std::string& base_name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ CoreMLExecutionProvider::CoreMLExecutionProvider(uint32_t coreml_flags)
: IExecutionProvider{onnxruntime::kCoreMLExecutionProvider},
coreml_flags_(coreml_flags),
coreml_version_(coreml::util::CoreMLVersion()) {
LOGS_DEFAULT(VERBOSE) << "CoreML version: " << coreml_version_;
if (coreml_version_ < MINIMUM_COREML_VERSION) {
LOGS_DEFAULT(ERROR) << "CoreML EP is not supported on this platform.";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,37 @@
const Initializer unpacked_tensor(*scales);
auto scales_data = unpacked_tensor.DataAsSpan<float>();
input_is_nchw = scales_data[1] == 1.0F;
float const scale_n = scales_data[0];
float const scale_c = input_is_nchw ? scales_data[1] : scales_data[3];
const float scale_n = scales_data[0];
const float scale_c = input_is_nchw ? scales_data[1] : scales_data[3];
const float scale_h = input_is_nchw ? scales_data[2] : scales_data[1];
const float scale_w = input_is_nchw ? scales_data[3] : scales_data[2];

if (scale_n != 1.0f || scale_c != 1.0f) {
LOGS_DEFAULT(VERBOSE) << "Scales of N/C channel should be 1"
<< "Resize of N/C channels are not supported"
<< ", scale_n, " << scale_n << ", scale_c, " << scale_c;
return false;
}

// if downsampling the input size must be evenly divisible by the output size to match the onnx output
if (scale_h < 1.0f || scale_w < 1.0f) {
// we also require input_shape to be known to check
auto h_in = input_is_nchw ? input_shape[2] : input_shape[1];
auto w_in = input_is_nchw ? input_shape[3] : input_shape[2];
if (h_in == 0 || w_in == 0) {
skottmckay marked this conversation as resolved.
Show resolved Hide resolved
LOGS_DEFAULT(VERBOSE) << "Input H and W must be known to downsample with scales";
return false;
}

// use double when applying the scale in case we get a value > 16,777,216, which is 1 << 24
// and the max integer value a 32-bit float can represent accurately with its mantissa
auto h_out = double(h_in) * scale_h;

Check warning on line 278 in onnxruntime/core/providers/nnapi/nnapi_builtin/builders/impl/resize_op_builder.cc

View workflow job for this annotation

GitHub Actions / Lint C++

[cpplint] reported by reviewdog 🐶 Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4] Raw Output: onnxruntime/core/providers/nnapi/nnapi_builtin/builders/impl/resize_op_builder.cc:278: Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4]
auto w_out = double(w_in) * scale_w;

Check warning on line 279 in onnxruntime/core/providers/nnapi/nnapi_builtin/builders/impl/resize_op_builder.cc

View workflow job for this annotation

GitHub Actions / Lint C++

[cpplint] reported by reviewdog 🐶 Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4] Raw Output: onnxruntime/core/providers/nnapi/nnapi_builtin/builders/impl/resize_op_builder.cc:279: Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4]
if (std::floor(h_out) != h_out || std::floor(w_in) != w_out) {
LOGS_DEFAULT(VERBOSE) << "Input size must be evenly divisible by output size when downsampling";
return false;
}
}
} else {
const auto* sizes = graph_viewer.GetConstantInitializer(inputs[3].node_arg.Name());
if (!sizes) {
Expand Down
23 changes: 22 additions & 1 deletion onnxruntime/core/providers/xnnpack/tensor/resize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,30 @@
InlinedVector<float> scale(4, 1.0F);
if (scale_tensor) {
const Initializer scale_val(*scale_tensor, node_unit.ModelPath());
if (scale_val.DataAsSpan<float>()[1] != 1.0F) {
const auto scales = scale_val.DataAsSpan<float>();
if (scales[1] != 1.0F) {
break;
}

// downsampling output seems to require the output size to be a factor of the input to match ONNX
if (scales[2] < 1.0f || scales[3] < 1.0f) {
// we also require input_shape to be known to check
int64_t h_in = x_shape->dim(2).dim_value();
int64_t w_in = x_shape->dim(3).dim_value();
if (h_in < 0 || w_in < 0) {
break;
}

float scale_h = scales[2];
float scale_w = scales[3];
// use double when applying the scale in case we get a value > 16,777,216, which is 1 << 24
// and the max integer value a 32-bit float can represent accurately with its mantissa
auto h_out = double(h_in) * scale_h;

Check warning on line 89 in onnxruntime/core/providers/xnnpack/tensor/resize.cc

View workflow job for this annotation

GitHub Actions / Lint C++

[cpplint] reported by reviewdog 🐶 Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4] Raw Output: onnxruntime/core/providers/xnnpack/tensor/resize.cc:89: Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4]
auto w_out = double(w_in) * scale_w;

Check warning on line 90 in onnxruntime/core/providers/xnnpack/tensor/resize.cc

View workflow job for this annotation

GitHub Actions / Lint C++

[cpplint] reported by reviewdog 🐶 Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4] Raw Output: onnxruntime/core/providers/xnnpack/tensor/resize.cc:90: Using deprecated casting style. Use static_cast<double>(...) instead [readability/casting] [4]
if (std::floor(h_out) != h_out || std::floor(w_in) != w_out) {
break;
}
}
}

if (size_tensor) {
Expand Down
152 changes: 124 additions & 28 deletions onnxruntime/test/providers/cpu/tensor/resize_op_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -227,28 +227,33 @@ TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_tf_crop_and_resize_without_e
}

TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear) {
OpTester test("Resize", 13);
std::vector<float> roi{};
std::vector<float> scales{1.0f, 1.0f, 0.6f, 0.6f};
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
std::vector<float> scales{1.0f, 1.0f, 0.6f, 0.6f};

test.AddAttribute("mode", "linear");
test.AddAttribute("mode", "linear");

constexpr int64_t N = 1, C = 1, H = 2, W = 4;
std::vector<float> X = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f};
constexpr int64_t N = 1, C = 1, H = 2, W = 4;
std::vector<float> X = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f};

test.AddInput<float>("X", {N, C, H, W}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {4}, scales);
test.AddInput<float>("X", {N, C, H, W}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {4}, scales, scales_in_initializer);

std::vector<float> Y = {2.66666651f, 4.3333331f};
std::vector<float> Y = {2.66666651f, 4.3333331f};

test.AddOutput<float>("Y", {N, C, static_cast<int64_t>(H * scales[2]), static_cast<int64_t>(W * scales[3])}, Y);
// QNN: result diff
// TRT: Segmentation fault in A100
std::unordered_set<std::string> excluded_providers({kQnnExecutionProvider});
test.Run(OpTester::ExpectResult::kExpectSuccess, "", ExcludeTrtOnA100(excluded_providers));
test.AddOutput<float>("Y", {N, C, static_cast<int64_t>(H * scales[2]), static_cast<int64_t>(W * scales[3])}, Y);
// QNN: result diff
// TRT: Segmentation fault in A100
std::unordered_set<std::string> excluded_providers({kQnnExecutionProvider});
test.Run(OpTester::ExpectResult::kExpectSuccess, "", ExcludeTrtOnA100(excluded_providers));
};

run_test(false);
run_test(true);
}

TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_4DBilinear) {
Expand Down Expand Up @@ -327,13 +332,14 @@ TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_4DBilinear_int8) {
// Since NNAPI(TFLite) only using the scale calculate using the input/output size
// For the above test (ResizeOpLinearDownSampleTest_4DBilinear)
// The output size is [1,1,2,4].*[1,1,0.6,0.6]=[1,1,1,2]
// NNAPI will recaluclate the scales as the output size divided by input size
// NNAPI will recalculate the scales as the output size divided by input size
// scales = [1,1,1,2]./[1,1,2,4] = [1,1,0.5,0.5]
// See:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/kernels/internal/reference/reference_ops.h
// So the result of the above example will be different than CPU EP
// Add the following 2 tests to test with scales valid to NNAPI
// Add the following 2 tests to test with scales valid to NNAPI.
// CoreML also doesn't handle a scale that doesn't divide the input size evenly.
TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand All @@ -360,8 +366,38 @@ TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1) {
run_test(true);
}

// Downsize with factor being an odd number (1/3)
TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1_OddNumber) {
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
std::vector<float> scales{1.0f, 1.0f, (1.f / 3), (1.f / 3)};

test.AddAttribute("mode", "linear");

constexpr int64_t N = 1, C = 1, H = 3, W = 6;
std::vector<float> X = {
1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f,
7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f,
13.0f, 14.0f, 15.0f, 16.0f, 17.0f, 18.0f};

test.AddInput<float>("X", {N, C, H, W}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {4}, scales, scales_in_initializer);

std::vector<float> Y = {8.f, 11.f};

test.AddOutput<float>("Y", {N, C, static_cast<int64_t>(H * scales[2]), static_cast<int64_t>(W * scales[3])}, Y);
test.Run(OpTester::ExpectResult::kExpectSuccess, "", ExcludeTrtOnA100());
};

run_test(false);
run_test(true);
}

TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1_WithSizes) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_and_sizes_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand Down Expand Up @@ -389,8 +425,32 @@ TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1_WithSizes) {
run_test(true);
}

// test handling for opset 11. scales input is provided but should be ignored in favor of sizes
TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1_WithSizesOpset11) {
OpTester test("Resize", 11);
std::vector<float> roi{};
std::vector<float> scales{};
constexpr int64_t N = 1, C = 1, H = 2, W = 4;
std::vector<int64_t> sizes{N, C, 1, 2};
test.AddAttribute("mode", "linear");

std::vector<float> X = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f};

test.AddInput<float>("X", {N, C, H, W}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("scales", {0}, scales);
test.AddInput<int64_t>("sizes", {4}, sizes, true); // add as initializer so CoreML EP can take

std::vector<float> Y = {3.5f, 5.5f};

test.AddOutput<float>("Y", sizes, Y);
test.Run();
}

TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear_align_corners) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand All @@ -416,15 +476,51 @@ TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear_align_corners) {

run_test(false);

#ifdef USE_NNAPI
// NNAPI will need the scales as an initializer
#if defined(USE_NNAPI) || defined(USE_COREML)
// NNAPI and CoreML need the scales as an initializer
// Also tensor RT EP will fail if scales is an initializer but will pass if it is not
run_test(true);
#endif
}

TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear_align_corners_sizes) {
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
std::vector<float> scales{};
std::vector<int64_t> sizes{1, 1, 1, 2};

test.AddAttribute("mode", "linear");
test.AddAttribute("coordinate_transformation_mode", "align_corners");

constexpr int64_t N = 1, C = 1, H = 2, W = 4;
std::vector<float> X = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f};

test.AddInput<float>("X", {N, C, H, W}, X);
test.AddInput<float>("roi", {0}, roi);
test.AddInput<float>("", {0}, scales);
test.AddInput<int64_t>("sizes", {4}, sizes, scales_in_initializer);

std::vector<float> Y = {1.0f, 4.0f};

test.AddOutput<float>("Y", {N, C, 1, 2}, Y);
test.Run(OpTester::ExpectResult::kExpectSuccess, "", ExcludeTrtOnA100());
};

run_test(false);

#if defined(USE_NNAPI) || defined(USE_COREML)
// NNAPI and CoreML will need the scales as an initializer
// Also tensor RT EP will fail if scales is an initializer but will pass if it is not
run_test(true);
#endif
}

TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_4DBilinear_align_corners_uint8) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand Down Expand Up @@ -456,7 +552,7 @@ TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_4DBilinear_align_corners_uin
}

TEST(ResizeOpTest, NhwcResizeOpLinearDownSampleTest_4DBilinear_align_corners_int8) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand Down Expand Up @@ -622,7 +718,7 @@ TEST(ResizeOpTest, ResizeOpLinearUpSampleTest_4DBilinear_asymmetric_scales) {
}

TEST(ResizeOpTest, NhwcResizeOpLinearUpSampleTest_4DBilinear_asymmetric_uint8) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand Down Expand Up @@ -668,7 +764,7 @@ TEST(ResizeOpTest, NhwcResizeOpLinearUpSampleTest_4DBilinear_asymmetric_uint8) {
}

TEST(ResizeOpTest, NhwcResizeOpLinearUpSampleTest_4DBilinear_asymmetric_int8) {
// To test NNAPI EP, we need the sclaes/sizes to be in initializers
// To test NNAPI EP, we need the scales/sizes to be in initializers
auto run_test = [](bool scales_in_initializer) {
OpTester test("Resize", 13);
std::vector<float> roi{};
Expand Down
Loading
Loading