diff --git a/include/onnxruntime/core/framework/op_kernel.h b/include/onnxruntime/core/framework/op_kernel.h index bfb63f9e49..6249d845bd 100644 --- a/include/onnxruntime/core/framework/op_kernel.h +++ b/include/onnxruntime/core/framework/op_kernel.h @@ -158,6 +158,11 @@ class OpKernelContext { */ Fence_t OutputFence(int index) const; + /** + Returns the opset domain of the underlying kernel + **/ + const std::string& GetOpDomain() const; + protected: onnxruntime::NodeIndex GetNodeIndex() const; diff --git a/onnxruntime/contrib_ops/cpu/quantize_ops.cc b/onnxruntime/contrib_ops/cpu/quantize_ops.cc new file mode 100644 index 0000000000..fb4a95b7dd --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/quantize_ops.cc @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/providers/cpu/tensor/quantize_linear.h" +#include "core/providers/common.h" + + +namespace onnxruntime { +namespace contrib { + +ONNX_CPU_OPERATOR_TYPED_MS_KERNEL( + DequantizeLinear, + 1, + uint8_t, + KernelDefBuilder() + .TypeConstraint("axis", DataTypeImpl::GetType()) + .TypeConstraint("x", DataTypeImpl::GetTensorType()) + .TypeConstraint("x_scale", DataTypeImpl::GetTensorType()) + .TypeConstraint("x_zero_point", DataTypeImpl::GetTensorType()) + .TypeConstraint("y", DataTypeImpl::GetTensorType()), + DequantizeLinear); + +ONNX_CPU_OPERATOR_TYPED_MS_KERNEL( + DequantizeLinear, + 1, + int8_t, + KernelDefBuilder() + .TypeConstraint("axis", DataTypeImpl::GetType()) + .TypeConstraint("x", DataTypeImpl::GetTensorType()) + .TypeConstraint("x_scale", DataTypeImpl::GetTensorType()) + .TypeConstraint("x_zero_point", DataTypeImpl::GetTensorType()) + .TypeConstraint("y", DataTypeImpl::GetTensorType()), + DequantizeLinear); + +ONNX_CPU_OPERATOR_TYPED_MS_KERNEL( + QuantizeLinear, + 1, + uint8_t, + KernelDefBuilder() + .TypeConstraint("axis", DataTypeImpl::GetType()) + .TypeConstraint("x", DataTypeImpl::GetTensorType()) + .TypeConstraint("y_scale", DataTypeImpl::GetTensorType()) + .TypeConstraint("y_zero_point", DataTypeImpl::GetTensorType()) + .TypeConstraint("y", DataTypeImpl::GetTensorType()), + QuantizeLinear); + +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu_contrib_kernels.cc b/onnxruntime/contrib_ops/cpu_contrib_kernels.cc index 7124360dc6..3d05e8a28f 100644 --- a/onnxruntime/contrib_ops/cpu_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/cpu_contrib_kernels.cc @@ -24,6 +24,9 @@ class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, Pad); class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, Unique); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, ConvTransposeWithDynamicPads); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, CropAndResize); +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); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, uint8_t, QuantizeLinear); // This section includes all opkernel declarations for former experimental ops which have now been removed from onnx. // To maintain backward compatibility these are added as contrib ops. @@ -94,6 +97,9 @@ void RegisterCpuContribKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, // These ops were experimental ops in onnx domain which have been removed now. We add them here as // contrib ops to main backward compatibility diff --git a/onnxruntime/core/framework/op_kernel.cc b/onnxruntime/core/framework/op_kernel.cc index b20930f6f6..10127b4fb9 100644 --- a/onnxruntime/core/framework/op_kernel.cc +++ b/onnxruntime/core/framework/op_kernel.cc @@ -126,6 +126,10 @@ onnxruntime::NodeIndex OpKernelContext::GetNodeIndex() const { return kernel_->Node().Index(); } +const std::string& OpKernelContext::GetOpDomain() const { + return kernel_->KernelDef().Domain(); +} + const OrtValue* OpKernelContext::GetInputMLValue(int index) const { if (index < 0 || index >= InputCount()) return nullptr; diff --git a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc index 66d8547446..383adf9286 100644 --- a/onnxruntime/core/graph/contrib_ops/contrib_defs.cc +++ b/onnxruntime/core/graph/contrib_ops/contrib_defs.cc @@ -1053,6 +1053,119 @@ activation and leaky_relu_alpha.)DOC") ONNX_CONTRIB_OPERATOR_SCHEMA_ELSEWHERE(AttnLSTM, RegisterAttnLSTMContribOpSchema); ONNX_CONTRIB_OPERATOR_SCHEMA_ELSEWHERE(Range, RegisterRangeOpSchema); + static const char* QuantizeLinear_ver1_doc = R"DOC( +The linear quantization operator. It consumes a full precision data, a scale, a zero point and computes the quantized data. +The quantization formula is y = (x / y_scale) + y_zero_point. For (x / y_scale), it computes the nearest integer value to arg (in floating-point format), + rounding halfway cases away from zero. Scale and zero point must have same shape. They must be either scalar (per tensor) or 1-D tensor (per 'axis').)DOC"; + + ONNX_CONTRIB_OPERATOR_SCHEMA(QuantizeLinear) + .SetDomain(kMSDomain) + .SinceVersion(1) + .Attr( + "axis", + "The axis along which same quantization parameters are applied. It's optional." + "If it's not specified, it means per-tensor quantization and input 'x_scale' and 'x_zero_point' must be scalars." + "If it's specified, it means per 'axis' quantization and input 'x_scale' and 'x_zero_point' must be 1-D tensors.", + AttributeProto::INT, + false) + .Input( + 0, + "x", + "N-D full precision Input tensor to be quantized.", + "T1") + .Input( + 1, + "y_scale", + "Scale for doing quantization to get 'y'. It could be a scalar or a 1-D tensor," + "which means a per-tensor or per-axis quantization. If it's a 1-D tensor, " + "its number of elements should be equal to the dimension value of 'axis' dimension of input 'x'.", + "T1") + .Input( + 2, + "y_zero_point", + "Zero point for doing quantization to get 'y'. It could be a scalar or a 1-D tensor, which means a per-tensor" + "or per-axis quantization. If it's a 1-D tensor, its number of elements should be equal to the dimension value of 'axis' dimension of input 'x'.", + "T2") + .Output( + 0, + "y", + "N-D quantized output tensor. It has same shape as input 'x'.", + "T2") + .TypeConstraint( + "T1", + {"tensor(float)"}, + "Constrain 'x', 'y_scale' to float tensors.") + .TypeConstraint( + "T2", + {"tensor(int8)", "tensor(uint8)"}, + "Constrain 'y_zero_point' and 'y' to 8-bit integer tensors.") + .SetDoc(QuantizeLinear_ver1_doc) + .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { + propagateElemTypeFromInputToOutput(ctx, 2, 0); + + if (!hasInputShape(ctx, 0)) + return; + + auto& input_shape = getInputShape(ctx, 0); + updateOutputShape(ctx, 0, input_shape); + }); + + static const char* DequantizeLinear_ver1_doc = R"DOC( +The linear dequantization operator. It consumes a quantized data, a scale, a zero point and computes the full precision data. +The dequantization formula is y = (x - x_zero_point) * x_scale. +Scale and zero point must have same shape. They must be either scalar (per tensor) or 1-D tensor (per 'axis').)DOC"; + + ONNX_CONTRIB_OPERATOR_SCHEMA(DequantizeLinear) + .SetDomain(kMSDomain) + .SinceVersion(1) + .Attr("axis", + "The axis along which same quantization parameters are applied. It's optional." + "If it's not specified, it means per-tensor quantization and input 'x_scale' and 'x_zero_point' must be scalars." + "If it's specified, it means per 'axis' quantization and input 'x_scale' and 'x_zero_point' must be 1-D tensors.", + AttributeProto::INT, + false) + .Input(0, + "x", + "N-D quantized Input tensor to be de-quantized.", + "T2") + .Input( + 1, + "x_scale", + "Scale for input 'x'. It could be a scalar or a 1-D tensor, which means a per-tensor or per-axis quantization." + "If it's a 1-D tensor, its number of elements should be equal to the dimension value of 'axis' dimension of input 'x'.", + "T1") + .Input( + 2, + "x_zero_point", + "Zero point for input 'x'. It could be a scalar or a 1-D tensor, which means a per-tensor or per-axis quantization." + "If it's a 1-D tensor, its number of elements should be equal to the dimension value of 'axis' dimension of input 'x'.", + "T2") + .Output( + 0, + "y", + "N-D full precision output tensor. It has same shape as input 'x'.", + "T1") + .TypeConstraint( + "T1", + {"tensor(float)"}, + "Constrain 'y', 'x_scale' to float tensors.") + .TypeConstraint( + "T2", + {"tensor(int8)", "tensor(uint8)"}, + "Constrain 'x_zero_point' and 'x' to 8-bit integer tensors.") + .SetDoc(DequantizeLinear_ver1_doc) + .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { + auto y_type = ctx.getOutputType(0); + // only float is supported + y_type->mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto::FLOAT); + + if (!hasInputShape(ctx, 0)) + return; + + auto& input_shape = getInputShape(ctx, 0); + updateOutputShape(ctx, 0, input_shape); + }); + static const char* Tokenizer_ver1_doc = R"DOC( Tokenizer divides each string in X into a vector of strings along the last axis. Allowed input shapes are [C] and [N, C]. If the maximum number of tokens found per input string is D, the output shape would be [N, C, D] when input shape is [N, C]. diff --git a/onnxruntime/core/providers/cpu/tensor/quantize_linear.cc b/onnxruntime/core/providers/cpu/tensor/quantize_linear.cc index e345ad4da3..5004300e18 100644 --- a/onnxruntime/core/providers/cpu/tensor/quantize_linear.cc +++ b/onnxruntime/core/providers/cpu/tensor/quantize_linear.cc @@ -2,7 +2,6 @@ // Licensed under the MIT License. #include "core/providers/cpu/tensor/quantize_linear.h" -#include "core/providers/cpu/math/element_wise_ops.h" #include "core/providers/common.h" #include #include @@ -38,23 +37,49 @@ Status DequantizeLinear::Compute(OpKernelContext* ctx) const { auto& x_scale = *ctx->Input(1); auto& x_zero_point = *ctx->Input(2); auto& y = *ctx->Output(0, x.Shape()); - const auto& x_shape = x.Shape(); - const auto& scale_shape = x_scale.Shape(); - const auto& zero_point_shape = x_zero_point.Shape(); - // enforce that scale and zero point are scalars or 1D tensors with size == 1 - ORT_ENFORCE(scale_shape.NumDimensions() == 0 || (scale_shape.NumDimensions() == 1 && scale_shape.GetDims().size() == 1), "x_scale must be a scalar."); - ORT_ENFORCE(zero_point_shape.NumDimensions() == 0 || (zero_point_shape.NumDimensions() == 1 && zero_point_shape.GetDims().size() == 1), "x_zero_point must be a scalar."); + int64_t axis = 0; + int64_t broadcastDim = x_shape[axis]; + size_t stride = 0; - const T zero_point = *(x_zero_point.template Data()); - const float scale = *(x_scale.template Data()); + if (has_axis_) { + axis = HandleNegativeAxis(axis_, x_shape.NumDimensions()); + broadcastDim = x_shape[axis]; + stride = 1; + + // if an axis was specified, ensure the scale and zero point are compatible + ORT_ENFORCE(x_scale.Shape().NumDimensions() == 1 && x_scale.Shape().Size() == broadcastDim, "x_scale must be 1D tensor with size ", broadcastDim); + ORT_ENFORCE(x_zero_point.Shape().NumDimensions() == 1 && x_zero_point.Shape().Size() == broadcastDim, "x_zero_point must be 1D tensor with size ", broadcastDim); + } else { + // if no axis, enforce that scale and zero point are scalars + ORT_ENFORCE(IsScalarOr1ElementVector(&x_scale), "x_scale must be a scalar or 1D tensor or size 1."); + ORT_ENFORCE(IsScalarOr1ElementVector(&x_zero_point), "x_zero_point must be a scalar or 1D tensor or size 1."); + } + + size_t N = x_shape.SizeToDimension(axis); + size_t block_size = x_shape.SizeFromDimension(axis + 1); + + const T* zero_point = x_zero_point.template Data(); + const float* scale = x_scale.template Data(); const T* input = x.template Data(); - auto* output = y.template MutableData(); - const auto num_of_elements = x_shape.Size(); + float* output = y.template MutableData(); - for (int i = 0; i < num_of_elements; ++i) { - output[i] = static_cast(static_cast(input[i]) - zero_point) * scale; + for (size_t n = 0; n < N; n++) { + const float* current_scale = scale; + const T* current_zero_point = zero_point; + + for (size_t bd = 0; bd < static_cast(broadcastDim); bd++) { + auto zp = static_cast(*current_zero_point); + auto sc = *current_scale; + + for (size_t bs = 0; bs < block_size; bs++) { + *output++ = static_cast(static_cast(*input++) - zp) * sc; + } + + current_scale += stride; + current_zero_point += stride; + } } return Status::OK(); @@ -85,7 +110,6 @@ static float RoundHalfToEven(float input) { auto result = std::nearbyintf(input); return result; } - template // formula is Y = X / Scale + ZeroPoint Status QuantizeLinear::Compute(OpKernelContext* ctx) const { @@ -93,28 +117,67 @@ Status QuantizeLinear::Compute(OpKernelContext* ctx) const { auto& y_scale = *ctx->Input(1); auto& y_zero_point = *ctx->Input(2); auto& y = *ctx->Output(0, x.Shape()); - const auto& x_shape = x.Shape(); - const auto& scale_shape = y_scale.Shape(); - const auto& zero_point_shape = y_zero_point.Shape(); + const float* input = x.template Data(); + T* output = y.template MutableData(); - //enforce that scale and zero point are scalars or 1D tensors with size == 1 - ORT_ENFORCE(scale_shape.NumDimensions() == 0 || (scale_shape.NumDimensions() == 1 && scale_shape.GetDims().size() == 1), "x_scale must be a scalar."); - ORT_ENFORCE(zero_point_shape.NumDimensions() == 0 || (zero_point_shape.NumDimensions() == 1 && zero_point_shape.GetDims().size() == 1), "x_zero_point must be a scalar."); - - const T zero_point = *(y_zero_point.template Data()); - const float scale = *(y_scale.template Data()); - const auto* input = x.template Data(); - auto* output = y.template MutableData(); - const auto num_of_elements = x_shape.Size(); const float qmax = std::numeric_limits::max(); const float qmin_default = std::numeric_limits::min(); // adjust qmin for int8 inputs. This is required to keep zero point as zero const float qmin = qmin_default == -128 ? -127 : qmin_default; - for (int i = 0; i < num_of_elements; ++i) { - output[i] = static_cast(clamp(RoundHalfToEven(static_cast(input[i]/scale)) + zero_point, qmin, qmax)); + // Schema of QuantizeLinearOp changed when it was promoted to onnx domain. In order to maintain backward compatiblity + // both the versions need to be supported. + if (ctx->GetOpDomain() != kMSDomain) { + ORT_ENFORCE(IsScalarOr1ElementVector(&y_scale), "x_scale must be a scalar or 1D tensor or size 1."); + ORT_ENFORCE(IsScalarOr1ElementVector(&y_zero_point), "x_zero_point must be a scalar or 1D tensor or size 1."); + + const T zero_point = *(y_zero_point.template Data()); + const float scale = *(y_scale.template Data()); + const auto num_of_elements = x_shape.Size(); + + for (int i = 0; i < num_of_elements; ++i) { + output[i] = static_cast(clamp(RoundHalfToEven(static_cast(input[i] / scale)) + zero_point, qmin, qmax)); + } + + } else { + size_t stride = 0; + const int64_t axis = HandleNegativeAxis(axis_, x_shape.NumDimensions()); + const auto& broadcastDim = x_shape[axis]; + + if (has_axis_) { + // if an axis was specified, ensure the scale and zero point are compatible + ORT_ENFORCE(y_scale.Shape().NumDimensions() == 1 && y_scale.Shape().Size() == broadcastDim, "x_scale must be 1D tensor with size ", broadcastDim); + ORT_ENFORCE(y_zero_point.Shape().NumDimensions() == 1 && y_zero_point.Shape().Size() == broadcastDim, "x_zero_point must be 1D tensor with size ", broadcastDim); + stride = 1; + } else { + // if no axis, enforce that scale and zero point are scalars + ORT_ENFORCE(IsScalarOr1ElementVector(&y_scale), "x_scale must be a scalar or 1D tensor or size 1."); + ORT_ENFORCE(IsScalarOr1ElementVector(&y_zero_point), "x_zero_point must be a scalar or 1D tensor or size 1."); + } + + size_t N = x_shape.SizeToDimension(axis); + size_t block_size = x_shape.SizeFromDimension(axis + 1); + const T* zero_point = y_zero_point.template Data(); + const float* scale = y_scale.template Data(); + + for (size_t n = 0; n < N; n++) { + const float* current_scale = scale; + const T* current_zero_point = zero_point; + + for (size_t bd = 0; bd < static_cast(broadcastDim); bd++) { + float zp = *current_zero_point; + auto sc = *current_scale; + + for (size_t bs = 0; bs < block_size; bs++) { + *output++ = static_cast(clamp(std::round(static_cast(*input++) / sc) + zp, qmin, qmax)); + } + + current_scale += stride; + current_zero_point += stride; + } + } } return Status::OK(); diff --git a/onnxruntime/core/providers/cpu/tensor/quantize_linear.h b/onnxruntime/core/providers/cpu/tensor/quantize_linear.h index 99429ef439..073edcf297 100644 --- a/onnxruntime/core/providers/cpu/tensor/quantize_linear.h +++ b/onnxruntime/core/providers/cpu/tensor/quantize_linear.h @@ -12,16 +12,28 @@ namespace onnxruntime { template class DequantizeLinear final : public OpKernel { public: - DequantizeLinear(const OpKernelInfo& info) : OpKernel(info) { } + DequantizeLinear(const OpKernelInfo& info) : OpKernel(info) { + has_axis_ = info.GetAttr("axis", &axis_).IsOK(); + } - Status Compute(OpKernelContext* context) const override; + Status Compute(OpKernelContext* context) const override; + + private: + int64_t axis_ = 0; + bool has_axis_; }; template class QuantizeLinear final : public OpKernel { public: - QuantizeLinear(const OpKernelInfo& info) : OpKernel(info) { } - + QuantizeLinear(const OpKernelInfo& info) : OpKernel(info) { + has_axis_ = info.GetAttr("axis", &axis_).IsOK(); + } + Status Compute(OpKernelContext* context) const override; + + private: + int64_t axis_ = 0; + bool has_axis_; }; } // namespace onnxruntime diff --git a/onnxruntime/test/contrib_ops/quantize_ops_test.cc b/onnxruntime/test/contrib_ops/quantize_ops_test.cc new file mode 100644 index 0000000000..4da03df8b1 --- /dev/null +++ b/onnxruntime/test/contrib_ops/quantize_ops_test.cc @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "gtest/gtest.h" +#include "test/providers/provider_test_utils.h" + +namespace onnxruntime { +namespace test { + +// 1d zero & scale with uint8 broadcast axis 0 +TEST(DequantizeLinearContribOpTest, DequantizeLinear_0) { + OpTester test("DequantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{3, 4}; + test.AddInput("X", dims, + {0, 1, 2, 3, + 0, 1, 2, 3, + 0, 10, 20, 30}); + test.AddAttribute("axis", 0); + test.AddInput("scale", {3}, + {1.0f, + 2.0f, + 4.0f}); + test.AddInput("zero_point", {3}, + {0, + 0, + 0}); + test.AddOutput("Y", dims, + {0, 1, 2, 3, + 0, 2, 4, 6, + 0, 40, 80, 120}); + test.Run(); +} + +// 1d zero & scale with int8 broadcast axis 1 +TEST(DequantizeLinearContribOpTest, DequantizeLinear_1) { + OpTester test("DequantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{3, 4}; + test.AddInput("X", dims, + {0, 1, 2, 3, + 0, 2, 4, 6, + 0, 10, 20, 30}); + test.AddAttribute("axis", 1); + test.AddInput("scale", {4}, {1, 2, 4, 8}); + test.AddInput("zero_point", {4}, {0, -10, -20, -30}); + test.AddOutput("Y", dims, + {0, 22, 88, 264, + 0, 24, 96, 288, + 0, 40, 160, 480}); + test.Run(); +} + +// 1d zero & scale with int8 broadcast axis 0 with 4d tensor input/output +TEST(DequantizeLinearContribOpTest, DequantizeLinear_2) { + OpTester test("DequantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{2, 3, 2, 4}; + test.AddInput("X", dims, + {7, 9, 10, 10, + 5, 8, 9, 1, + + 8, 6, 7, 9, + 10, 0, 7, 10, + + 8, 2, 6, 0, + 5, 9, 8, 1, + + 2, 7, 5, 3, + 2, 4, 1, 3, + + 8, 7, 4, 8, + 10, 1, 5, 5, + + 7, 7, 0, 2, + 4, 4, 0, 5}); + test.AddAttribute("axis", 1); + test.AddInput("scale", {3}, {1, 10, 7}); + test.AddInput("zero_point", {3}, {10, 2, 1}); + test.AddOutput("Y", dims, + {-3, -1, 0, 0, + -5, -2, -1, -9, + + 60, 40, 50, 70, + 80, -20, 50, 80, + + 49, 7, 35, -7, + 28, 56, 49, 0, + + -8, -3, -5, -7, + -8, -6, -9, -7, + + 60, 50, 20, 60, + 80, -10, 30, 30, + + 42, 42, -7, 7, + 21, 21, -7, 28}); + test.Run(); +} + +// 1d zero & scale with uint8 broadcast axis -2 (-2 resolves to axis 0) +TEST(DequantizeLinearContribOpTest, DequantizeLinear_3) { + OpTester test("DequantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{3, 4}; + test.AddInput("X", dims, + {0, 1, 2, 3, + 0, 1, 2, 3, + 0, 10, 20, 30}); + test.AddAttribute("axis", -2); + test.AddInput("scale", {3}, + {1.0f, + 2.0f, + 4.0f}); + test.AddInput("zero_point", {3}, + {0, + 0, + 0}); + test.AddOutput("Y", dims, + {0, 1, 2, 3, + 0, 2, 4, 6, + 0, 40, 80, 120}); + test.Run(); +} + +// quantize with scalar zero point and scale +TEST(QuantizeLinearContribOpTest, QuantizeLinear_0) { + OpTester test("QuantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{6}; + test.AddInput("x", dims, {0, 2, 3, 1000, -254, -1000}); + test.AddInput("y_scale", {}, {2.0f}); + test.AddInput("y_zero_point", {}, {128}); + test.AddOutput("y", dims, {128, 129, 130, 255, 1, 0}); + test.Run(); +} + +// quantize with broadcasting +TEST(QuantizeLinearContribOpTest, QuantizeLinear_1) { + OpTester test("QuantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{3, 4}; + test.AddInput("X", dims, + {0, 2, 3, 1000, + 0, 2, 3, 1000, + 0, 2, 3, 1000}); + test.AddAttribute("axis", 0); + test.AddInput("scale", {3}, {1, 2, 4}); + test.AddInput("zero_point", {3}, {0, 0, 0}); + test.AddOutput("Y", dims, + {0, 2, 3, 255, + 0, 1, 2, 255, + 0, 1, 1, 250}); + test.Run(); +} + +// quantize with broadcasting and negative axis (-2 resolves to axis 0) +TEST(QuantizeLinearContribOpTest, QuantizeLinear_2) { + OpTester test("QuantizeLinear", 1, onnxruntime::kMSDomain); + std::vector dims{3, 4}; + test.AddInput("X", dims, + {0, 2, 3, 1000, + 0, 2, 3, 1000, + 0, 2, 3, 1000}); + test.AddAttribute("axis", -2); + test.AddInput("scale", {3}, {1, 2, 4}); + test.AddInput("zero_point", {3}, {0, 0, 0}); + test.AddOutput("Y", dims, + {0, 2, 3, 255, + 0, 1, 2, 255, + 0, 1, 1, 250}); + test.Run(); +} +} // namespace test +} // namespace onnxruntime \ No newline at end of file