[WebNN EP] Align QDQ ops with latest Chromium implementation (#22180)

- Pass inputs to WebNN directly, WebNN will handle the broadcasting
- If `zero_point` is not provided, make a WebNN Constant with 0 values
and same shape as `scale` input
This commit is contained in:
Wanming Lin 2024-11-05 04:21:49 +08:00 committed by GitHub
parent 74adfc2099
commit 57668339e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 67 deletions

View file

@ -42,70 +42,28 @@ Status QDQOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder,
int32_t zero_point_type = 0;
ORT_RETURN_IF_NOT(GetType(*input_defs[0], input_type, logger), "Cannot get input data type");
ORT_RETURN_IF_NOT(GetType(*output_defs[0], output_type, logger), "Cannot get output data type");
emscripten::val input = model_builder.GetOperand(input_defs[0]->Name());
emscripten::val scale = model_builder.GetOperand(input_defs[1]->Name());
emscripten::val zero_point = emscripten::val::null();
if (input_defs.size() == 3 && input_defs[2]->Exists()) {
zero_point = model_builder.GetOperand(node.InputDefs()[2]->Name());
} else {
// DequantizeLinear: x_zero_point's data type equals to input data type
// QuantizeLinear: x_zero_point's data type equals to output data type
// WebNN requires the zero_point to have the same shape as the scale
zero_point_type = op_type == "DequantizeLinear" ? input_type : output_type;
zero_point = model_builder.GetZeroConstant(zero_point_type);
}
emscripten::val output;
NodeAttrHelper helper(node);
int32_t axis = helper.Get("axis", 1);
int32_t block_size = helper.Get("block_size", 0);
// axis is valid for input shape greater than 1D.
if (input_shape.size() > 1) {
axis = static_cast<int32_t>(HandleNegativeAxis(axis, input_shape.size()));
}
// Insert ones before and after the axis dimension for broadcasting of 1D scale tensor.
if (1 == scale_shape.size() && 1 < input_shape.size()) {
std::vector<int32_t> target_shape{static_cast<int>(input_shape[axis])};
target_shape.insert(target_shape.begin(), axis, 1);
target_shape.insert(target_shape.end(), input_shape.size() - axis - 1, 1);
emscripten::val reshape_scale_options = emscripten::val::object();
reshape_scale_options.set("label", node.Name() + "_reshape_scale");
scale = model_builder.GetBuilder().call<emscripten::val>("reshape",
scale,
emscripten::val::array(target_shape),
reshape_scale_options);
emscripten::val reshape_zero_point_options = emscripten::val::object();
reshape_zero_point_options.set("label", node.Name() + "_reshape_zero_point");
zero_point = model_builder.GetBuilder().call<emscripten::val>("reshape",
zero_point,
emscripten::val::array(target_shape),
reshape_zero_point_options);
}
// If block_size is specified, we need to expand the scale and zero_point tensors.
if (block_size > 1) {
emscripten::val concat_scale_inputs = emscripten::val::array();
emscripten::val concat_zero_point_inputs = emscripten::val::array();
for (int i = 0; i < block_size; i++) {
concat_scale_inputs.call<void>("push", scale);
concat_zero_point_inputs.call<void>("push", zero_point);
}
emscripten::val concat_scale_options = emscripten::val::object();
concat_scale_options.set("label", node.Name() + "_concat_scale");
scale = model_builder.GetBuilder().call<emscripten::val>("concat", concat_scale_inputs, axis, concat_scale_options);
emscripten::val concat_zero_point_options = emscripten::val::object();
concat_zero_point_options.set("label", node.Name() + "_concat_zero_point");
zero_point = model_builder.GetBuilder().call<emscripten::val>(
"concat", concat_zero_point_inputs, axis, concat_zero_point_options);
const auto zero_point_shape = GetVecUint32FromVecInt64(scale_shape);
zero_point = model_builder.GetZeroConstant(zero_point_type, zero_point_shape);
}
emscripten::val options = emscripten::val::object();
options.set("label", node.Name());
std::string webnn_op_type;
ORT_RETURN_IF_NOT(GetWebNNOpType(op_type, webnn_op_type), "Cannot get WebNN op type");
output = model_builder.GetBuilder().call<emscripten::val>(webnn_op_type.c_str(), input, scale, zero_point, options);
emscripten::val output =
model_builder.GetBuilder().call<emscripten::val>(webnn_op_type.c_str(), input, scale, zero_point, options);
model_builder.AddOperand(output_defs[0]->Name(), std::move(output));

View file

@ -14,6 +14,7 @@
#include "core/providers/common.h"
#include "core/providers/shared/utils/utils.h"
#include <sstream>
#include <utility>
namespace onnxruntime {
@ -384,51 +385,62 @@ void ModelBuilder::AddOperand(const std::string& name, const emscripten::val& op
wnn_operands_.insert(std::make_pair(name, operand));
}
// Get the zero scalar constant.
// Workaround for builer.constant(value, type) method since it has not been implemented now.
// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-constant-value-type
// BTW, the spec is discussing if the builer.constant(value, type) should be dropped at
// https://github.com/webmachinelearning/webnn/issues/475. Fix me according to the spec decision.
const emscripten::val& ModelBuilder::GetZeroConstant(const int32_t& data_type) {
// Get the zero constant with shape.
const emscripten::val& ModelBuilder::GetZeroConstant(const int32_t& data_type,
const std::vector<uint32_t>& shape) {
std::string name = "webnn_zero_constant_" + std::to_string(data_type);
emscripten::val dims = emscripten::val::array();
if (!shape.empty()) {
dims = emscripten::val::array(shape);
std::ostringstream name_stream;
name_stream << name;
for (const auto& dim : shape) {
name_stream << "_" << dim;
}
name = name_stream.str();
}
// If the operand does not exist, create it.
if (wnn_operands_.find(name) == wnn_operands_.end()) {
emscripten::val desc = emscripten::val::object();
emscripten::val dims = emscripten::val::array();
desc.set("dimensions", dims);
desc.set("shape", dims);
emscripten::val zero_buffer = emscripten::val::undefined();
if (!SetWebnnDataType(desc, data_type)) {
ORT_THROW("Unsupported data type: " + std::to_string(data_type));
}
auto num_elements = Product(shape);
switch (data_type) {
case ONNX_NAMESPACE::TensorProto_DataType_BOOL:
case ONNX_NAMESPACE::TensorProto_DataType_INT4:
case ONNX_NAMESPACE::TensorProto_DataType_UINT4:
// For WebNN int4 and uint4 tensors are stored in Uint8Array,
// so we need to adjust the number of elements.
num_elements = (num_elements + 1) / 2;
zero_buffer = emscripten::val::global("Uint8Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_BOOL:
case ONNX_NAMESPACE::TensorProto_DataType_UINT8:
zero_buffer = emscripten::val::global("Uint8Array").new_(1);
zero_buffer = emscripten::val::global("Uint8Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_INT8:
zero_buffer = emscripten::val::global("Int8Array").new_(1);
zero_buffer = emscripten::val::global("Int8Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_FLOAT16:
zero_buffer = emscripten::val::global("Uint16Array").new_(1);
zero_buffer = emscripten::val::global("Uint16Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_FLOAT:
zero_buffer = emscripten::val::global("Float32Array").new_(1);
zero_buffer = emscripten::val::global("Float32Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_INT32:
zero_buffer = emscripten::val::global("Int32Array").new_(1);
zero_buffer = emscripten::val::global("Int32Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_INT64:
zero_buffer = emscripten::val::global("BigInt64Array").new_(1);
zero_buffer = emscripten::val::global("BigInt64Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_UINT32:
zero_buffer = emscripten::val::global("Uint32Array").new_(1);
zero_buffer = emscripten::val::global("Uint32Array").new_(num_elements);
break;
case ONNX_NAMESPACE::TensorProto_DataType_UINT64:
zero_buffer = emscripten::val::global("BigUint64Array").new_(1);
zero_buffer = emscripten::val::global("BigUint64Array").new_(num_elements);
break;
default:
break;

View file

@ -38,7 +38,8 @@ class ModelBuilder {
const emscripten::val& GetOpSupportLimits() const { return wnn_limits_; }
void AddOperand(const std::string& name, const emscripten::val& operand);
const emscripten::val& GetZeroConstant(const int32_t& data_type);
const emscripten::val& GetZeroConstant(
const int32_t& data_type, const std::vector<uint32_t>& shape = {});
// Use the buffers to persist WebNN allocated data like transposed weight.
// It ensures the validity during inference session.
std::vector<std::unique_ptr<uint8_t[]>> mem_persist_buffers_;