Cjian/where python operator (#12795)

**Description**: 
This PR will enable the python tool to run QWhere and QDQWhere operation

**Limitation**:
s8s8 Where is still not supported.
This commit is contained in:
Jian Chen 2022-12-12 13:27:47 -08:00 committed by GitHub
parent b23395f977
commit d7d932c1c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1056 additions and 26 deletions

View file

@ -58,6 +58,7 @@ Do not modify directly.*
* <a href="#com.microsoft.QLinearReduceMean">com.microsoft.QLinearReduceMean</a>
* <a href="#com.microsoft.QLinearSigmoid">com.microsoft.QLinearSigmoid</a>
* <a href="#com.microsoft.QLinearSoftmax">com.microsoft.QLinearSoftmax</a>
* <a href="#com.microsoft.QLinearWhere">com.microsoft.QLinearWhere</a>
* <a href="#com.microsoft.QOrderedAttention">com.microsoft.QOrderedAttention</a>
* <a href="#com.microsoft.QOrderedGelu">com.microsoft.QOrderedGelu</a>
* <a href="#com.microsoft.QOrderedLayerNormalization">com.microsoft.QOrderedLayerNormalization</a>
@ -3000,6 +3001,56 @@ This version of the operator has been available since version 1 of the 'com.micr
</dl>
### <a name="com.microsoft.QLinearWhere"></a><a name="com.microsoft.qlinearwhere">**com.microsoft.QLinearWhere**</a>
Return elements, either from X or Y, depending on condition.
#### Version
This version of the operator has been available since version 1 of the 'com.microsoft' operator set.
#### Inputs
<dl>
<dt><tt>condition</tt> : B</dt>
<dd> When True (nonzero), yield x, otherwise yield y</dd>
<dt><tt>X</tt> : T</dt>
<dd>Y's zero point.</dd>
<dt><tt>x_scale</tt> : TF</dt>
<dd>X's scale.</dd>
<dt><tt>x_zero_point</tt> : T</dt>
<dd>X's zero point.</dd>
<dt><tt>Y</tt> : T</dt>
<dd>Y's zero point.</dd>
<dt><tt>y_scale</tt> : TF</dt>
<dd>Y's scale.</dd>
<dt><tt>y_zero_point</tt> : T</dt>
<dd>Y's zero point.</dd>
<dt><tt>z_scale</tt> : TF</dt>
<dd>Z's scale.</dd>
<dt><tt>z_zero_point</tt> : T</dt>
<dd>Z's zero point.</dd>
</dl>
#### Outputs
<dl>
<dt><tt>Z</tt> : T</dt>
<dd>Tensor of shape equal to the broadcasted shape of condition, X, and Y</dd>
</dl>
#### Type Constraints
<dl>
<dt><tt>B</tt> : tensor(bool)</dt>
<dd>Constrain input and output types to 8 bit signed and unsigned tensors.</dd>
<dt><tt>TF</tt> : tensor(float)</dt>
<dd>Constrain scale types to any float tensor type.</dd>
<dt><tt>T</tt> : tensor(uint8), tensor(int8)</dt>
<dd>Constrain input and output types to 8 bit signed and unsigned tensors.</dd>
</dl>
### <a name="com.microsoft.QOrderedAttention"></a><a name="com.microsoft.qorderedattention">**com.microsoft.QOrderedAttention**</a>
Quantized version of simplified Multi-Head Self Attention(using int8 with specific matrix Layout).

View file

@ -433,6 +433,7 @@ Do not modify directly.*
|QLinearMul|*in* A:**T**<br> *in* A_scale:**tensor(float)**<br> *in* A_zero_point:**T**<br> *in* B:**T**<br> *in* B_scale:**tensor(float)**<br> *in* B_zero_point:**T**<br> *in* C_scale:**tensor(float)**<br> *in* C_zero_point:**T**<br> *out* C:**T**|1+|**T** = tensor(int8), tensor(uint8)|
|QLinearSigmoid|*in* X:**T**<br> *in* X_scale:**tensor(float)**<br> *in* X_zero_point:**T**<br> *in* Y_scale:**tensor(float)**<br> *in* Y_zero_point:**T**<br> *out* Y:**T**|1+|**T** = tensor(int8), tensor(uint8)|
|QLinearSoftmax|*in* X:**T**<br> *in* X_scale:**tensor(float)**<br> *in* x_zero_point:**T**<br> *in* y_scale:**tensor(float)**<br> *in* y_zero_point:**T**<br> *out* Y:**T**|1+|**T** = tensor(int8), tensor(uint8)|
|QLinearWhere|*in* condition:**B**<br> *in* X:**T**<br> *in* x_scale:**TF**<br> *in* x_zero_point:**T**<br> *in* Y:**T**<br> *in* y_scale:**TF**<br> *in* y_zero_point:**T**<br> *in* z_scale:**TF**<br> *in* z_zero_point:**T**<br> *out* Z:**T**|1+|**T** = tensor(int8), tensor(uint8)|
|QuantizeLinear|*in* x:**T1**<br> *in* y_scale:**T1**<br> *in* y_zero_point:**T2**<br> *out* y:**T2**|1+|**T1** = tensor(float)<br/> **T2** = tensor(int8), tensor(uint8)|
|QuickGelu|*in* X:**T**<br> *out* Y:**T**|1+|**T** = tensor(float)|
|Range|*in* start:**T**<br> *in* limit:**T**<br> *in* delta:**T**<br> *out* Y:**T**|1+|**T** = tensor(double), tensor(float), tensor(int16), tensor(int32), tensor(int64)|

View file

@ -47,6 +47,7 @@ class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, Quick
class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MatMulInteger16);
class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearGlobalAveragePool);
class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearConcat);
class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearWhere);
class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearAveragePool);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, uint8_t, DequantizeLinear);
class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, int8_t, DequantizeLinear);
@ -145,6 +146,7 @@ Status RegisterQuantizationKernels(KernelRegistry& kernel_registry) {
BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MatMulInteger16)>,
BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearGlobalAveragePool)>,
BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearConcat)>,
BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearWhere)>,
BuildKernelCreateInfo<ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, QLinearAveragePool)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, uint8_t, DequantizeLinear)>,
BuildKernelCreateInfo<ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, int8_t, DequantizeLinear)>,

View file

@ -1,6 +1,7 @@
// Copyright (c Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "qlinear_util.h"
#include "qlinear_concat.h"
#include "qlinear_lookup_table.h"
@ -12,24 +13,6 @@
namespace onnxruntime {
namespace contrib {
constexpr int LOOKUP_TABLE_IS_FIXED = 1;
constexpr int LOOKUP_TABLE_IS_COPY = 2;
static inline bool has_same_scale(const Tensor* tensor_x_scale, const Tensor* tensor_y_scale) {
return *(tensor_x_scale->Data<float>()) == *(tensor_y_scale->Data<float>());
}
static inline bool has_same_zero_point(bool is_signed, const Tensor* tensor_x_zero_point, const Tensor* tensor_y_zero_point) {
if (is_signed) {
const int8_t X_zero_point = (tensor_x_zero_point == nullptr) ? static_cast<int8_t>(0) : *(tensor_x_zero_point->Data<int8_t>());
const int8_t Y_zero_point = (tensor_y_zero_point == nullptr) ? static_cast<int8_t>(0) : *(tensor_y_zero_point->Data<int8_t>());
return X_zero_point == Y_zero_point;
} else {
const uint8_t X_zero_point = (tensor_x_zero_point == nullptr) ? static_cast<uint8_t>(0) : *(tensor_x_zero_point->Data<uint8_t>());
const uint8_t Y_zero_point = (tensor_y_zero_point == nullptr) ? static_cast<uint8_t>(0) : *(tensor_y_zero_point->Data<uint8_t>());
return X_zero_point == Y_zero_point;
}
}
QLinearConcat::QLinearConcat(const OpKernelInfo& info) : OpKernel(info), ConcatBase(info) {
size_t input_def_count = info.node().InputDefs().size();

View file

@ -0,0 +1,32 @@
// Copyright (c Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef ONNXRUNTIME_QLINEAR_UTIL_H
#define ONNXRUNTIME_QLINEAR_UTIL_H
#include <cstdint>
#include "core/framework/tensor.h"
namespace onnxruntime {
namespace contrib {
constexpr int LOOKUP_TABLE_IS_FIXED = 1;
constexpr int LOOKUP_TABLE_IS_COPY = 2;
static inline bool has_same_scale(const Tensor* tensor_x_scale, const Tensor* tensor_y_scale) {
return *(tensor_x_scale->Data<float>()) == *(tensor_y_scale->Data<float>());
}
static inline bool has_same_zero_point(bool is_signed, const Tensor* tensor_x_zero_point, const Tensor* tensor_y_zero_point) {
if (is_signed) {
const int8_t X_zero_point = (tensor_x_zero_point == nullptr) ? static_cast<int8_t>(0) : *(tensor_x_zero_point->Data<int8_t>());
const int8_t Y_zero_point = (tensor_y_zero_point == nullptr) ? static_cast<int8_t>(0) : *(tensor_y_zero_point->Data<int8_t>());
return X_zero_point == Y_zero_point;
}
const uint8_t X_zero_point = (tensor_x_zero_point == nullptr) ? static_cast<uint8_t>(0) : *(tensor_x_zero_point->Data<uint8_t>());
const uint8_t Y_zero_point = (tensor_y_zero_point == nullptr) ? static_cast<uint8_t>(0) : *(tensor_y_zero_point->Data<uint8_t>());
return X_zero_point == Y_zero_point;
}
} // namespace contrib
} // namespace onnxruntime
#endif // ONNXRUNTIME_QLINEAR_UTIL_H

View file

@ -0,0 +1,404 @@
#include "qlinear_where.h"
#include "qlinear_lookup_table.h"
#include "core/providers/common.h"
#include "core/mlas/inc/mlas.h"
#include "core/platform/threadpool.h"
#include "core/providers/cpu/math/element_wise_ops.h"
namespace onnxruntime {
namespace {
template <typename T, typename R>
using EnableIfEigenScalar = typename std::enable_if<std::is_arithmetic<T>::value, R>::type;
template <typename T, typename R>
using EnableIfEigenNotScalar = typename std::enable_if<!std::is_arithmetic<T>::value, R>::type;
template <typename T>
ProcessBroadcastSpanFuncs CreateScalarBroadcastFuncs() {
return ProcessBroadcastSpanFuncs{
// Scalar Condition + Eigen Input -> Eigen output
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
bool condition = per_iter_bh.ScalarInput0<bool>();
auto value = per_iter_bh.EigenInput1<T>();
auto output = per_iter_bh.OutputEigen<T>();
if (condition == target) {
output = value;
} else {
output = EigenVectorMap<T>::PlainObject::Constant(value.size(), T{});
}
// Transform the output to the correct value from LookupTable
if (!is_copy) {
auto* look_up_table = user_data + 2;
std::transform(value.cbegin(), value.cend(), output.begin(),
[condition, target, &look_up_table](const T& value_element) {
return condition == target ? look_up_table[value_element] : T{};
});
}
},
// Eigen Condition + Scalar Input -> Eigen Output
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
auto condition = per_iter_bh.EigenInput0<bool>();
const T& value = per_iter_bh.ScalarInput1<T>();
auto output = per_iter_bh.OutputEigen<T>();
output = (condition.array() == target)
.select(value, EigenVectorMap<T>::PlainObject::Constant(condition.size(), T{}));
// Transform the output to the correct value from LookupTable
if (!is_copy) {
auto* look_up_table = user_data + 2;
std::transform(condition.cbegin(), condition.cend(), output.begin(),
[target, &look_up_table, &value](bool condition_element) {
return condition_element == target ? look_up_table[value] : T{};
});
}
},
// Eigen Condition + Eigen Input -> Eigen Output
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
auto condition = per_iter_bh.EigenInput0<bool>();
auto value = per_iter_bh.EigenInput1<T>();
auto output = per_iter_bh.OutputEigen<T>();
output = (condition.array() == target)
.select(value, EigenVectorMap<T>::PlainObject::Constant(condition.size(), T{}));
// Transform the output to the correct value from LookupTable
if (!is_copy) {
auto* look_up_table = user_data + 2;
std::transform(condition.cbegin(), condition.cend(), value.cbegin(), output.begin(),
[target, &look_up_table](bool condition_element, const T& value_element) {
return (condition_element == target) ? look_up_table[value_element] : T{};
});
}
}};
}
template <typename T>
ProcessBroadcastSpanFuncs CreateNonScalarBroadcastFuncs() {
return ProcessBroadcastSpanFuncs{
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
auto* look_up_table = user_data + 2;
bool condition = per_iter_bh.ScalarInput0<bool>();
auto value = per_iter_bh.SpanInput1<T>();
auto output = per_iter_bh.OutputSpan<T>();
if (condition == target) {
// Transform the output to the correct value from LookupTable
std::transform(value.cbegin(), value.cend(), output.begin(),
[condition, target, &look_up_table,is_copy](const T& value_element) {
return is_copy ? value_element : look_up_table[value_element];
});
} else {
std::fill(output.begin(), output.end(), T{});
}
},
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
auto* look_up_table = user_data + 2;
auto condition = per_iter_bh.SpanInput0<bool>();
const T& value = per_iter_bh.ScalarInput1<T>();
auto output = per_iter_bh.OutputSpan<T>();
// Transform the output to the correct value from LookupTable
std::transform(condition.begin(), condition.end(), output.begin(),
[target, &value,&look_up_table,is_copy](bool condition_element) {
return condition_element == target ? is_copy ? value : look_up_table[value] : T{};
});
},
[](BroadcastHelper& per_iter_bh) {
auto* user_data = static_cast<T*>(per_iter_bh.GetUserData());
bool target = user_data[0] == 1;
bool is_copy = user_data[1] == 1;
auto* look_up_table = user_data + 2;
auto condition = per_iter_bh.SpanInput0<bool>();
auto value = per_iter_bh.SpanInput1<T>();
auto output = per_iter_bh.OutputSpan<T>();
// Transform the output to the correct value from LookupTable
std::transform(condition.begin(), condition.end(), value.cbegin(), output.begin(),
[target,&look_up_table,is_copy](bool condition_element, const T& value_element) {
return condition_element == target ? is_copy ? value_element : look_up_table[value_element] : T{};
});
}};
}
template <typename T>
EnableIfEigenScalar<T, ProcessBroadcastSpanFuncs> SelectBroadcastFuncs() {
// NOTE: Workaround a VS2017 bug by calling a separate function to create the broadcast funcs.
// If we create them directly here it doesn't bring in the definitions of the Eigen classes leading to
// a 'class has no constructors' error
return CreateScalarBroadcastFuncs<T>();
}
template <typename T>
EnableIfEigenNotScalar<T, ProcessBroadcastSpanFuncs> SelectBroadcastFuncs() {
return CreateNonScalarBroadcastFuncs<T>();
}
// function pointer to create typed tensor from type agnostic code whilst avoiding the overhead of std::function
using AllocTensorFunc = std::unique_ptr<Tensor> (*)(const TensorAllocator& allocator, const TensorShape& shape);
static std::unique_ptr<Tensor> UntypedSelect(OpKernelContext& context, std::vector<uint8_t>& user_data, const ProcessBroadcastSpanFuncs& functors, const TensorAllocator& allocator, AllocTensorFunc allocate_tensor) {
const auto& condition = *context.Input<Tensor>(0);
// select the X input (input 1) for 'true', and Y input (input 2) for 'false'
bool target = user_data[0] == 1;
const auto& values = *context.Input<Tensor>(target ? 1 : 4);
InputBroadcaster input_broadcaster(condition, values);
std::unique_ptr<Tensor> selection_tensor = allocate_tensor(allocator, input_broadcaster.GetOutputShape());
OutputBroadcaster output_broadcaster(input_broadcaster.GetSpanSize(), *selection_tensor);
// store value of 'target' directly in void* for user_data so it's accessible in the state-less functors
BroadcastHelper broadcast_helper(input_broadcaster, output_broadcaster, reinterpret_cast<void*>(user_data.data()));
BroadcastLooper(broadcast_helper, functors);
return selection_tensor;
}
// Merging Functions
template <typename T>
void MergeScalarAndVector(EigenVectorMap<T> output, const T& scalar_value, ConstEigenVectorMap<T> vector_value) {
if (scalar_value != T{}) {
output = EigenVectorMap<T>::PlainObject::Constant(vector_value.size(), scalar_value);
} else {
output = vector_value;
}
};
template <typename T>
EnableIfEigenScalar<T, ProcessBroadcastSpanFuncs> MergeBroadcastFuncs() {
return ProcessBroadcastSpanFuncs{
[](BroadcastHelper& per_iter_bh) {
MergeScalarAndVector(per_iter_bh.OutputEigen<T>(),
per_iter_bh.ScalarInput0<T>(), // X selection
per_iter_bh.EigenInput1<T>()); // Y selection
},
[](BroadcastHelper& per_iter_bh) {
MergeScalarAndVector(per_iter_bh.OutputEigen<T>(),
per_iter_bh.ScalarInput1<T>(), // Y selection
per_iter_bh.EigenInput0<T>()); // X selection
},
[](BroadcastHelper& per_iter_bh) {
auto X_selection = per_iter_bh.EigenInput0<T>();
auto Y_selection = per_iter_bh.EigenInput1<T>();
per_iter_bh.OutputEigen<T>() = X_selection.binaryExpr(Y_selection,
[](T x, T y) -> T {
return x != T{} ? x : y;
});
}};
}
template <typename T>
void MergeScalarAndVector(gsl::span<T> output, const T& scalar_value, gsl::span<const T> vector_value) {
if (!scalar_value.empty()) {
std::fill(output.begin(), output.end(), scalar_value);
} else {
std::copy(vector_value.cbegin(), vector_value.cend(), output.begin());
}
};
template <typename T>
EnableIfEigenNotScalar<T, ProcessBroadcastSpanFuncs> MergeBroadcastFuncs() {
return ProcessBroadcastSpanFuncs{
[](BroadcastHelper& per_iter_bh) {
MergeScalarAndVector(per_iter_bh.OutputSpan<T>(),
per_iter_bh.ScalarInput0<T>(), // X selection
per_iter_bh.SpanInput1<T>()); // Y selection
},
[](BroadcastHelper& per_iter_bh) {
MergeScalarAndVector(per_iter_bh.OutputSpan<T>(),
per_iter_bh.ScalarInput1<T>(), // Y selection
per_iter_bh.SpanInput0<T>()); // X selection
},
[](BroadcastHelper& per_iter_bh) {
auto X_selection = per_iter_bh.SpanInput0<T>();
auto Y_selection = per_iter_bh.SpanInput1<T>();
auto output = per_iter_bh.OutputSpan<T>();
std::transform(X_selection.cbegin(), X_selection.cend(), Y_selection.cbegin(), output.begin(),
[](const T& x, const T& y) { return !x.empty() ? x : y; });
}};
}
static void UntypedMerge(OpKernelContext& context,
const Tensor& X_selection_tensor, const Tensor& Y_selection_tensor,
const ProcessBroadcastSpanFuncs& functors) {
InputBroadcaster merge_broadcaster{X_selection_tensor, Y_selection_tensor};
Tensor& output = *context.Output(0, merge_broadcaster.GetOutputShape());
OutputBroadcaster output_broadcaster{merge_broadcaster.GetSpanSize(), output};
BroadcastHelper broadcast_helper(merge_broadcaster, output_broadcaster);
BroadcastLooper(broadcast_helper, functors);
}
} // namespace
namespace contrib {
QLinearWhere::QLinearWhere(const OpKernelInfo& info) : OpKernel(info) {
size_t input_def_count = info.node().InputDefs().size();
ORT_ENFORCE(input_def_count == kExpected_input_count,
"There must be ", kExpected_input_count, " inputs! (condition, x, x_scale, x_zero_point, y, y_scale, y_zero_point, z_scale, z_zero_point)");
const Tensor* tensor_x_scale = nullptr;
const Tensor* tensor_x_zero_point = nullptr;
const Tensor* tensor_y_scale = nullptr;
const Tensor* tensor_y_zero_point = nullptr;
const Tensor* tensor_z_scale = nullptr;
const Tensor* tensor_z_zero_point = nullptr;
bool get_x_scale = info.TryGetConstantInput(2, &tensor_x_scale);
bool get_x_zero_point = info.TryGetConstantInput(3, &tensor_x_zero_point);
bool get_y_scale = info.TryGetConstantInput(5, &tensor_y_scale);
bool get_y_zero_point = info.TryGetConstantInput(6, &tensor_y_zero_point);
bool get_z_scale = info.TryGetConstantInput(7, &tensor_z_scale);
bool get_z_zero_point = info.TryGetConstantInput(8, &tensor_z_zero_point);
if (!get_z_scale || !get_z_zero_point) {
// Can not build fix lookup table
return;
}
ORT_ENFORCE(
tensor_x_zero_point->GetElementType() == tensor_y_zero_point->GetElementType() &&
tensor_x_zero_point->GetElementType() == tensor_z_zero_point->GetElementType() &&
tensor_y_zero_point->GetElementType() == tensor_z_zero_point->GetElementType(),
"Wrong input type encountered for zero point input def of x, y, z");
bool is_signed_int8 = tensor_z_zero_point->IsDataType<int8_t>();
const auto identity_float = [](float v) -> float { return v; };
if (get_x_scale && get_x_zero_point) {
// Build fix lookup table for x
is_x_fixed_copy_ = has_same_scale(tensor_x_scale, tensor_z_scale) &&
has_same_zero_point(is_signed_int8, tensor_x_zero_point, tensor_z_zero_point);
if (!is_x_fixed_copy_) {
x_fixed_lookup_table_.resize(256);
if (is_signed_int8) {
QlinearBuildLookupTable<int8_t>(
x_fixed_lookup_table_.data(), tensor_x_scale, tensor_x_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
} else {
QlinearBuildLookupTable<uint8_t>(
x_fixed_lookup_table_.data(), tensor_x_scale, tensor_x_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
}
}
is_x_dynamic_ = false;
}
if (get_y_scale && get_y_zero_point) {
// Build fix lookup table for y
is_y_fixed_copy_ = has_same_scale(tensor_y_scale, tensor_z_scale) &&
has_same_zero_point(is_signed_int8, tensor_y_zero_point, tensor_z_zero_point);
if (!is_y_fixed_copy_) {
y_fixed_lookup_table_.resize(256);
if (is_signed_int8) {
QlinearBuildLookupTable<int8_t>(
y_fixed_lookup_table_.data(), tensor_y_scale, tensor_y_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
} else {
QlinearBuildLookupTable<uint8_t>(
y_fixed_lookup_table_.data(), tensor_y_scale, tensor_y_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
}
}
is_y_dynamic_ = false;
}
}
Status QLinearWhere::Compute(OpKernelContext* ctx) const {
// const auto* tensor_condition = ctx->Input<Tensor>(0);
// const auto* tensor_x_input = ctx->Input<Tensor>(1);
const auto* tensor_x_scale = ctx->Input<Tensor>(2);
const auto* tensor_x_zero_point = ctx->Input<Tensor>(3);
// const auto* tensor_y_input = ctx->Input<Tensor>(4);
const auto* tensor_y_scale = ctx->Input<Tensor>(5);
const auto* tensor_y_zero_point = ctx->Input<Tensor>(6);
const auto* tensor_z_scale = ctx->Input<Tensor>(7);
const auto* tensor_z_zero_point = ctx->Input<Tensor>(8);
// auto* tensor_output = ctx->Output(0, tensor_condition->Shape());
ORT_ENFORCE(tensor_x_scale->IsDataType<float>(), "Input scale is not float for quantized input x @ 2");
ORT_ENFORCE(tensor_y_scale->IsDataType<float>(), "Input scale is not float for quantized input y @ 5");
ORT_ENFORCE(tensor_z_scale->IsDataType<float>(), "Input scale is not float for quantized output z @ 7");
ORT_ENFORCE(tensor_x_zero_point->GetElementType() == tensor_y_zero_point->GetElementType() &&
tensor_x_zero_point->GetElementType() == tensor_z_zero_point->GetElementType() &&
tensor_y_zero_point->GetElementType() == tensor_z_zero_point->GetElementType(),
"Wrong input type encountered for zero point of quantized input @", 3, 6, 8);
bool is_signed_int8 = tensor_z_zero_point->IsDataType<int8_t>();
const auto identity_float = [](float v) -> float { return v; };
std::vector<uint8_t> x_dynamic_lookup_table;
bool is_x_copy = !is_x_dynamic_ ? is_x_fixed_copy_ : has_same_scale(tensor_x_scale, tensor_z_scale) && has_same_zero_point(is_signed_int8, tensor_x_zero_point, tensor_z_zero_point);
if (is_x_dynamic_ && !is_x_copy) {
x_dynamic_lookup_table.resize(256);
if (is_signed_int8) {
QlinearBuildLookupTable<int8_t>(
x_dynamic_lookup_table.data(), tensor_x_scale, tensor_x_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
} else {
QlinearBuildLookupTable<uint8_t>(
x_dynamic_lookup_table.data(), tensor_x_scale, tensor_x_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
}
}
// Build dynamic lookup table for y
std::vector<uint8_t> y_dynamic_lookup_table;
bool is_y_copy = !is_y_dynamic_ ? is_y_fixed_copy_ : has_same_scale(tensor_y_scale, tensor_z_scale) && has_same_zero_point(is_signed_int8, tensor_y_zero_point, tensor_z_zero_point);
if (is_y_dynamic_ && !is_y_copy) {
y_dynamic_lookup_table.resize(256);
if (is_signed_int8) {
QlinearBuildLookupTable<int8_t>(
y_dynamic_lookup_table.data(), tensor_y_scale, tensor_y_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
} else {
QlinearBuildLookupTable<uint8_t>(
y_dynamic_lookup_table.data(), tensor_y_scale, tensor_y_zero_point,
tensor_z_scale, tensor_z_zero_point, identity_float);
}
}
// Each lookup table is 256 bytes
const auto& x_lookup_table = is_x_dynamic_ ? x_dynamic_lookup_table : x_fixed_lookup_table_;
const auto& y_lookup_table = is_y_dynamic_ ? y_dynamic_lookup_table : y_fixed_lookup_table_;
// Each user_data will sized at 2 + 256 bytes, and contain {is_x/y, is_x/y_copy, x/y_lookup_table} respectively
std::vector<uint8_t> x_user_data(258);
std::vector<uint8_t> y_user_data(258);
x_user_data[0] = 1;
y_user_data[0] = 0;
x_user_data[1] = is_x_copy ? 1 : 0;
y_user_data[1] = is_y_copy ? 1 : 0;
if (!is_x_copy) {
std::copy(x_lookup_table.begin(), x_lookup_table.end(), x_user_data.begin() + 2);
}
if (!is_y_copy) {
std::copy(y_lookup_table.begin(), y_lookup_table.end(), y_user_data.begin() + 2);
}
//Allocator, Allocation, and SelectBroadcastFuncs are the same implementation from where_op.cc
const auto typed_tensor_allocation = [](const TensorAllocator& allocator,
const TensorShape& shape) {
return allocator.Allocate<uint8_t>(shape);
};
TensorAllocator tensor_allocator{*ctx};
ProcessBroadcastSpanFuncs funcs = SelectBroadcastFuncs<uint8_t>();
// UntypedSelect is MODIFIED from where_op.cc
auto X_selection_tensor = UntypedSelect(*ctx, x_user_data, funcs, tensor_allocator, typed_tensor_allocation);
auto Y_selection_tensor = UntypedSelect(*ctx, y_user_data, funcs, tensor_allocator, typed_tensor_allocation);
// UntypedMerge is the same as from where_op.cc
UntypedMerge(*ctx, *X_selection_tensor, *Y_selection_tensor, MergeBroadcastFuncs<uint8_t>());
return Status();
}
ONNX_CPU_OPERATOR_MS_KERNEL(
QLinearWhere,
1,
KernelDefBuilder().TypeConstraint(
"T",
{DataTypeImpl::GetTensorType<uint8_t>(), DataTypeImpl::GetTensorType<int8_t>()}),
QLinearWhere)
} // namespace contrib
} // namespace onnxruntime

View file

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "qlinear_util.h"
#include "core/common/common.h"
#include "core/framework/op_kernel.h"
#include "core/util/math_cpuonly.h"
#include "core/providers/cpu/tensor/where_op.h"
namespace onnxruntime {
namespace contrib {
class QLinearWhere final : public OpKernel {
public:
QLinearWhere(const OpKernelInfo& info);
Status Compute(OpKernelContext* context) const override;
private:
constexpr static size_t kExpected_input_count = 9;
std::vector<uint8_t> y_fixed_lookup_table_;
std::vector<uint8_t> x_fixed_lookup_table_;
bool is_x_dynamic_ = true;
bool is_y_dynamic_ = true;
bool is_x_fixed_copy_ = false;
bool is_y_fixed_copy_ = false;
};
} // namespace contrib
} // namespace onnxruntime

View file

@ -26,6 +26,7 @@ class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QEmbedLayerNormalization
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QGemm);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearAdd);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearConcat);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearWhere);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearLeakyRelu);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearMul);
class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearReduceMean);
@ -108,6 +109,7 @@ class OpSet_Microsoft_ver1 {
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QGemm)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearAdd)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearConcat)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearWhere)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearLeakyRelu)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearMul)>());
fn(GetOpSchema<ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, QLinearReduceMean)>());

View file

@ -9,6 +9,7 @@
#include "core/graph/constants.h"
#include "core/graph/contrib_ops/contrib_defs.h"
#include "core/graph/contrib_ops/shape_inference_functions.h"
#include "onnx/onnx-ml.pb.h" // ?
// Suppress a warning: global initializer calls a non-constexpr function 'symbol' which is from
// ONNX_OPERATOR_SET_SCHEMA_EX macro and only happens in debug build
@ -817,10 +818,46 @@ ONNX_MS_OPERATOR_SET_SCHEMA(
}
}
if (all_lengths_known) {
output_shape->mutable_dim(axis)->set_dim_value(total_length);
}
}));
if (all_lengths_known) {
output_shape->mutable_dim(axis)->set_dim_value(total_length);
}
}));
ONNX_MS_OPERATOR_SET_SCHEMA(QLinearWhere, 1, OpSchema()
.SetDoc("Return elements, either from X or Y, depending on condition.")
.Input(0, "condition", " When True (nonzero), yield x, otherwise yield y", "B")
.Input(1, "X", "Y's zero point.", "T")
.Input(2, "x_scale", "X's scale.", "TF")
.Input(3, "x_zero_point", "X's zero point.", "T")
.Input(4, "Y", "Y's zero point.", "T")
.Input(5, "y_scale", "Y's scale.", "TF")
.Input(6, "y_zero_point", "Y's zero point.", "T")
.Input(7, "z_scale", "Z's scale.", "TF")
.Input(8, "z_zero_point", "Z's zero point.", "T")
.Output(0, "Z", "Tensor of shape equal to the broadcasted shape of condition, X, and Y", "T")
.TypeConstraint(
"B",
{"tensor(bool)"},
"Constrain input and output types to 8 bit signed and unsigned tensors.")
.TypeConstraint(
"TF",
{"tensor(float)"},
"Constrain scale types to any float tensor type.")
.TypeConstraint(
"T",
{"tensor(uint8)", "tensor(int8)"},
"Constrain input and output types to 8 bit signed and unsigned tensors.")
.TypeAndShapeInferenceFunction([](InferenceContext& ctx) {
propagateElemTypeFromInputToOutput(ctx, 1, 0);
if (hasNInputShapes(ctx, 9)) {
std::vector<const onnx::TensorShapeProto*> shapes;
shapes.push_back(&ctx.getInputType(0)->tensor_type().shape());
shapes.push_back(&ctx.getInputType(1)->tensor_type().shape());
shapes.push_back(&ctx.getInputType(4)->tensor_type().shape());
multidirectionalBroadcastShapeInference(
shapes, *ctx.getOutputType(0)->mutable_tensor_type()->mutable_shape());
}
}));
ONNX_MS_OPERATOR_SET_SCHEMA(
QGemm, 1,

View file

@ -72,7 +72,22 @@ std::vector<NodeAndMoveInfo> ConvMoves() {
return moves;
}
std::vector<NodeAndMoveInfo> WhereMoves(){
NTO::NodeLocation dq_x{NTO::NodeType::kInput, 0};
NTO::NodeLocation dq_y{NTO::NodeType::kInput, 1};
NTO::NodeLocation target{NTO::NodeType::kTarget, 0};
NTO::NodeLocation q{NTO::NodeType::kOutput, 0};
std::vector<NodeAndMoveInfo> moves{
MoveAndAppend(target,ArgType::kInput, 0, ArgType::kInput), // move the condition to the new node
MoveAll(dq_x, ArgType::kInput), // append all inputs from x
MoveAll(dq_y, ArgType::kInput), // append all inputs from x
MoveAndAppend(q, ArgType::kInput, 1, ArgType::kInput), // append scale (input 1) from q
MoveAndAppend(q, ArgType::kInput, 2, ArgType::kInput), // append zp (input 2) from q
MoveAll(q, ArgType::kOutput)
};
return moves;
}
QDQReplaceWithNew SplitReplacer() {
NTO::NodeLocation dq{NTO::NodeType::kInput, 0};
NTO::NodeLocation q{NTO::NodeType::kOutput, 0};
@ -224,7 +239,9 @@ VariadicReplaceWithQLinear::VariadicReplaceWithQLinear(std::string domain)
ConvReplaceWithQLinear::ConvReplaceWithQLinear()
: ReplaceWithQLinear(kOnnxDomain, ConvMoves()) {
}
WhereReplaceWithQLinear::WhereReplaceWithQLinear()
: ReplaceWithQLinear(kMSDomain, WhereMoves()) {
}
MatMulReplaceWithQLinear::MatMulReplaceWithQLinear()
: matmul_int_to_float_replacer_{MatMulIntToFloatReplacer()},
qlinear_matmul_replacer_{kOnnxDomain} {

View file

@ -59,7 +59,9 @@ struct VariadicReplaceWithQLinear : ReplaceWithQLinear {
struct ConvReplaceWithQLinear : ReplaceWithQLinear {
ConvReplaceWithQLinear();
};
struct WhereReplaceWithQLinear : ReplaceWithQLinear {
WhereReplaceWithQLinear();
};
struct SplitReplaceWithQuant : public Action {
Status Run(Graph&, const NodesToOptimize& selected_nodes) const override;
};

View file

@ -206,6 +206,26 @@ void GemmQDQRules(SelectorActionRegistry& qdq_selector_action_registry) {
#endif
}
void WhereQDQRules (SelectorActionRegistry& qdq_selector_action_registry) {
// 3 nodes. 2 x DQ for inputs and 1X Q for output
// Compare to other BinaryOperators (Add, Mul), Where also have a special case that it has boolean input
// Where Replace with QLinearWhere
// Delete all original nodes.
const std::string action_name{"Where"};
std::unique_ptr<Action> action = std::make_unique<QDQ::WhereReplaceWithQLinear>();
#if !defined(ORT_MINIMAL_BUILD)
std::unique_ptr<NodeSelector> selector = std::make_unique<QDQ::WhereSelector>();
qdq_selector_action_registry.RegisterSelectorAndAction(action_name,
{{"Where", {}}},
std::move(selector),
std::move(action));
#else
qdq_selector_action_registry.RegisterAction(action_name, std::move(action));
#endif
}
SelectorActionRegistry CreateSelectorActionRegistry(bool is_int8_allowed) {
SelectorActionRegistry qdq_selector_action_registry;
SplitQDQRules(qdq_selector_action_registry);
@ -217,6 +237,7 @@ SelectorActionRegistry CreateSelectorActionRegistry(bool is_int8_allowed) {
ConvQDQRules(qdq_selector_action_registry, is_int8_allowed);
MatMulQDQRules(qdq_selector_action_registry, is_int8_allowed);
GemmQDQRules(qdq_selector_action_registry);
WhereQDQRules(qdq_selector_action_registry);
return qdq_selector_action_registry;
}

View file

@ -309,6 +309,22 @@ void GemmSelector::UpdateBuilder(NodesToOptimizeIndicesBuilder& builder) const {
builder.input_nodes.resize(3, NodesToOptimizeIndices::kEmptyNodeIndex);
}
bool WhereNodeGroupSelector::Check(const GraphViewer &graph_viewer, const Node &node,
const std::vector<const Node *> &dq_nodes,
const std::vector<const Node *> &q_nodes) const {
// Where has 1 boolean input and 2 dq inputs
if (!CheckQDQNodes(graph_viewer, node, dq_nodes, q_nodes,2)) {
return false;
}
const int32_t dt_input_1 = dq_nodes[0]->InputDefs()[0]->TypeAsProto()->tensor_type().elem_type();
const int32_t dt_input_2 = dq_nodes[1]->InputDefs()[0]->TypeAsProto()->tensor_type().elem_type();
const int32_t dt_output = q_nodes[0]->OutputDefs()[0]->TypeAsProto()->tensor_type().elem_type();
return dt_input_1 == dt_input_2 &&
dt_input_1 == dt_output;
}
} // namespace QDQ
} // namespace onnxruntime

View file

@ -103,6 +103,17 @@ class ConvNodeGroupSelector : public NodeGroupSelector {
bool int8_allowed_;
};
class WhereNodeGroupSelector : public NodeGroupSelector {
public:
WhereNodeGroupSelector()= default;
private:
bool Check(const GraphViewer& graph_viewer, const Node& node,
const std::vector<const Node*>& dq_nodes,
const std::vector<const Node*>& q_nodes) const override;
};
// 2 DQ nodes for input -> node -> optional Q if QLinearMatMul, MatMulIntegerToFloat if not
// The lack of a trailing Q isn't really a QDQ node group, so we default support for that to off.
class MatMulNodeGroupSelector : public NodeGroupSelector {
@ -200,7 +211,10 @@ class ConvSelector : public BaseSelector {
void UpdateBuilder(NodesToOptimizeIndicesBuilder&) const override;
};
class WhereSelector : public BaseSelector {
public:
WhereSelector() : BaseSelector(std::make_unique<WhereNodeGroupSelector>()) {}
};
// 2 DQ nodes for input -> node -> optional Q if QLinearMatMul, MatMulIntegerToFloat if not
class MatMulSelector : public BaseSelector {
public:

View file

@ -1057,7 +1057,6 @@ class ONNXQuantizer:
if node.input[0] not in self.tensors_range.keys() or node.output[0] not in self.tensors_range.keys():
continue
self.tensors_range[node.input[0]] = self.tensors_range[node.output[0]]
quantization_params = {}
for tensor_name in self.tensors_range.keys():
rmin, rmax = self.tensors_range[tensor_name]

View file

@ -0,0 +1,87 @@
import onnx
from ..quant_utils import TENSOR_NAME_QUANT_SUFFIX, QuantizedValue, QuantizedValueType, attribute_to_kwarg, ms_domain
from .base_operator import QuantOperatorBase
from .qdq_base_operator import QDQOperatorBase
class QLinearWhere(QuantOperatorBase):
def should_quantize(self):
return True
def quantize(self):
node = self.node
assert node.op_type == "Where"
if not self.quantizer.force_quantize_no_input_check:
self.quantizer.new_nodes += [node]
return
(
data_found,
output_scale_name,
output_zp_name,
_,
_,
) = self.quantizer._get_quantization_params(node.output[0])
(
q_input_names,
zero_point_names,
scale_names,
nodes,
) = self.quantizer.quantize_activation(node, [1, 2])
if not data_found or q_input_names is None:
return super().quantize()
qlinear_output = node.output[0] + TENSOR_NAME_QUANT_SUFFIX
qlinear_output_name = node.name + "_quant" if node.name != "" else ""
q_output = QuantizedValue(
node.output[0],
qlinear_output,
output_scale_name,
output_zp_name,
QuantizedValueType.Input,
)
self.quantizer.quantized_value_map[node.output[0]] = q_output
kwargs = {}
for attribute in node.attribute:
kwargs.update(attribute_to_kwarg(attribute))
kwargs["domain"] = ms_domain
qlwhere_inputs = [
node.input[0],
q_input_names[0],
scale_names[0],
zero_point_names[0],
q_input_names[1],
scale_names[1],
zero_point_names[1],
output_scale_name,
output_zp_name,
]
qlwhere_node = onnx.helper.make_node(
"QLinearWhere", qlwhere_inputs, [qlinear_output], qlinear_output_name, **kwargs
)
self.quantizer.new_nodes += nodes
self.quantizer.new_nodes += [qlwhere_node]
class QDQWhere(QDQOperatorBase):
def quantize(self):
node = self.node
assert node.op_type == "Where"
if self.quantizer.force_quantize_no_input_check:
if not self.quantizer.is_tensor_quantized(node.input[1]):
self.quantizer.quantize_activation_tensor(node.input[1])
if not self.quantizer.is_tensor_quantized(node.input[2]):
self.quantizer.quantize_activation_tensor(node.input[2])
if not self.disable_qdq_for_node_output:
for output in node.output:
self.quantizer.quantize_activation_tensor(output)
elif (
self.quantizer.is_tensor_quantized(node.input[1])
and self.quantizer.is_tensor_quantized(node.input[2])
and not self.disable_qdq_for_node_output
):
for output in node.output:
self.quantizer.quantize_activation_tensor(output)

View file

@ -19,6 +19,7 @@ from .operators.qdq_base_operator import QDQOperatorBase
from .operators.resize import QDQResize, QResize
from .operators.softmax import QDQSoftmax, QLinearSoftmax
from .operators.split import QDQSplit, QSplit
from .operators.where import QDQWhere, QLinearWhere
from .quant_utils import QuantizationMode
CommonOpsRegistry = {
@ -57,6 +58,7 @@ QLinearOpsRegistry = {
"AveragePool": QLinearPool,
"Concat": QLinearConcat,
"Softmax": QLinearSoftmax,
"Where": QLinearWhere,
}
QLinearOpsRegistry.update(CommonOpsRegistry)
@ -76,6 +78,7 @@ QDQRegistry = {
"Split": QDQSplit,
"Gather": QDQGather,
"Softmax": QDQSoftmax,
"Where": QDQWhere,
}

View file

@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "gtest/gtest.h"
#include "test/providers/provider_test_utils.h"
#include <array>
namespace onnxruntime {
namespace test {
template <typename T>
void RunQLinearWhere(
OpTester &test,
const std::vector<T> &x,
const std::vector<T> &y,
const std::vector<T> &z,
float x_scale,
T x_zero_point,
float y_scale,
T y_zero_point,
float z_scale,
T z_zero_point,
const std::vector<int64_t> &x_shape,
const std::vector<int64_t> &y_shape,
const std::vector<int64_t> &z_shape) {
test.AddInput<T>("X", x_shape, x);
test.AddInput<float>("x_scale", {}, {x_scale}, true);
test.AddInput<T>("x_zero_point", {}, {x_zero_point}, true);
test.AddInput<T>("Y", y_shape, y);
test.AddInput<float>("y_scale", {}, {y_scale}, true);
test.AddInput<T>("y_zero_point", {}, {y_zero_point}, true);
test.AddInput<float>("z_scale", {}, {z_scale});
test.AddInput<T>("z_zero_point", {}, {z_zero_point});
test.AddOutput<T>("Z", z_shape, z);
test.Run(OpTester::ExpectResult::kExpectSuccess);
}
template <typename T>
void QLinearWhereScalarAll() {
constexpr float scale = 0.039f;
constexpr uint8_t zp = 135;
OpTester test("QLinearWhere", 1, onnxruntime::kMSDomain);
test.AddInput<bool>("condition", {1}, {true}, true );
RunQLinearWhere<T>(
test,
{1}, {2}, {1},// x ,y ,z
scale, zp, scale, zp, scale, zp, // x_scale, x_zp, y_scale, y_zp, z_scale, z_zp
{1}, {1}, {1});
}
TEST(QLinearWhereTest, QLinearWhereScalarAll) {
QLinearWhereScalarAll<int8_t>();
QLinearWhereScalarAll<uint8_t>();
}
template <typename T>
void QLinearWhereVectorAll() {
constexpr float scale = 0.039f;
constexpr uint8_t zp = 135;
OpTester test("QLinearWhere", 1, onnxruntime::kMSDomain);
test.AddInput<bool>("condition", {4}, {true,false,false,true}, true );
RunQLinearWhere<T>(
test,
{1,1,1,1}, {2,2,2,2}, {1,2,2,1},// x ,y ,z
scale, zp, scale, zp, scale, zp, // x_scale, x_zp, y_scale, y_zp, z_scale, z_zp
{4}, {4}, {4});
}
TEST(QLinearWhereTest,QLinearWhereVectorAll){
QLinearWhereVectorAll<int8_t>();
QLinearWhereVectorAll<uint8_t>();
}
template <typename T>
void QLinearWhereMatrixAll() {
constexpr float scale = 0.039f;
constexpr uint8_t zp = 135;
OpTester test("QLinearWhere", 1, onnxruntime::kMSDomain);
test.AddInput<bool>("condition", {2,2}, {true,false,false,true}, true );
RunQLinearWhere<T>(
test,
{1,1,1,1}, {2,2,2,2}, {1,2,2,1},// x ,y ,z
scale, zp, scale, zp, scale, zp, // x_scale, x_zp, y_scale, y_zp, z_scale, z_zp
{2,2}, {2,2}, {2,2});
}
TEST(QLinearWhereTest,QLinearWhereMatrixAll){
QLinearWhereMatrixAll<int8_t>();
QLinearWhereMatrixAll<uint8_t>();
}
template <typename T>
void QLinearWhereScalarX_VectorY_MatrixCondition() {
constexpr float scale = 0.039f;
constexpr uint8_t zp = 135;
OpTester test("QLinearWhere", 1, onnxruntime::kMSDomain);
test.AddInput<bool>("condition", {2,2}, {true,false,false,true}, true );
RunQLinearWhere<T>(
test,
{1}, {2,2}, {1,2,2,1},// x ,y ,z
scale, zp, scale, zp, scale, zp, // x_scale, x_zp, y_scale, y_zp, z_scale, z_zp
{1}, {2}, {2,2});
}
TEST(QLinearWhereTest,QLinearWhereScalarX_VectorY_MatrixCondition){
QLinearWhereScalarX_VectorY_MatrixCondition<int8_t>();
QLinearWhereScalarX_VectorY_MatrixCondition<uint8_t>();
}
template <typename T>
void QLinearWhereVectorX_VectorY_MatrixCondition() {
constexpr float scale = 0.039f;
constexpr uint8_t zp = 135;
OpTester test("QLinearWhere", 1, onnxruntime::kMSDomain);
test.AddInput<bool>("condition", {2,2}, {true,false,false,true}, true );
RunQLinearWhere<T>(
test,
{1,1}, {2,2}, {1,2,2,1},// x ,y ,z
scale, zp, scale, zp, scale, zp, // x_scale, x_zp, y_scale, y_zp, z_scale, z_zp
{2}, {2}, {2,2});
}
TEST(QLinearWhereTest,QLinearWhereVectorX_VectorY_MatrixCondition){
QLinearWhereVectorX_VectorY_MatrixCondition<int8_t>();
QLinearWhereVectorX_VectorY_MatrixCondition<uint8_t>();
}
} // namespace test
} // namespace onnxruntime

View file

@ -389,6 +389,37 @@ GetQDQTestCaseFn BuildQDQSplitTestCase(
builder.AddQuantizeLinearNode<OutputType>(split_output_3, .003f, q_zp, q_split_output_3);
};
}
template <typename InputType>
GetQDQTestCaseFn BuildQDQWhereTestCase(
const std::vector<int64_t>& cond_shape,
const std::vector<int64_t>& x_shape,
const std::vector<int64_t>& y_shape
) {
return [cond_shape,x_shape,y_shape](ModelTestBuilder& builder)
{
auto* input_cond_arg = builder.MakeInputBool(cond_shape);
auto* input_x_arg = builder.MakeInput<InputType>(x_shape,
std::numeric_limits<InputType>::min(),
std::numeric_limits<InputType>::max());
auto* input_y_arg = builder.MakeInput<InputType>(y_shape,
std::numeric_limits<InputType>::min(),
std::numeric_limits<InputType>::max());
InputType zp = std::numeric_limits<InputType>::max() / 2;
constexpr float scale = 0.003f;
auto* dq_x_output = builder.MakeIntermediate();
auto* dq_y_output = builder.MakeIntermediate();
builder.AddDequantizeLinearNode<InputType>(input_x_arg, scale, zp, dq_x_output);
builder.AddDequantizeLinearNode<InputType>(input_y_arg, scale, zp, dq_y_output);
// add Where
auto* where_output = builder.MakeIntermediate();
builder.AddNode("Where", {input_cond_arg,dq_x_output,dq_y_output}, {where_output});
// add Q
auto* q_where_output = builder.MakeOutput();
builder.AddQuantizeLinearNode<InputType>(where_output, scale, zp, q_where_output); // Model input (node_token_1)
};
}
template <typename InputType, typename OutputType>
GetQDQTestCaseFn BuildQDQTransposeTestCase(

View file

@ -779,6 +779,21 @@ TEST(QDQTransformerTests, Split) {
};
test_case({6, 18, 54}, 0);
}
TEST(QDQTransformerTests, Where) {
auto test_case = [&](const std::vector<int64_t>& cond_shape, const std::vector<int64_t>& x_shape,const std::vector<int64_t>& y_shape) {
auto check_graph = [&](InferenceSessionWrapper& session) {
auto op_to_count = CountOpsInGraph(session.GetGraph());
EXPECT_EQ(op_to_count["com.microsoft.QLinearWhere"], 1);
EXPECT_EQ(op_to_count["QuantizeLinear"], 0);
EXPECT_EQ(op_to_count["DequantizeLinear"], 0);
};
TransformerTester(BuildQDQWhereTestCase<int8_t>(cond_shape,x_shape,y_shape),
check_graph,
TransformerLevel::Level1,
TransformerLevel::Level2);
};
test_case({1},{1},{1});
}
TEST(QDQTransformerTests, Transpose) {
auto test_case = [&](const std::vector<int64_t>& input_shape, const std::vector<int64_t>& perms) {

View file

@ -0,0 +1,155 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
import unittest
import numpy as np
from onnx import TensorProto, helper, save
from op_test_utils import TestDataFeeds, check_model_correctness, check_op_type_count, check_qtype_by_node_type
from onnxruntime.quantization import QuantFormat, QuantType, quantize_static
class TestWhereModel(unittest.TestCase):
@staticmethod
def input_feeds_for_where(n, name2shape):
input_data_list = []
for i in range(n):
inputs = {}
for name, shape in name2shape.items():
if name == "condition":
inputs.update({name: np.ones(shape).astype(np.bool_)})
else:
inputs.update({name: np.ones(shape).astype(np.float32)})
input_data_list.extend([inputs])
dr = TestDataFeeds(input_data_list)
return dr
@staticmethod
def construct_model(model_path, input_shape):
initializers = []
input_condition = helper.make_tensor_value_info("condition", TensorProto.BOOL, input_shape)
input_x = helper.make_tensor_value_info("x", TensorProto.FLOAT, input_shape)
input_y = helper.make_tensor_value_info("y", TensorProto.FLOAT, input_shape)
out_put = helper.make_tensor_value_info("z", TensorProto.FLOAT, input_shape)
node = helper.make_node(
"Where",
inputs=["condition", "x", "y"],
outputs=["z"],
name="where_node",
)
graph = helper.make_graph(
[node],
"quant_where_op_test",
[input_condition, input_x, input_y],
[out_put],
initializer=initializers,
)
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 16)])
save(model, model_path)
def quantize_where_test(self, activation_type, weight_type, extra_options={}):
model_fp32_path = "where_fp32.onnx"
input_shape = [2, 2]
self.construct_model(model_fp32_path, input_shape)
data_reader = self.input_feeds_for_where(
1,
{
"condition": input_shape,
"x": input_shape,
"y": input_shape,
},
)
activation_proto_qtype = TensorProto.UINT8 if activation_type == QuantType.QUInt8 else TensorProto.INT8
activation_type_str = "u8" if (activation_type == QuantType.QUInt8) else "s8"
weight_type_str = "u8" if (weight_type == QuantType.QUInt8) else "s8"
model_uint8_path = f"where_{activation_type_str}{weight_type_str}_{'QNoInCk' if extra_options['ForceQuantizeNoInputCheck'] else 'NoQNoInCk'}.onnx"
model_uint8_qdq_path = f"where_{activation_type_str}{weight_type_str}_{'QNoInCk' if extra_options['ForceQuantizeNoInputCheck'] else 'NoQNoInCk'}_qdq.onnx"
# Verify QOperator mode
data_reader.rewind()
quantize_static(
model_fp32_path,
model_uint8_path,
data_reader,
quant_format=QuantFormat.QOperator,
activation_type=activation_type,
weight_type=weight_type,
extra_options=extra_options,
)
qnode_counts = (
{
"QLinearWhere": 1,
"QuantizeLinear": 2,
"DequantizeLinear": 1,
}
if extra_options["ForceQuantizeNoInputCheck"]
else {
"Where": 1,
"QuantizeLinear": 0,
"DequantizeLinear": 0,
}
)
check_op_type_count(self, model_uint8_path, **qnode_counts)
qnode_io_qtypes = {
"QuantizeLinear": [
["i", 2, activation_proto_qtype],
["o", 0, activation_proto_qtype],
]
}
check_qtype_by_node_type(self, model_uint8_path, qnode_io_qtypes)
data_reader.rewind()
check_model_correctness(self, model_fp32_path, model_uint8_path, data_reader.get_next())
# Verify QDQ mode
data_reader.rewind()
quantize_static(
model_fp32_path,
model_uint8_qdq_path,
data_reader,
quant_format=QuantFormat.QDQ,
activation_type=activation_type,
weight_type=weight_type,
extra_options=extra_options,
)
qdqnode_counts = (
{
"Where": 1,
"QuantizeLinear": 3,
"DequantizeLinear": 3,
}
if extra_options["ForceQuantizeNoInputCheck"]
else {
"Where": 1,
"QuantizeLinear": 0,
"DequantizeLinear": 0,
}
)
check_op_type_count(self, model_uint8_qdq_path, **qdqnode_counts)
qnode_io_qtypes = {
"QuantizeLinear": [
["i", 2, activation_proto_qtype],
["o", 0, activation_proto_qtype],
]
}
check_qtype_by_node_type(self, model_uint8_qdq_path, qnode_io_qtypes)
data_reader.rewind()
check_model_correctness(self, model_fp32_path, model_uint8_qdq_path, data_reader.get_next())
def test_quantize_where_u8u8(self):
self.quantize_where_test(QuantType.QUInt8, QuantType.QUInt8, extra_options={"ForceQuantizeNoInputCheck": True})
print(__name__)
def test_quantize_where_u8u8_no_ForceQuantizeNoInputCheck(self):
self.quantize_where_test(QuantType.QUInt8, QuantType.QUInt8, extra_options={"ForceQuantizeNoInputCheck": False})
print(__name__)
if __name__ == "__main__":
unittest.main()