Reverse infer (#8767)

This commit is contained in:
undef-nnov 2021-12-01 10:49:04 +03:00 committed by GitHub
parent ab22d7d041
commit c1515d92e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 202 additions and 48 deletions

View File

@ -18,5 +18,7 @@ class PlaceholderFrontExtractor(FrontExtractorOp):
'shape': tf_tensor_shape(node.pb.attr["shape"].shape),
'permute_attrs': PermuteAttrs().update_attrs(attrs=[('shape', 'output:0')])
}
if node.pb.attr["shape"].shape.unknown_rank:
attrs['shape'] = None
Parameter.update_node_stat(node, attrs)
return cls.enabled

View File

@ -4,7 +4,7 @@
import numpy as np
from mo.front.common.partial_infer.utils import unmask_shape
from mo.graph.graph import Graph
from mo.graph.graph import Graph, Node
from mo.middle.passes.convert_data_type import np_data_type_to_destination_type
from mo.ops.op import Op, PermuteAttrs
@ -19,6 +19,7 @@ class Parameter(Op):
'version': 'opset1',
'infer': self.infer,
'reverse_infer': self.reverse_infer,
'is_input': True,
'data_type': None,
@ -49,3 +50,12 @@ class Parameter(Op):
node.out_port(0).data.set_shape(node.shape)
PermuteAttrs.create_permute_attrs(node, attrs=[('shape', 'output:0')])
@staticmethod
def reverse_infer(node: Node):
# update node 'shape' attribute (if it is not defined) from the output port shape which was calculated
# during the reverse_infer phase
shape = node.soft_get('shape', None)
if shape is None and node.out_port(0).data.get_shape() is not None:
node['shape'] = node.out_port(0).data.get_shape()

View File

@ -24,6 +24,16 @@ def shape_array(value, dtype=np.int64):
return np.ma.masked_equal(value, dynamic_dimension_value).astype(dtype=dtype)
def undefined_shape_of_rank(rank: int):
"""
Create a shape of specified rank with all dynamic dimensions.
:param rank: requested rank of the output shape
:return: shape array
"""
return shape_array([dynamic_dimension_value] * rank)
def compatible_dims(dim1, dim2):
"""
Compare if dim1 is equal to dim2 or any of them is dynamic

View File

@ -2,12 +2,12 @@
# SPDX-License-Identifier: Apache-2.0
import logging as log
from typing import List
import networkx as nx
from mo.front.common.partial_infer.utils import dynamic_dimension
from mo.graph.graph import Node, Graph
from mo.graph.graph import dict_includes
from mo.graph.graph import Node, Graph, dict_includes
from mo.utils.error import Error
from mo.utils.utils import refer_to_faq_msg, shrink_str_value
@ -93,18 +93,59 @@ def partial_infer(graph: Graph, start_node: str = None):
nx.set_node_attributes(G=graph.subgraph(nodes[start_index:]), name='is_partial_inferred', values=False)
else:
nx.set_node_attributes(G=graph, name='is_partial_inferred', values=False)
debug_logger = log.getLogger().isEnabledFor(log.DEBUG)
nx.set_node_attributes(G=graph, name='executable',
values={n: True for n in graph.get_nodes_with_attributes(kind='data')})
# first we infer constant sub-graphs so the reverse infer could use constant values sub-graphs. For example,
# convolution weights may be reshuffled by some operation in the graph and are not directly consumed by the conv
# node
infer_nodes(graph, nodes, True)
# we may need to deduce shape for Parameter node(s) if it is not defined
need_reverse_infer = False
for parameter in graph.get_op_nodes(op='Parameter'):
if parameter.soft_get('shape', None) is None:
need_reverse_infer = True
if need_reverse_infer:
reverse_infer(graph, nodes)
infer_nodes(graph, nodes, False)
not_fully_inferred = graph.get_nodes_with_attributes(is_not_fully_inferred=True)
for n in not_fully_inferred:
node = Node(graph, n)
if node.has('infer') and not node.infer is None:
node.infer(node)
return graph
def infer_nodes(graph: Graph, nodes: List[Node], constant_subgraph_only: bool = False):
"""
Run "infer" function of the specified nodes.
:param graph: graph with nodes
:param nodes: list of node ids in the topological order
:param constant_subgraph_only: flag which specifies whether only inference of constant sub-graphs should be done
"""
debug_logger = log.getLogger().isEnabledFor(log.DEBUG)
for n in nodes:
# Data Flow Infer
node = Node(graph, n)
node_name = node.soft_get('name', node.id)
try:
node = Node(graph, n)
node_name = node.soft_get('name')
if node.has('is_partial_inferred') and not node.is_partial_inferred:
if node.has('infer') and not node.infer is None:
# we consider that operation will produce value if all inputs are constants or it is
# 'ShapeOf' operation
if constant_subgraph_only:
in_values = [port.data.get_value() for port in node.in_ports().values()]
if node.soft_get('op') == 'Parameter' or any(value is None for value in in_values) or \
(node.soft_get('op') == 'ShapeOf' and node.in_port(0).data.get_shape() is None):
continue
if debug_logger:
log.debug('-' * 20)
log.debug('Partial infer for {}'.format(node.soft_get('name')))
@ -125,18 +166,19 @@ def partial_infer(graph: Graph, start_node: str = None):
log.debug('Outputs:')
log_debug_dict(node.out_nodes(), 'output')
not_all_output_shapes = False
for out_port, out_node in out_nodes.items():
if not constant_subgraph_only:
not_all_output_shapes = False
if not out_node.has_valid('shape'):
log.error('Shape is not defined for output {} of "{}".'.format(out_port, node_name))
not_all_output_shapes = True
if not_all_output_shapes:
raise Error('Not all output shapes were inferred or fully defined for node "{}". ' +
refer_to_faq_msg(40),
node_name)
for out_port, out_node in out_nodes.items():
not_all_output_shapes = False
if not out_node.has_valid('shape'):
log.error('Shape is not defined for output {} of "{}".'.format(out_port, node_name))
not_all_output_shapes = True
if not_all_output_shapes:
raise Error('Not all output shapes were inferred or fully defined for node "{}". ' +
refer_to_faq_msg(40),
node_name)
elif node.kind != 'data':
raise Error(
'There is no registered "infer" function for node "{}" with op = "{}". ' +
@ -146,7 +188,6 @@ def partial_infer(graph: Graph, start_node: str = None):
node.soft_get('op')
)
node.is_partial_inferred = True
except Exception as err:
log.error('Cannot infer shapes or values for node "{}".'.format(node.soft_get('name')))
log.error(str(err))
@ -169,14 +210,6 @@ def partial_infer(graph: Graph, start_node: str = None):
refer_to_faq_msg(38)) from err
control_flow_infer(graph, n)
not_fully_inferred = graph.get_nodes_with_attributes(is_not_fully_inferred=True)
for n in not_fully_inferred:
node = Node(graph, n)
if node.has('infer') and not node.infer is None:
node.infer(node)
return graph
def override_batch(graph: Graph, batch: int):
"""
@ -286,3 +319,13 @@ def copy_type_infer(node):
out_port.set_data_type(connected_in_ports[0].get_data_type())
else:
raise Error('No input ports of node {} to determine data type'.format(node.soft_get('name')))
def reverse_infer(graph: Graph, nodes: list):
nodes = reversed(nodes)
for n in nodes:
node = Node(graph, n)
if node.has_valid('reverse_infer'):
log.debug("Executed reverse infer for node '{}'".format(node.soft_get('name', node.id)))
node.reverse_infer(node)

View File

@ -6,7 +6,7 @@ import logging as log
import numpy as np
from mo.front.common.partial_infer.utils import int64_array, mark_input_bins, assign_dims_to_weights, \
tf_window_op_pad_infer, dynamic_dimension_value, shape_array
tf_window_op_pad_infer, dynamic_dimension_value, shape_array, is_fully_defined, undefined_shape_of_rank
from mo.front.onnx.extractors.utils import get_backend_pad
from mo.graph.graph import Node, Graph
from mo.graph.perm_inputs import PermuteInputs
@ -23,6 +23,7 @@ class Convolution(Op):
'op': self.op,
'version': 'opset1',
'infer': self.infer,
'reverse_infer': self.reverse_infer,
'multiplication_transparent': True,
'multiplication_transparent_ports': [(0, 0), (1, 0)],
'in_ports_count': 3,
@ -112,18 +113,29 @@ class Convolution(Op):
log.error('Cannot reshape kernel due to not all required attrs was set to {} node'.format(node.id))
return
# layout for Convolution weights is OIHW
kernel_shape = int64_array([node.output, input_shape[node.channel_dims].item() / node.group,
kernel_shape = shape_array([node.output, input_shape[node.channel_dims].item() / node.group,
*[node.kernel_spatial[i] for i in range(len(node.kernel_spatial))]])
if node.type == 'Deconvolution': # layout for Deconvolution weights is IOHW
kernel_shape[[0, 1]] = kernel_shape[[1, 0]]
if np.prod(kernel_shape) != np.prod(node.in_node(weights_index).value.shape):
if is_fully_defined(kernel_shape) and np.prod(kernel_shape) != np.prod(node.in_node(weights_index).value.shape):
log.error("Size of weights {} does not match kernel shape: {}\n"
"".format(np.prod(node.in_node(weights_index).value.shape), kernel_shape) +
" Possible reason is wrong channel number in input shape\n")
raise Error("Cannot reshape weights to kernel shape")
node.in_node(weights_index).shape = np.array(kernel_shape)
if not is_fully_defined(kernel_shape):
num_undefined = np.count_nonzero(kernel_shape.mask is True) # pylint: disable=no-member
if num_undefined > 1:
raise Error('Too many undefined dimensions of the kernel shape for node {}. Use --input_shape '
'command line parameter to specify model input shapes'.format(node.soft_get('name',
node.id)))
kernel_size = np.prod(node.in_node(weights_index).value.shape)
# calculate undefined dimension using fully defined shape of the weights input and known kernel_shape
# dimensions
kernel_shape[np.where(kernel_shape == np.ma.masked)[0][0]] = kernel_size // np.prod(kernel_shape)
node.in_node(weights_index).shape = shape_array(kernel_shape)
node.in_node(weights_index).value = np.reshape(node.in_node(weights_index).value, kernel_shape)
node.reshape_kernel = False
@ -262,3 +274,16 @@ class Convolution(Op):
PermuteAttrs.set_permutation(node.in_node(weights_index), node, node.soft_get('get_weights_permute', None))
PermuteInputs().set_input_permutation(
node.in_node(weights_index), node, 'input:{}'.format(weights_index), 'transpose')
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
if input_shape is None:
shape = None
# TODO FIXME this is ugly solution based on various attributes which may not be set in some cases
for attr in ['dilation', 'stride', 'pad']:
if node.has_valid(attr):
shape = undefined_shape_of_rank(len(node.soft_get(attr)))
break
if shape is not None:
node.in_port(0).data.set_shape(shape)

View File

@ -3,8 +3,8 @@
import numpy as np
from mo.front.common.partial_infer.utils import is_fully_defined, shape_array
from mo.graph.graph import Graph
from mo.front.common.partial_infer.utils import is_fully_defined, shape_array, undefined_shape_of_rank
from mo.graph.graph import Graph, Node
from mo.graph.perm_inputs import PermuteInputs
from mo.ops.op import Op
@ -29,6 +29,7 @@ class Pad(Op):
'version': 'opset1',
'infer': self.infer,
'reverse_infer': self.reverse_infer,
'mode': 'constant',
@ -85,6 +86,13 @@ class Pad(Op):
PermuteInputs().set_input_permutation(node.in_node(1), node, 'input:0', 'shape')
PermuteInputs().set_input_permutation(node.in_node(2), node, 'input:0', 'shape')
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
if input_shape is None and node.is_in_port_connected(2) and node.in_port(2).data.get_shape() is not None:
shape = undefined_shape_of_rank(node.in_port(2).data.get_shape()[0])
node.in_port(0).data.set_shape(shape)
class AttributedPad(Op):
""" Pad operation that explicitly extends an input tensor at borders.
@ -131,6 +139,7 @@ class TFPad(Op):
'mode': 'constant',
}, attrs)
class ONNXPad(Op):
""" Pad operation that explicitly extends an input tensor at borders.

View File

@ -4,7 +4,7 @@
import numpy as np
from mo.front.common.partial_infer.utils import tf_window_op_pad_infer, int64_array, shape_array, \
dynamic_dimension_value, dynamic_dimension
dynamic_dimension_value, dynamic_dimension, undefined_shape_of_rank
from mo.front.onnx.extractors.utils import get_backend_pad
from mo.graph.graph import Node, Graph
from mo.middle.passes.convert_data_type import np_data_type_to_destination_type
@ -13,6 +13,12 @@ from mo.utils.error import Error
from mo.front.extractor import bool_to_str
poolings_map = {
'max': {'version': 'opset8', 'out_ports_count': 2},
'avg': {'version': 'opset1', 'out_ports_count': 1}
}
class PoolingV2(Op):
"""
TensorFlow MaxPoolV2 and AvgPoolV2 operations expect windows_size and strides values from inputs not from
@ -28,29 +34,35 @@ class PoolingV2(Op):
'op': self.op,
'version': None,
'infer': self.infer,
'reverse_infer': self.reverse_infer,
'in_ports_count': 3,
'out_ports_count': 1,
}, attrs)
@staticmethod
def infer(node: Node):
assert (len(node.in_nodes()) == 3), 'MaxPoolV2 node {} from must have only 3 inputs: input, window size, and strides ' \
'but instead got {} inputs'.format(node.soft_get('name', node.id), len(node.in_nodes()))
assert (len(node.in_nodes()) == 3), 'MaxPoolV2 node {} from must have only 3 inputs: input, window size, and ' \
'strides but instead got {} inputs'.format(node.soft_get('name', node.id),
len(node.in_nodes()))
node['window'] = node.in_port(1).data.get_value()
node['stride'] = node.in_port(2).data.get_value()
if node['window'] is None:
raise Error('The non-constant window size for MaxPoolV2 node {} is not supported'.format(node.soft_get('name', node.id)))
raise Error('The non-constant window size for MaxPoolV2 node {} is not supported'
''.format(node.soft_get('name', node.id)))
if node['stride'] is None:
raise Error('The non-constant strides for MaxPoolV2 node {} is not supported'.format(node.soft_get('name', node.id)))
raise Error('The non-constant strides for MaxPoolV2 node {} is not supported'
''.format(node.soft_get('name', node.id)))
Pooling.pool_infer(node)
poolings_map = {
'max': {'version': 'opset8', 'out_ports_count': 2},
'avg': {'version': 'opset1', 'out_ports_count': 1}
}
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
window_shape = node.in_port(1).data.get_shape()
# use the value of the 'window' input to determine input tensor rank
if input_shape is None and window_shape is not None:
node.in_port(0).data.set_shape(undefined_shape_of_rank(window_shape[0]))
class Pooling(Op):
@ -62,8 +74,10 @@ class Pooling(Op):
'op': self.op,
'version': poolings_map[attrs.get('pool_method')]['version'],
'infer': self.infer,
'reverse_infer': self.reverse_infer,
'in_ports_count': 1,
'out_ports_count': 1 if attrs.get('version') == 'opset1' else poolings_map[attrs.get('pool_method')]['out_ports_count']
'out_ports_count': 1 if attrs.get('version') == 'opset1' else
poolings_map[attrs.get('pool_method')]['out_ports_count']
}, attrs)
def backend_attrs(self):
@ -184,3 +198,10 @@ class Pooling(Op):
('window', 'input:0'),
('spatial_dims', 'input:0'),
('dilation', 'input:0')])
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
window = node.soft_get('window', None)
if input_shape is None and window is not None:
node.in_port(0).data.set_shape(undefined_shape_of_rank(len(window)))

View File

@ -4,7 +4,8 @@
import numpy as np
from mo.front.caffe.extractors.utils import get_canonical_axis_index
from mo.front.common.partial_infer.utils import int64_array, dynamic_dimension, shape_delete, is_fully_defined
from mo.front.common.partial_infer.utils import int64_array, dynamic_dimension, shape_delete, is_fully_defined, \
undefined_shape_of_rank
from mo.graph.graph import Node
from mo.graph.perm_inputs import PermuteInputs
from mo.ops.op import Op
@ -26,6 +27,7 @@ class Squeeze(Op):
'in_ports_count': 2,
'out_ports_count': 1,
'infer': self.infer,
'reverse_infer': self.reverse_infer,
}, attrs)
@staticmethod
@ -69,3 +71,13 @@ class Squeeze(Op):
# the squeeze_dim attribute will be converted to the second input in the end of the Middle phase
PermuteInputs().set_input_permutation(node.in_node(1), node, 'input:0', 'axis')
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
output_shape = node.out_port(0).data.get_shape()
squeeze_dims = node.in_port(1).data.get_value()
if input_shape is None and output_shape is not None and squeeze_dims is not None:
num_squeeze_dims = 1 if int64_array(squeeze_dims).ndim == 0 else len(squeeze_dims)
shape = undefined_shape_of_rank(len(output_shape) + num_squeeze_dims)
node.in_port(0).data.set_shape(shape)

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import numpy as np
from mo.front.common.partial_infer.utils import int64_array, shape_array, is_fully_defined, shape_insert
from mo.front.common.partial_infer.utils import int64_array, is_fully_defined, shape_insert, undefined_shape_of_rank
from mo.graph.graph import Node
from mo.graph.perm_inputs import PermuteInputs
from mo.ops.op import Op
from mo.utils.error import Error
@ -26,7 +25,8 @@ class Unsqueeze(Op):
'reinterp_shape': True,
'in_ports_count': 2,
'out_ports_count': 1,
'infer': self.infer
'infer': self.infer,
'reverse_infer': self.reverse_infer,
}, attrs)
@staticmethod
@ -61,3 +61,13 @@ class Unsqueeze(Op):
node.out_port(0).data.set_shape(output_shape)
PermuteInputs().set_input_permutation(node.in_node(1), node, 'input:0', 'axis')
@staticmethod
def reverse_infer(node: Node):
input_shape = node.in_port(0).data.get_shape()
output_shape = node.out_port(0).data.get_shape()
unsqueeze_dims = node.in_port(1).data.get_value()
if input_shape is None and output_shape is not None and unsqueeze_dims is not None:
num_unsqueeze_dims = 1 if int64_array(unsqueeze_dims).ndim == 0 else len(unsqueeze_dims)
shape = undefined_shape_of_rank(len(output_shape) - num_unsqueeze_dims)
node.in_port(0).data.set_shape(shape)

View File

@ -54,7 +54,7 @@ def send_shapes_info(framework: str, graph: Graph):
shape_str = ""
is_partially_defined = "0"
for shape in shapes:
shape_str += np.array2string(int64_array(unmask_shape(shape))) + ","
shape_str += (np.array2string(int64_array(unmask_shape(shape))) if shape is not None else "Undefined") + ","
if not is_fully_defined(shape):
is_partially_defined = "1"
message_str = "{fw:" + framework + ",shape:\"" + shape_str[:-1] + "\"}"

View File

@ -74,6 +74,18 @@ class TestTelemetryUtils(unittest.TestCase):
tm.Telemetry.send_event.assert_any_call('mo', 'partially_defined_shape',
'{partially_defined_shape:1,fw:framework}')
def test_send_undefined_shapes(self):
graph = build_graph({**regular_op('placeholder1', {'shape': None,
'type': 'Parameter'}),
**regular_op('mul', {'shape': int64_array([7, 8]), 'type': 'Multiply'})}, [])
self.init_telemetry_mocks()
send_shapes_info('framework', graph)
tm.Telemetry.send_event.assert_any_call('mo', 'input_shapes', '{fw:framework,shape:"Undefined"}')
tm.Telemetry.send_event.assert_any_call('mo', 'partially_defined_shape',
'{partially_defined_shape:1,fw:framework}')
def test_send_dynamic_shapes_case2(self):
graph = build_graph({**regular_op('placeholder1', {'shape': int64_array([2, 3, 20, 20]), 'type': 'Parameter'}),
**regular_op('placeholder2', {'shape': int64_array([7, 4, 10]), 'type': 'Parameter'}),