[WebNN EP] Use explicit padding (#18688)

WebNN will remove autoPad option, we need to use explicit padding
values.
Compute padding values of autopad(same-upper, same-lower) for Op Pool,
Conv and ConvTranspose.
This commit is contained in:
zesongw 2023-12-15 06:33:44 +08:00 committed by GitHub
parent 1db1c75048
commit 6d5ee4d69b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 121 deletions

View file

@ -19,9 +19,10 @@ common::Status ComputeConvPads(const std::vector<int64_t> input_shape,
const std::vector<int64_t>& onnx_strides,
const std::vector<int64_t>& onnx_dilations,
AutoPadType auto_pad_type,
std::vector<int64_t>& pads_out) {
const int64_t input_size_y = input_shape[2];
const int64_t input_size_x = input_shape[3];
std::vector<int64_t>& pads_out,
bool use_nchw) {
const int64_t input_size_y = use_nchw ? input_shape[2] : input_shape[1];
const int64_t input_size_x = use_nchw ? input_shape[3] : input_shape[2];
const int64_t stride_y = onnx_strides[0];
const int64_t stride_x = onnx_strides[1];
const int64_t dilation_y = onnx_dilations[0];
@ -53,32 +54,17 @@ common::Status HandleAutoPad(const std::vector<int64_t> input_shape,
const std::vector<int64_t>& onnx_strides,
const std::vector<int64_t>& onnx_dilations,
AutoPadType auto_pad_type,
AutoPadType& auto_pad_type_out) {
auto_pad_type_out = auto_pad_type;
if (auto_pad_type == AutoPadType::NOTSET && onnx_dilations == std::vector<int64_t>{1, 1}) {
{
std::vector<int64_t> same_upper_pads;
ORT_RETURN_IF_ERROR(ComputeConvPads(input_shape, weight_size_y, weight_size_x,
onnx_pads, onnx_strides, onnx_dilations,
AutoPadType::SAME_UPPER, same_upper_pads));
if (onnx_pads == same_upper_pads) {
auto_pad_type_out = AutoPadType::SAME_UPPER;
return Status::OK();
}
}
{
std::vector<int64_t> same_lower_pads;
ORT_RETURN_IF_ERROR(ComputeConvPads(input_shape, weight_size_y, weight_size_x,
onnx_pads, onnx_strides, onnx_dilations,
AutoPadType::SAME_LOWER, same_lower_pads));
if (onnx_pads == same_lower_pads) {
auto_pad_type_out = AutoPadType::SAME_LOWER;
return Status::OK();
}
}
std::vector<int64_t>& pads_out,
bool use_nchw) {
if (AutoPadType::SAME_UPPER == auto_pad_type) {
ORT_RETURN_IF_ERROR(ComputeConvPads(input_shape, weight_size_y, weight_size_x,
onnx_pads, onnx_strides, onnx_dilations,
AutoPadType::SAME_UPPER, pads_out, use_nchw));
} else {
ORT_RETURN_IF_ERROR(ComputeConvPads(input_shape, weight_size_y, weight_size_x,
onnx_pads, onnx_strides, onnx_dilations,
AutoPadType::SAME_LOWER, pads_out, use_nchw));
}
return Status::OK();
}

View file

@ -21,7 +21,8 @@ common::Status HandleAutoPad(const std::vector<int64_t> input_shape,
const std::vector<int64_t>& onnx_strides,
const std::vector<int64_t>& onnx_dilations,
AutoPadType auto_pad_type,
AutoPadType& auto_pad_type_out) ORT_MUST_USE_RESULT;
std::vector<int64_t>& pads_out,
bool use_nchw) ORT_MUST_USE_RESULT;
} // namespace webnn
} // namespace onnxruntime

View file

@ -44,7 +44,7 @@ common::Status SetConvBaseOptions(ModelBuilder& model_builder,
const Node& node, emscripten::val& options,
const std::vector<int32_t>& strides,
const std::vector<int32_t>& dilations,
const std::vector<int32_t>& pads,
std::vector<int32_t>& pads,
const logging::Logger& logger) {
NodeAttrHelper helper(node);
const auto group = helper.Get("group", static_cast<int32_t>(1));
@ -55,29 +55,85 @@ common::Status SetConvBaseOptions(ModelBuilder& model_builder,
options.set("dilations", emscripten::val::array(dilations));
options.set("groups", group);
// Add Padding.
// Usually using autopadding is more efficient than using explicit padding.
// Try to see if we can map explicit padding to auto padding.
std::vector<int64_t> input_shape;
ORT_RETURN_IF_NOT(GetShape(*input_defs[0], input_shape, logger), "Cannot get shape");
AutoPadType auto_pad_type;
ORT_RETURN_IF_ERROR(HandleAutoPad(input_shape, weight_shape[2], weight_shape[3],
helper.Get("pads", std::vector<int64_t>{0, 0, 0, 0}),
helper.Get("strides", std::vector<int64_t>{1, 1}),
helper.Get("dilations", std::vector<int64_t>{1, 1}),
StringToAutoPadType(helper.Get("auto_pad", "NOTSET")),
auto_pad_type));
if (AutoPadType::SAME_UPPER == auto_pad_type || AutoPadType::SAME_LOWER == auto_pad_type) {
if (AutoPadType::SAME_LOWER == auto_pad_type) { // default is SAME_UPPER
options.set("autoPad", emscripten::val("same-lower"));
AutoPadType auto_pad_type = StringToAutoPadType(helper.Get("auto_pad", "NOTSET"));
if (node.OpType() == "Conv") {
// Calculate explicit padding for autoPad.
if (AutoPadType::SAME_UPPER == auto_pad_type || AutoPadType::SAME_LOWER == auto_pad_type) {
std::vector<int64_t> pads_out;
ORT_RETURN_IF_ERROR(HandleAutoPad(input_shape, weight_shape[2], weight_shape[3],
helper.Get("pads", std::vector<int64_t>{0, 0, 0, 0}),
helper.Get("strides", std::vector<int64_t>{1, 1}),
helper.Get("dilations", std::vector<int64_t>{1, 1}),
auto_pad_type,
pads_out,
model_builder.GetPreferredLayout() == DataLayout::NCHW));
std::transform(pads_out.begin(), pads_out.end(), pads.begin(),
[](int64_t pad) -> int32_t { return static_cast<int32_t>(pad); });
}
} else if (node.OpType() == "ConvTranspose") {
// When the 'output_shape' is specificed, the 'output_padding' values
// in options.outputPadding are ignored.
std::vector<int32_t> dim;
std::vector<int32_t> output_padding{0, 0};
if (helper.HasAttr("output_shape")) {
// Default value of 'output_shape' will be ignore as we already check if
// it's existed.
dim = helper.Get("output_shape", std::vector<int32_t>{-1, -1});
// Extract the height and width.
std::vector<int32_t> output_shape;
if (dim.size() == 2) {
output_shape = dim;
} else if (dim.size() == 4) {
output_shape = {dim[2], dim[3]};
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid output shape");
}
// Padding values are auto generated.
if (helper.HasAttr("kernel_shape")) {
std::vector<int32_t> kernel_shape = helper.Get("kernel_shape", std::vector<int32_t>{-1, -1});
std::vector<int32_t> total_padding(2);
std::vector<int64_t> input_shape;
ORT_RETURN_IF_NOT(GetShape(*input_defs[0], input_shape, logger), "Cannot get shape");
for (size_t i = 0; i < 2; i++) {
// Get the dimensions of H and W.
// For NHWC layout, the dimensions of H and W correspond to index 1 and 2.
// For NCHW layout, the dimensions of H and W correspond to index 2 and 3.
if (model_builder.GetPreferredLayout() == DataLayout::NHWC) {
total_padding[i] = strides[i] * (narrow<size_t>(input_shape[i + 1]) - 1) +
output_padding[i] + ((kernel_shape[i] - 1) * dilations[i] + 1) - output_shape[i];
} else {
ORT_RETURN_IF_NOT(model_builder.GetPreferredLayout() == DataLayout::NCHW,
"WebNN GPU backend preferred layout should be NCHW.");
total_padding[i] = strides[i] * (narrow<size_t>(input_shape[i + 2]) - 1) +
output_padding[i] + ((kernel_shape[i] - 1) * dilations[i] + 1) - output_shape[i];
}
}
AutoPadType auto_pad_type = StringToAutoPadType(helper.Get("auto_pad", "NOTSET"));
if (AutoPadType::SAME_UPPER == auto_pad_type || AutoPadType::SAME_LOWER == auto_pad_type) {
pads[0] = total_padding[0] / 2;
pads[1] = total_padding[0] - pads[0];
pads[2] = total_padding[1] / 2;
pads[3] = total_padding[1] - pads[2];
if (AutoPadType::SAME_LOWER == auto_pad_type) {
std::swap(pads[0], pads[1]);
std::swap(pads[2], pads[3]);
}
}
}
options.set("outputSizes", emscripten::val::array(output_shape));
} else {
options.set("autoPad", emscripten::val("same-upper"));
output_padding = helper.Get("output_padding", std::vector<int32_t>{0, 0});
options.set("outputPadding", emscripten::val::array(output_padding));
}
} else {
// Permute the ONNX's pads, which is [beginning_height, beginning_width, ending_height, ending_width],
// while WebNN's padding is [beginning_height, ending_height, beginning_width, ending_width].
const std::vector<int32_t> padding{pads[0], pads[2], pads[1], pads[3]};
options.set("padding", emscripten::val::array(padding));
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "conv_op_builder only supports Op Conv and ConvTranspose.");
}
// Permute the ONNX's pads, which is [beginning_height, beginning_width, ending_height, ending_width],
// while WebNN's padding is [beginning_height, ending_height, beginning_width, ending_width].
const std::vector<int32_t> padding{pads[0], pads[2], pads[1], pads[3]};
options.set("padding", emscripten::val::array(padding));
// Add bias if present.
if (input_defs.size() > 2) {
@ -198,17 +254,17 @@ Status ConvOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const N
const auto strides = helper.Get("strides", std::vector<int32_t>{1, 1});
const auto dilations = helper.Get("dilations", std::vector<int32_t>{1, 1});
auto pads = helper.Get("pads", std::vector<int32_t>{0, 0, 0, 0});
const auto& weight = input_defs[1]->Name();
const auto& weight_name = input_defs[1]->Name();
emscripten::val options = emscripten::val::object();
ORT_RETURN_IF_ERROR(SetConvBaseOptions(model_builder, node, options, strides, dilations, pads, logger));
if (op_type == "Conv") {
emscripten::val options = emscripten::val::object();
ORT_RETURN_IF_ERROR(SetConvBaseOptions(model_builder, node, options, strides, dilations, pads, logger));
int groups = options["groups"].as<int>();
std::vector<int64_t> input_shape;
ORT_RETURN_IF_NOT(GetShape(*input_defs[0], input_shape, logger), "Cannot get shape");
if (model_builder.GetPreferredLayout() == DataLayout::NHWC) {
bool depthwise = (groups == input_shape[3] && groups != 1);
options.set("inputLayout", emscripten::val("nhwc"));
ORT_RETURN_IF_ERROR(AddInitializerInNewLayout(model_builder, weight, !depthwise));
ORT_RETURN_IF_ERROR(AddInitializerInNewLayout(model_builder, weight_name, !depthwise));
if (!depthwise) {
options.set("filterLayout", emscripten::val("ohwi"));
} else {
@ -219,61 +275,10 @@ Status ConvOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const N
output = model_builder.GetBuilder().call<emscripten::val>("conv2d", input, filter, options);
} else {
emscripten::val options = emscripten::val::object();
ORT_RETURN_IF_ERROR(SetConvBaseOptions(model_builder, node, options, strides, dilations, pads, logger));
if (model_builder.GetPreferredLayout() == DataLayout::NHWC) {
options.set("inputLayout", emscripten::val("nhwc"));
options.set("filterLayout", emscripten::val("ohwi"));
ORT_RETURN_IF_ERROR(AddInitializerInNewLayout(model_builder, weight, false));
}
// When the 'output_shape' is specificed, the 'output_padding' values
// in options.outputPadding are ignored.
std::vector<int32_t> dim;
std::vector<int32_t> output_padding{0, 0};
if (helper.HasAttr("output_shape")) {
// Default value of 'output_shape' will be ignore as we already check if
// it's existed.
dim = helper.Get("output_shape", std::vector<int32_t>{-1, -1});
// Extract the height and width.
std::vector<int32_t> output_shape;
if (dim.size() == 2) {
output_shape = dim;
} else if (dim.size() == 4) {
output_shape = {dim[2], dim[3]};
} else {
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid output shape");
}
// Padding values are auto generated.
if (helper.HasAttr("kernel_shape")) {
std::vector<int32_t> kernel_shape = helper.Get("kernel_shape", std::vector<int32_t>{-1, -1});
std::vector<int32_t> total_padding(2);
std::vector<int64_t> input_shape;
ORT_RETURN_IF_NOT(GetShape(*input_defs[0], input_shape, logger), "Cannot get shape");
for (size_t i = 0; i < 2; i++) {
// Get the dimensions of H and W.
// For NHWC layout, the dimensions of H and W correspond to index 1 and 2.
// For NCHW layout, the dimensions of H and W correspond to index 2 and 3.
if (model_builder.GetPreferredLayout() == DataLayout::NHWC) {
total_padding[i] = strides[i] * (narrow<size_t>(input_shape[i + 1]) - 1) +
output_padding[i] + ((kernel_shape[i] - 1) * dilations[i] + 1) - output_shape[i];
} else {
ORT_RETURN_IF_NOT(model_builder.GetPreferredLayout() == DataLayout::NCHW,
"WebNN GPU backend preferred layout should be NCHW.");
total_padding[i] = strides[i] * (narrow<size_t>(input_shape[i + 2]) - 1) +
output_padding[i] + ((kernel_shape[i] - 1) * dilations[i] + 1) - output_shape[i];
}
}
pads[0] = total_padding[0] - (total_padding[0] / 2);
pads[1] = total_padding[0] / 2;
pads[2] = total_padding[1] - (total_padding[1] / 2);
pads[3] = total_padding[1] / 2;
options.set("padding", emscripten::val::array(pads));
}
options.set("outputSizes", emscripten::val::array(output_shape));
} else {
output_padding = helper.Get("output_padding", std::vector<int32_t>{0, 0});
options.set("outputPadding", emscripten::val::array(output_padding));
ORT_RETURN_IF_ERROR(AddInitializerInNewLayout(model_builder, weight_name, false));
}
emscripten::val filter = model_builder.GetOperand(input_defs[1]->Name());
output = model_builder.GetBuilder().call<emscripten::val>("convTranspose2d", input, filter, options);

View file

@ -81,28 +81,26 @@ Status PoolOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
const auto onnx_kernel_shape = helper.Get("kernel_shape", std::vector<int64_t>{0, 0});
const auto onnx_strides = helper.Get("strides", std::vector<int64_t>{1, 1});
const auto onnx_pads = helper.Get("pads", std::vector<int64_t>{0, 0, 0, 0});
auto pads = helper.Get("pads", std::vector<int32_t>{0, 0, 0, 0});
std::vector<int64_t> input_shape;
ORT_RETURN_IF_NOT(GetShape(*input_defs[0], input_shape, logger), "Cannot get shape");
AutoPadType auto_pad_type;
ORT_RETURN_IF_ERROR(HandleAutoPad(input_shape, onnx_kernel_shape[0], onnx_kernel_shape[1],
onnx_pads, onnx_strides, {1, 1} /* dilations */,
StringToAutoPadType(helper.Get("auto_pad", "NOTSET")),
auto_pad_type));
AutoPadType auto_pad_type = StringToAutoPadType(helper.Get("auto_pad", "NOTSET"));
if (AutoPadType::SAME_UPPER == auto_pad_type || AutoPadType::SAME_LOWER == auto_pad_type) {
if (AutoPadType::SAME_LOWER == auto_pad_type) { // default is SAME_UPPER
options.set("autoPad", "same-lower");
} else {
options.set("autoPad", "same-upper");
}
} else {
const std::vector<int32_t> pads = helper.Get("pads", std::vector<int32_t>{0, 0, 0, 0});
// Permute the ONNX's pads, which is [beginning_height, beginning_width, ending_height, ending_width],
// while WebNN's padding is [beginning_height, ending_height, beginning_width, ending_width].
const std::vector<int32_t> padding{pads[0], pads[2], pads[1], pads[3]};
options.set("padding", emscripten::val::array(padding));
std::vector<int64_t> pads_out;
ORT_RETURN_IF_ERROR(HandleAutoPad(input_shape, onnx_kernel_shape[0], onnx_kernel_shape[1],
onnx_pads,
helper.Get("strides", std::vector<int64_t>{1, 1}),
helper.Get("dilations", std::vector<int64_t>{1, 1}),
auto_pad_type,
pads_out,
model_builder.GetPreferredLayout() == DataLayout::NCHW));
std::transform(pads_out.begin(), pads_out.end(), pads.begin(),
[](int64_t pad) -> int32_t { return static_cast<int32_t>(pad); });
}
// Permute the ONNX's pads, which is [beginning_height, beginning_width, ending_height, ending_width],
// while WebNN's padding is [beginning_height, ending_height, beginning_width, ending_width].
const std::vector<int32_t> padding{pads[0], pads[2], pads[1], pads[3]};
options.set("padding", emscripten::val::array(padding));
const auto ceil_mode = helper.Get("ceil_mode", 0);
options.set("roundingType", ceil_mode == 0 ? emscripten::val("floor")