diff --git a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/model_builder.cc b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/model_builder.cc index 2c2d6b97d2..8c748a4a74 100644 --- a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/model_builder.cc +++ b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/model_builder.cc @@ -235,7 +235,7 @@ Status ModelBuilder::RegisterInitializers() { shape.push_back(SafeInt(dim)); } - ORT_RETURN_IF_NOT(!shape.empty(), "NNAPI does not support scalar initializer"); + ORT_RETURN_IF_NOT(!shape.empty(), "NNAPI does not support scalar initializer, tensor name, ", name); Type type = Type::TENSOR_FLOAT32; switch (tensor.data_type()) { diff --git a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/op_builder.cc b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/op_builder.cc index 68e4cdd505..d455e3801d 100644 --- a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/op_builder.cc +++ b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/op_builder.cc @@ -1852,13 +1852,13 @@ bool SoftMaxOpBuilder::IsOpSupportedImpl(ModelBuilder& model_builder, const Node return false; } - const auto android_skd_ver = model_builder.GetAndroidSdkVer(); - if (android_skd_ver < 29) { + const auto android_sdk_ver = model_builder.GetAndroidSdkVer(); + if (android_sdk_ver < 29) { NodeAttrHelper helper(node); int32_t axis = helper.Get("axis", 1); if (axis != 1) { LOGS_DEFAULT(VERBOSE) - << "SoftMax only support axis 1 on Android API level: " << android_skd_ver + << "SoftMax only support axis 1 on Android API level: " << android_sdk_ver << " input axis: " << axis; return false; } @@ -1871,13 +1871,13 @@ Status SoftMaxOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, cons auto& shaper(model_builder.GetShaper()); const auto& operand_indices(model_builder.GetOperandIndices()); const auto& operand_types(model_builder.GetOperandTypes()); - const auto android_skd_ver = model_builder.GetAndroidSdkVer(); + const auto android_sdk_ver = model_builder.GetAndroidSdkVer(); NodeAttrHelper helper(node); auto input = node.InputDefs()[0]->Name(); bool input_is_nhwc = model_builder.IsOperandNHWC(input); bool output_is_nhwc = input_is_nhwc; - if (android_skd_ver < 29) { + if (android_sdk_ver < 29) { if (model_builder.IsOperandNHWC(input)) { output_is_nhwc = false; // We want to transpose nhwc operand back to nchw before softmax @@ -1901,7 +1901,7 @@ Status SoftMaxOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, cons input_indices.push_back(operand_indices.at(input)); ADD_SCALAR_OPERAND(model_builder, input_indices, beta); - if (android_skd_ver > 28) { + if (android_sdk_ver > 28) { // you can only specify axis for android api level 29+ ADD_SCALAR_OPERAND(model_builder, input_indices, axis); } @@ -2494,8 +2494,6 @@ Status QuantizeLinearOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builde ORT_RETURN_IF_ERROR(GetQuantizationZeroPoint(model_builder, node, 2, zero_point)); } - LOGS_DEFAULT(VERBOSE) << "scale: " << scale << " zp: " << zero_point; - ORT_RETURN_IF_ERROR(shaper.Identity(input, output)); const OperandType output_operand_type(output_type, shaper[output], scale, zero_point); std::vector input_indices; @@ -2624,12 +2622,12 @@ Status LRNOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const No const auto& operand_indices(model_builder.GetOperandIndices()); const auto& operand_types(model_builder.GetOperandTypes()); NodeAttrHelper helper(node); - const auto android_skd_ver = model_builder.GetAndroidSdkVer(); + const auto android_sdk_ver = model_builder.GetAndroidSdkVer(); auto input = node.InputDefs()[0]->Name(); const auto& output = node.OutputDefs()[0]->Name(); bool output_is_nhwc = model_builder.IsOperandNHWC(input); - if (android_skd_ver < 29) { + if (android_sdk_ver < 29) { // on android api level 28, we need to transpose the nchw input to nhwc output_is_nhwc = true; if (!model_builder.IsOperandNHWC(input)) { @@ -2657,7 +2655,7 @@ Status LRNOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const No ADD_SCALAR_OPERAND(model_builder, input_indices, beta); // specify axis is only available on api level >= 29 - if (android_skd_ver > 28) { + if (android_sdk_ver > 28) { // ONNX LRN is always performed on C dimension int32_t axis = output_is_nhwc ? 3 // nhwc @@ -2674,6 +2672,311 @@ Status LRNOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const No #pragma endregion +#pragma region op_clip + +class ClipOpBuilder : public BaseOpBuilder { + public: + void AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) override; + + private: + bool IsOpSupportedImpl(ModelBuilder& model_builder, const Node& node) override; + Status AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node) override ORT_MUST_USE_RESULT; + static bool GetMinMax(ModelBuilder& model_builder, const Node& node, float& min, float& max); +}; + +void ClipOpBuilder::AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) { + if (node.InputDefs().size() > 1) + model_builder.AddInitializerToSkip(node.InputDefs()[1]->Name()); // min + + if (node.InputDefs().size() > 2) + model_builder.AddInitializerToSkip(node.InputDefs()[2]->Name()); // max +} + +/* static */ bool ClipOpBuilder::GetMinMax(ModelBuilder& model_builder, const Node& node, float& min, float& max) { + if (node.SinceVersion() < 11) { // Clip opset 1, 6 is using attributes for min/max + NodeAttrHelper helper(node); + min = helper.Get("min", std::numeric_limits::lowest()); + max = helper.Get("max", std::numeric_limits::max()); + } else { + const auto& initializers(model_builder.GetInitializerTensors()); + + if (node.InputDefs().size() > 1) { // we have input min + const auto& min_name = node.InputDefs()[1]->Name(); + if (!Contains(initializers, min_name)) { + LOGS_DEFAULT(VERBOSE) << "Input min of Clip must be known"; + return false; + } + min = GetTensorFloatData(initializers.at(min_name))[0]; + } + + if (node.InputDefs().size() > 2) { // we have input max + const auto& max_name = node.InputDefs()[2]->Name(); + if (!Contains(initializers, max_name)) { + LOGS_DEFAULT(VERBOSE) << "Input max of Clip must be known"; + return false; + } + max = GetTensorFloatData(initializers.at(max_name))[0]; + } + } + + return true; +} + +bool ClipOpBuilder::IsOpSupportedImpl(ModelBuilder& model_builder, const Node& node) { + float min = std::numeric_limits::lowest(); + float max = std::numeric_limits::max(); + if (!GetMinMax(model_builder, node, min, max)) + return false; + + // We only supoort relu6 or relu1 + // TODO, support clip between 2 arbitrary numbers + if ((min == 0.0f && max == 6.0f) || (min == -1.0f && max == 1.0f)) { + return true; + } else { + LOGS_DEFAULT(VERBOSE) << "Clip only supports [min, max] = [0, 6] or [-1, 1], the input is [" + << min << ", " << max << "]"; + return false; + } + + return true; +} + +Status ClipOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node) { + auto& shaper(model_builder.GetShaper()); + const auto& operand_indices(model_builder.GetOperandIndices()); + const auto& operand_types(model_builder.GetOperandTypes()); + + const auto& input = node.InputDefs()[0]->Name(); + const auto& output = node.OutputDefs()[0]->Name(); + bool output_is_nhwc = model_builder.IsOperandNHWC(input); + + ORT_RETURN_IF_ERROR(shaper.Identity(input, output)); + const OperandType output_operand_type(operand_types.at(input).type, shaper[output]); + + float min = std::numeric_limits::lowest(); + float max = std::numeric_limits::max(); + GetMinMax(model_builder, node, min, max); + + int32_t op_code; + if (min == 0.0f && max == 6.0f) + op_code = ANEURALNETWORKS_RELU6; + else if (min == -1.0f && max == 1.0f) + op_code = ANEURALNETWORKS_RELU1; + else + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "ClipOpBuilder, unsupported input [", min, ", ", max, "].", + "We should not reach here, ClipOpBuilder::IsOpSupportedImpl should have caught this."); + + std::vector input_indices; + input_indices.push_back(operand_indices.at(input)); + ORT_RETURN_IF_ERROR(model_builder.AddOperation(op_code, input_indices, + {output}, {output_operand_type}, {output_is_nhwc})); + return Status::OK(); +} + +#pragma endregion + +#pragma region op_Resize + +class ResizeOpBuilder : public BaseOpBuilder { + public: + void AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) override; + + private: + bool IsOpSupportedImpl(ModelBuilder& model_builder, const Node& node) override; + + int32_t GetMinSupportedSdkVer(ModelBuilder& model_builder, const Node& node) const override; + + Status AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node) override ORT_MUST_USE_RESULT; +}; + +int32_t ResizeOpBuilder::GetMinSupportedSdkVer(ModelBuilder& /* model_builder */, const Node& /* node */) const { + return 28; +} + +void ResizeOpBuilder::AddInitializersToSkip(ModelBuilder& model_builder, const Node& node) { + // We will still add scales to the skipped list even sizes are present + // since there is no use of it, we will not process it later + model_builder.AddInitializerToSkip(node.InputDefs()[2]->Name()); // scales + + if (node.InputDefs().size() > 3) + model_builder.AddInitializerToSkip(node.InputDefs()[3]->Name()); // sizes +} + +bool ResizeOpBuilder::IsOpSupportedImpl(ModelBuilder& model_builder, const Node& node) { + // Resize opset 10- is very different than Resize opset 11+, with many key attributes missing + // We only support Resize opset 11+ here + if (node.SinceVersion() < 11) { + LOGS_DEFAULT(VERBOSE) << "Resize only supports opset 11+"; + return false; + } + + Shape input_shape; + if (!GetShape(*node.InputDefs()[0], input_shape)) + return false; + + const auto input_size = input_shape.size(); + if (input_size != 4) { + LOGS_DEFAULT(VERBOSE) << "Resize only support 4d shape, input is " + << input_size << "d shape"; + return false; + } + + { // check attributes + const auto android_sdk_ver = model_builder.GetAndroidSdkVer(); + + NodeAttrHelper helper(node); + const auto mode = helper.Get("mode", "nearest"); + if (mode != "linear") { + LOGS_DEFAULT(VERBOSE) << "Resize unsupported input mode, " << mode; + return false; + } + + const auto coord_trans_mode = helper.Get("coordinate_transformation_mode", "half_pixel"); + bool using_half_pixel = coord_trans_mode == "half_pixel"; + bool using_align_corners = coord_trans_mode == "align_corners"; + if (!using_half_pixel && !using_align_corners && coord_trans_mode != "asymmetric") { + LOGS_DEFAULT(VERBOSE) << "Resize, unsupported coord_trans_mode, " << coord_trans_mode; + return false; + } + + if (android_sdk_ver < 30 && (using_half_pixel || using_align_corners)) { + LOGS_DEFAULT(VERBOSE) << "Resize only support half_pixel/align_corners on API level 30+, current API level is " + << android_sdk_ver; + return false; + } + + const auto exclude_outside = helper.Get("exclude_outside", 0); + if (exclude_outside != 0) { + LOGS_DEFAULT(VERBOSE) << "Resize does not support exclude_outside for now"; + return false; + } + } + + { // scales and sizes (if present) must be initializers + const auto& initializers(model_builder.GetInitializerTensors()); + const auto input_defs = node.InputDefs(); + // scales + if (input_defs.size() < 3 || !Contains(initializers, input_defs[2]->Name())) { + LOGS_DEFAULT(VERBOSE) << "Input scales of Resize must be known"; + return false; + } + + // sizes + if (input_defs.size() > 3 && !Contains(initializers, input_defs[3]->Name())) { + LOGS_DEFAULT(VERBOSE) << "Input sizes of Resize must be known"; + return false; + } + + // We want to check if the scales or sizes are not trying to resize on N/C channels here + if (input_defs.size() == 3) { // we are using scales + const auto& scales_tensor = initializers.at(input_defs[2]->Name()); + const float* scales_data = GetTensorFloatData(scales_tensor); + float scale_n = scales_data[0]; + float scale_c = scales_data[1]; + 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; + } + } else { + // we are using sizes + const auto& sizes_name = input_defs[3]->Name(); + const auto& sizes_tensor = initializers.at(sizes_name); + const int64_t* sizes_data = GetTensorInt64Data(sizes_tensor); + uint32_t size_n = SafeInt(sizes_data[0]); + uint32_t size_c = SafeInt(sizes_data[1]); + if (size_n != input_shape[0] || size_c != input_shape[1]) { + LOGS_DEFAULT(VERBOSE) << "Output sizes of N/C chanel should match the input sizes, " + << "Resize of N/C channels are not supported" + << ", input_size_n, " << input_shape[0] << ", output_size_n, " << size_n + << ". input_size_c, " << input_shape[1] << ", output_size_c, " << size_c; + return false; + } + } + } + return true; +} + +Status ResizeOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node) { + auto& shaper(model_builder.GetShaper()); + const auto& operand_indices(model_builder.GetOperandIndices()); + const auto& operand_types(model_builder.GetOperandTypes()); + const auto& initializers(model_builder.GetInitializerTensors()); + NodeAttrHelper helper(node); + const auto input_defs = node.InputDefs(); + const auto android_sdk_ver = model_builder.GetAndroidSdkVer(); + const auto& output = node.OutputDefs()[0]->Name(); + + auto input = input_defs[0]->Name(); + bool use_nchw = model_builder.UseNCHW(); + bool input_is_nhwc = model_builder.IsOperandNHWC(input); + bool output_is_nhwc = false; + if (use_nchw) { + ORT_RETURN_IF_NOT(!input_is_nhwc, "model_builder.UseNCHW() but input is NHWC"); + } else { + output_is_nhwc = true; + if (!input_is_nhwc) { + const auto& nchw_input = input_defs[0]->Name(); + if (!model_builder.GetNHWCOperand(nchw_input, input)) { + input = model_builder.GetUniqueName(nchw_input + "_nchw_to_nhwc"); + ORT_RETURN_IF_ERROR(TransposeNCHWToNHWC(model_builder, nchw_input, input)); + } + } + } + + // TODO, add support for nearest neighbor + int32_t operationCode = ANEURALNETWORKS_RESIZE_BILINEAR; + + const auto coord_trans_mode = helper.Get("coordinate_transformation_mode", "half_pixel"); + bool using_half_pixel = coord_trans_mode == "half_pixel"; + bool using_align_corners = coord_trans_mode == "align_corners"; + + if (input_defs.size() == 3) { // we are using scales + const auto& scales_name = input_defs[2]->Name(); + const auto& scales_tensor = initializers.at(scales_name); + const float* scales_data = GetTensorFloatData(scales_tensor); + float scale_h = scales_data[2]; + float scale_w = scales_data[3]; + ORT_RETURN_IF_ERROR( + shaper.ResizeUsingScales(input, scale_h, scale_w, use_nchw, output)); + } else { // we are using sizes + const auto& sizes_name = input_defs[3]->Name(); + const auto& sizes_tensor = initializers.at(sizes_name); + const int64_t* sizes_data = GetTensorInt64Data(sizes_tensor); + ORT_RETURN_IF_ERROR( + shaper.ResizeUsingOutputSizes(input, SafeInt(sizes_data[2]), SafeInt(sizes_data[3]), use_nchw, output)); + } + + const auto& output_shape = shaper[output]; + int32_t output_h = use_nchw ? output_shape[2] : output_shape[1]; + int32_t output_w = use_nchw ? output_shape[3] : output_shape[2]; + + std::vector input_indices; + input_indices.push_back(operand_indices.at(input)); + ADD_SCALAR_OPERAND(model_builder, input_indices, output_w); + ADD_SCALAR_OPERAND(model_builder, input_indices, output_h); + + if (android_sdk_ver > 28) { + // using nchw is only available on API level 29 + ADD_SCALAR_OPERAND(model_builder, input_indices, use_nchw); + } + + if (android_sdk_ver > 29 && (using_align_corners || using_half_pixel)) { + ADD_SCALAR_OPERAND(model_builder, input_indices, using_align_corners); + if (using_half_pixel) + ADD_SCALAR_OPERAND(model_builder, input_indices, using_half_pixel); + } + + const OperandType output_operand_type(operand_types.at(input).type, output_shape); + ORT_RETURN_IF_ERROR(model_builder.AddOperation(operationCode, input_indices, + {output}, {output_operand_type}, {output_is_nhwc})); + + return Status::OK(); +} + +#pragma endregion + #pragma region CreateOpBuilders std::unordered_map> @@ -2736,6 +3039,8 @@ CreateOpBuilders() { op_map.emplace("QuantizeLinear", std::make_shared()); op_map.emplace("DequantizeLinear", std::make_shared()); op_map.emplace("LRN", std::make_shared()); + op_map.emplace("Clip", std::make_shared()); + op_map.emplace("Resize", std::make_shared()); return op_map; } diff --git a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.cc b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.cc index b052e90bb4..cbf2300ef6 100644 --- a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.cc +++ b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.cc @@ -130,6 +130,20 @@ Status Shaper::Squeeze(const std::string& input_name, SHAPER_FUNC(Squeeze, input_name, axes, output_name); } +Status Shaper::ResizeUsingScales(const std::string& input_name, + const float scale_h, const float scale_w, + bool nchw, + const std::string& output_name) { + SHAPER_FUNC(ResizeUsingScales, input_name, scale_h, scale_w, nchw, output_name); +} + +Status Shaper::ResizeUsingOutputSizes(const std::string& input_name, + const uint32_t output_h, const uint32_t output_w, + bool nchw, + const std::string& output_name) { + SHAPER_FUNC(ResizeUsingOutputSizes, input_name, output_h, output_w, nchw, output_name); +} + #undef SHAPER_FUNC Status Shaper::ConvImpl(const std::string& input_name, @@ -384,6 +398,38 @@ Status Shaper::SqueezeImpl(const std::string& input_name, return Status::OK(); } +Status Shaper::ResizeUsingScalesImpl(const std::string& input_name, + const float scale_h, const float scale_w, + bool nchw, + const std::string& output_name) { + Shape output_dimen = shape_map_.at(input_name); + if (nchw) { + output_dimen[2] *= scale_h; + output_dimen[3] *= scale_w; + } else { // nhwc + output_dimen[1] *= scale_h; + output_dimen[2] *= scale_w; + } + shape_map_[output_name] = output_dimen; + return Status::OK(); +} + +Status Shaper::ResizeUsingOutputSizesImpl(const std::string& input_name, + const uint32_t output_h, const uint32_t output_w, + bool nchw, + const std::string& output_name) { + Shape output_dimen = shape_map_.at(input_name); + if (nchw) { + output_dimen[2] = output_h; + output_dimen[3] = output_w; + } else { // nhwc + output_dimen[1] = output_h; + output_dimen[2] = output_w; + } + shape_map_[output_name] = output_dimen; + return Status::OK(); +} + void Shaper::AddShape(const std::string& name, const Shape& shape) { shape_map_[name] = shape; } diff --git a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.h b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.h index 4b88be2fa0..07fbd36ed1 100644 --- a/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.h +++ b/onnxruntime/core/providers/nnapi/nnapi_builtin/builders/shaper.h @@ -57,9 +57,18 @@ class Shaper { Status Concat(const std::vector& input_names, const int32_t axis, const std::string& output_name) ORT_MUST_USE_RESULT; - Status Squeeze(const std::string& input, const std::vector& axes, const std::string& output) + Status Squeeze(const std::string& input_name, const std::vector& axes, const std::string& output_name) ORT_MUST_USE_RESULT; + Status ResizeUsingScales(const std::string& input_name, + const float scale_h, const float scale_w, + bool nchw, + const std::string& output_name) ORT_MUST_USE_RESULT; + Status ResizeUsingOutputSizes(const std::string& input_name, + const uint32_t output_h, const uint32_t output_w, + bool nchw, + const std::string& output_name) ORT_MUST_USE_RESULT; + // If the shape of certain input is dynamic // Use the following 2 functions to update the particular shape // and calculate the new output shape @@ -104,8 +113,16 @@ class Shaper { ORT_MUST_USE_RESULT; Status ConcatImpl(const std::vector& input_names, const int32_t axis, const std::string& output_name) ORT_MUST_USE_RESULT; - Status SqueezeImpl(const std::string& input, const std::vector& axes, const std::string& output) + Status SqueezeImpl(const std::string& input_names, const std::vector& axes, const std::string& output_name) ORT_MUST_USE_RESULT; + Status ResizeUsingScalesImpl(const std::string& input_name, + const float scale_h, const float scale_w, + bool nchw, + const std::string& output_name) ORT_MUST_USE_RESULT; + Status ResizeUsingOutputSizesImpl(const std::string& input_name, + const uint32_t output_h, const uint32_t output_w, + bool nchw, + const std::string& output_name) ORT_MUST_USE_RESULT; std::unordered_map shape_map_; std::vector> shape_ops_; diff --git a/onnxruntime/core/providers/nnapi/nnapi_builtin/nnapi_execution_provider.cc b/onnxruntime/core/providers/nnapi/nnapi_builtin/nnapi_execution_provider.cc index 4416c0b4c4..f25839b6a5 100644 --- a/onnxruntime/core/providers/nnapi/nnapi_builtin/nnapi_execution_provider.cc +++ b/onnxruntime/core/providers/nnapi/nnapi_builtin/nnapi_execution_provider.cc @@ -303,8 +303,12 @@ common::Status NnapiExecutionProvider::Compile(const std::vector dims{3, 3}; test.AddInput("X", dims, + {11, 4, 127, + -1, 3, 64, + -5, 9, 82}); + test.AddOutput("Y", dims, {11, 4, 127, -1, 3, 64, -5, 9, 82}); - test.AddOutput("Y", dims, - {11, 4, 127, - -1, 3, 64, - -5, 9, 82}); // TensorRT, nGraph does not support Clip opset 12 yet. test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kNGraphExecutionProvider}); @@ -72,13 +72,13 @@ TEST(MathOpTest, Clip_Default_uint8) { std::vector dims{3, 3}; test.AddInput("X", dims, - {11, 4, 255, - 1, 3, 64, - 5, 9, 82}); - test.AddOutput("Y", dims, {11, 4, 255, 1, 3, 64, 5, 9, 82}); + test.AddOutput("Y", dims, + {11, 4, 255, + 1, 3, 64, + 5, 9, 82}); // TensorRT, nGraph does not support Clip opset 12 yet. test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kNGraphExecutionProvider}); @@ -89,53 +89,110 @@ TEST(MathOpTest, Clip_Default_int64) { std::vector dims{3, 3}; test.AddInput("X", dims, - {11, 4, 432, - -1, 3, 64, - -5, 9, 82}); - test.AddOutput("Y", dims, {11, 4, 432, -1, 3, 64, -5, 9, 82}); + test.AddOutput("Y", dims, + {11, 4, 432, + -1, 3, 64, + -5, 9, 82}); // TensorRT, nGraph does not support Clip opset 12 yet. test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kNGraphExecutionProvider}); } - TEST(MathOpTest, Clip_Default_uint64) { OpTester test("Clip", 12); std::vector dims{3, 3}; test.AddInput("X", dims, - {11, 4, 432, - 1, 3, 64, - 5, 9, 82}); - test.AddOutput("Y", dims, {11, 4, 432, 1, 3, 64, 5, 9, 82}); + test.AddOutput("Y", dims, + {11, 4, 432, + 1, 3, 64, + 5, 9, 82}); // TensorRT, nGraph does not support Clip opset 12 yet. test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider, kNGraphExecutionProvider}); } TEST(MathOpTest, Clip) { - OpTester test("Clip", 11); + // To test NNAPI EP, we need the min/max to be in initializers + auto run_test = [](bool min_max_are_initializer) { + OpTester test("Clip", 11); - std::vector dims{3, 3}; - test.AddInput("X", dims, - {-1.0f, 0.0f, 1.0f, - -6.0f, 0.0f, 6.0f, - -5.4f, 2.0f, 6.0f}); - test.AddInput("min", {}, {-5}); - test.AddInput("max", {}, {5}); - test.AddOutput("Y", dims, - {-1.0f, 0.0f, 1.0f, - -5.0f, 0.0f, 5.0f, - -5.0f, 2.0f, 5.0f}); + std::vector dims{3, 3}; + test.AddInput("X", dims, + {-1.0f, 0.0f, 1.0f, + -6.0f, 0.0f, 6.0f, + -5.4f, 2.0f, 6.0f}); + test.AddInput("min", {}, {-5}, min_max_are_initializer); + test.AddInput("max", {}, {5}, min_max_are_initializer); + test.AddOutput("Y", dims, + {-1.0f, 0.0f, 1.0f, + -5.0f, 0.0f, 5.0f, + -5.0f, 2.0f, 5.0f}); - // TensorRT, nGraph and Tensorrt does not support Clip opset 11 yet. - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kNGraphExecutionProvider, kTensorrtExecutionProvider}); + // TensorRT, nGraph and Tensorrt does not support Clip opset 11 yet. + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kNGraphExecutionProvider, kTensorrtExecutionProvider}); + }; + + run_test(false); + run_test(true); +} + +// Use clip between [0, 6] as Relu6 (for some EPs, such as NNAPI) +TEST(MathOpTest, Clip_Relu6) { + // To test NNAPI EP, we need the min/max to be in initializers + auto run_test = [](bool min_max_are_initializer) { + OpTester test("Clip", 11); + + std::vector dims{3, 3}; + test.AddInput("X", dims, + {-1.0f, 0.0f, 1.0f, + -6.0f, 3.5f, 6.0f, + -5.4f, 2.0f, 8.0f}); + test.AddInput("min", {}, {0.0f}, min_max_are_initializer); + test.AddInput("max", {}, {6.0f}, min_max_are_initializer); + test.AddOutput("Y", dims, + {0.0f, 0.0f, 1.0f, + 0.0f, 3.5f, 6.0f, + 0.0f, 2.0f, 6.0f}); + + // TensorRT, nGraph and Tensorrt does not support Clip opset 11 yet. + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kNGraphExecutionProvider, kTensorrtExecutionProvider}); + }; + + run_test(false); + run_test(true); +} + +// Use clip between [-1, 1] as Relu1 (for some EPs, such as NNAPI) +TEST(MathOpTest, Clip_Relu1) { + // To test NNAPI EP, we need the min/max to be in initializers + auto run_test = [](bool min_max_are_initializer) { + OpTester test("Clip", 11); + + std::vector dims{3, 3}; + test.AddInput("X", dims, + {-1.0f, 0.0f, 1.0f, + -6.0f, 3.5f, 6.0f, + -5.4f, 2.0f, 8.0f}); + test.AddInput("min", {}, {-1.0f}, min_max_are_initializer); + test.AddInput("max", {}, {1.0f}, min_max_are_initializer); + test.AddOutput("Y", dims, + {-1.0f, 0.0f, 1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 1.0f}); + + // TensorRT, nGraph and Tensorrt does not support Clip opset 11 yet. + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kNGraphExecutionProvider, kTensorrtExecutionProvider}); + }; + + run_test(false); + run_test(true); } TEST(MathOpTest, ClipDimWithZero) { diff --git a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc index b19a5b3fc4..fc8fc5d3bd 100644 --- a/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/resize_op_test.cc @@ -87,27 +87,103 @@ TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear) { test.Run(); } +// Since NNAPI(TFLite) only using the scale calulate 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 +// 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 +TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1) { + // To test NNAPI EP, we need the sclaes/sizes to be in initializers + auto run_test = [](bool scales_in_initializer) { + OpTester test("Resize", 11); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, 0.5f, 0.5f}; + + test.AddAttribute("mode", "linear"); + + const int64_t N = 1, C = 1, H = 2, W = 4; + std::vector X = { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f}; + + test.AddInput("X", {N, C, H, W}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {4}, scales, scales_in_initializer); + + std::vector Y = {3.5f, 5.5f}; + + test.AddOutput("Y", {N, C, static_cast(H * scales[2]), static_cast(W * scales[3])}, Y); + test.Run(); + }; + + run_test(false); + run_test(true); +} + +TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear1_WithSizes) { + // To test NNAPI EP, we need the sclaes/sizes to be in initializers + auto run_test = [](bool scales_and_sizes_in_initializer) { + OpTester test("Resize", 11); + std::vector roi{}; + std::vector scales{}; + const int64_t N = 1, C = 1, H = 2, W = 4; + std::vector sizes{N, C, 1, 2}; + test.AddAttribute("mode", "linear"); + + std::vector X = { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f}; + + test.AddInput("X", {N, C, H, W}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {0}, scales, scales_and_sizes_in_initializer); + test.AddInput("sizes", {4}, sizes, scales_and_sizes_in_initializer); + + std::vector Y = {3.5f, 5.5f}; + + test.AddOutput("Y", sizes, Y); + test.Run(); + }; + + run_test(false); + run_test(true); +} + TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_4DBilinear_align_corners) { - OpTester test("Resize", 11); - std::vector roi{}; - std::vector scales{1.0f, 1.0f, 0.6f, 0.6f}; + // To test NNAPI EP, we need the sclaes/sizes to be in initializers + auto run_test = [](bool scales_in_initializer) { + OpTester test("Resize", 11); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, 0.6f, 0.6f}; - test.AddAttribute("mode", "linear"); - test.AddAttribute("coordinate_transformation_mode", "align_corners"); + test.AddAttribute("mode", "linear"); + test.AddAttribute("coordinate_transformation_mode", "align_corners"); - const int64_t N = 1, C = 1, H = 2, W = 4; - std::vector X = { - 1.0f, 2.0f, 3.0f, 4.0f, - 5.0f, 6.0f, 7.0f, 8.0f}; + const int64_t N = 1, C = 1, H = 2, W = 4; + std::vector X = { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f}; - test.AddInput("X", {N, C, H, W}, X); - test.AddInput("roi", {0}, roi); - test.AddInput("scales", {4}, scales); + test.AddInput("X", {N, C, H, W}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {4}, scales, scales_in_initializer); - std::vector Y = {1.0f, 4.0f}; + std::vector Y = {1.0f, 4.0f}; - test.AddOutput("Y", {N, C, static_cast(H * scales[2]), static_cast(W * scales[3])}, Y); - test.Run(); + test.AddOutput("Y", {N, C, static_cast(H * scales[2]), static_cast(W * scales[3])}, Y); + test.Run(); + }; + + run_test(false); + +#ifdef USE_NNAPI + // NNAPI 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, ResizeOpLinearDownSampleTest_2DBilinear_pytorch_half_pixel) { @@ -139,37 +215,43 @@ TEST(ResizeOpTest, ResizeOpLinearDownSampleTest_2DBilinear_pytorch_half_pixel) { } TEST(ResizeOpTest, ResizeOpLinearUpSampleTest_4DBilinear_asymmetric) { - OpTester test("Resize", 11); - std::vector roi{}; - std::vector scales{1.0f, 1.0f, 2.0f, 4.0f}; + // To test NNAPI EP, we need the sclaes/sizes to be in initializers + auto run_test = [](bool scales_in_initializer) { + OpTester test("Resize", 11); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, 2.0f, 4.0f}; - test.AddAttribute("mode", "linear"); - test.AddAttribute("coordinate_transformation_mode", "asymmetric"); + test.AddAttribute("mode", "linear"); + test.AddAttribute("coordinate_transformation_mode", "asymmetric"); - const int64_t N = 2, C = 1, H = 2, W = 2; - std::vector X = {1.0f, 3.0f, - 4.0f, 8.0f, + const int64_t N = 2, C = 1, H = 2, W = 2; + std::vector X = {1.0f, 3.0f, + 4.0f, 8.0f, - 6.0f, 2.0f, - 7.0f, 11.0f}; + 6.0f, 2.0f, + 7.0f, 11.0f}; - test.AddInput("X", {N, C, H, W}, X); - test.AddInput("roi", {0}, roi); - test.AddInput("scales", {4}, scales); + test.AddInput("X", {N, C, H, W}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {4}, scales, scales_in_initializer); - std::vector Y = { - 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.0f, 3.0f, 3.0f, - 2.5f, 3.25f, 4.0f, 4.75f, 5.5f, 5.5f, 5.5f, 5.5f, - 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 8.0f, 8.0f, 8.0f, - 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 8.0f, 8.0f, 8.0f, + std::vector Y = { + 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.0f, 3.0f, 3.0f, + 2.5f, 3.25f, 4.0f, 4.75f, 5.5f, 5.5f, 5.5f, 5.5f, + 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 8.0f, 8.0f, 8.0f, + 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 8.0f, 8.0f, 8.0f, - 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 2.0f, 2.0f, 2.0f, - 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, - 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 11.0f, 11.0f, 11.0f, - 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 11.0f, 11.0f, 11.0f}; + 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 2.0f, 2.0f, 2.0f, + 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, 6.5f, + 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 11.0f, 11.0f, 11.0f, + 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 11.0f, 11.0f, 11.0f}; - test.AddOutput("Y", {N, C, static_cast(H * scales[2]), static_cast(W * scales[3])}, Y); - test.Run(); + test.AddOutput("Y", {N, C, static_cast(H * scales[2]), static_cast(W * scales[3])}, Y); + test.Run(); + }; + + run_test(false); + run_test(true); } TEST(ResizeOpTest, ResizeOpLinearUpSampleTest_2DBilinear_align_corners) { @@ -257,30 +339,36 @@ TEST(ResizeOpTest, ResizeOpLinearUpSampleTest_5DTrilinear_pytorch_half_pixel) { } TEST(ResizeOpTest, ResizeOpLinearScalesNoOpTest) { - OpTester test("Resize", 11); - std::vector roi{}; - std::vector scales{1.0f, 1.0f, 1.0f, 1.0f}; - test.AddAttribute("mode", "linear"); + // To test NNAPI EP, we need the sclaes/sizes to be in initializers + auto run_test = [](bool scales_in_initializer) { + OpTester test("Resize", 11); + std::vector roi{}; + std::vector scales{1.0f, 1.0f, 1.0f, 1.0f}; + test.AddAttribute("mode", "linear"); - const int64_t N = 2, C = 1, H = 2, W = 2; - std::vector X = {1.0f, 3.0f, - 4.0f, 8.0f, + const int64_t N = 2, C = 1, H = 2, W = 2; + std::vector X = {1.0f, 3.0f, + 4.0f, 8.0f, - 6.0f, 2.0f, - 7.0f, 11.0f}; + 6.0f, 2.0f, + 7.0f, 11.0f}; - test.AddInput("X", {N, C, H, W}, X); - test.AddInput("roi", {0}, roi); - test.AddInput("scales", {4}, scales); + test.AddInput("X", {N, C, H, W}, X); + test.AddInput("roi", {0}, roi); + test.AddInput("scales", {4}, scales, scales_in_initializer); - std::vector Y = {1.0f, 3.0f, - 4.0f, 8.0f, + std::vector Y = {1.0f, 3.0f, + 4.0f, 8.0f, - 6.0f, 2.0f, - 7.0f, 11.0f}; + 6.0f, 2.0f, + 7.0f, 11.0f}; - test.AddOutput("Y", {N, C, H, W}, Y); - test.Run(); + test.AddOutput("Y", {N, C, H, W}, Y); + test.Run(); + }; + + run_test(false); + run_test(true); } TEST(ResizeOpTest, ResizeOpNearestDownSampleTest) {