[WebNN] Support negative steps for slice (#22871)

Slice with negative steps can be emulated by reverse+slice.
This commit is contained in:
shiyi 2024-11-26 15:06:23 +08:00 committed by GitHub
parent 558ae8621c
commit afbb53937c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 59 deletions

View file

@ -85,7 +85,7 @@ operators and the supported opset domain/versions in **WebNN EP** by ONNX Runtim
| ReduceSumSquare | ai.onnx(7-10, 11-12, 13-17, 18+) | reduceSumSquare | ✓ | ✓ | Input 'axes' if present should be a constant |
| Relu | ai.onnx(7-12, 13, 14+) | relu | ✓ | ✓ | |
| Reshape | ai.onnx(7-12, 13, 14-18, 19-20, 21+) | reshape | ✓ | ✓ | Input 'shape' should be a constant, 0 dimension value in 'shape' is not supported |
| Resize | ai.onnx(11-12, 13-17, 18, 19+) | resample2d | ✓ | ✓ | Only supports 4-D input, antialias == 0, coordinate_transformation_mode == 'half_pixel', exclude_outside == 0, keep_aspect_ratio_policy == 'stretch', 'linear' and 'nearest' modes, input 'scales' and 'sizes' if present must be a constant |
| Resize | ai.onnx(11-12, 13-17, 18, 19+) | resample2d | ✓ | ✓ | Only supports 4-D input, antialias == 0, exclude_outside == 0, keep_aspect_ratio_policy == 'stretch', 'linear' and 'nearest' modes, input 'scales' and 'sizes' if present must be a constant |
| ScatterElements | ai.onnx(11-12, 13-15, 16-17, 18+) | scatterElements | ✗ | ✓ | Only supports 'reduction' == 'none' |
| ScatterND | ai.onnx(11-12, 13-15, 16-17, 18+) | scatterND | ✗ | ✓ | Only supports 'reduction' == 'none' |
| Shape | ai.onnx(7-12, 13-14, 15-18, 19-20, 21+) | slice | ✓ | ✓ | |
@ -95,7 +95,7 @@ operators and the supported opset domain/versions in **WebNN EP** by ONNX Runtim
| Softplus | ai.onnx(7+) | softplus | ✓ | ✓ | |
| Softsign | ai.onnx(7+) | softsign | ✓ | ✓ | |
| Sin | ai.onnx(7+) | sin | ✓ | ✓ | |
| Slice | ai.onnx(7-9, 10, 11-12, 13+) | slice | ✓ | ✓ | Input 'starts', 'ends', 'axes', and 'steps' if present must be a constant, only supports 'steps' value >= 1 |
| Slice | ai.onnx(7-9, 10, 11-12, 13+) | slice, reverse | ✓ | ✓ | Input 'starts', 'ends', 'axes', and 'steps' if present must be a constant |
| Softmax | ai.onnx(7-10, 11-12, 13+) | softmax | ✓ | ✓ | |
| Split | ai.onnx(7-10, 11-12, 13-17, 18+) | split | ✓ | ✓ | Input 'split' if present should be a constant |
| Sqrt | ai.onnx(7-12, 13+) | sqrt | ✓ | ✓ | |

View file

@ -82,7 +82,7 @@ inline std::string GetTensorName(const ConstPointerContainer<std::vector<NodeArg
return (input_defs.size() > index) ? std::string(input_defs[index]->Name()) : "";
}
inline std::vector<uint32_t> GetVecUint32FromVecInt64(const std::vector<int64_t>& int64_vec) {
inline std::vector<uint32_t> GetVecUint32FromVecInt64(gsl::span<const int64_t> int64_vec) {
std::vector<uint32_t> uint32_vec;
uint32_vec.reserve(int64_vec.size());
std::transform(int64_vec.begin(), int64_vec.end(),

View file

@ -40,8 +40,7 @@ void SliceOpBuilder::AddInitializersToSkip(ModelBuilder& model_builder, const No
}
}
Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
const Node& node,
Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const Node& node,
const logging::Logger& logger) const {
const auto& input_defs = node.InputDefs();
std::vector<int64_t> input_shape;
@ -49,10 +48,7 @@ Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
auto rank = input_shape.size();
NodeAttrHelper helper(node);
emscripten::val inputs = model_builder.GetOperand(input_defs[0]->Name());
std::vector<int32_t> starts(rank);
std::vector<int32_t> sizes(rank);
std::vector<int32_t> steps(rank);
emscripten::val input = model_builder.GetOperand(input_defs[0]->Name());
// Copy the data from the starts/ends/axes/steps initializers.
std::vector<int64_t> input_starts;
@ -76,8 +72,7 @@ Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
const auto& initializers(model_builder.GetInitializerTensors());
const auto& tensor = *initializers.at(input_name);
if (!ReadIntArrayFrom1DTensor(tensor, data, logger)) {
return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL,
"Data type for starts and ends inputs is not supported in this build.");
return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Data type for starts and ends inputs is not supported in this build.");
}
return Status::OK();
@ -89,31 +84,55 @@ Status SliceOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
ORT_RETURN_IF_ERROR(
SliceOp::PrepareForComputeHelper(input_starts, input_ends, input_axes, input_steps, compute_metadata));
std::transform(compute_metadata.starts_.cbegin(), compute_metadata.starts_.cend(),
starts.begin(),
[](int64_t i) { return SafeInt<uint32_t>(i); });
std::transform(compute_metadata.ends_.cbegin(), compute_metadata.ends_.cend(), compute_metadata.starts_.cbegin(),
sizes.begin(),
[](int64_t i, int64_t j) { return SafeInt<uint32_t>(i - j); });
std::transform(compute_metadata.steps_.cbegin(), compute_metadata.steps_.cend(), steps.begin(),
[](int64_t i) { return SafeInt<uint32_t>(i); });
// Check if reverse op is needed.
std::vector<uint32_t> reverse_axes;
emscripten::val reverse_output = input;
for (size_t i = 0; i < rank; ++i) {
if (compute_metadata.steps_[i] < 0) {
reverse_axes.push_back(SafeInt<uint32_t>(i));
compute_metadata.steps_[i] = -compute_metadata.steps_[i];
compute_metadata.starts_[i] = input_shape[i] - 1 - compute_metadata.starts_[i];
compute_metadata.ends_[i] = input_shape[i] - 1 - compute_metadata.ends_[i];
}
}
if (!reverse_axes.empty()) {
emscripten::val reverse_options = emscripten::val::object();
reverse_options.set("axes", emscripten::val::array(reverse_axes));
reverse_options.set("label", node.Name() + "_reverse");
reverse_output = model_builder.GetBuilder().call<emscripten::val>("reverse", input, reverse_options);
}
emscripten::val options = emscripten::val::object();
options.set("strides", emscripten::val::array(steps));
options.set("label", node.Name());
emscripten::val output = model_builder.GetBuilder().call<emscripten::val>("slice", inputs,
emscripten::val::array(starts),
emscripten::val::array(sizes),
options);
// Check if slice op is needed.
bool is_slice_required = false;
for (size_t i = 0; i < rank; ++i) {
if (compute_metadata.steps_[i] != 1 || compute_metadata.starts_[i] != 0 ||
compute_metadata.ends_[i] != input_shape[i]) {
is_slice_required = true;
break;
}
}
emscripten::val output = reverse_output;
if (is_slice_required) {
std::vector<uint32_t> starts = GetVecUint32FromVecInt64(compute_metadata.starts_);
std::vector<uint32_t> steps = GetVecUint32FromVecInt64(compute_metadata.steps_);
std::vector<uint32_t> sizes(rank);
std::transform(compute_metadata.ends_.cbegin(), compute_metadata.ends_.cend(), compute_metadata.starts_.cbegin(),
sizes.begin(), [](int64_t i, int64_t j) { return SafeInt<uint32_t>(i - j); });
emscripten::val options = emscripten::val::object();
options.set("strides", emscripten::val::array(steps));
options.set("label", node.Name());
output = model_builder.GetBuilder().call<emscripten::val>("slice", reverse_output, emscripten::val::array(starts),
emscripten::val::array(sizes), options);
}
model_builder.AddOperand(node.OutputDefs()[0]->Name(), std::move(output));
return Status::OK();
}
bool SliceOpBuilder::IsOpSupportedImpl(const InitializedTensorSet& initializers,
const Node& node,
const WebnnDeviceType /* device_type */,
const logging::Logger& logger) const {
bool SliceOpBuilder::IsOpSupportedImpl(const InitializedTensorSet& initializers, const Node& node,
const WebnnDeviceType /* device_type */, const logging::Logger& logger) const {
const auto& name = node.Name();
const auto& op_type = node.OpType();
const auto& input_defs = node.InputDefs();
@ -133,39 +152,12 @@ bool SliceOpBuilder::IsOpSupportedImpl(const InitializedTensorSet& initializers,
// Optional tensors (axes, steps) can be indicated by an empty name, just ignore it.
const std::string input_name = GetTensorName(input_defs, i);
if (!input_name.empty() && !Contains(initializers, input_name)) {
LOGS(logger, VERBOSE) << "Input [" << input_name << "] of " << op_type
<< " [" << name << "] must be known as initializer";
LOGS(logger, VERBOSE) << "Input [" << input_name << "] of " << op_type << " [" << name
<< "] must be known as initializer";
return false;
}
}
if (input_defs.size() == 5) { // Check steps.
const auto& steps_tensor = *initializers.at(input_defs[4]->Name());
std::vector<uint8_t> unpacked_tensor;
auto status = onnxruntime::utils::UnpackInitializerData(steps_tensor, unpacked_tensor);
if (!status.IsOK()) {
LOGS(logger, ERROR) << "Error while unpacking steps_tensor: " << status.ErrorMessage();
return false;
}
const auto data_type = steps_tensor.data_type();
// WebNN doesn't support steps less than 1.
if (data_type == ONNX_NAMESPACE::TensorProto_DataType_INT64) {
if (std::any_of(reinterpret_cast<int64_t*>(unpacked_tensor.data()),
reinterpret_cast<int64_t*>(unpacked_tensor.data() + unpacked_tensor.size()),
[](int64_t i) { return i < 1; })) {
LOGS(logger, VERBOSE) << "WebNN slice doesn't support steps less than 1";
return false;
}
} else if (data_type == ONNX_NAMESPACE::TensorProto_DataType_INT32) {
if (std::any_of(reinterpret_cast<int32_t*>(unpacked_tensor.data()),
reinterpret_cast<int32_t*>(unpacked_tensor.data()) + unpacked_tensor.size() / sizeof(int32_t),
[](int32_t i) { return i < 1; })) {
LOGS(logger, VERBOSE) << "WebNN slice doesn't support steps less than 1";
return false;
}
}
}
return true;
}