diff --git a/onnxruntime/core/providers/qnn/builder/op_builder_factory.cc b/onnxruntime/core/providers/qnn/builder/op_builder_factory.cc index 0462cc9ba0..881d441fde 100644 --- a/onnxruntime/core/providers/qnn/builder/op_builder_factory.cc +++ b/onnxruntime/core/providers/qnn/builder/op_builder_factory.cc @@ -71,6 +71,7 @@ OpBuilderRegistrations::OpBuilderRegistrations() { { CreatePoolOpBuilder("GlobalAveragePool", *this); + CreatePoolOpBuilder("AveragePool", *this); CreatePoolOpBuilder("MaxPool", *this); } diff --git a/onnxruntime/core/providers/qnn/builder/opbuilder/base_op_builder.h b/onnxruntime/core/providers/qnn/builder/opbuilder/base_op_builder.h index 0e548b8861..5f507aad43 100644 --- a/onnxruntime/core/providers/qnn/builder/opbuilder/base_op_builder.h +++ b/onnxruntime/core/providers/qnn/builder/opbuilder/base_op_builder.h @@ -143,6 +143,7 @@ class BaseOpBuilder : public IOpBuilder { {"Conv", "Conv2d"}, {"GlobalAveragePool", "PoolAvg2d"}, + {"AveragePool", "PoolAvg2d"}, {"MaxPool", "PoolMax2d"}, {"Reshape", "Reshape"}, diff --git a/onnxruntime/core/providers/qnn/builder/opbuilder/pool_op_builder.cc b/onnxruntime/core/providers/qnn/builder/opbuilder/pool_op_builder.cc index e713defde8..6f8b379184 100644 --- a/onnxruntime/core/providers/qnn/builder/opbuilder/pool_op_builder.cc +++ b/onnxruntime/core/providers/qnn/builder/opbuilder/pool_op_builder.cc @@ -33,11 +33,11 @@ class PoolOpBuilder : public BaseOpBuilder { bool do_op_validation) const override ORT_MUST_USE_RESULT; private: - Status SetParamForMaxPool(const NodeAttrHelper& node_helper, std::vector& filter_size, - std::vector& pad_amount, std::vector& stride, - int32_t& ceil_mode, - std::vector&& input_shape, - std::vector&& output_shape) const; + Status SetCommonPoolParams(const NodeAttrHelper& node_helper, std::vector& filter_size, + std::vector& pad_amount, std::vector& stride, + int32_t& ceil_mode, + std::vector&& input_shape, + std::vector&& output_shape) const; }; // Pool ops are sensitive with data layout, no special validation so far @@ -76,12 +76,12 @@ Status PoolOpBuilder::IsOpSupported(QnnModelWrapper& qnn_model_wrapper, return Status::OK(); } -Status PoolOpBuilder::SetParamForMaxPool(const NodeAttrHelper& node_helper, - std::vector& filter_size, - std::vector& pad_amount, std::vector& stride, - int32_t& ceil_mode, - std::vector&& input_shape, - std::vector&& output_shape) const { +Status PoolOpBuilder::SetCommonPoolParams(const NodeAttrHelper& node_helper, + std::vector& filter_size, + std::vector& pad_amount, std::vector& stride, + int32_t& ceil_mode, + std::vector&& input_shape, + std::vector&& output_shape) const { auto kernel_shape = node_helper.Get("kernel_shape", std::vector{1, 1}); ORT_RETURN_IF_NOT(kernel_shape.size() == 2, "QNN only support kernel_shape with shape[2]."); filter_size.clear(); @@ -149,12 +149,13 @@ Status PoolOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wra std::vector pad_amount{0, 0, 0, 0}; std::vector pad_amount_dim{2, 2}; int32_t ceil_mode = 0; - if (node_unit.OpType() == "MaxPool") { + if (node_unit.OpType() == "MaxPool" || node_unit.OpType() == "AveragePool") { const auto& outputs = node_unit.Outputs(); std::vector output_shape; ORT_RETURN_IF_NOT(qnn_model_wrapper.GetOnnxShape(outputs[0].node_arg, output_shape), "Cannot get shape"); - ORT_RETURN_IF_ERROR(SetParamForMaxPool(node_helper, filter_size, pad_amount, stride, ceil_mode, std::move(input_shape), std::move(output_shape))); + ORT_RETURN_IF_ERROR(SetCommonPoolParams(node_helper, filter_size, pad_amount, stride, ceil_mode, + std::move(input_shape), std::move(output_shape))); } std::vector param_tensor_names; @@ -202,7 +203,18 @@ Status PoolOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wra scalar_param); param_tensor_names.push_back(count_pad_for_edges_param.GetParamTensorName()); qnn_model_wrapper.AddParamWrapper(std::move(count_pad_for_edges_param)); + } else if (node_unit.OpType() == "AveragePool") { + Qnn_Scalar_t scalar_param = QNN_SCALAR_INIT; + scalar_param.dataType = QNN_DATATYPE_BOOL_8; + scalar_param.bool8Value = static_cast(node_helper.Get("count_include_pad", static_cast(0)) != 0); + QnnParamWrapper count_pad_for_edges_param(node_unit.Index(), + node_unit.Name(), + qnn_def::count_pad_for_edges, + scalar_param); + param_tensor_names.push_back(count_pad_for_edges_param.GetParamTensorName()); + qnn_model_wrapper.AddParamWrapper(std::move(count_pad_for_edges_param)); } + output_count_ = 1; ORT_RETURN_IF_ERROR(ProcessOutputs(qnn_model_wrapper, node_unit, std::move(input_names), diff --git a/onnxruntime/test/providers/qnn/average_pool_test.cc b/onnxruntime/test/providers/qnn/average_pool_test.cc new file mode 100644 index 0000000000..c31356611d --- /dev/null +++ b/onnxruntime/test/providers/qnn/average_pool_test.cc @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if !defined(ORT_MINIMAL_BUILD) + +#include +#include + +#include "test/optimizer/qdq_test_utils.h" +#include "test/providers/qnn/qnn_test_utils.h" + +#include "onnx/onnx_pb.h" + +#include "gtest/gtest.h" + +namespace onnxruntime { +namespace test { + +// Returns a function that creates a graph with a single AveragePool operator. +static GetTestModelFn BuildAveragePoolTestCase(const std::vector& shape, + const std::vector& kernel_shape, + const std::vector& strides, + const std::vector& pads, + int64_t count_include_pad, + const std::string& auto_pad = "NOTSET") { + return [shape, kernel_shape, strides, pads, + count_include_pad, auto_pad](ModelTestBuilder& builder) { + + // Random input data + auto input = builder.MakeInput(shape, 0.0f, 10.0f); + + auto* output = builder.MakeOutput(); + Node& pool_node = builder.AddNode("AveragePool", {input}, {output}); + + pool_node.AddAttribute("kernel_shape", kernel_shape); + + if (!strides.empty()) { + pool_node.AddAttribute("strides", strides); + } + + pool_node.AddAttribute("auto_pad", auto_pad); + + if (!pads.empty() && auto_pad == "NOTSET") { + pool_node.AddAttribute("pads", pads); + } + + if (count_include_pad > 0) { + pool_node.AddAttribute("count_include_pad", count_include_pad); + } + }; +} + +// Returns a function that creates a graph with a QDQ AveragePool operator. +template +GetQDQTestCaseFn BuildAveragePoolQDQTestCase(const std::vector& shape, + const std::vector& kernel_shape, + const std::vector& strides, + const std::vector& pads, + int64_t count_include_pad, + const std::string& auto_pad = "NOTSET") { + return [shape, kernel_shape, strides, pads, + count_include_pad, auto_pad](ModelTestBuilder& builder) { + + float dq_scale = 0.0038f; + float pool_output_scale = 0.0038f; + float q_scale = 0.0038f; + QuantType dq_zp = std::numeric_limits::max() / 2; + QuantType pool_output_zp = std::numeric_limits::max() / 2; + QuantType q_zp = std::numeric_limits::max() / 2; + + auto* input_arg = builder.MakeInput(shape, -1.0f, 1.0f); + auto* output_arg = builder.MakeOutput(); + + // add QDQ + AveragePool + auto* dq_output = AddQDQNodePair(builder, input_arg, dq_scale, dq_zp); + auto* averagepool_output = builder.MakeIntermediate(); + Node& pool_node = builder.AddNode("AveragePool", {dq_output}, {averagepool_output}); + + pool_node.AddAttribute("kernel_shape", kernel_shape); + + if (!strides.empty()) { + pool_node.AddAttribute("strides", strides); + } + + pool_node.AddAttribute("auto_pad", auto_pad); + + if (!pads.empty() && auto_pad == "NOTSET") { + pool_node.AddAttribute("pads", pads); + } + + if (count_include_pad > 0) { + pool_node.AddAttribute("count_include_pad", count_include_pad); + } + + // add QDQ output + auto* q_output = builder.MakeIntermediate(); + builder.AddQuantizeLinearNode(averagepool_output, + pool_output_scale, + pool_output_zp, + q_output); + builder.AddDequantizeLinearNode(q_output, + q_scale, + q_zp, + output_arg); + }; +} + + +// Runs an AveragePool model on the QNN CPU backend. Checks the graph node assignment, and that inference +// outputs for QNN and CPU match. +static void RunAveragePoolOpTest(const std::vector& shape, + const std::vector& kernel_shape, + const std::vector& strides, + const std::vector& pads, + int64_t count_include_pad, + const std::string& auto_pad, + ExpectedEPNodeAssignment expected_ep_assignment, const char* test_description, + int opset = 18) { + ProviderOptions provider_options; +#if defined(_WIN32) + provider_options["backend_path"] = "QnnCpu.dll"; +#else + provider_options["backend_path"] = "libQnnCpu.so"; +#endif + + constexpr int expected_nodes_in_partition = 1; + RunQnnModelTest(BuildAveragePoolTestCase(shape, kernel_shape, strides, pads, count_include_pad, auto_pad), + provider_options, + opset, + expected_ep_assignment, + expected_nodes_in_partition, + test_description); +} + +// Runs a QDQ AveragePool model on the QNN HTP backend. Checks the graph node assignment, and that inference +// outputs for QNN and CPU match. +template +static void RunQDQAveragePoolOpTest(const std::vector& shape, + const std::vector& kernel_shape, + const std::vector& strides, + const std::vector& pads, + int64_t count_include_pad, + const std::string& auto_pad, + ExpectedEPNodeAssignment expected_ep_assignment, const char* test_description, + int opset = 18, float fp32_abs_err = 1e-5f) { + ProviderOptions provider_options; +#if defined(_WIN32) + provider_options["backend_path"] = "QnnHtp.dll"; +#else + provider_options["backend_path"] = "libQnnHtp.so"; +#endif + + constexpr int expected_nodes_in_partition = 1; + RunQnnModelTest(BuildAveragePoolQDQTestCase(shape, kernel_shape, strides, pads, count_include_pad, + auto_pad), + provider_options, + opset, + expected_ep_assignment, + expected_nodes_in_partition, + test_description, + fp32_abs_err); +} + +// +// CPU tests: +// + +// AveragePool with kernel size equal to the spatial dimension of input tensor. +TEST(QnnCPUBackendTests, TestAveragePool_Global) { + RunAveragePoolOpTest({1, 2, 3, 3}, // shape + {3, 3}, // kernel_shape + {3, 3}, // strides + {0, 0, 0, 0}, // pads + 0, // count_include_pad + "NOTSET", + ExpectedEPNodeAssignment::All, "TestAveragePool_Global"); +} + +// AveragePool that counts padding. +TEST(QnnCPUBackendTests, TestAveragePool_CountIncludePad) { + RunAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {0, 0, 0, 0}, // pads + 1, // count_include_pad + "NOTSET", + ExpectedEPNodeAssignment::All, "TestAveragePool_CountIncludePad"); +} + +// AveragePool that use auto_pad 'SAME_UPPER'. +TEST(QnnCPUBackendTests, TestAveragePool_AutopadSameUpper) { + RunAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {}, // pads + 1, // count_include_pad + "SAME_UPPER", + ExpectedEPNodeAssignment::All, "TestAveragePool_CountIncludePad"); +} + +// AveragePool that use auto_pad 'SAME_LOWER'. +TEST(QnnCPUBackendTests, TestAveragePool_AutopadSameLower) { + RunAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {}, // pads + 1, // count_include_pad + "SAME_LOWER", + ExpectedEPNodeAssignment::All, "TestAveragePool_CountIncludePad"); +} + + +#if defined(__aarch64__) || defined(_M_ARM64) || defined(__linux__) +// +// HTP tests: +// + +// QDQ AveragePool with kernel size equal to the spatial dimension of input tensor. +TEST_F(QnnHTPBackendTests, TestAveragePool_Global_HTP_u8) { + RunQDQAveragePoolOpTest({1, 2, 3, 3}, // shape + {3, 3}, // kernel_shape + {3, 3}, // strides + {0, 0, 0, 0}, // pads + 0, // count_include_pad + "NOTSET", + ExpectedEPNodeAssignment::All, "TestAveragePool_Global_HTP_u8"); +} + +// QDQ AveragePool that counts padding. +TEST_F(QnnHTPBackendTests, TestAveragePool_CountIncludePad_HTP_u8) { + RunQDQAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {0, 0, 0, 0}, // pads + 1, // count_include_pad + "NOTSET", + ExpectedEPNodeAssignment::All, "TestAveragePool_CountIncludePad_HTP_u8", + 18, 0.00381f); +} + +// QDQ AveragePool that use auto_pad 'SAME_UPPER'. +TEST_F(QnnHTPBackendTests, TestAveragePool_AutopadSameUpper_HTP_u8) { + RunQDQAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {}, // pads + 0, // count_include_pad + "SAME_UPPER", + ExpectedEPNodeAssignment::All, "TestAveragePool_AutopadSameUpper_HTP_u8", + 18, 0.00381f); +} + +// QDQ AveragePool that use auto_pad 'SAME_LOWER'. +TEST_F(QnnHTPBackendTests, TestAveragePool_AutopadSameLower_HTP_u8) { + RunQDQAveragePoolOpTest({1, 2, 3, 3}, // shape + {1, 1}, // kernel_shape + {1, 1}, // strides + {}, // pads + 0, // count_include_pad + "SAME_LOWER", + ExpectedEPNodeAssignment::All, "TestAveragePool_AutopadSameLower_HTP_u8", + 18, 0.00381f); +} + +#endif // defined(__aarch64__) || defined(_M_ARM64) || defined(__linux__) + +} // namespace test +} // namespace onnxruntime + +#endif // !defined(ORT_MINIMAL_BUILD) \ No newline at end of file