[QNN EP] Support AveragePool operator (#15419)

### Description
Adds support for the AveragePool operator to QNN EP.

### Motivation and Context
This is needed to enable more models to run with QNN EP.
This commit is contained in:
Adrian Lizarraga 2023-04-07 10:09:55 -07:00 committed by GitHub
parent 139f3df4d2
commit c294040bac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 297 additions and 13 deletions

View file

@ -71,6 +71,7 @@ OpBuilderRegistrations::OpBuilderRegistrations() {
{
CreatePoolOpBuilder("GlobalAveragePool", *this);
CreatePoolOpBuilder("AveragePool", *this);
CreatePoolOpBuilder("MaxPool", *this);
}

View file

@ -143,6 +143,7 @@ class BaseOpBuilder : public IOpBuilder {
{"Conv", "Conv2d"},
{"GlobalAveragePool", "PoolAvg2d"},
{"AveragePool", "PoolAvg2d"},
{"MaxPool", "PoolMax2d"},
{"Reshape", "Reshape"},

View file

@ -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<uint32_t>& filter_size,
std::vector<uint32_t>& pad_amount, std::vector<uint32_t>& stride,
int32_t& ceil_mode,
std::vector<uint32_t>&& input_shape,
std::vector<uint32_t>&& output_shape) const;
Status SetCommonPoolParams(const NodeAttrHelper& node_helper, std::vector<uint32_t>& filter_size,
std::vector<uint32_t>& pad_amount, std::vector<uint32_t>& stride,
int32_t& ceil_mode,
std::vector<uint32_t>&& input_shape,
std::vector<uint32_t>&& 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<uint32_t>& filter_size,
std::vector<uint32_t>& pad_amount, std::vector<uint32_t>& stride,
int32_t& ceil_mode,
std::vector<uint32_t>&& input_shape,
std::vector<uint32_t>&& output_shape) const {
Status PoolOpBuilder::SetCommonPoolParams(const NodeAttrHelper& node_helper,
std::vector<uint32_t>& filter_size,
std::vector<uint32_t>& pad_amount, std::vector<uint32_t>& stride,
int32_t& ceil_mode,
std::vector<uint32_t>&& input_shape,
std::vector<uint32_t>&& output_shape) const {
auto kernel_shape = node_helper.Get("kernel_shape", std::vector<int32_t>{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<uint32_t> pad_amount{0, 0, 0, 0};
std::vector<uint32_t> 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<uint32_t> 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<std::string> 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<uint8_t>(node_helper.Get("count_include_pad", static_cast<int64_t>(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),

View file

@ -0,0 +1,270 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#if !defined(ORT_MINIMAL_BUILD)
#include <string>
#include <unordered_map>
#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<int64_t>& shape,
const std::vector<int64_t>& kernel_shape,
const std::vector<int64_t>& strides,
const std::vector<int64_t>& 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<float>(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 <typename QuantType>
GetQDQTestCaseFn BuildAveragePoolQDQTestCase(const std::vector<int64_t>& shape,
const std::vector<int64_t>& kernel_shape,
const std::vector<int64_t>& strides,
const std::vector<int64_t>& 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<QuantType>::max() / 2;
QuantType pool_output_zp = std::numeric_limits<QuantType>::max() / 2;
QuantType q_zp = std::numeric_limits<QuantType>::max() / 2;
auto* input_arg = builder.MakeInput<float>(shape, -1.0f, 1.0f);
auto* output_arg = builder.MakeOutput();
// add QDQ + AveragePool
auto* dq_output = AddQDQNodePair<QuantType>(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<QuantType>(averagepool_output,
pool_output_scale,
pool_output_zp,
q_output);
builder.AddDequantizeLinearNode<QuantType>(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<int64_t>& shape,
const std::vector<int64_t>& kernel_shape,
const std::vector<int64_t>& strides,
const std::vector<int64_t>& 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 <typename QuantType>
static void RunQDQAveragePoolOpTest(const std::vector<int64_t>& shape,
const std::vector<int64_t>& kernel_shape,
const std::vector<int64_t>& strides,
const std::vector<int64_t>& 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<QuantType>(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<uint8_t>({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<uint8_t>({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<uint8_t>({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<uint8_t>({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)