Skip to content

Commit 15efe1c

Browse files
authored
[QNN-EP] Support pad op pre-opset11 (microsoft#26248)
### Description - Support pre-opset11 `Pad` in the QNN op builder. - Add GPU backend tests for `Pad`. ### Motivation and Context - Enables `Pad` translation in models using older opsets.
1 parent a5831a3 commit 15efe1c

3 files changed

Lines changed: 252 additions & 46 deletions

File tree

onnxruntime/core/providers/qnn/builder/opbuilder/pad_op_builder.cc

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,43 @@ Status PadOpBuilder::ProcessInputs(QnnModelWrapper& qnn_model_wrapper,
3333
std::vector<std::string>& input_names,
3434
bool do_op_validation) const {
3535
const auto& inputs = node_unit.Inputs();
36-
// QNN Pad only has 1 input, the pads input & constant_value input need to be initializer and set as Qnn node parameter, axes input is not supported.
36+
// QNN Pad only has 1 input, the pads input & constant_value input need to be initializer (opset >= 11) and set as Qnn node
37+
// parameter, axes input is not supported.
3738
if (do_op_validation) {
38-
ORT_RETURN_IF(inputs.size() > 3, "QNN Pad doesn't support axes.");
39-
ORT_RETURN_IF(inputs.size() < 2, "QNN Pad requires the pads input.");
39+
const int opset_version = node_unit.SinceVersion();
40+
const std::string domain = node_unit.Domain();
41+
42+
if (domain == kMSDomain) {
43+
// Pad in the com.microsoft domain accepts 2-3 inputs (data, pads, value).
44+
ORT_RETURN_IF(inputs.size() < 2, "QNN Pad requires the pads input.");
45+
} else {
46+
// Pad in the ONNX domain accepts only 1 input (data) before opset 11.
47+
// For opset 11 and after, it accepts 2-4 inputs (data, pads, constant_value, axes), although QNN pad
48+
// does not support the axes input.
49+
50+
// Reject Pad opset 1, which differs slightly from Pad for 2 <= opset < 11.
51+
// We could support it, but nodes below opset 7 should be rejected earlier by ORT, anyway.
52+
ORT_RETURN_IF(opset_version < 2, "Pad with opset < 2 is not supported");
53+
54+
ORT_RETURN_IF(opset_version < 11 && inputs.size() > 1,
55+
"Pads should be specified in an attribute for opset < 11");
56+
ORT_RETURN_IF(opset_version >= 11 && inputs.size() > 3, "QNN Pad doesn't support axes.");
57+
ORT_RETURN_IF(opset_version >= 11 && inputs.size() < 2, "QNN Pad requires the pads input.");
58+
}
4059

4160
std::vector<uint32_t> input_shape;
4261
ORT_RETURN_IF_NOT(qnn_model_wrapper.GetOnnxShape(inputs[0].node_arg, input_shape), "Cannot get shape of input 0.");
43-
ORT_RETURN_IF(input_shape.size() > 5, "QNN Pad doesn't support more than 5 dimension");
44-
45-
auto& pads_input_name = inputs[1].node_arg.Name();
46-
ORT_RETURN_IF_NOT(qnn_model_wrapper.IsConstantInput(pads_input_name),
47-
"Qnn doesn't support dynamic pad input");
48-
if (inputs.size() > 2 && inputs[2].node_arg.Exists()) {
49-
auto& constant_value_input_name = inputs[2].node_arg.Name();
50-
ORT_RETURN_IF_NOT(qnn_model_wrapper.IsConstantInput(constant_value_input_name),
51-
"Qnn doesn't support dynamic constant_value input");
62+
ORT_RETURN_IF(input_shape.size() > 5, "QNN Pad doesn't support more than 5 dimensions");
63+
64+
if (opset_version >= 11 || domain == kMSDomain) {
65+
auto& pads_input_name = inputs[1].node_arg.Name();
66+
ORT_RETURN_IF_NOT(qnn_model_wrapper.IsConstantInput(pads_input_name),
67+
"Qnn doesn't support dynamic pad input");
68+
if (inputs.size() > 2 && inputs[2].node_arg.Exists()) {
69+
auto& constant_value_input_name = inputs[2].node_arg.Name();
70+
ORT_RETURN_IF_NOT(qnn_model_wrapper.IsConstantInput(constant_value_input_name),
71+
"Qnn doesn't support dynamic constant_value input");
72+
}
5273
}
5374
}
5475

@@ -175,18 +196,35 @@ Status PadOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wrap
175196
const logging::Logger& logger,
176197
bool do_op_validation) const {
177198
std::vector<std::string> param_tensor_names;
178-
// Process pads input
179-
// Already confirmed pads input is initializer in ProcessInputs()
199+
const int opset_version = node_unit.SinceVersion();
200+
const std::string domain = node_unit.Domain();
201+
180202
const auto& inputs = node_unit.Inputs();
181-
const auto& pads_input_name = inputs[1].node_arg.Name();
203+
NodeAttrHelper node_helper(node_unit);
204+
205+
const int64_t* tensor_data = nullptr;
206+
size_t size = 0;
182207

208+
const auto pads_attr = node_helper.GetInt64s("pads");
183209
std::vector<uint8_t> unpacked_tensor;
184-
const auto& input_tensor = qnn_model_wrapper.GetConstantTensor(pads_input_name);
185-
ORT_RETURN_IF_ERROR(qnn_model_wrapper.UnpackInitializerData(*input_tensor, unpacked_tensor));
186-
// Onnx Pads are int64, Qnn use uint32
187-
const int64_t* tensor_data = reinterpret_cast<const int64_t*>(unpacked_tensor.data());
188-
size_t tensor_byte_size = unpacked_tensor.size();
189-
size_t size = tensor_byte_size / sizeof(int64_t);
210+
211+
if (opset_version < 11 && domain != kMSDomain) {
212+
// Process pads attribute
213+
// NodeAttrHelper::GetInt64s returns an allocated copy, not a view, so we must call it in the outer scope
214+
ORT_RETURN_IF_NOT(pads_attr.has_value(), "Failed to get pads attribute.");
215+
tensor_data = pads_attr.value().data();
216+
size = pads_attr.value().size();
217+
} else {
218+
// Process pads input
219+
// Already confirmed pads input is initializer in ProcessInputs()
220+
const auto& pads_input_name = inputs[1].node_arg.Name();
221+
222+
const auto& input_tensor = qnn_model_wrapper.GetConstantTensor(pads_input_name);
223+
ORT_RETURN_IF_ERROR(qnn_model_wrapper.UnpackInitializerData(*input_tensor, unpacked_tensor));
224+
tensor_data = reinterpret_cast<const int64_t*>(unpacked_tensor.data());
225+
size_t tensor_byte_size = unpacked_tensor.size();
226+
size = tensor_byte_size / sizeof(int64_t);
227+
}
190228

191229
bool has_negative = std::any_of(tensor_data, tensor_data + size, [](int64_t item) { return item < 0; });
192230
bool has_positive = std::any_of(tensor_data, tensor_data + size, [](int64_t item) { return item > 0; });
@@ -197,6 +235,7 @@ Status PadOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wrap
197235
return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Got QNN invalid zero only padding value.");
198236
}
199237

238+
// Onnx Pads are int64, Qnn uses uint32
200239
std::vector<uint32_t> pad_amount;
201240
std::transform(tensor_data, tensor_data + size, std::back_inserter(pad_amount),
202241
[](int64_t item) { return item < 0 ? SafeInt<uint32_t>(0) : SafeInt<uint32_t>(item); });
@@ -208,7 +247,6 @@ Status PadOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wrap
208247
std::vector<uint32_t> input_shape;
209248
ORT_RETURN_IF_NOT(qnn_model_wrapper.GetOnnxShape(inputs[0].node_arg, input_shape), "Cannot get shape of input 0.");
210249

211-
NodeAttrHelper node_helper(node_unit);
212250
std::string mode = node_helper.Get("mode", "constant");
213251

214252
if ("reflect" == mode && has_negative) {
@@ -241,10 +279,21 @@ Status PadOpBuilder::ProcessAttributesAndOutputs(QnnModelWrapper& qnn_model_wrap
241279
param_tensor_names.push_back(pad_amount_param.GetParamTensorName());
242280
qnn_model_wrapper.AddParamWrapper(std::move(pad_amount_param));
243281

244-
// Process optional input constant_value
245-
if (inputs.size() > 2 && inputs[2].node_arg.Exists()) {
282+
if (opset_version < 11 && domain != kMSDomain && node_helper.HasAttr("value")) {
283+
// Process optional attribute value
284+
Qnn_Scalar_t constant_value_qnn_scalar = QNN_SCALAR_INIT;
285+
constant_value_qnn_scalar.dataType = QNN_DATATYPE_FLOAT_32;
286+
constant_value_qnn_scalar.floatValue = node_helper.GetFloat("value").value();
287+
QnnParamWrapper constant_value_param(node_unit.Index(),
288+
node_unit.Name(),
289+
QNN_OP_PAD_PARAM_PAD_CONSTANT_VALUE,
290+
constant_value_qnn_scalar);
291+
param_tensor_names.push_back(constant_value_param.GetParamTensorName());
292+
qnn_model_wrapper.AddParamWrapper(std::move(constant_value_param));
293+
} else if ((opset_version >= 11 || domain == kMSDomain) && inputs.size() > 2 && inputs[2].node_arg.Exists()) {
294+
// Process optional input constant_value
246295
ORT_RETURN_IF_ERROR(ProcessConstantValue(qnn_model_wrapper, param_tensor_names, node_unit, inputs[2]));
247-
} // constant_value
296+
}
248297

249298
if (!has_negative) {
250299
// Non-negative pads maps directly onto QNN pad.

onnxruntime/test/providers/cpu/tensor/pad_test.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ static void RunOnnxOpsetTypedTest(
4242
if constexpr (std::is_same_v<T, int8_t>) {
4343
provider_types.insert(kTensorrtExecutionProvider);
4444
}
45+
// Exclude QNN due to a few test failures with the CPU backend.
46+
provider_types.insert(kQnnExecutionProvider);
4547
SessionOptions so;
4648
// Don't fail early on shape inference so that we can test the op's error handling.
4749
if (expect != OpTester::ExpectResult::kExpectSuccess) {
@@ -537,6 +539,7 @@ TYPED_TEST(PadOpTest, Pad_Edge_3D_Last_Slice_Inner_No_Padding) {
537539

538540
TYPED_TEST(PadOpTest, Pad_Reflect_3D_Inner_No_Padding) {
539541
using T = TypeParam;
542+
540543
RunAllOpsetAllDomainPadTests<T>({3, 2, 5},
541544
{T(1), T(2), T(3), T(4), T(5),
542545
T(6), T(7), T(8), T(9), T(10),

0 commit comments

Comments
 (0)