[Quantization] Tensor quant overrides and QNN EP quantization configuration (#18465)

### Description
#### 1. Adds `TensorQuantOverrides` extra option
Allows specifying a dictionary of tensor-level quantization overrides:
```
TensorQuantOverrides = dictionary :
    Default is {}. Set tensor quantization overrides. The key is a tensor name and the value is a
    list of dictionaries. For per-tensor quantization, the list contains a single dictionary. For
    per-channel quantization, the list contains a dictionary for each channel in the tensor.
    Each dictionary contains optional overrides with the following keys and values.
          'quant_type' = QuantType : The tensor's quantization data type.
          'scale' =  Float         : The scale value to use. Must also specify `zero_point` if set.
          'zero_point' = Int       : The zero-point value to use. Must also specify `scale` is set.
          'symmetric' = Bool       : If the tensor should use symmetric quantization. Invalid if also
                                     set `scale` or `zero_point`.
          'reduce_range' = Bool    : If the quantization range should be reduced. Invalid if also
                                     set `scale` or `zero_point`.
          'rmax' = Float           : Override the maximum real tensor value in calibration data.
                                     Invalid if also set `scale` or `zero_point`.
          'rmin' = Float           : Override the minimum real tensor value in calibration data.
                                     Invalid if also set `scale` or `zero_point`.
```

- All of the options are optional.
- Some combinations are invalid.
- Ex: `rmax` and `rmin` are unnecessary if the `zero_point` and `scale`
are also specified.

Example for per-tensor quantization overrides:
```Python3
extra_options = {
    "TensorQuantOverrides": {
        "SIG_OUT": [{"scale": 1.0, "zero_point": 127}],
        "WGT": [{"quant_type": quantization.QuantType.QInt8, "symmetric": True, "reduce_range": True}],
        "BIAS": [{"quant_type": quantization.QuantType.QInt8, "symmetric": True, "reduce_range": True}],
    },
}
```

Example for per-channel quantization overrides (Conv weight and bias):
```Python3
extra_options = {
    "TensorQuantOverrides": {
        "WGT": [
            {
                "quant_type": quantization.QuantType.QUInt8,
                "rmin": 0.0,
                "rmax": 2.5,
                "reduce_range": True,
            },
            {
                "quant_type": quantization.QuantType.QUInt8,
                "rmin": 0.2,
                "rmax": 2.55,
                "reduce_range": False,
            },
        ],
        "BIAS": [
            {"zero_point": 0, "scale": 0.000621},
            {"zero_point": 0, "scale": 0.23},
        ],
    },
}
```

#### 2. Adds utilities to get the default QDQ configs for QNN EP
Added a `quantization.execution_providers.qnn.get_qnn_qdq_config` method
that inspects the model and returns suitable quantization
configurations.

Example usage:
```python3
from quantization import quantize, QuantType
from quantization.execution_providers.qnn import get_qnn_qdq_config

qnn_config = get_qnn_qdq_config(input_model_path,
                                data_reader,
                                activation_type=QuantType.QUInt16,
                                weight_type=QuantType.QUInt8)
                                
quantize(input_model_path,
         output_model_path,
         qnn_config)
```

### Motivation and Context
Make it possible to create more QDQ models that run on QNN EP.

---------

Signed-off-by: adrianlizarraga <adlizarraga@microsoft.com>
This commit is contained in:
Adrian Lizarraga 2023-12-04 17:54:58 -08:00 committed by GitHub
parent 01b5c78917
commit e066fca777
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 825 additions and 56 deletions

View file

@ -453,6 +453,9 @@ file(GLOB onnxruntime_python_quantization_operators_src CONFIGURE_DEPENDS
file(GLOB onnxruntime_python_quantization_cal_table_flatbuffers_src CONFIGURE_DEPENDS
"${ONNXRUNTIME_ROOT}/python/tools/quantization/CalTableFlatBuffers/*.py"
)
file(GLOB onnxruntime_python_quantization_ep_qnn_src CONFIGURE_DEPENDS
"${ONNXRUNTIME_ROOT}/python/tools/quantization/execution_providers/qnn/*.py"
)
file(GLOB onnxruntime_python_transformers_src CONFIGURE_DEPENDS
"${ONNXRUNTIME_ROOT}/python/tools/transformers/*.py"
)
@ -547,6 +550,8 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/operators
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/CalTableFlatBuffers
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/execution_providers
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/execution_providers/qnn
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/quantization
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/transformers
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/transformers/test_data/models
@ -617,6 +622,9 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy
${onnxruntime_python_quantization_cal_table_flatbuffers_src}
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/CalTableFlatBuffers/
COMMAND ${CMAKE_COMMAND} -E copy
${onnxruntime_python_quantization_ep_qnn_src}
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/quantization/execution_providers/qnn/
COMMAND ${CMAKE_COMMAND} -E copy
${onnxruntime_python_transformers_src}
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/transformers/

View file

@ -0,0 +1 @@
from .quant_config import get_qnn_qdq_config # noqa: F401

View file

@ -0,0 +1,84 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from pathlib import Path
import onnx
from ...calibrate import CalibrationDataReader, CalibrationMethod
from ...quant_utils import QuantType
from ...quantize import StaticQuantConfig
Q16_TYPES = {QuantType.QInt16, QuantType.QUInt16}
Q8_TYPES = {QuantType.QInt8, QuantType.QUInt8}
OP_TYPES_TO_EXCLUDE = {"Cast"}
def get_qnn_qdq_config(
model_input: Path,
calibration_data_reader: CalibrationDataReader,
calibrate_method=CalibrationMethod.MinMax,
activation_type=QuantType.QUInt8,
weight_type=QuantType.QUInt8,
per_channel=False,
):
if per_channel:
raise ValueError("QNN EP does not yet support per-channel quantization.")
# Process model nodes to setup overrides.
model = onnx.load_model(model_input)
op_types = set()
tensor_quant_overrides = {}
name_to_initializer = {initializer.name: initializer for initializer in model.graph.initializer}
for node in model.graph.node:
op_types.add(node.op_type)
if node.op_type == "MatMul" and activation_type in Q16_TYPES and weight_type in Q8_TYPES:
weight_symmetric = weight_type == QuantType.QInt8
# Override initializers to use the weight_type
for input_name in node.input:
if input_name in name_to_initializer:
tensor_quant_overrides[input_name] = [{"quant_type": weight_type, "symmetric": weight_symmetric}]
elif node.op_type == "LayerNormalization" and activation_type in Q16_TYPES and weight_type in Q8_TYPES:
weight_symmetric = weight_type == QuantType.QInt8
# Override initializers to use the weight_type. Don't override the bias input.
for i in range(2):
input_name = node.input[i]
if input_name in name_to_initializer:
tensor_quant_overrides[input_name] = [{"quant_type": weight_type, "symmetric": weight_symmetric}]
elif node.op_type == "Sigmoid":
if activation_type == QuantType.QUInt16:
tensor_quant_overrides[node.output[0]] = [{"scale": 1.0 / 65536.0, "zero_point": 0}]
elif activation_type == QuantType.QInt16:
tensor_quant_overrides[node.output[0]] = [{"scale": 1.0 / 32768.0, "zero_point": 0}]
elif node.op_type == "Tanh":
if activation_type == QuantType.QUInt16:
tensor_quant_overrides[node.output[0]] = [{"scale": 1.0 / 32768.0, "zero_point": 32768}]
elif activation_type == QuantType.QInt16:
tensor_quant_overrides[node.output[0]] = [{"scale": 1.0 / 32768.0, "zero_point": 0}]
extra_options = {
"MinimumRealRange": 0.0001,
"DedicatedQDQPair": False, # Let ORT optimizer duplicate DQ nodes
"TensorQuantOverrides": tensor_quant_overrides,
}
# TODO: Remove this extra option once ORT uses an ONNX version that supports 16-bit Q/DQ ops.
if activation_type in Q16_TYPES or weight_type in Q16_TYPES:
extra_options["UseQDQContribOps"] = True
return StaticQuantConfig(
calibration_data_reader,
calibrate_method=calibrate_method,
activation_type=activation_type,
weight_type=weight_type,
op_types_to_quantize=list(op_types.difference(OP_TYPES_TO_EXCLUDE)),
extra_options=extra_options,
)

View file

@ -37,6 +37,7 @@ from .quant_utils import (
model_has_infer_metadata,
ms_domain,
quantize_data,
quantize_nparray,
save_and_reload_model_with_shape_infer,
tensor_proto_to_array,
)
@ -49,8 +50,8 @@ class QuantizationParams:
for k, v in data.items():
if not isinstance(k, str):
raise TypeError(f"Keys must be strings not {type(k)}.")
if not isinstance(v, (int, float, str)):
raise TypeError(f"Values must be int, float, str not {type(v)}.")
if not isinstance(v, (int, float, str, QuantType)):
raise TypeError(f"Values must be int, float, str, or QuantType not {type(v)}.")
self.data[k] = v
def __iter__(self):
@ -148,6 +149,7 @@ class ONNXQuantizer:
if self.mode not in QuantizationMode:
raise ValueError(f"unsupported quantization mode {self.mode}")
self.tensor_quant_overrides = self._get_and_check_tensor_quant_overrides()
self.quantization_params = self.calculate_quantization_params()
# QuantizeRange tensor name and zero tensor name for scale and zero point calculation.
@ -167,6 +169,87 @@ class ONNXQuantizer:
# to store specified scale and zeropoint instead of calculated value, tensor_name->(scale, zeropoint)
self.used_scale_zp_map = {}
def _get_and_check_tensor_quant_overrides(self):
"""
Get tensor quantization overrides and check correctness.
"""
tensor_quant_overrides = self.extra_options.get("TensorQuantOverrides", {})
# Validate that compatible/valid overrides are provided.
if tensor_quant_overrides:
initializer_names = self.model.get_initializer_name_set()
value_info_names = set(self.value_infos.keys())
keys_unsupported_with_scale_zp = {"symmetric", "reduce_range", "rmax", "rmin"}
for tensor_name, quant_overrides_list in tensor_quant_overrides.items():
if tensor_name not in initializer_names and tensor_name not in value_info_names:
raise ValueError(f"Tensor '{tensor_name}' in TensorQuantOverrides is not present in the model")
if not isinstance(quant_overrides_list, list):
raise ValueError(f"Tensor quantization overrides for '{tensor_name}' are not in a list")
is_initializer = tensor_name in initializer_names
if not is_initializer and len(quant_overrides_list) > 1:
raise ValueError(
f"Tensor '{tensor_name}' has a list of per-channel overrides, but is not an initializer"
)
quant_type = None
for index, quant_overrides in enumerate(quant_overrides_list):
if not isinstance(quant_overrides, dict):
raise ValueError(
f"Tensor quantization overrides at index {index} for '{tensor_name}' are not in a dict"
)
# For per-channel quantization, all channels must use the same quantization type.
# Therefore, if the user tries to override the quant_type for a channel, it must match in all
# other channels.
if index == 0:
quant_type = quant_overrides.get("quant_type")
elif quant_type != quant_overrides.get("quant_type"):
raise ValueError(
"Channel quantization types for tensor '{tensor_name}' do not match at index {index}."
)
has_scale = "scale" in quant_overrides
has_zero_point = "zero_point" in quant_overrides
if (has_scale and not has_zero_point) or (has_zero_point and not has_scale):
raise ValueError(
"Must provide both 'scale' and 'zero_point' if one of the overrides is provided"
)
if has_scale:
for key in keys_unsupported_with_scale_zp:
if key in quant_overrides:
raise ValueError(
f"Tensor override option '{key}' is invalid with 'scale' and 'zero_point'"
)
return tensor_quant_overrides
def get_per_tensor_quant_overrides(self, tensor_name):
quant_overrides_list = self.tensor_quant_overrides.get(tensor_name, [{}])
num_overrides = len(quant_overrides_list)
if num_overrides > 1:
raise ValueError(
f"Expected tensor '{tensor_name}' to use per-tensor quantization overrides, "
f"but found {num_overrides} per-channel overrides."
)
return quant_overrides_list[0] if num_overrides > 0 else {}
def get_per_channel_quant_overrides(self, tensor_name, num_channels):
quant_overrides_list = self.tensor_quant_overrides.get(tensor_name, [{} for i in range(num_channels)])
if len(quant_overrides_list) != num_channels:
raise ValueError(
f"Expected tensor '{tensor_name}' to have {num_channels} per-channel quantization overrides, "
f"but found {len(quant_overrides_list)} instead."
)
return quant_overrides_list
# routines for subgraph support
def quantize_subgraph(self, subgraph, graph_key):
"""
@ -587,6 +670,8 @@ class ONNXQuantizer:
parameter param_name: Name of the quantization parameter.
return: result, scale_name, zero_point_name, scale_shape, zero_point_shape.
"""
zero_point_type = self.activation_qType
if use_scale is None or use_zeropoint is None:
if self.quantization_params is None or param_name not in self.quantization_params:
logging.info(f'Quantization parameters for tensor:"{param_name}" not specified')
@ -595,21 +680,21 @@ class ONNXQuantizer:
params = self.quantization_params[param_name]
if not isinstance(params, QuantizationParams):
raise TypeError(f"Unexpected type {type(params)} for {param_name!r}.")
if params is None or len(params) != 2:
if params is None or len(params) != 3:
raise ValueError(
"Quantization parameters should contain zero point and scale. "
"Quantization parameters should contain zero point, scale, quant type. "
f"Specified values for output {param_name}: {params}"
)
zero_point_values = [params["zero_point"]]
scale_values = [params["scale"]]
zero_point_type = params["quant_type"]
else:
zero_point_values = [use_zeropoint]
scale_values = [use_scale]
zero_point_shape = []
zero_point_name = param_name + "_zero_point"
zero_point_type = self.activation_qType
scale_shape = []
scale_name = param_name + "_scale"
@ -991,16 +1076,25 @@ class ONNXQuantizer:
zp_name = weight.name + "_zero_point"
scale_name = weight.name + "_scale"
# Update packed weight, zero point, and scale initializers
# Quantize weight data. Use quantization overrides if provided by the user.
weight_data = tensor_proto_to_array(weight)
w_data = weight_data.flatten().tolist()
_, _, zero_point, scale, q_weight_data = quantize_data(
w_data,
qType,
self.is_weight_symmetric,
self.reduce_range and reduce_range,
self.min_real_range,
)
quant_overrides = self.get_per_tensor_quant_overrides(weight.name)
if "quant_type" in quant_overrides:
qType = quant_overrides["quant_type"].tensor_type # noqa: N806
if "scale" in quant_overrides and "zero_point" in quant_overrides:
zero_point, scale = quant_overrides["zero_point"], quant_overrides["scale"]
q_weight_data = quantize_nparray(qType, weight_data.flatten(), scale, zero_point)
else:
_, _, zero_point, scale, q_weight_data = quantize_data(
weight_data.flatten().tolist(),
qType,
quant_overrides.get("symmetric", self.is_weight_symmetric),
reduce_range=quant_overrides.get("reduce_range", self.reduce_range and reduce_range),
min_real_range=self.min_real_range,
rmin_override=quant_overrides.get("rmin"),
rmax_override=quant_overrides.get("rmax"),
)
if qType in {
onnx.TensorProto.FLOAT8E4M3FN,
@ -1076,23 +1170,43 @@ class ONNXQuantizer:
weights = tensor_proto_to_array(initializer)
channel_count = weights.shape[channel_axis]
rmin_list = []
rmax_list = []
quant_overrides_for_channels = self.get_per_channel_quant_overrides(weight_name, channel_count)
# If user provides per-channel quantization overrides, all channels must use the same quantization type.
# So, just use the first channel's type.
if "quant_type" in quant_overrides_for_channels[0]:
weight_qType = quant_overrides_for_channels[0]["quant_type"].tensor_type # noqa: N806
zero_point_list = []
scale_list = []
quantized_per_channel_data_list = []
for i in range(channel_count):
per_channel_data = weights.take(i, channel_axis)
rmin, rmax, zero_point, scale, quantized_per_channel_data = quantize_data(
per_channel_data.flatten().tolist(),
weight_qType,
self.is_weight_symmetric
or weight_qType in (onnx_proto.TensorProto.INT8, onnx_proto.TensorProto.FLOAT8E4M3FN),
self.reduce_range and reduce_range,
self.min_real_range,
)
rmin_list.append(rmin)
rmax_list.append(rmax)
channel_quant_overrides = quant_overrides_for_channels[i]
if "scale" in channel_quant_overrides and "zero_point" in channel_quant_overrides:
zero_point, scale = channel_quant_overrides["zero_point"], channel_quant_overrides["scale"]
quantized_per_channel_data = quantize_nparray(
weight_qType, per_channel_data.flatten(), scale, zero_point
)
else:
symmetric = channel_quant_overrides.get(
"symmetric",
(
self.is_weight_symmetric
or weight_qType in (onnx_proto.TensorProto.INT8, onnx_proto.TensorProto.FLOAT8E4M3FN)
),
)
_, _, zero_point, scale, quantized_per_channel_data = quantize_data(
per_channel_data.flatten().tolist(),
weight_qType,
symmetric,
reduce_range=channel_quant_overrides.get("reduce_range", self.reduce_range and reduce_range),
min_real_range=self.min_real_range,
rmin_override=channel_quant_overrides.get("rmin"),
rmax_override=channel_quant_overrides.get("rmax"),
)
zero_point_list.append(zero_point)
scale_list.append(scale)
quantized_per_channel_data_list.append(quantized_per_channel_data)
@ -1205,15 +1319,25 @@ class ONNXQuantizer:
td = self.tensors_range[tensor_name]
if not isinstance(td, TensorData):
raise TypeError(f"Unexpected type {type(td)} for {tensor_name!r}.")
if self.activation_qType == onnx.TensorProto.FLOAT8E4M3FN:
zero, scale = compute_scale_zp_float8(self.activation_qType, td.avg_std[1])
else:
rmin, rmax = td.range_value
qmin, qmax = get_qmin_qmax_for_qType(self.activation_qType, symmetric=self.is_activation_symmetric)
zero, scale = compute_scale_zp(
rmin, rmax, qmin, qmax, self.is_activation_symmetric, self.min_real_range
)
quantization_params[tensor_name] = QuantizationParams(zero_point=zero, scale=scale)
quant_overrides = self.get_per_tensor_quant_overrides(tensor_name)
quant_type = self.activation_qType
if "quant_type" in quant_overrides:
quant_type = quant_overrides["quant_type"].tensor_type
if "scale" in quant_overrides and "zero_point" in quant_overrides:
zero, scale = quant_overrides["zero_point"], quant_overrides["scale"]
elif quant_type == onnx.TensorProto.FLOAT8E4M3FN:
zero, scale = compute_scale_zp_float8(quant_type, td.avg_std[1])
else:
rmin = quant_overrides.get("rmin", td.range_value[0])
rmax = quant_overrides.get("rmax", td.range_value[1])
symmetric = quant_overrides.get("symmetric", self.is_activation_symmetric)
reduce_range = quant_overrides.get("reduce_range", False)
qmin, qmax = get_qmin_qmax_for_qType(quant_type, reduce_range=reduce_range, symmetric=symmetric)
zero, scale = compute_scale_zp(rmin, rmax, qmin, qmax, symmetric, self.min_real_range)
quantization_params[tensor_name] = QuantizationParams(zero_point=zero, scale=scale, quant_type=quant_type)
return quantization_params

View file

@ -6,24 +6,32 @@
from .qdq_base_operator import QDQOperatorBase
class QDQInstanceNormalization(QDQOperatorBase):
class QDQNormalization(QDQOperatorBase):
def __init__(self, onnx_quantizer, onnx_node):
super().__init__(onnx_quantizer, onnx_node)
def quantize(self):
node = self.node
assert node.op_type == "InstanceNormalization"
assert node.op_type == "InstanceNormalization" or node.op_type == "LayerNormalization"
# Input
self.quantizer.quantize_activation_tensor(node.input[0])
if not self.disable_qdq_for_node_output:
self.quantizer.quantize_activation_tensor(node.output[0])
# Scale
if self.quantizer.is_per_channel():
self.quantizer.quantize_weight_tensor_per_channel(node.input[1], axis=1)
else:
scale_is_initializer = self.quantizer.is_input_a_initializer(node.input[1])
if self.quantizer.is_per_channel() and scale_is_initializer:
channel_axis = self.quantizer.qdq_op_type_per_channel_support_to_axis.get(node.op_type, 1)
self.quantizer.quantize_weight_tensor_per_channel(node.input[1], axis=channel_axis)
elif scale_is_initializer:
self.quantizer.quantize_weight_tensor(node.input[1])
else:
self.quantizer.quantize_activation_tensor(node.input[1])
# Bias
self.quantizer.quantize_bias_tensor(node.input[2], node.input[0], node.input[1])
# Output
if not self.disable_qdq_for_node_output:
for output_name in node.output:
self.quantizer.quantize_activation_tensor(output_name)

View file

@ -85,11 +85,22 @@ class QLinearSoftmax(QuantOperatorBase):
class QDQSoftmax(QDQOperatorBase):
def quantize(self):
super().quantize()
symmetric = self.quantizer.is_activation_symmetric
output_name = self.node.output[0]
quant_overrides = self.quantizer.get_per_tensor_quant_overrides(output_name)
# Enforce Softmax range: 0.0 to 1.0
rmin, rmax = 0.0, 1.0
qmin, qmax = get_qmin_qmax_for_qType(self.quantizer.activation_qType, symmetric=symmetric)
out_zero_point, out_scale = compute_scale_zp(rmin, rmax, qmin, qmax, symmetric=symmetric)
quant_type = self.quantizer.activation_qType
if "quant_type" in quant_overrides:
quant_type = quant_overrides["quant_type"].tensor_type
self.quantizer.set_quant_scale_zp(self.node.output[0], (out_scale, out_zero_point))
if "scale" in quant_overrides and "zero_point" in quant_overrides:
out_zero_point, out_scale = quant_overrides["zero_point"], quant_overrides["scale"]
else:
# Unless overridden by the user, force Softmax to range from 0.0 to 1.0
rmin = quant_overrides.get("rmin", 0.0)
rmax = quant_overrides.get("rmax", 1.0)
symmetric = quant_overrides.get("symmetric", self.quantizer.is_activation_symmetric)
reduce_range = quant_overrides.get("reduce_range", False)
qmin, qmax = get_qmin_qmax_for_qType(quant_type, reduce_range=reduce_range, symmetric=symmetric)
out_zero_point, out_scale = compute_scale_zp(rmin, rmax, qmin, qmax, symmetric=symmetric)
self.quantizer.set_quant_scale_zp(output_name, (out_scale, out_zero_point))

View file

@ -204,6 +204,17 @@ class QDQQuantizer(ONNXQuantizer):
logging.warning(f"only support per-channel quantization on weight. Tensor: {tensor_name} is not quantized.")
def quantize_bias_tensor(self, bias_name, input_name, weight_name, beta=1.0):
# If the user provided quantization overrides for this tensor, treat it as a regular weight.
if self.tensor_quant_overrides.get(bias_name):
logging.info(
f"Quantizing bias tensor '{bias_name}' as a weight due to the presence of user-specified overrides"
)
if self.per_channel:
self.quantize_weight_tensor_per_channel(bias_name, 0)
else:
self.quantize_weight_tensor(bias_name)
return
weight = find_by_name(bias_name, self.model.initializer())
if weight is not None:
if weight.data_type == onnx_proto.TensorProto.FLOAT:

View file

@ -260,13 +260,17 @@ def compute_scale_zp_float8(element_type, std):
return [zero, scale]
def quantize_data(data, qType, symmetric, reduce_range=False, min_real_range=None):
def quantize_data(
data, qType, symmetric, reduce_range=False, min_real_range=None, rmin_override=None, rmax_override=None
):
"""
:param data: data to quantize
:param qType: data type to quantize to. Supported types UINT8 and INT8
:param symmetric: whether symmetric quantization is used or not. This is applied to INT8.
:parameter reduce_range: True if the quantization range should be reduced. Defaults to False.
:parameter min_real_range: Minimum floating-point range (i.e., rmax - rmin) to enforce. Defaults to None.
:parameter rmin_override: The value of rmin to use if not None. Otherwise, uses min(data).
:parameter rmax_override: The value of rmax to use if not None. Otherwise, uses max(data).
:return: minimum, maximum, zero point, scale, and quantized weights
To pack weights, we compute a linear transformation
@ -284,13 +288,19 @@ def quantize_data(data, qType, symmetric, reduce_range=False, min_real_range=Non
- *S*: scale
- *z*: zero point
"""
rmin = 0
rmax = 0
if rmin_override is not None:
rmin = rmin_override
else:
rmin = min(data) if len(data) else 0
if rmax_override is not None:
rmax = rmax_override
else:
rmax = max(data) if len(data) else 0
zero_point = 0
scale = 1.0
if len(data):
rmin = min(data)
rmax = max(data)
if qType == TensorProto.FLOAT8E4M3FN:
if reduce_range:

View file

@ -155,6 +155,33 @@ class StaticQuantConfig(QuantConfig):
SmoothQuantFolding = True/False :
Default is True. It only works if SmoothQuant is True. If enabled, inserted Mul ops during
SmoothQuant will be folded into the previous op if the previous op is foldable.
UseQDQContribOps = True/False :
Default is False. If enabled, the inserted QuantizeLinear and DequantizeLinear ops will have the
`com.microsoft` domain, which forces use of ONNX Runtime's QuantizeLinear and DequantizeLinear
contrib op implementations. The contrib op implementations may support features not standardized
into the ONNX specification (e.g., 16-bit quantization types).
MinimumRealRange = float|None :
Default is None. If set to a floating-point value, the calculation of the quantization parameters
(i.e., scale and zero point) will enforce a minimum range between rmin and rmax. If (rmax-rmin)
is less than the specified minimum range, rmax will be set to rmin + MinimumRealRange. This is
necessary for EPs like QNN that require a minimum floating-point range when determining
quantization parameters.
TensorQuantOverrides = dictionary :
Default is {}. Set tensor quantization overrides. The key is a tensor name and the value is a
list of dictionaries. For per-tensor quantization, the list contains a single dictionary. For
per-channel quantization, the list contains a dictionary for each channel in the tensor.
Each dictionary contains optional overrides with the following keys and values.
'quant_type' = QuantType : The tensor's quantization data type.
'scale' = Float : The scale value to use. Must also specify `zero_point` if set.
'zero_point' = Int : The zero-point value to use. Must also specify `scale` is set.
'symmetric' = Bool : If the tensor should use symmetric quantization. Invalid if also
set `scale` or `zero_point`.
'reduce_range' = Bool : If the quantization range should be reduced. Invalid if also
set `scale` or `zero_point`.
'rmax' = Float : Override the maximum real tensor value in calibration data.
Invalid if also set `scale` or `zero_point`.
'rmin' = Float : Override the minimum real tensor value in calibration data.
Invalid if also set `scale` or `zero_point`.
execution_provider : A enum indicates the Execution Provider such as: CPU, TRT, NNAPI, SNE, etc.
Raises:
ValueError: Raise ValueError if execution provider is unknown
@ -376,6 +403,22 @@ def quantize_static(
is less than the specified minimum range, rmax will be set to rmin + MinimumRealRange. This is
necessary for EPs like QNN that require a minimum floating-point range when determining
quantization parameters.
TensorQuantOverrides = dictionary :
Default is {}. Set tensor quantization overrides. The key is a tensor name and the value is a
list of dictionaries. For per-tensor quantization, the list contains a single dictionary. For
per-channel quantization, the list contains a dictionary for each channel in the tensor.
Each dictionary contains optional overrides with the following keys and values.
'quant_type' = QuantType : The tensor's quantization data type.
'scale' = Float : The scale value to use. Must also specify `zero_point` if set.
'zero_point' = Int : The zero-point value to use. Must also specify `scale` is set.
'symmetric' = Bool : If the tensor should use symmetric quantization. Invalid if also
set `scale` or `zero_point`.
'reduce_range' = Bool : If the quantization range should be reduced. Invalid if also
set `scale` or `zero_point`.
'rmax' = Float : Override the maximum real tensor value in calibration data.
Invalid if also set `scale` or `zero_point`.
'rmin' = Float : Override the minimum real tensor value in calibration data.
Invalid if also set `scale` or `zero_point`.
"""
if activation_type == QuantType.QFLOAT8E4M3FN or weight_type == QuantType.QFLOAT8E4M3FN:
if calibrate_method != CalibrationMethod.Distribution:

View file

@ -10,10 +10,10 @@ from .operators.embed_layernorm import EmbedLayerNormalizationQuant
from .operators.gather import GatherQuant, QDQGather
from .operators.gavgpool import QGlobalAveragePool
from .operators.gemm import QDQGemm, QLinearGemm
from .operators.instnorm import QDQInstanceNormalization
from .operators.lstm import LSTMQuant
from .operators.matmul import MatMulInteger, QDQMatMul, QLinearMatMul
from .operators.maxpool import QDQMaxPool, QMaxPool
from .operators.norm import QDQNormalization
from .operators.pad import QPad
from .operators.pooling import QLinearPool
from .operators.qdq_base_operator import QDQOperatorBase
@ -81,7 +81,8 @@ QDQRegistry = {
"Gather": QDQGather,
"Softmax": QDQSoftmax,
"Where": QDQWhere,
"InstanceNormalization": QDQInstanceNormalization,
"InstanceNormalization": QDQNormalization,
"LayerNormalization": QDQNormalization,
}

View file

@ -0,0 +1,467 @@
#!/usr/bin/env python
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
import struct
import unittest
import numpy as np
import onnx
from onnxruntime import quantization
from onnxruntime.quantization.quant_utils import compute_scale_zp, get_qmin_qmax_for_qType
class TestTensorQuantOverridesOption(unittest.TestCase):
def setUp(self):
self.activations = [
np.array([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]], dtype="float32"),
]
self.weight = np.array([[[-1.0, -2.0], [1.0, 2.0]], [[-0.5, -1.5], [0.5, 1.5]]], dtype=np.float32)
self.bias = np.array([0.0, 1.0], dtype=np.float32)
self.default_act_qtype = onnx.TensorProto.UINT8
self.default_wgt_qtype = onnx.TensorProto.UINT8
self.default_wgt_qtype_per_channel = onnx.TensorProto.INT8
self.default_bias_qtype = onnx.TensorProto.INT32
self.default_zp_scales = {
"INP": (0, np.float32(0.0235294122248888)),
"SIG_OUT": (0, np.float32(0.003911871928721666)),
"WGT": (128, np.float32(0.01568627543747425)),
"BIAS": (0, np.float32(0.0000613626980339177)), # zp == 0, scale = weight_scale * sig_out_scale
"OUT": (0, np.float32(0.005075461231172085)),
}
self.default_zp_scales_per_channel = {
"INP": (0, np.float32(0.0235294122248888)),
"SIG_OUT": (0, np.float32(0.003911871928721666)),
"WGT": ([0, 0], [np.float32(0.015748031437397003), np.float32(0.011811023578047752)]),
"BIAS": ([0, 0], [np.float32(0.00006160428165458143), np.float32(0.00004620321124093607)]),
"OUT": (0, np.float32(0.005075461231172085)),
}
def perform_qdq_quantization(self, output_model_name, tensor_quant_overrides=None, per_channel=False):
# (input)
# |
# Sigmoid
# |
# Conv
# |
# (output)
inp = onnx.helper.make_tensor_value_info("INP", onnx.TensorProto.FLOAT, self.activations[0].shape)
sigmoid_node = onnx.helper.make_node("Sigmoid", ["INP"], ["SIG_OUT"])
out = onnx.helper.make_tensor_value_info("OUT", onnx.TensorProto.FLOAT, [None, None, None])
wgt_init = onnx.numpy_helper.from_array(self.weight, "WGT")
bias_init = onnx.numpy_helper.from_array(self.bias, "BIAS")
conv_node = onnx.helper.make_node("Conv", ["SIG_OUT", "WGT", "BIAS"], ["OUT"])
graph = onnx.helper.make_graph(
[sigmoid_node, conv_node], "test", [inp], [out], initializer=[wgt_init, bias_init]
)
model = onnx.helper.make_model(graph, opset_imports=[onnx.helper.make_opsetid("", 13)])
onnx.save(model, "model.onnx")
# Quantize model
class DummyDataReader(quantization.CalibrationDataReader):
def __init__(self, activations):
self.iterator = ({"INP": act} for act in activations)
def get_next(self):
return next(self.iterator, None)
extra_options = {}
if tensor_quant_overrides is not None:
extra_options["TensorQuantOverrides"] = tensor_quant_overrides
quantization.quantize_static(
model_input="model.onnx",
model_output=output_model_name,
calibration_data_reader=DummyDataReader(self.activations),
quant_format=quantization.QuantFormat.QDQ,
activation_type=self.default_act_qtype,
weight_type=self.default_wgt_qtype,
per_channel=per_channel,
op_types_to_quantize=["Conv", "Sigmoid"],
extra_options=extra_options,
)
# Extract quantization parameters: scales and zero points for activations and weights.
model = onnx.load(output_model_name)
inp_zp = next(init for init in model.graph.initializer if init.name == "INP_zero_point")
inp_sc = next(init for init in model.graph.initializer if init.name == "INP_scale")
sig_out_zp = next(init for init in model.graph.initializer if init.name == "SIG_OUT_zero_point")
sig_out_sc = next(init for init in model.graph.initializer if init.name == "SIG_OUT_scale")
wgt_zp = next(init for init in model.graph.initializer if init.name == "WGT_zero_point")
wgt_sc = next(init for init in model.graph.initializer if init.name == "WGT_scale")
bias_zp = next(
init
for init in model.graph.initializer
if init.name == "BIAS_quantized_zero_point" or init.name == "BIAS_zero_point"
)
bias_sc = next(
init for init in model.graph.initializer if init.name == "BIAS_quantized_scale" or init.name == "BIAS_scale"
)
out_zp = next(init for init in model.graph.initializer if init.name == "OUT_zero_point")
out_sc = next(init for init in model.graph.initializer if init.name == "OUT_scale")
# Return quantization parameters
return inp_zp, inp_sc, sig_out_zp, sig_out_sc, wgt_zp, wgt_sc, bias_zp, bias_sc, out_zp, out_sc
def test_qdq_default(self):
"""
Test default behavior without specifying the TensorQuantOverrides option.
"""
(
inp_zp,
inp_sc,
sig_out_zp,
sig_out_sc,
wgt_zp,
wgt_sc,
bias_zp,
bias_sc,
out_zp,
out_sc,
) = self.perform_qdq_quantization(
"model_default_quant_overrides.onnx",
tensor_quant_overrides=None, # default behavior
)
# No overrides set. Expect default values
self.assertEqual(inp_zp.int32_data[0], self.default_zp_scales["INP"][0])
self.assertEqual(inp_zp.data_type, self.default_act_qtype)
self.assertEqual(inp_sc.float_data[0], self.default_zp_scales["INP"][1])
self.assertEqual(sig_out_zp.int32_data[0], self.default_zp_scales["SIG_OUT"][0])
self.assertEqual(sig_out_zp.data_type, self.default_act_qtype)
self.assertEqual(sig_out_sc.float_data[0], self.default_zp_scales["SIG_OUT"][1])
self.assertEqual(wgt_zp.int32_data[0], self.default_zp_scales["WGT"][0])
self.assertEqual(wgt_zp.data_type, self.default_wgt_qtype)
self.assertEqual(wgt_sc.float_data[0], self.default_zp_scales["WGT"][1])
self.assertEqual(bias_zp.int32_data[0], self.default_zp_scales["BIAS"][0])
self.assertEqual(bias_zp.data_type, self.default_bias_qtype)
self.assertEqual(bias_sc.float_data[0], self.default_zp_scales["BIAS"][1])
self.assertEqual(out_zp.int32_data[0], self.default_zp_scales["OUT"][0])
self.assertEqual(out_zp.data_type, self.default_act_qtype)
self.assertEqual(out_sc.float_data[0], self.default_zp_scales["OUT"][1])
def test_qdq_default_per_channel(self):
"""
Test default per-channel behavior without specifying the TensorQuantOverrides option.
"""
(
inp_zp,
inp_sc,
sig_out_zp,
sig_out_sc,
wgt_zp,
wgt_sc,
bias_zp,
bias_sc,
out_zp,
out_sc,
) = self.perform_qdq_quantization(
"model_default_per_channel_quant_overrides.onnx",
tensor_quant_overrides=None, # default behavior
per_channel=True,
)
# No overrides set. Expect default values
self.assertEqual(inp_zp.int32_data[0], self.default_zp_scales["INP"][0])
self.assertEqual(inp_zp.data_type, self.default_act_qtype)
self.assertEqual(inp_sc.float_data[0], self.default_zp_scales["INP"][1])
self.assertEqual(sig_out_zp.int32_data[0], self.default_zp_scales["SIG_OUT"][0])
self.assertEqual(sig_out_zp.data_type, self.default_act_qtype)
self.assertEqual(sig_out_sc.float_data[0], self.default_zp_scales["SIG_OUT"][1])
self.assertEqual(wgt_zp.data_type, self.default_wgt_qtype_per_channel)
for index, zp in enumerate(self.default_zp_scales_per_channel["WGT"][0]):
self.assertEqual(wgt_zp.int32_data[index], zp)
for index, scale in enumerate(self.default_zp_scales_per_channel["WGT"][1]):
self.assertEqual(wgt_sc.float_data[index], scale)
self.assertEqual(bias_zp.data_type, self.default_bias_qtype)
num_bias_zps = len(self.default_zp_scales_per_channel["BIAS"][0])
actual_bias_zps = struct.unpack(f"<{num_bias_zps}i", bias_zp.raw_data)
for index, zp in enumerate(self.default_zp_scales_per_channel["BIAS"][0]):
self.assertEqual(actual_bias_zps[index], zp)
num_bias_scales = len(self.default_zp_scales_per_channel["BIAS"][1])
actual_bias_scales = struct.unpack(f"<{num_bias_scales}f", bias_sc.raw_data)
for index, scale in enumerate(self.default_zp_scales_per_channel["BIAS"][1]):
self.assertEqual(actual_bias_scales[index], scale)
self.assertEqual(out_zp.int32_data[0], self.default_zp_scales["OUT"][0])
self.assertEqual(out_zp.data_type, self.default_act_qtype)
self.assertEqual(out_sc.float_data[0], self.default_zp_scales["OUT"][1])
def test_qdq_overrides1(self):
"""
Test overriding:
- scale/zp for Sigmoid output
- quant_type, symmetric, reduce_range for Conv weight
- quant_type, symmetric, reduce_range for Conv bias
"""
inp_zp, inp_sc, sig_out_zp, sig_out_sc, wgt_zp, wgt_sc, bias_zp, bias_sc, _, _ = self.perform_qdq_quantization(
"model_quant_overrides1.onnx",
tensor_quant_overrides={
"SIG_OUT": [{"scale": 1.0, "zero_point": 127}],
"WGT": [{"quant_type": quantization.QuantType.QInt8, "symmetric": True, "reduce_range": True}],
"BIAS": [{"quant_type": quantization.QuantType.QInt8, "symmetric": True, "reduce_range": True}],
},
)
# Input should have same quant params
self.assertEqual(inp_zp.int32_data[0], self.default_zp_scales["INP"][0])
self.assertEqual(inp_zp.data_type, self.default_act_qtype)
self.assertEqual(inp_sc.float_data[0], self.default_zp_scales["INP"][1])
# Sigmoid output should have overridden scale/zp
self.assertEqual(sig_out_zp.int32_data[0], 127)
self.assertEqual(sig_out_zp.data_type, self.default_act_qtype)
self.assertEqual(sig_out_sc.float_data[0], np.float32(1.0))
# Weight should have different type, zero_point, and scale
self.assertEqual(wgt_zp.data_type, quantization.QuantType.QInt8.tensor_type)
wgt_qmin, wgt_qmax = get_qmin_qmax_for_qType(wgt_zp.data_type, reduce_range=True, symmetric=True)
wgt_rmin, wgt_rmax = np.min(self.weight), np.max(self.weight)
new_wgt_zp, new_wgt_sc = compute_scale_zp(wgt_rmin, wgt_rmax, wgt_qmin, wgt_qmax, symmetric=True)
self.assertEqual(wgt_zp.int32_data[0], new_wgt_zp)
self.assertEqual(wgt_sc.float_data[0], np.float32(new_wgt_sc))
# Bias should now be treated as a weight and should have different type, zero_point, and scale
self.assertEqual(bias_zp.data_type, quantization.QuantType.QInt8.tensor_type)
bias_qmin, bias_qmax = get_qmin_qmax_for_qType(bias_zp.data_type, reduce_range=True, symmetric=True)
bias_rmin, bias_rmax = np.min(self.bias), np.max(self.bias)
new_bias_zp, new_bias_sc = compute_scale_zp(bias_rmin, bias_rmax, bias_qmin, bias_qmax, symmetric=True)
self.assertEqual(bias_zp.int32_data[0], new_bias_zp)
self.assertEqual(bias_sc.float_data[0], np.float32(new_bias_sc))
def test_qdq_overrides2(self):
"""
Test overriding rmin/rmax for Sigmoid output.
"""
sigmoid_rmin, sigmoid_rmax = 0.0, 0.5
inp_zp, inp_sc, sig_out_zp, sig_out_sc, _, _, _, _, _, _ = self.perform_qdq_quantization(
"model_quant_overrides2.onnx",
tensor_quant_overrides={"SIG_OUT": [{"rmin": sigmoid_rmin, "rmax": sigmoid_rmax}]},
)
# Input should have same quant params
self.assertEqual(inp_zp.int32_data[0], self.default_zp_scales["INP"][0])
self.assertEqual(inp_zp.data_type, self.default_act_qtype)
self.assertEqual(inp_sc.float_data[0], self.default_zp_scales["INP"][1])
# Sigmoid output should have different scale/zp due to overridden rmin/rmax
self.assertEqual(sig_out_zp.data_type, self.default_act_qtype)
sigmoid_qmin, sigmoid_qmax = get_qmin_qmax_for_qType(sig_out_zp.data_type)
new_sigmoid_zp, new_sigmoid_sc = compute_scale_zp(sigmoid_rmin, sigmoid_rmax, sigmoid_qmin, sigmoid_qmax)
self.assertEqual(sig_out_zp.int32_data[0], new_sigmoid_zp)
self.assertEqual(sig_out_sc.float_data[0], np.float32(new_sigmoid_sc))
def test_qdq_overrides3(self):
"""
Test overriding rmin and rmax for Conv weight
"""
wgt_rmin, wgt_rmax = 0.0, 1.0
_, _, _, _, wgt_zp, wgt_sc, _, _, _, _ = self.perform_qdq_quantization(
"model_quant_overrides3.onnx",
tensor_quant_overrides={
"WGT": [{"rmin": wgt_rmin, "rmax": wgt_rmax}],
},
)
# Weight should have different zero_point and scale
self.assertEqual(wgt_zp.data_type, self.default_wgt_qtype)
self.assertNotEqual(wgt_rmin, np.min(self.weight))
self.assertNotEqual(wgt_rmax, np.max(self.weight))
wgt_qmin, wgt_qmax = get_qmin_qmax_for_qType(wgt_zp.data_type)
new_wgt_zp, new_wgt_sc = compute_scale_zp(wgt_rmin, wgt_rmax, wgt_qmin, wgt_qmax)
self.assertEqual(wgt_zp.int32_data[0], new_wgt_zp)
self.assertEqual(wgt_sc.float_data[0], np.float32(new_wgt_sc))
def test_qdq_overrides4(self):
"""
Test overriding scale and zero_point for Conv weight
"""
wgt_zp_val, wgt_scale_val = 4, 0.5
_, _, _, _, wgt_zp, wgt_sc, _, _, _, _ = self.perform_qdq_quantization(
"model_quant_overrides4.onnx",
tensor_quant_overrides={
"WGT": [{"zero_point": wgt_zp_val, "scale": wgt_scale_val}],
},
)
# Weight should have have the expected zero_point and scale
self.assertEqual(wgt_zp.data_type, self.default_wgt_qtype)
self.assertEqual(wgt_zp.int32_data[0], wgt_zp_val)
self.assertEqual(wgt_sc.float_data[0], np.float32(wgt_scale_val))
def test_qdq_overrides_per_channel1(self):
"""
Test per-channel overriding of scale/zero_point for Conv weight and bias.
"""
zp_vals, scale_vals = [2, 4], [0.5, 0.2]
(
_,
_,
_,
_,
wgt_zp,
wgt_sc,
bias_zp,
bias_sc,
_,
_,
) = self.perform_qdq_quantization(
"model_per_channel_quant_overrides1.onnx",
tensor_quant_overrides={
"WGT": [
{"zero_point": zp_vals[0], "scale": scale_vals[0]},
{"zero_point": zp_vals[1], "scale": scale_vals[1]},
],
"BIAS": [
{"zero_point": zp_vals[0], "scale": scale_vals[0]},
{"zero_point": zp_vals[1], "scale": scale_vals[1]},
],
},
per_channel=True,
)
self.assertEqual(wgt_zp.data_type, self.default_wgt_qtype_per_channel)
for index, zp in enumerate(zp_vals):
self.assertEqual(wgt_zp.int32_data[index], zp)
for index, scale in enumerate(scale_vals):
self.assertEqual(wgt_sc.float_data[index], np.float32(scale))
# NOTE: Bias with overrides is treated as a weight.
self.assertEqual(bias_zp.data_type, self.default_wgt_qtype_per_channel)
for index, zp in enumerate(zp_vals):
self.assertEqual(bias_zp.int32_data[index], zp)
for index, scale in enumerate(scale_vals):
self.assertEqual(bias_sc.float_data[index], np.float32(scale))
def test_qdq_overrides_per_channel2(self):
"""
Test per-channel overriding of rmin, rmax, reduce_range, and quant_type for Conv weight.
"""
rmin_vals = [0.0, 0.2]
rmax_vals = [1.0, 0.8]
quant_type = quantization.QuantType.QUInt8
reduce_ranges = [True, False]
(
_,
_,
_,
_,
wgt_zp,
wgt_sc,
bias_zp,
bias_sc,
_,
_,
) = self.perform_qdq_quantization(
"model_per_channel_quant_overrides2.onnx",
tensor_quant_overrides={
"WGT": [
{
"quant_type": quant_type,
"rmin": rmin_vals[0],
"rmax": rmax_vals[0],
"reduce_range": reduce_ranges[0],
},
{
"quant_type": quant_type,
"rmin": rmin_vals[1],
"rmax": rmax_vals[1],
"reduce_range": reduce_ranges[1],
},
],
},
per_channel=True,
)
self.assertEqual(wgt_zp.data_type, quant_type.tensor_type)
for index, (zp, scale) in enumerate(zip(wgt_zp.int32_data, wgt_sc.float_data)):
wgt_qmin, wgt_qmax = get_qmin_qmax_for_qType(wgt_zp.data_type, reduce_range=reduce_ranges[index])
expected_zp, expected_scale = compute_scale_zp(rmin_vals[index], rmax_vals[index], wgt_qmin, wgt_qmax)
self.assertEqual(zp, expected_zp)
self.assertEqual(scale, np.float32(expected_scale))
def test_override_validation_nonexisting_tensor(self):
"""
Test that specifying a non-existing tensor should fail.
"""
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"NON_EXISTING": [{"rmin": 0.0, "rmax": 0.5}]},
)
self.assertIn("is not present in the model", str(context.exception))
def test_override_validation_scale_missing_zp(self):
"""
Test that specifying a scale without zero_point should fail.
"""
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"SIG_OUT": [{"scale": 0.0}]},
)
self.assertIn("Must provide both 'scale' and 'zero_point'", str(context.exception))
def test_override_validation_bad_combination(self):
"""
Test that specifying a scale/zero_point with rmax/rmin/symmetric/reduce_range should fail.
"""
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"SIG_OUT": [{"scale": 0.0, "zero_point": 0, "rmax": 10.0}]},
)
self.assertIn("option 'rmax' is invalid with 'scale' and 'zero_point'", str(context.exception))
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"SIG_OUT": [{"scale": 0.0, "zero_point": 0, "rmin": 10.0}]},
)
self.assertIn("option 'rmin' is invalid with 'scale' and 'zero_point'", str(context.exception))
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"SIG_OUT": [{"scale": 0.0, "zero_point": 0, "symmetric": True}]},
)
self.assertIn("option 'symmetric' is invalid with 'scale' and 'zero_point'", str(context.exception))
with self.assertRaises(ValueError) as context:
self.perform_qdq_quantization(
"model_validation.onnx",
tensor_quant_overrides={"SIG_OUT": [{"scale": 0.0, "zero_point": 0, "reduce_range": True}]},
)
self.assertIn("option 'reduce_range' is invalid with 'scale' and 'zero_point'", str(context.exception))
if __name__ == "__main__":
unittest.main()

View file

@ -408,6 +408,7 @@ packages = [
"onnxruntime.quantization",
"onnxruntime.quantization.operators",
"onnxruntime.quantization.CalTableFlatBuffers",
"onnxruntime.quantization.execution_providers.qnn",
"onnxruntime.transformers",
"onnxruntime.transformers.models.bart",
"onnxruntime.transformers.models.bert",