Skip to content

Commit

Permalink
Merge branch 'master' into tom/verify_dtype
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWildenhain-Microsoft committed Apr 7, 2021
2 parents ce67e78 + c9a0a5a commit c0bfc04
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 43 deletions.
24 changes: 24 additions & 0 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,16 @@ def func(x):
return tf.identity(x_, name=_TFOUTPUT)
_ = self._run_test_case(func, [_OUTPUT], {_INPUT: x_val})

@check_tf_min_version("1.15")
@check_opset_min_version(10, "quantize_and_dequantize")
def test_qdq_dyn_range_unsigned_input(self):
x_shape = [3, 3, 2]
x_val = np.arange(1, 1+np.prod(x_shape)).astype("float32").reshape(x_shape) + 0.1
def func(x):
x_ = quantize_and_dequantize(x, 1.0, 6.0, signed_input=False, range_given=False)
return tf.identity(x_, name=_TFOUTPUT)
_ = self._run_test_case(func, [_OUTPUT], {_INPUT: x_val})

@skip_tflite("tflite converter mistranslates quantize op")
@check_tf_min_version("1.15")
@check_opset_min_version(10, "quantize_and_dequantize")
Expand All @@ -2580,6 +2590,20 @@ def func(x):
return tf.identity(x_, name=_TFOUTPUT)
_ = self._run_test_case(func, [_OUTPUT], {_INPUT: x_val})

@skip_tflite("tflite converter crashes")
@check_tf_min_version("2.0")
@check_opset_min_version(13, "quantize_and_dequantize")
def test_qdq_dyn_range_per_channel_signed_input(self):
x_shape = [3, 3, 2]
x_val = np.arange(-np.prod(x_shape)/2, np.prod(x_shape)/2).astype("float32").reshape(x_shape)
def func(x):
x_ = quantize_and_dequantize(x, np.array([-1.72, -3.89]).astype(np.float32), \
np.array([5.12, 2.36]).astype(np.float32), \
signed_input=True, narrow_range=False, \
range_given=False, axis=-1)
return tf.identity(x_, name=_TFOUTPUT)
_ = self._run_test_case(func, [_OUTPUT], {_INPUT: x_val})

@skip_caffe2_backend()
@check_opset_min_version(7, "resize_nearest_neighbor")
def test_resize_nearest_neighbor(self):
Expand Down
6 changes: 3 additions & 3 deletions tf2onnx/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def from_keras(model, input_signature=None, opset=None, custom_ops=None, custom_
frozen_graph,
name=model.name,
continue_on_error=True,
target=None,
target=target,
opset=opset,
custom_op_handlers=custom_ops,
extra_opset=extra_opset,
Expand Down Expand Up @@ -388,7 +388,7 @@ def from_function(function, input_signature=None, opset=None, custom_ops=None, c
frozen_graph,
name=concrete_func.name,
continue_on_error=True,
target=None,
target=target,
opset=opset,
custom_op_handlers=custom_ops,
extra_opset=extra_opset,
Expand Down Expand Up @@ -447,7 +447,7 @@ def from_graph_def(graph_def, name=None, input_names=None, output_names=None, op
frozen_graph,
name=name,
continue_on_error=True,
target=None,
target=target,
opset=opset,
custom_op_handlers=custom_ops,
extra_opset=extra_opset,
Expand Down
115 changes: 75 additions & 40 deletions tf2onnx/rewriter/quantization_ops_rewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import numpy as np
from onnx import TensorProto, helper
from tf2onnx.graph_matcher import OpTypePattern, GraphMatcher
from tf2onnx import utils

Expand All @@ -24,6 +25,7 @@ def create_qdq_nodes(g, match_results):
# Get the attributes of qdq node
narrow_range = qdq_node.attr['narrow_range'].i
signed_input = qdq_node.attr['signed_input'].i
range_given = qdq_node.get_attr_value("range_given", qdq_node.type != "QuantizeAndDequantizeV2")

min_quantized, max_quantized = [-127, 127]
if not narrow_range and signed_input:
Expand All @@ -33,64 +35,97 @@ def create_qdq_nodes(g, match_results):
min_quantized, max_quantized = [0, 255]

# Get axis attribute for per channel implementation.
if 'axis' in qdq_node.attr:
axis = qdq_node.attr['axis'].i
axis = qdq_node.get_attr_value('axis', -1)
q_attrs = {}

# Get the min and max value of the inputs to QDQ op
min_value = extract_numpy_array(qdq_node.inputs[1])
max_value = extract_numpy_array(qdq_node.inputs[2])
quantized_np_dtype = np.int8 if signed_input else np.uint8
quantized_dtype = TensorProto.INT8 if signed_input else TensorProto.UINT8

num_channels = min_value.shape[0]
scales = np.zeros(num_channels, dtype=np.float32)
zero_point_dtype = np.int8 if signed_input else np.uint8
zero_point = np.zeros(num_channels, dtype=zero_point_dtype)

for i in range(num_channels):
# Calculate scales from the min and max values
scale_from_min_side = min_quantized/min_value[i] if min_quantized*min_value[i] > 0 else max_quantized
scale_from_max_side = max_quantized/max_value[i] if max_quantized*max_value[i] > 0 else max_quantized

if scale_from_min_side < scale_from_max_side:
scale = scale_from_min_side
if axis != -1:
utils.make_sure(g.opset >= 13, "Opset >= 13 is required for per channel quantization")
q_attrs['axis'] = axis

if not range_given:
min_np = np.array(min_quantized, np.float32)
max_np = np.array(max_quantized, np.float32)
max_quantized_const = g.make_const(utils.make_name("max_const"), max_np).output[0]
if signed_input:
min_quantized_const = g.make_const(utils.make_name("min_const"), min_np).output[0]
reduce_attr = {'keepdims': 0}
if axis != -1:
inp_rank = g.get_rank(qdq_node.input[0])
utils.make_sure(inp_rank is not None, "Input rank cannot be unknown for qdq op %s", qdq_node.name)
reduce_axes = [i for i in range(inp_rank) if i != axis]
reduce_attr['axes'] = reduce_axes

max_value = g.make_node("ReduceMax", [qdq_node.input[0]], attr=reduce_attr).output[0]
if signed_input:
min_value = g.make_node("ReduceMin", [qdq_node.input[0]], attr=reduce_attr).output[0]

scale_from_max_side = g.make_node("Div", [max_value, max_quantized_const]).output[0]
if signed_input:
scale_from_min_side = g.make_node("Div", [min_value, min_quantized_const]).output[0]
scale = g.make_node("Max", [scale_from_min_side, scale_from_max_side]).output[0]
else:
scale = scale_from_max_side

utils.make_sure(scale > 0, "Quantize/Dequantize scale must be greater than zero")
scales[i] = np.float32(scale)

# Set scalars for scale and zero point for per layer quantization
if num_channels == 1:
scales = scales[0]
zero_point = zero_point[0]
attrs = {}
if axis == -1:
zero_point_np = np.zeros([], dtype=quantized_np_dtype)
zero_point = g.make_const(utils.make_name("zero_point"), zero_point_np).output[0]
else:
zero_tensor = helper.make_tensor("value", quantized_dtype, dims=[1], vals=[0])
scale_shape = g.make_node("Shape", [scale]).output[0]
zero_point = g.make_node("ConstantOfShape", inputs=[scale_shape], attr={"value": zero_tensor}).output[0]
else:
utils.make_sure(axis and axis != -1, "Axis must be specified for per channel quantization")
utils.make_sure(g.opset >= 13, "Opset >= 13 is required for per channel quantization")
attrs = {'axis': axis}
# Get the min and max value of the inputs to QDQ op
min_value = extract_numpy_array(qdq_node.inputs[1])
max_value = extract_numpy_array(qdq_node.inputs[2])

num_channels = min_value.shape[0]
scales = np.zeros(num_channels, dtype=np.float32)

for i in range(num_channels):
# Calculate scales from the min and max values
scale_from_min_side = min_value[i] / min_quantized if min_quantized < 0 else 0
scale_from_max_side = max_value[i] / max_quantized if max_quantized > 0 else 0

if scale_from_min_side > scale_from_max_side:
scale = scale_from_min_side
else:
scale = scale_from_max_side

utils.make_sure(scale > 0, "Quantize/Dequantize scale must be greater than zero")
scales[i] = np.float32(scale)

# Set scalars for scale and zero point for per layer quantization
if num_channels == 1:
scales = scales[0]
zero_point_np = np.zeros([], dtype=quantized_np_dtype)
else:
utils.make_sure(axis != -1, "Axis must be specified for per channel quantization")
zero_point_np = np.zeros([num_channels], dtype=quantized_np_dtype)

# Split it into QuantizeLinear and DequantizeLinear and remove the QDQ node reference
cast_scale = scales.astype(np.float32)
scale = g.make_const(name=utils.make_name("quant_scale"), np_val=cast_scale).output[0]
zero_point = g.make_const(utils.make_name("zero_point"), zero_point_np).output[0]

# Split it into QuantizeLinear and DequantizeLinear and remove the QDQ node reference
inverse_scale = (1/scales).astype(np.float32)
y_quant_scale = g.make_const(name=utils.make_name("y_quant_scale"), np_val=inverse_scale)
y_zero_point = g.make_const(name=utils.make_name("y_zero_point"), np_val=zero_point)
quant_node = g.make_node(op_type="QuantizeLinear",
inputs=[qdq_node.input[0], y_quant_scale.output[0],
y_zero_point.output[0]],
inputs=[qdq_node.input[0], scale, zero_point],
shapes=[qdq_node_output_shape],
attr=attrs,
attr=q_attrs,
dtypes=[quantized_dtype],
name=utils.make_name("QuantLinearNode"))

g.set_shape(quant_node.output[0], qdq_node_output_shape)

g.remove_node(qdq_node.name)

y_dequant_scale = g.make_const(name=utils.make_name("y_dequant_scale"), np_val=inverse_scale)
y_inv_zero_point = g.make_const(name=utils.make_name("y_inv_zero_point"), np_val=zero_point)
dequant_node = g.make_node(op_type="DequantizeLinear",
inputs=[quant_node.output[0], y_dequant_scale.output[0],
y_inv_zero_point.output[0]],
inputs=[quant_node.output[0], scale, zero_point],
outputs=[qdq_node.output[0]],
shapes=[qdq_node_output_shape],
attr=attrs,
attr=q_attrs,
dtypes=[qdq_node_output_dtype],
name=utils.make_name("DequantLinearNode"))
g.set_shape(dequant_node.output[0], qdq_node_output_shape)
Expand Down

0 comments on commit c0bfc04

Please sign in to comment.