[MO] Support ONNX QuantizeLinear (#1451)

* [MO] Support ONNX QuantizeLinear

* Update docs

* Fix cast type

* Fix error messages
This commit is contained in:
Maxim Vafin authored and GitHub committed 2020-08-10 21:10:45 +03:00
1 parent e2e2785131
commit a6efc86a6a
6 files changed
+315 -1

No files matched your search

@@ -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 |
@@ -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
@@ -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
@@ -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]
@@ -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)
@@ -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']