Model Optimizer incorrectly convert per-channel Quantize/Dequantize operatros to FakeQuantize (#8321)

* add q_dq_resolver

* prepare FQ for offline transfornations

* fix quantize_dequantize_linear_resolver.py

* fix tools/mo/openvino/tools/mo/middle/dequantize_linear_resolver.py

* enable compress_quantized_weights.py

* fix sub case

* delete force_clean_up

* add isolated attribute

* add test for quantize_dequantize_linear_resolver

* fix comments

* apply comments

* aplying comments

* applying comments

* applying comments

* Replace q_dq_resolver using pattern

* clean imports

* fix imports

* fix pattern

* delete refernce to unit tests
This commit is contained in:
Eugeny Volosenkov authored and GitHub committed 2022-01-25 15:35:43 +03:00
1 parent 9f343fa37f
commit 439e3b26aa
8 files changed
+289 -58

No files matched your search

+1
View File
@@ -788,6 +788,7 @@ openvino/tools/mo/middle/permute_tensor_iterator.py
openvino/tools/mo/middle/PoolV2ToAttributedPool.py
openvino/tools/mo/middle/preprocessing.py
openvino/tools/mo/middle/PreserveRuntimeInfo.py
openvino/tools/mo/middle/quantize_dequantize_linear_resolver.py
openvino/tools/mo/middle/quantize_fuses.py
openvino/tools/mo/middle/quantize_linear_resolver.py
openvino/tools/mo/middle/ReluQuantizeFuse.py
@@ -232,6 +232,9 @@ class CompressQuantizeWeights(BackReplacementPattern):
def replace_pattern(self, graph: Graph, match: Dict[str, Node]):
fake_quantize = match['fake_quantize']
if fake_quantize.has_and_set('stop_compression'):
return
if 'convert' in match:
dst_type = match['convert'].dst_type
match['convert']['stop_value_propagation'] = False
@@ -183,6 +183,10 @@ def float32_array(value: Union[Iterable[Union[float, int]], float, int]) -> np.n
return np.array(value, dtype=np.float32)
def int8_array(value: Union[Iterable[Union[float, int]], float, int]) -> np.ndarray:
return np.array(value, dtype=np.int8)
def float_array(value: Union[Iterable[Union[float, int]], float, int]) -> np.ndarray:
return float32_array(value)
+5
View File
@@ -42,4 +42,9 @@ class Sub(FrontReplacementPattern):
def find_and_replace_pattern(self, graph: Graph):
for sub in graph.get_op_nodes(op='Sub'):
# The attribute zero_point_sub indicates that the node can be used in ConvertQuantizeDequantize
# transformation (offline transformations). Pattern of such transformation expects Subtract node.
if sub.has_and_set('zero_point_sub'):
continue
self.sub_to_add_replacement(sub)
@@ -25,6 +25,10 @@ class DequantizeLinearResolver(MiddleReplacementPattern):
enabled = True
graph_condition = [lambda graph: graph.graph['layout'] == 'NCHW']
def run_after(self):
from openvino.tools.mo.middle.quantize_dequantize_linear_resolver import QuantizeDequantizeLinearResolver
return [QuantizeDequantizeLinearResolver]
def find_and_replace_pattern(self, graph: Graph):
for dequantize_node in graph.get_op_nodes(op='DequantizeLinear'):
node_name = dequantize_node.soft_get('name', dequantize_node.id)
@@ -33,11 +37,13 @@ class DequantizeLinearResolver(MiddleReplacementPattern):
model_data_type = data_type_str_to_np(graph.graph['cmd_params'].data_type)
cast = Cast(graph, {'dst_type': model_data_type, 'name': node_name + '/Cast'}).create_node()
dequantize_node.in_port(0).get_connection().set_destination(cast.in_port(0))
mul = Mul(graph, {}).create_node()
mul = Mul(graph, {'can_be_fused': False}).create_node()
is_second_port_connected = dequantize_node.is_in_port_connected(2)
if is_second_port_connected:
sub = Sub(graph, {'name': node_name + '/Sub'}).create_node()
# its is necessary not to replace subrtract for pattern in offline transformations
# See ConvertQuantizeDequantize transformation in ngraph
sub = Sub(graph, {'name': node_name + '/Sub', 'zero_point_sub': True}).create_node()
cast.out_port(0).connect(sub.in_port(0))
dequantize_node.in_port(2).get_connection().set_destination(sub.in_port(1))
sub.out_port(0).connect(mul.in_port(0))
@@ -55,12 +61,12 @@ class DequantizeLinearResolver(MiddleReplacementPattern):
target_shape[axis] = input_shape[axis]
mul_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Mul'})
{'name': node_name + '/Reshape/Mul'})
mul.in_port(1).get_connection().set_destination(mul_reshape.in_port(0))
mul_reshape.out_port(0).connect(mul.in_port(1))
if is_second_port_connected:
sub_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Sub'})
{'name': node_name + '/Reshape/Sub'})
sub.in_port(1).get_connection().set_destination(sub_reshape.in_port(0))
sub_reshape.out_port(0).connect(sub.in_port(1))
@@ -0,0 +1,62 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import numpy as np
from openvino.tools.mo.graph.graph import Graph, rename_nodes
from openvino.tools.mo.middle.quantize_linear_resolver import QuantizeLinearResolver
from openvino.tools.mo.middle.replacement import MiddleReplacementPattern
class QuantizeDequantizeLinearResolver(MiddleReplacementPattern):
"""
This transformation replaces QuantizeLinear in pair QuantizeLinear/DequantizeLinear with
constant inputs to FakeQuantize with flag stop_value_propagation=True. This transformation prepare FakeQuantize for
ConvertQuantizeDequantize in offline transformations.
"""
enabled = True
graph_condition = [lambda graph: graph.graph['layout'] == 'NCHW']
def pattern(self):
return dict(
nodes=[('const_input', dict(kind='op', op='Const')),
('const_input_d', dict(kind='data')),
('quantize', dict(kind='op', op='QuantizeLinear')),
('quantize_d', dict(kind='data')),
('dequantize', dict(kind='op', op='DequantizeLinear')),
],
edges=[('const_input', 'const_input_d'),
('const_input_d', 'quantize', {'in': 0}),
('quantize', 'quantize_d'),
('quantize_d', 'dequantize', {'in': 0})
]
)
def run_after(self):
from openvino.tools.mo.middle.quantize_fuses import MarkNodesToFuseUpToFakeQuantize
return [MarkNodesToFuseUpToFakeQuantize]
def replace_pattern(self, graph: Graph, match: dict):
dequantize_node = match['dequantize']
quantize_node = match['quantize']
scale_zerop_is_exist = quantize_node.is_in_port_connected(1) and quantize_node.is_in_port_connected(2) and \
dequantize_node.is_in_port_connected(1) and dequantize_node.is_in_port_connected(2)
if not scale_zerop_is_exist:
return
q_scale = quantize_node.in_port(1).get_source().node
q_zerop = quantize_node.in_port(2).get_source().node
dq_scale = dequantize_node.in_port(1).get_source().node
dq_zerop = dequantize_node.in_port(2).get_source().node
scales_and_zerop_is_const = q_scale.soft_get('type') == 'Const' and dq_scale.soft_get('type') == 'Const' and \
q_zerop.soft_get('type') == 'Const' and dq_zerop.soft_get('type') == 'Const'
scales_and_zerop_equals = np.array_equal(q_scale.value, dq_scale.value) and \
np.array_equal(q_zerop.value, dq_zerop.value)
# only constant as for zero_point/scale supported
# only patterns with same scale/zero_point values for Q and DQ are supported
if not (scales_and_zerop_is_const or scales_and_zerop_equals):
return
QuantizeLinearResolver.quantize_to_fakequantize(graph, quantize_node, True)
quantize_node['isolated'] = True
@@ -9,7 +9,7 @@ from openvino.tools.mo.ops.fakequantize import FakeQuantize
from openvino.tools.mo.front.common.partial_infer.utils import float_array, int64_array
from openvino.tools.mo.front.common.partial_infer.utils import mo_array
from openvino.tools.mo.front.tf.graph_utils import create_op_with_const_inputs
from openvino.tools.mo.graph.graph import Graph, rename_nodes
from openvino.tools.mo.graph.graph import Graph, rename_nodes, Node
from openvino.tools.mo.middle.replacement import MiddleReplacementPattern
from openvino.tools.mo.ops.const import Const
from openvino.tools.mo.ops.reshape import Reshape
@@ -35,68 +35,79 @@ class QuantizeLinearResolver(MiddleReplacementPattern):
graph_condition = [lambda graph: graph.graph['layout'] == 'NCHW']
def run_after(self):
from openvino.tools.mo.middle.quantize_fuses import MarkNodesToFuseUpToFakeQuantize
return [MarkNodesToFuseUpToFakeQuantize]
from openvino.tools.mo.middle.quantize_dequantize_linear_resolver import QuantizeDequantizeLinearResolver
return [QuantizeDequantizeLinearResolver]
def find_and_replace_pattern(self, graph: Graph):
for quantize_node in graph.get_op_nodes(op='QuantizeLinear'):
node_name = quantize_node.soft_get('name', quantize_node.id)
axis = quantize_node.soft_get('axis', None)
scale_y_shape = quantize_node.in_port(1).data.get_shape()
if quantize_node.has_and_set('isolated'): # node is detached and will be eliminated
# during the next clean up
continue
QuantizeLinearResolver.quantize_to_fakequantize(graph, quantize_node)
if quantize_node.is_in_port_connected(2):
zerop = quantize_node.in_port(2).get_source().node
else:
zerop = Const(graph,
{'value': mo_array(0, dtype=np.uint8), 'name': node_name + '/ZeroPoint'}).create_node()
@staticmethod
def quantize_to_fakequantize(graph: Graph, quantize_node: Node, set_stop_value_propagation=False):
node_name = quantize_node.soft_get('name', quantize_node.id)
axis = quantize_node.soft_get('axis', None)
scale_y_shape = quantize_node.in_port(1).data.get_shape()
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')))
if quantize_node.is_in_port_connected(2):
zerop = quantize_node.in_port(2).get_source().node
else:
zerop = Const(graph,
{'value': mo_array(0, dtype=np.uint8), 'name': node_name + '/ZeroPoint'}).create_node()
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'})
quantize_node.in_port(0).get_connection().set_destination(fake_quantize.in_port(0))
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')))
# 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'})
quantize_node.in_port(1).get_connection().set_destination(mul_low.in_port(0))
mul_low.out_port(0).connect(fake_quantize.in_port(1))
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'})
if set_stop_value_propagation:
fake_quantize['stop_compression'] = True
fake_quantize['stop_value_propagation'] = True
quantize_node.in_port(0).get_connection().set_destination(fake_quantize.in_port(0))
# 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))
# 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'})
quantize_node.in_port(1).get_connection().set_destination(mul_low.in_port(0))
mul_low.out_port(0).connect(fake_quantize.in_port(1))
cast = Cast(graph, {'dst_type': zero_point_type, 'name': node_name + '/Cast'}).create_node()
fake_quantize.out_port(0).connect(cast.in_port(0))
quantize_node.out_port(0).get_connection().set_source(cast.out_port(0))
rename_nodes([(quantize_node, node_name + '/TBD'), (cast, node_name)])
# 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))
assert scale_y_shape is not None
if axis is not None and len(scale_y_shape) > 0 and scale_y_shape[0] > 1:
input_shape = fake_quantize.in_port(0).data.get_shape()
target_shape = np.ones(len(input_shape), np.int)
target_shape[axis] = input_shape[axis]
mul_low_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Mul/Low'})
mul_high_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Mul/high'})
cast = Cast(graph, {'dst_type': zero_point_type, 'name': node_name + '/Cast'}).create_node()
fake_quantize.out_port(0).connect(cast.in_port(0))
quantize_node.out_port(0).get_connection().set_source(cast.out_port(0))
rename_nodes([(quantize_node, node_name + '/TBD'), (cast, node_name)])
fake_quantize.in_port(1).get_connection().set_destination(mul_low_reshape.in_port(0))
fake_quantize.in_port(2).get_connection().set_destination(mul_high_reshape.in_port(0))
assert scale_y_shape is not None, "{0} contains scale(input with port 1) with shape None".\
format(quantize_node.soft_get('name', soft_get('id')))
if axis is not None and len(scale_y_shape) > 0 and scale_y_shape[0] > 1:
input_shape = fake_quantize.in_port(0).data.get_shape()
target_shape = np.ones(len(input_shape), np.int)
target_shape[axis] = input_shape[axis]
mul_low_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Mul/Low'})
mul_high_reshape = create_op_with_const_inputs(graph, Reshape, {1: int64_array(target_shape)},
{'name': node_name + '/Reshape/Mul/high'})
mul_low_reshape.out_port(0).connect(fake_quantize.in_port(1))
mul_high_reshape.out_port(0).connect(fake_quantize.in_port(2))
fake_quantize.in_port(1).get_connection().set_destination(mul_low_reshape.in_port(0))
fake_quantize.in_port(2).get_connection().set_destination(mul_high_reshape.in_port(0))
mul_low_reshape.out_port(0).connect(fake_quantize.in_port(1))
mul_high_reshape.out_port(0).connect(fake_quantize.in_port(2))
@@ -0,0 +1,139 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import numpy as np
import unittest
from openvino.tools.mo.front.common.partial_infer.utils import int64_array, float32_array, int8_array
from openvino.tools.mo.middle.quantize_dequantize_linear_resolver import QuantizeDequantizeLinearResolver
from openvino.tools.mo.middle.quantize_linear_resolver import QuantizeLinearResolver
from openvino.tools.mo.utils.ir_engine.compare_graphs import compare_graphs
from unit_tests.utils.graph import build_graph, regular_op_with_shaped_data, result, connect, connect_data, \
valued_const_with_data, regular_op_with_empty_data
nodes_attributes = {
**valued_const_with_data('y_scale_1', float32_array(1.0 / 255.0)),
**valued_const_with_data('y_scale_2', float32_array(1.0 / 255.0)),
**valued_const_with_data('y_zeropoint_1', int8_array(0)),
**valued_const_with_data('y_zeropoint_2', int8_array(0)),
**valued_const_with_data('x_scale_1', float32_array(1.0 / 255.0)),
**valued_const_with_data('x_scale_2', float32_array(1.0 / 255.0)),
**valued_const_with_data('x_zeropoint_1', int8_array(0)),
**valued_const_with_data('x_zeropoint_2', int8_array(0)),
**valued_const_with_data('const_input', float32_array([[0.3, 0.6], [-0.7, -0.9]])),
**valued_const_with_data('in_low', float32_array(-128.0)),
**valued_const_with_data('in_high', float32_array(127.0)),
**valued_const_with_data('out_low', float32_array(-128.0)),
**valued_const_with_data('out_high', float32_array(127.0)),
**valued_const_with_data('non_const_in_low', float32_array(-128.0)),
**valued_const_with_data('non_const_in_high', float32_array(127.0)),
**valued_const_with_data('non_const_out_low', float32_array(-128.0)),
**valued_const_with_data('non_const_out_high', float32_array(127.0)),
**regular_op_with_shaped_data('input', [1, 2, 2], {'op': 'Parameter', 'type': 'Parameter'}),
**regular_op_with_empty_data('const_quantize', {'op': 'QuantizeLinear'}),
**regular_op_with_empty_data('non_const_quantize', {'op': 'QuantizeLinear'}),
**regular_op_with_empty_data('const_dequantize', {'op': 'DequantizeLinear'}),
**regular_op_with_empty_data('non_const_dequantize', {'op': 'DequantizeLinear'}),
**regular_op_with_empty_data('add', {'op': 'Add'}),
**regular_op_with_empty_data('mul_low', {'op': 'Mul'}),
**regular_op_with_empty_data('mul_high', {'op': 'Mul'}),
**regular_op_with_empty_data('const_fq', {'op': 'FakeQuantize'}),
**regular_op_with_empty_data('const_cast', {'op': 'Cast', 'type': 'Convert', 'dst_type': np.uint8}),
**regular_op_with_empty_data('non_const_mul_low', {'op': 'Mul'}),
**regular_op_with_empty_data('non_const_mul_high', {'op': 'Mul'}),
**regular_op_with_empty_data('non_const_fq', {'op': 'FakeQuantize'}),
**regular_op_with_empty_data('non_const_cast', {'op': 'Cast', 'type': 'Convert', 'dst_type': np.uint8}),
**result('result'),
}
class QuantizeDequantizeLinearResolverTest(unittest.TestCase):
def test_quantize_dequantize_linear_resolver(self):
graph = build_graph(nodes_attrs=nodes_attributes,
edges=[
*connect('input', '0:non_const_quantize'),
*connect('y_scale_2', '1:non_const_quantize'),
*connect('y_zeropoint_2', '2:non_const_quantize'),
*connect('non_const_quantize', '0:non_const_dequantize'),
*connect('x_scale_2', '1:non_const_dequantize'),
*connect('x_zeropoint_2', '2:non_const_dequantize'),
*connect('const_input', '0:const_quantize'),
*connect('y_scale_1', '1:const_quantize'),
*connect('y_zeropoint_1', '2:const_quantize'),
*connect('const_quantize', '0:const_dequantize'),
*connect('x_scale_1', '1:const_dequantize'),
*connect('x_zeropoint_1', '2:const_dequantize'),
*connect('const_dequantize', '0:add'),
*connect('non_const_dequantize', '1:add'),
*connect('add', 'result')
], nodes_with_edges_only=True)
const_ref_graph = build_graph(nodes_attrs=nodes_attributes,
edges=[
*connect('input', '0:non_const_quantize'),
*connect('y_scale_2', '1:non_const_quantize'),
*connect('y_zeropoint_2', '2:non_const_quantize'),
*connect('non_const_quantize', '0:non_const_dequantize'),
*connect('x_scale_2', '1:non_const_dequantize'),
*connect('x_zeropoint_2', '2:non_const_dequantize'),
*connect('const_input', '0:const_fq'),
*connect('y_scale_1:0', '0:mul_low'),
*connect('in_low', '1:mul_low'),
('y_scale_1_d', 'mul_high', {'out': 1, 'in': 0}),
*connect('in_high', '1:mul_high'),
*connect('mul_low', '1:const_fq'),
*connect('mul_high', '2:const_fq'),
*connect('out_low', '3:const_fq'),
*connect('out_high', '4:const_fq'),
*connect('const_fq', 'const_cast'),
*connect('const_cast', '0:const_dequantize'),
*connect('x_scale_1', '1:const_dequantize'),
*connect('x_zeropoint_1', '2:const_dequantize'),
*connect('const_dequantize', '0:add'),
*connect('non_const_dequantize', '1:add'),
*connect('add', 'result')
],nodes_with_edges_only=True)
QuantizeDequantizeLinearResolver().find_and_replace_pattern(graph)
graph.graph['layout'] = 'NCHW'
(flag, resp) = compare_graphs(graph, const_ref_graph, 'result')
self.assertTrue(flag, resp)
ref_graph = build_graph(nodes_attrs=nodes_attributes,
edges=[
*connect('input', '0:non_const_fq'),
*connect('y_scale_2:0', '0:non_const_mul_low'),
*connect('non_const_in_low', '1:non_const_mul_low'),
('y_scale_2_d', 'non_const_mul_high', {'out': 1, 'in': 0}),
*connect('non_const_in_high', '1:non_const_mul_high'),
*connect('non_const_mul_low', '1:non_const_fq'),
*connect('non_const_mul_high', '2:non_const_fq'),
*connect('non_const_out_low', '3:non_const_fq'),
*connect('non_const_out_high', '4:non_const_fq'),
*connect('non_const_fq', 'non_const_cast'),
*connect('non_const_cast', '0:non_const_dequantize'),
*connect('x_scale_2', '1:non_const_dequantize'),
*connect('x_zeropoint_2', '2:non_const_dequantize'),
*connect('const_input', '0:const_fq'),
*connect('y_scale_1:0', '0:mul_low'),
*connect('in_low', '1:mul_low'),
('y_scale_1_d', 'mul_high', {'out': 1, 'in': 0}),
*connect('in_high', '1:mul_high'),
*connect('mul_low', '1:const_fq'),
*connect('mul_high', '2:const_fq'),
*connect('out_low', '3:const_fq'),
*connect('out_high', '4:const_fq'),
*connect('const_fq', 'const_cast'),
*connect('const_cast', '0:const_dequantize'),
*connect('x_scale_1', '1:const_dequantize'),
*connect('x_zeropoint_1', '2:const_dequantize'),
*connect('const_dequantize', '0:add'),
*connect('non_const_dequantize', '1:add'),
*connect('add', 'result')
], nodes_with_edges_only=True)
QuantizeLinearResolver().find_and_replace_pattern(graph)
(flag, resp) = compare_graphs(graph, ref_graph, 'result')
self.assertTrue(flag, resp)