From a6efc86a6ac0eb66fa55e5d618fa24004ae0b04a Mon Sep 17 00:00:00 2001 From: Maxim Vafin Date: Mon, 10 Aug 2020 21:10:45 +0300 Subject: [PATCH] [MO] Support ONNX QuantizeLinear (#1451) * [MO] Support ONNX QuantizeLinear * Update docs * Fix cast type * Fix error messages --- .../Supported_Frameworks_Layers.md | 2 +- model-optimizer/automation/package_BOM.txt | 3 + .../front/onnx/quantize_linear_ext.py | 32 ++++ .../front/onnx/quantize_linear_resolver.py | 81 +++++++++ .../onnx/quantize_linear_resolver_test.py | 161 ++++++++++++++++++ .../extensions/ops/quantize_linear.py | 37 ++++ 6 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 model-optimizer/extensions/front/onnx/quantize_linear_ext.py create mode 100644 model-optimizer/extensions/front/onnx/quantize_linear_resolver.py create mode 100644 model-optimizer/extensions/front/onnx/quantize_linear_resolver_test.py create mode 100644 model-optimizer/extensions/ops/quantize_linear.py diff --git a/docs/MO_DG/prepare_model/Supported_Frameworks_Layers.md b/docs/MO_DG/prepare_model/Supported_Frameworks_Layers.md index ab4cc96c0cc..afd35f1cf20 100644 --- a/docs/MO_DG/prepare_model/Supported_Frameworks_Layers.md +++ b/docs/MO_DG/prepare_model/Supported_Frameworks_Layers.md @@ -360,7 +360,7 @@ Standard ONNX\* operators: | Pad | No | | Pow | No | | PriorBox (Intel experimental) | No | -| QuantizeLinear | Only in combination with DequantizeLinear. When the ops following each other in the graph and the scale and zero-point values for these operations are the same (or explicitly shared), the combination is fused into a 'FakeQuantization'| +| QuantizeLinear | No | | RNN | No | | ROIAlign | No | | Range | No | diff --git a/model-optimizer/automation/package_BOM.txt b/model-optimizer/automation/package_BOM.txt index 5674b91db30..7e30913cb32 100644 --- a/model-optimizer/automation/package_BOM.txt +++ b/model-optimizer/automation/package_BOM.txt @@ -287,6 +287,8 @@ extensions/front/onnx/priorgridgenerator_ext.py extensions/front/onnx/proposal_ext.py extensions/front/onnx/quantize_dequantize_linear.py extensions/front/onnx/quantize_ext.py +extensions/front/onnx/quantize_linear_ext.py +extensions/front/onnx/quantize_linear_resolver.py extensions/front/onnx/range_ext.py extensions/front/onnx/reduce_l2_ext.py extensions/front/onnx/reduce_max_ext.py @@ -646,6 +648,7 @@ extensions/ops/proposal.py extensions/ops/proposal_onnx.py extensions/ops/proposal_python_example.py extensions/ops/psroipooling.py +extensions/ops/quantize_linear.py extensions/ops/range.py extensions/ops/rank.py extensions/ops/ReduceOps.py diff --git a/model-optimizer/extensions/front/onnx/quantize_linear_ext.py b/model-optimizer/extensions/front/onnx/quantize_linear_ext.py new file mode 100644 index 00000000000..ba3dbd4166d --- /dev/null +++ b/model-optimizer/extensions/front/onnx/quantize_linear_ext.py @@ -0,0 +1,32 @@ +""" + Copyright (C) 2020 Intel Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from extensions.ops.quantize_linear import QuantizeLinear +from mo.front.extractor import FrontExtractorOp +from mo.front.onnx.extractors.utils import onnx_attr, get_onnx_opset_version + + +class QuantizeLinearFrontExtractor(FrontExtractorOp): + op = 'QuantizeLinear' + enabled = True + + @classmethod + def extract(cls, node): + attrs = {} + if get_onnx_opset_version(node) >= 13: + axis = onnx_attr(node, 'axis', 'i', default=1) + attrs.update(axis=axis) + QuantizeLinear.update_node_stat(node, attrs) + return cls.enabled diff --git a/model-optimizer/extensions/front/onnx/quantize_linear_resolver.py b/model-optimizer/extensions/front/onnx/quantize_linear_resolver.py new file mode 100644 index 00000000000..6cb2facfa4e --- /dev/null +++ b/model-optimizer/extensions/front/onnx/quantize_linear_resolver.py @@ -0,0 +1,81 @@ +""" + Copyright (C) 2020 Intel Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import numpy as np + +from extensions.front.onnx.quantize_dequantize_linear import QuantizeDequantizeLinear +from extensions.ops.Cast import Cast +from extensions.ops.elementwise import Mul +from extensions.ops.fakequantize import FakeQuantize +from mo.front.common.partial_infer.utils import float_array +from mo.front.common.replacement import FrontReplacementOp +from mo.front.tf.graph_utils import create_op_with_const_inputs +from mo.graph.graph import Graph, Node, rename_nodes +from mo.ops.const import Const +from mo.utils.error import Error + + +class QuantizeLinearResolver(FrontReplacementOp): + """ + Replaces QuantizeLinear with FakeQuantize + """ + op = "QuantizeLinear" + enabled = True + + def run_after(self): + return [QuantizeDequantizeLinear] + + def replace_op(self, graph: Graph, node: Node): + node_name = node.soft_get('name', node.id) + + if node.is_in_port_connected(2): + zerop = node.in_port(2).get_source().node + else: + zerop = Const(graph, {'value': np.array(0, dtype=np.uint8), 'name': node_name + '/ZeroPoint'}).create_node() + + assert zerop.soft_get('type') == 'Const', 'only constant for zero_point is supported for QuantizeLinear' + zero_point_type = zerop.value.dtype + # data type affects range of output values: [-128..127] or [0..255] + if zero_point_type == np.int8: + output_low_value = -128.0 + output_high_value = 127.0 + elif zero_point_type == np.uint8: + output_low_value = 0.0 + output_high_value = 255.0 + else: + raise Error('Not expected type {} for zero point value in node {}'.format( + zero_point_type, zerop.soft_get('name'))) + fake_quantize = create_op_with_const_inputs(graph, FakeQuantize, {3: float_array(output_low_value), + 4: float_array(output_high_value)}, + {'levels': 256, 'name': node_name + '/FakeQuantize'}) + node.in_port(0).get_connection().set_destination(fake_quantize.in_port(0)) + + # Calculate input_low value + mul_low = create_op_with_const_inputs(graph, Mul, {1: float_array(output_low_value - zerop.value)}, + {'name': node_name + '/Mul/Low'}) + node.in_port(1).get_connection().set_destination(mul_low.in_port(0)) + mul_low.out_port(0).connect(fake_quantize.in_port(1)) + + # Calculate input_high value + mul_high = create_op_with_const_inputs(graph, Mul, {1: float_array(output_high_value - zerop.value)}, + {'name': node_name + '/Mul/High'}) + mul_low.in_port(0).get_connection().add_destination(mul_high.in_port(0)) + mul_high.out_port(0).connect(fake_quantize.in_port(2)) + + cast = Cast(graph, {'dst_type': zero_point_type, 'name': node_name + '/Cast'}).create_node() + rename_nodes([(node, node_name + '/TBD'), (cast, node_name)]) + fake_quantize.out_port(0).connect(cast.in_port(0)) + + return [cast.id] diff --git a/model-optimizer/extensions/front/onnx/quantize_linear_resolver_test.py b/model-optimizer/extensions/front/onnx/quantize_linear_resolver_test.py new file mode 100644 index 00000000000..3c4c14b54e4 --- /dev/null +++ b/model-optimizer/extensions/front/onnx/quantize_linear_resolver_test.py @@ -0,0 +1,161 @@ +""" + Copyright (C) 2020 Intel Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import unittest +from argparse import Namespace + +import numpy as np + +from extensions.front.onnx.quantize_linear_resolver import QuantizeLinearResolver +from mo.utils.ir_engine.compare_graphs import compare_graphs +from mo.utils.unittest.graph import build_graph + +nodes1_attributes = { + 'input': {'kind': 'op', 'op': 'AnyOp'}, + 'quantize': {'kind': 'op', 'op': 'QuantizeLinear'}, + 'scale_param_q': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'zerop_param_q': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'out': {'kind': 'op', 'op': 'AnyOp'}, +} + +nodes_ref_attributes = { + 'input': {'kind': 'op', 'op': 'AnyOp'}, + 'cast': {'kind': 'op', 'op': 'Cast', 'type': 'Convert'}, + 'f_quantize': {'kind': 'op', 'op': 'FakeQuantize', 'type': 'FakeQuantize'}, + 'mul1': {'kind': 'op', 'op': 'Mul', 'type': 'Multiply'}, + 'mul2': {'kind': 'op', 'op': 'Mul', 'type': 'Multiply'}, + 'scale_param_q': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'in_low': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'in_high': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'out_low': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'out_high': {'kind': 'op', 'type': 'Const', 'op': 'Const'}, + 'out': {'kind': 'op', 'op': 'AnyOp'}, +} + + +class TestQuantizeLinearResolver(unittest.TestCase): + + def test_quantize_uint8(self): + graph = build_graph(nodes1_attributes, + [('input', 'quantize'), + ('scale_param_q', 'quantize'), + ('zerop_param_q', 'quantize'), + ('quantize', 'out'), + ], + {'scale_param_q': {'shape': np.array([]), 'value': np.float32(1.0 / 255)}, + 'zerop_param_q': {'shape': np.array([]), 'value': np.uint8(128)}, + }, nodes_with_edges_only=True) + graph.graph['cmd_params'] = Namespace(keep_shape_ops=True) + + graph_ref = build_graph(nodes_ref_attributes, + [('input', 'f_quantize'), + ('scale_param_q', 'mul1', {'out': 0}), + ('in_low', 'mul1'), + ('mul1', 'f_quantize'), + ('scale_param_q', 'mul2', {'out': 0}), + ('in_high', 'mul2'), + ('mul2', 'f_quantize'), + ('out_low', 'f_quantize'), + ('out_high', 'f_quantize'), + ('f_quantize', 'cast'), + ('cast', 'out'), + ], + {'in_low': {'shape': np.array([]), 'value': -128}, + 'in_high': {'shape': np.array([]), 'value': 127}, + 'out_low': {'shape': np.array([]), 'value': 0}, + 'out_high': {'shape': np.array([]), 'value': 255}, + 'cast': {'dst_type': np.uint8} + }, nodes_with_edges_only=True) + + graph.stage = 'front' + QuantizeLinearResolver().find_and_replace_pattern(graph) + + (flag, resp) = compare_graphs(graph, graph_ref, 'out', check_op_attrs=True) + self.assertTrue(flag, resp) + + def test_quantize_int8(self): + graph = build_graph(nodes1_attributes, + [('input', 'quantize'), + ('scale_param_q', 'quantize'), + ('zerop_param_q', 'quantize'), + ('quantize', 'out'), + ], + {'scale_param_q': {'shape': np.array([]), 'value': np.float32(1.0 / 255)}, + 'zerop_param_q': {'shape': np.array([]), 'value': np.int8(0)}, + }, nodes_with_edges_only=True) + graph.graph['cmd_params'] = Namespace(keep_shape_ops=True) + + graph_ref = build_graph(nodes_ref_attributes, + [('input', 'f_quantize'), + ('scale_param_q', 'mul1', {'out': 0}), + ('in_low', 'mul1'), + ('mul1', 'f_quantize'), + ('scale_param_q', 'mul2', {'out': 0}), + ('in_high', 'mul2'), + ('mul2', 'f_quantize'), + ('out_low', 'f_quantize'), + ('out_high', 'f_quantize'), + ('f_quantize', 'cast'), + ('cast', 'out'), + ], + {'in_low': {'shape': np.array([]), 'value': -128}, + 'in_high': {'shape': np.array([]), 'value': 127}, + 'out_low': {'shape': np.array([]), 'value': -128}, + 'out_high': {'shape': np.array([]), 'value': 127}, + 'cast': {'dst_type': np.int8} + }, nodes_with_edges_only=True) + + graph.stage = 'front' + QuantizeLinearResolver().find_and_replace_pattern(graph) + + (flag, resp) = compare_graphs(graph, graph_ref, 'out', check_op_attrs=True) + self.assertTrue(flag, resp) + + def test_quantize_no_zerop(self): + graph = build_graph(nodes1_attributes, + [('input', 'quantize'), + ('scale_param_q', 'quantize'), + ('quantize', 'out'), + ], + {'scale_param_q': {'shape': np.array([]), 'value': np.float32(1.0 / 255)}, + }, nodes_with_edges_only=True) + graph.graph['cmd_params'] = Namespace(keep_shape_ops=True) + + graph_ref = build_graph(nodes_ref_attributes, + [('input', 'f_quantize'), + ('scale_param_q', 'mul1', {'out': 0}), + ('in_low', 'mul1'), + ('mul1', 'f_quantize'), + ('scale_param_q', 'mul2', {'out': 0}), + ('in_high', 'mul2'), + ('mul2', 'f_quantize'), + ('out_low', 'f_quantize'), + ('out_high', 'f_quantize'), + ('f_quantize', 'cast'), + ('cast', 'out'), + ], + {'in_low': {'shape': np.array([]), 'value': 0}, + 'in_high': {'shape': np.array([]), 'value': 255}, + 'out_low': {'shape': np.array([]), 'value': 0}, + 'out_high': {'shape': np.array([]), 'value': 255}, + 'cast': {'dst_type': np.uint8} + }, nodes_with_edges_only=True) + + graph.stage = 'front' + QuantizeLinearResolver().find_and_replace_pattern(graph) + + (flag, resp) = compare_graphs(graph, graph_ref, 'out', check_op_attrs=True) + self.assertTrue(flag, resp) diff --git a/model-optimizer/extensions/ops/quantize_linear.py b/model-optimizer/extensions/ops/quantize_linear.py new file mode 100644 index 00000000000..ae5717579cf --- /dev/null +++ b/model-optimizer/extensions/ops/quantize_linear.py @@ -0,0 +1,37 @@ +""" + Copyright (C) 2020 Intel Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from mo.graph.graph import Graph +from mo.ops.op import Op + + +class QuantizeLinear(Op): + op = 'QuantizeLinear' + enabled = False + + def __init__(self, graph: Graph, attrs: dict): + mandatory_props = { + 'type': None, + 'op': self.op, + 'version': None, + 'infer': None, + 'out_ports_count': 1, + 'in_ports_count': 3, + } + super().__init__(graph, mandatory_props, attrs) + + def supported_attrs(self): + return ['axis']