def replace_sub_graph(self, graph: Graph, match: dict): node = match['softmax'] if 'temperature' in node and node['temperature'] != 1.0: in_node = node.in_node() out_nodes = [node for node in node.out_nodes().values()] graph.remove_edge(node.in_node().id, node.id) temperature = np.array([1.0 / node.temperature]) scalar_value_op = Const(graph, dict(value=temperature, shape=temperature.shape, symbol_dict={'name': node.id + '/const'})) mul_op = Mul(graph, dict(name=node.id + '/mul_', symbol_dict={'name': node.id + '/mul_'})) mul_node = mul_op.create_node(inputs=[in_node, scalar_value_op.create_node()]) edge_attrs = graph.get_edge_data(node.id, out_nodes[0].id)[0] graph.add_edges_from([(mul_node.id, node.id, edge_attrs)])
def replace_pattern(self, graph: Graph, match: dict): """ Adds layers with type 'Const' that produce blob from 'bin' file. The pass finds data nodes with one output which doesn't have edge with 'bin' attribute (or with two outputs and at least one output doesn't have 'bin' attr) and generate Const op node before the node and data node before the Const node. The data node before 'Const' node is needed because the op node dumps input tensors to bin file. """ node = match['data'] if len(node.in_nodes()) > 0: return if self._check_bin_attrs(node): if node.has_valid('value'): const_node_name = node.soft_get('name', node.id) const_node_name = graph.unique_id( re.sub(r'\/Output_\d+\/Data_(.?)+', '', const_node_name)) log.debug("Added Const node '{}'".format(const_node_name)) const_node = Const( graph, { 'name': const_node_name, 'value': node.value, 'force_shape': node.soft_get('force_shape', None), 'override_output_shape': node.has_valid('force_shape'), 'force_type': node.soft_get('force_type', None), 'correct_data_type': node.soft_get('correct_data_type', None), 'rt_info': node.soft_get('rt_info', RTInfo()), }).create_node() const_node.add_input_port(0) graph.add_edges_from([(const_node_name, node.id, {'out': 0})]) node_copy = node.copy_node() const_node.type_infer(const_node) graph.add_edges_from([(node_copy.id, const_node_name, { 'in': 0, 'bin': 'custom' })]) elif not self._check_that_node_from_body(node): log.debug('node = {}'.format(node.graph.node[node.id])) raise Error( 'Discovered data node without inputs and value, node.name = {}, consumer.name = {}. ' + refer_to_faq_msg(23), node.soft_get('name'), node.out_node().soft_get('name') if len(node.out_nodes()) else "<no consumer>")
def replace_sub_graph(self, graph: Graph, match: dict): ph = match['placeholder'] if ph.name in graph.graph['freeze_placeholder']: name = ph.name if ph.has_and_set('data_type'): data_type = ph.data_type else: data_type = SUPPORTED_DATA_TYPES[ graph.graph['cmd_params'].data_type][0] string_value = graph.graph['freeze_placeholder'][name] try: if data_type != np.bool: value = mo_array(string_value, dtype=data_type) # TODO: investigate why boolean type is allowed only for TensorFlow elif data_type == np.bool and graph.graph['fw'] == 'tf': from openvino.tools.mo.front.tf.common import tf_data_type_cast if isinstance(string_value, list): casted_list = list() for v in mo_array(string_value): casted_list.append( tf_data_type_cast[ph.data_type](v)) value = mo_array(string_value, dtype=data_type) else: value = tf_data_type_cast[ph.data_type](string_value) else: raise Error("Cannot cast value {} to {} data_type".format( string_value, data_type)) except: raise Error("Cannot cast value {} to {} data_type".format( string_value, data_type)) try: value = np.reshape(a=value, newshape=ph.shape) except: raise Error("Can not reshape value {} to shape {}".format( value, ph.shape)) out_edges = list(graph.out_edges(ph.id, data=True)) new_node = Const(graph).create_node( attrs={ 'value': value, 'data_type': type(value), 'name': name + '/const_placeholder', 'shape': ph.shape }) graph.erase_node(ph) graph.add_edges_from([(new_node.id, v, attrs) for u, v, attrs in out_edges]) log.info( "Placeholder node \"{}\" was replaced with Const node \"{}\" with value \"{}\"" .format(name, new_node.name, value))
def test_sub_graph_between_nodes_do_not_include_incoming_edges_for_input_nodes( self): """ Check that the function doesn't add input nodes for the start nodes of the sub-graph. For example, we do not need to add node 5 in the case below if we find match from node 1 till node 4. 5-> \ 1 -> 2 -> 3 -> 4 """ graph = Graph() graph.add_nodes_from(list(range(1, 6))) graph.add_edges_from([(1, 2), (2, 3), (3, 4), (5, 2)]) sub_graph_nodes = sub_graph_between_nodes(graph, [2], [4]) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), [2, 3, 4])
def test_sub_graph_between_nodes_placeholder_excluded(self): """ Check that the function do not check that node is Placeholders for the nodes not included into the sub-graph. For example, node 5 is Placeholder but it is not included into the sub-graph, so this attribute is ignored. 5-> \ 1 -> 2 -> 3 -> 4 """ graph = Graph() graph.add_nodes_from(list(range(1, 6))) graph.node[5]['op'] = 'Parameter' graph.add_edges_from([(1, 2), (2, 3), (3, 4), (5, 2)]) sub_graph_nodes = sub_graph_between_nodes(graph, [2], [4]) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), [2, 3, 4])
def test_sub_graph_between_nodes_control_flow_not_included_forward(self): """ Check that the function works correctly for case when control flow edges should not be traversed (edge 3 -> 5). 1 -> 2 -> 3 -> 4 \ -> 5 -> 6 """ graph = Graph() graph.add_nodes_from(list(range(1, 7))) graph.add_edges_from([(1, 2), (2, 3), (3, 4), (3, 5, { 'control_flow_edge': True }), (5, 6)]) sub_graph_nodes = sub_graph_between_nodes(graph, [1], [4], include_control_flow=False) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), sorted([1, 2, 3, 4]))
def build_matcher(graph: Graph, nodes: list, edges: list, node_attrs: list = None, edge_attrs: list = None): if node_attrs is not None or edge_attrs is not None: log.warning( '\'edge_attrs\' or `\'node_attrs\'` parameter was passed to function \'find_pattern_matches\', ' 'but they are not used anymore. Pattern matching proceeds according to \'nodes\' and \'edges\' ' 'parameters. Please avoid passing \'edge_attrs\' and \'node_attrs\' parameters to any pattern ' 'matching function like \'find_pattern_matches\', \'apply_pattern\' and \'pattern\' because it ' 'will be deprecated in the next release.') subgraph = Graph(name='pattern') subgraph.add_nodes_from(nodes) subgraph.add_edges_from(edges) return ism.MultiDiGraphMatcher(graph, subgraph, node_match, edge_match)
def test_sub_graph_between_nodes_control_flow_included(self): """ Check that the function works correctly for case when control flow edges must be traversed (edge 5 -> 2). 6 -> 5-> \ 1 -> 2 -> 3 -> 4 """ graph = Graph() graph.add_nodes_from(list(range(1, 7))) graph.add_edges_from([(1, 2), (2, 3), (3, 4), (5, 2, { 'control_flow_edge': True }), (6, 5)]) sub_graph_nodes = sub_graph_between_nodes(graph, [1], [4], include_control_flow=True) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), sorted([1, 2, 3, 4, 5, 6]))
def find_and_replace_pattern(self, graph: Graph): """ This function finds all const data nodes that have more that one consumer and then duplicate them """ data_nodes = [Node(graph, id) for id in graph.nodes() if Node(graph, id).soft_get('kind') == 'data'] for node in data_nodes: # Check that node has const values and more than one consumer if len(node.in_nodes()) and node.in_node().soft_get('type') == 'Const' and len(node.out_nodes()) > 1 and \ node.value is not None: # Here we delete all edges between base node and it's consumers (except first), and then duplicate this # node to connect with other consumers for v, d in node.get_outputs(): out_node = Node(graph, v) e_attrs = d graph.remove_edge(node.id, out_node.id) data = Op.create_input_data_node(graph, "Copy_{}".format(node.id), mo_array(node.value), graph.node[node.id]) graph.add_edges_from([(data.id, out_node.id, e_attrs)])
def add_reshape_before_op_node(graph: Graph, data_node_name: str, op_node_name: str, edge_attrs: dict): """ Adds reshape operation which expands dimension of the specified data tensor to 4D. :param graph: graph to operate on. :param data_node_name: the name of the data node to be reshaped to 4D tensor. :param op_node_name: name of the TFCustomSubgraphCall node which produces the tensor. :param edge_attrs: edge attributes which should be preserved. :return: None """ data_node = Node(graph, data_node_name) graph.remove_edge(data_node_name, op_node_name) assert data_node['shape'] is not None new_shape = CustomSubgraphCall.make_shape_4d(data_node['shape']) # reshape shape data node reshape_shape_data_node_name = graph.unique_id("Reshape_shape_") graph.add_node(reshape_shape_data_node_name, kind='data', name=reshape_shape_data_node_name, value=new_shape, shape=[1]) # reshape operation node reshape_node_name = graph.unique_id("Reshape_") graph.add_node(reshape_node_name, kind='op', type='Reshape', name=reshape_node_name, op='Reshape', data_type=data_node['data_type']) update_ie_fields(graph.node[reshape_node_name]) # reshaped data node reshaped_value = None if data_node['value'] is not None: reshaped_value = np.reshape(data_node['value'], new_shape) reshaped_data_node_name = graph.unique_id("reshaped_data_") graph.add_node(reshaped_data_node_name, kind='data', name=reshaped_data_node_name, shape=new_shape, value=reshaped_value, nchw_layout=True) graph.add_edges_from([ (data_node_name, reshape_node_name, {'in': 0}), (reshape_shape_data_node_name, reshape_node_name, {'in': 1}), (reshape_node_name, reshaped_data_node_name, {'out': 0}), (reshaped_data_node_name, op_node_name, edge_attrs) ])
def test_sub_graph_between_nodes_include_incoming_edges_for_internal_nodes( self): """ Check that the function adds input nodes for the internal nodes of the graph. For example, we need to add node 5 and 6 in the case below if we find match from node 1 till node 4. 6 -> 5 -> \ 1 -> 2 -> 3 -> 4 :return: """ graph = Graph() graph.add_nodes_from(list(range(1, 7))) graph.add_edges_from([(1, 2), (2, 3), (3, 4), (5, 2), (6, 5)]) sub_graph_nodes = sub_graph_between_nodes(graph, [1], [4]) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), list(range(1, 7))) sub_graph_nodes = sub_graph_between_nodes(graph, [1], [2]) self.assertIsNotNone(sub_graph_nodes) self.assertListEqual(sorted(sub_graph_nodes), [1, 2, 5, 6])
def replace_pattern(self, graph: Graph, match: dict): conv = match['conv'] stb = match['space_to_batch'] bts = match['batch_to_space'] block_size = match['stb_bs'] input = match['input'] output = match['output'] stb_out = match['stb_output'] conv_out = match['conv_output'] in_edge_attrs = graph.get_edge_data(input.id, stb.id)[0] out_edge_attrs = graph.get_edge_data(bts.id, output.id)[0] graph.remove_edge(input.id, stb.id) graph.remove_edge(stb_out.id, conv.id) graph.remove_edge(conv.id, conv_out.id) graph.remove_edge(bts.id, output.id) conv.dilation[conv.spatial_dims] = block_size.value[conv.spatial_dims] pad_begin = match['stb_pad_begin'].value - match['bts_crop_begin'].value pad_end = match['stb_pad_end'].value - match['bts_crop_end'].value conv.pad[conv.spatial_dims] = [[pad_begin[x], pad_end[x]] for x in conv.spatial_dims] conv['auto_pad'] = None graph.add_edges_from([ (input.id, conv.id, { 'in': 0, **in_edge_attrs }), (conv.id, output.id, { 'out': 0, **out_edge_attrs }), ])
def create_and_connect_input_data_node(graph: Graph, op_node: Node, attrs: dict = None, edge_attrs: dict = None): assert op_node is not None and op_node.kind == 'op' if attrs is None: attrs = {} if edge_attrs is None: edge_attrs = {} data_node = graph.unique_id(op_node.id) default_attrs = dict(kind='data', name=data_node, value=None, shape=None, data_type=None, infer=None) default_attrs.update(attrs) graph.add_node(data_node, **add_attrs_props(default_attrs)) data_node = Node(graph, data_node) op_node.add_input_port(edge_attrs['in'], skip_if_exist=True) graph.add_edges_from([(data_node.id, op_node.id, edge_attrs)]) return data_node
def replace_pattern(self, graph: Graph, match: dict): node = match['op'] node.op = 'Conv2D' if node.bias_term: num_inputs = len(node.in_nodes()) - 2 w_node = node.in_node(len(node.in_nodes()) - 2) b_node = node.in_node(len(node.in_nodes()) - 1) else: num_inputs = len(node.in_nodes()) - 1 w_node = node.in_node(len(node.in_nodes()) - 1) for i in range(1, num_inputs): in_i = node.in_node(i) out_i = node.out_node(i) conv_id = graph.unique_id(node.id + '__') graph.add_node(conv_id, **copy.deepcopy(node.get_attrs())) new_conv = Node(graph, conv_id) new_conv.name = conv_id graph.remove_edge(in_i.id, node.id) graph.remove_edge(node.id, out_i.id) graph.add_edges_from([ (w_node.id, conv_id, { 'in': 1, 'bin': 'weights' }), ]) if node.bias_term: graph.add_edges_from([ (b_node.id, conv_id, { 'in': 2, 'bin': 'biases' }), ]) graph.add_edges_from([ (in_i.id, conv_id, { 'in': 0 }), ]) graph.add_edge(conv_id, out_i.id, **{'out': 0})
def load_parallel_component(file_descr, graph: Graph, prev_layer_id): """ Load ParallelComponent of the Kaldi model. ParallelComponent contains parallel nested networks. VariadicSplit is inserted before nested networks. Outputs of nested networks concatenate with layer Concat. :param file_descr: descriptor of the model file :param graph: graph with the topology. :param prev_layer_id: id of the input layers for parallel component layer :return: id of the concat layer - last layer of the parallel component layers """ nnet_count = read_token_value(file_descr, b'<NestedNnetCount>') log.debug( 'Model contains parallel component with {} nested networks'.format( nnet_count)) split_points = [] outputs = [] inputs = [] for i in range(nnet_count): read_token_value(file_descr, b'<NestedNnet>') collect_until_token(file_descr, b'<Nnet>') g = Graph() load_kalid_nnet1_model(g, file_descr, 'Nested_net_{}'.format(i)) # input to nnet1 models is of a rank 1 but we also insert batch_size to 0th axis # 1st axis contains input_size of the nested subnetwork # we split input from the main network to subnetworks input_node = Node(g, 'Parameter') split_points.append(input_node['shape'][1]) g.remove_node(input_node.id) mapping = { node: graph.unique_id(node) for node in g.nodes(data=False) if node in graph } g = nx.relabel_nodes(g, mapping) for val in mapping.values(): g.node[val]['name'] = val graph.add_nodes_from(g.nodes(data=True)) graph.add_edges_from(g.edges(data=True)) sorted_nodes = tuple(nx.topological_sort(g)) outputs.append(Node(graph, sorted_nodes[-1])) inputs.append(Node(graph, sorted_nodes[0])) split_id = graph.unique_id(prefix='NestedNets/VariadicSplit') attrs = { 'out_ports_count': nnet_count, 'size_splits': split_points, 'axis': 1, 'name': split_id } variadic_split_node = AttributedVariadicSplit(graph, attrs).create_node() prev_layer_node = Node(graph, prev_layer_id) prev_layer_node.add_output_port(0) graph.create_edge( prev_layer_node, variadic_split_node, 0, 0, create_edge_attrs(prev_layer_id, variadic_split_node.id, prev_layer_id)) concat_id = graph.unique_id(prefix='Concat') graph.add_node(concat_id, parameters=None, op='concat', kind='op') concat_node = Node(graph, concat_id) # Connect each output of variadic_split_node to each subnetwork's inputs in ParallelComponent # and each subnetwork's output to concat_node for i, (input_node, output_node) in enumerate(zip(inputs, outputs)): output_node.add_output_port(0) concat_node.add_input_port(i) graph.create_edge( output_node, concat_node, 0, i, create_edge_attrs(output_node.id, concat_id, output_node.id, i, 0)) graph.create_edge( variadic_split_node, input_node, i, 0, create_edge_attrs(variadic_split_node.id, input_node.id, variadic_split_node.id, 0, i)) return concat_id
def replace_pattern(graph, match: dict): # Here we will found all parts of TI: condition, inputs/outputs, back edges, body and create TensorIterator Op # and make all checks needed for TensorIterator work cond_data = match['condition'].out_node( 0) if not match['condition'].out_port(0).disconnected() else None time_data = match['condition'].out_node(1) if len( match['condition'].out_nodes()) >= 1 else None name = match['condition'].name back_edges = [] inputs = [] outputs = [] if cond_data is not None: for node in cond_data.out_nodes(): if node['kind'] == 'op' and node[ 'op'] == 'TensorIteratorBackEdge': back_edges.append(node.id) elif node['kind'] == 'op' and node[ 'op'] == 'TensorIteratorInput': inputs.append(node.id) elif node['kind'] == 'op' and node[ 'op'] == 'TensorIteratorOutput': outputs.append(node.id) if time_data is not None: for node in time_data.out_nodes(): if node['kind'] == 'op' and node['op'] == 'TensorIteratorInput': inputs.append(node.id) elif node['kind'] == 'op' and node[ 'op'] == 'TensorIteratorOutput': outputs.append(node.id) else: # something goes wrong here assert False condition = match['condition'] tensor_sequence_length = condition.in_node(0) nodes_to_remove = [ n.id for n in (condition, cond_data, time_data, tensor_sequence_length) if n is not None ] graph.remove_nodes_from(nodes_to_remove) body_nodes, extra_inputs = get_body(graph, inputs, outputs) if cond_data is not None: body_nodes = list(set(body_nodes) - set([cond_data])) inputs += extra_inputs assert all([node in graph.nodes() for node in body_nodes]) inputs = [Node(graph, node) for node in inputs] outputs = [Node(graph, node) for node in outputs] back_edges = [Node(graph, node) for node in back_edges] external_inputs = [{ 'external_data_id': node.in_node(1 if node.has_valid('axis') else 0), 'internal_data_id': node.out_node(0), 'axis': node.axis, 'start': node.start, 'end': node.end, 'stride': node.stride, 'part_size': node.part_size } for node in inputs] external_outputs = [{ 'external_data_id': node.out_node(0), 'internal_data_id': node.in_node(1 if node.has_valid('axis') else 0), 'axis': node.axis, 'start': node.start, 'end': node.end, 'stride': node.stride, 'part_size': node.part_size } for node in outputs] back_edges_data = [{ 'from_data_id': node.in_node(1), 'to_data_id': node.out_node(0), 'init_data_id': node.in_node(0), } for node in back_edges] body = Graph(name='body') body.graph = graph.graph body.add_nodes_from([(node, graph.node[node]) for node in body_nodes]) body.add_edges_from([ (u, v, k, d) for u, v, k, d in graph.edges(data=True, keys=True) if u in body_nodes and v in body_nodes ]) graph.remove_nodes_from(body_nodes + [match['condition'].id] + [inp.id for inp in inputs] + [out.id for out in outputs]) internal_id_count = 0 real_back_edges = [] for edge in back_edges_data: assert edge['from_data_id'].id in body.nodes() assert edge['to_data_id'].id in body.nodes() assert edge['init_data_id'].id in body.nodes() edge['from_data_id'] = Node(body, edge['from_data_id'].id) edge['to_data_id'] = Node(body, edge['to_data_id'].id) edge['init_data_id'] = Node(body, edge['init_data_id'].id) add_opoutput(body, edge['from_data_id'].id, 0, False) # Assign/reuse ids for the back-edge start; it comes from from_data_id assert len(edge['from_data_id'].in_nodes()) == 1 # layer id if not edge['from_data_id'].in_node().has_valid( 'internal_layer_id'): edge['from_data_id'].in_node( )['internal_layer_id'] = internal_id_count internal_id_count += 1 edge['from_layer'] = edge['from_data_id'].in_node( )['internal_layer_id'] # port id if 'internal_port_id' not in edge['from_data_id'].in_edge(): edge['from_data_id'].in_edge( )['internal_port_id'] = internal_id_count internal_id_count += 1 edge['from_port'] = edge['from_data_id'].in_edge( )['internal_port_id'] # Look at all consumers for a data that ends a back-edge # For each such consumer, there will be a separate back-edge (and input) current_real_back_edges = [] for _, consumer, key, edge_attrs in body.out_edges( edge['to_data_id'].id, data=True, keys=True): real_edge = {} real_edge.update( edge) # all real back_edges have the same back-edge start consumer = Node(body, consumer) if real_edge['to_data_id'].in_node().has_valid( 'internal_layer_id'): assert False real_edge['to_data_id'].out_node()['internal_layer_id'] = \ real_edge['to_data_id'].in_node().internal_layer_id elif not consumer.has_valid('internal_layer_id'): consumer['internal_layer_id'] = internal_id_count internal_id_count += 1 real_edge['to_layer'] = consumer['internal_layer_id'] assert 'internal_port_id' not in edge_attrs assert len(real_edge['init_data_id'].out_edges()) == 1 assert not 'internal_port_id' in real_edge[ 'init_data_id'].out_edge() edge_attrs['internal_port_id'] = internal_id_count internal_id_count += 1 real_edge['to_port'] = edge_attrs['internal_port_id'] real_edge['consumer'] = consumer real_edge['consumer_key'] = key real_edge['attrs'] = deepcopy(edge_attrs) current_real_back_edges.append(real_edge) # connect initial data node with each consumer providing actual edge attributes body.add_edges_from([ (real_edge['init_data_id'].id, real_edge['consumer'].id, real_edge['consumer_key'], real_edge['attrs']) for real_edge in current_real_back_edges ]) body.remove_nodes_from( [edge['to_data_id'].id, edge['to_data_id'].in_node().id]) real_back_edges += current_real_back_edges real_external_inputs = [] for ext_inp in external_inputs: assert ext_inp['external_data_id'].id not in body.nodes() assert ext_inp['internal_data_id'].id in body.nodes() ext_inp['internal_data_id'] = Node(body, ext_inp['internal_data_id'].id) if ext_inp['axis'] is not None: # Insert squeezing resize at input port that has partitioning shape = ext_inp['internal_data_id'].shape.copy() assert not ext_inp['internal_data_id'].has_valid('value') new_input_data = Op._create_data_node( body, ext_inp['internal_data_id'].name + '/UnsqueezedInput', dict(shape=shape_insert(shape, ext_inp['axis'], 1))) reshape_op = Squeeze( body, dict(name=ext_inp['internal_data_id'].name + '/InputSqueeze')) reshape_dim_data = Const( body, { 'name': ext_inp['internal_data_id'].name + '/ReshapeDim', 'value': ext_inp['axis'] }).create_node_with_data() reshape_op.create_node_with_data( [new_input_data, reshape_dim_data], data_nodes=[ext_inp['internal_data_id']]) ext_inp['internal_data_id'] = new_input_data ext_inp['internal_data_id']['is_input'] = True assert len(ext_inp['internal_data_id'].in_nodes()) == 0 ext_inp['external_port_id'] = internal_id_count internal_id_count += 1 for _, consumer, edge_attrs in body.out_edges( ext_inp['internal_data_id'].id, data=True): real_ext_inp = {} real_ext_inp.update(ext_inp) consumer = Node(body, consumer) if not consumer.has_valid('internal_layer_id'): consumer['internal_layer_id'] = internal_id_count internal_id_count += 1 if not 'internal_port_id' in edge_attrs: edge_attrs['internal_port_id'] = internal_id_count internal_id_count += 1 real_ext_inp['internal_layer_id'] = consumer[ 'internal_layer_id'] real_ext_inp['internal_port_id'] = edge_attrs[ 'internal_port_id'] real_external_inputs.append(real_ext_inp) for ext_out in external_outputs: assert ext_out['external_data_id'].id not in body.nodes() assert ext_out['internal_data_id'].id in body.nodes() ext_out['internal_data_id'] = Node(body, ext_out['internal_data_id'].id) if ext_out['axis'] is not None: # Insert unsqueezing resize at output port that has partitioning reshape_op = Unsqueeze( body, dict(name=ext_out['internal_data_id'].name + '/OutputUnsqueeze')) reshape_dim_data = Const( body, { 'name': ext_out['internal_data_id'].name + '/ReshapeDim', 'value': ext_out['axis'] }).create_node_with_data() ext_out['internal_data_id'] = reshape_op.create_node_with_data( [ext_out['internal_data_id'], reshape_dim_data]) # TODO: add here working with simple outputs if not any([ out_node.soft_get('op', None) == 'Result' for out_node in ext_out['internal_data_id'].out_nodes() ]): add_opoutput(body, ext_out['internal_data_id'].id, 0, False) # assert len(ext_out['internal_data_id'].out_nodes()) == 0 assert len(ext_out['internal_data_id'].in_nodes()) == 1 if not 'internal_layer_id' in ext_out['internal_data_id'].in_node( ): ext_out['internal_data_id'].in_node( )['internal_layer_id'] = internal_id_count internal_id_count += 1 if not 'internal_port_id' in ext_out['internal_data_id'].in_edge(): ext_out['internal_data_id'].in_edge( )['internal_port_id'] = internal_id_count internal_id_count += 1 ext_out['internal_layer_id'] = ext_out['internal_data_id'].in_node( )['internal_layer_id'] ext_out['internal_port_id'] = ext_out['internal_data_id'].in_edge( )['internal_port_id'] ext_out['external_port_id'] = internal_id_count internal_id_count += 1 # create TensorIterator layer with pre-computed components ti_op = TensorIterator( graph, { 'name': name + '/TensorIterator', 'body': body, 'in_ports_count': len(external_inputs), 'out_ports_count': len(external_outputs), 'input_port_map': [{ field: external_input[field] for field in [ 'external_port_id', 'internal_layer_id', 'internal_port_id', 'axis', 'stride', 'part_size', 'start', 'end' ] } for external_input in real_external_inputs], 'output_port_map': [{ field: external_output[field] for field in [ 'external_port_id', 'internal_layer_id', 'internal_port_id', 'axis', 'stride', 'part_size', 'start', 'end' ] } for external_output in external_outputs], 'back_edges': [{ field: edge[field] for field in ['from_layer', 'from_port', 'to_layer', 'to_port'] } for edge in real_back_edges], }) ti_outs = ti_op.create_node_with_data( inputs=[inp['external_data_id'] for inp in external_inputs], edge_attrs=[{ 'external_port_id': inp['external_port_id'] } for inp in external_inputs], data_nodes=[out['external_data_id'] for out in external_outputs]) if not isinstance(ti_outs, list): ti_outs = [ti_outs] for i, out in enumerate(ti_outs): out.in_edge( )['external_port_id'] = external_outputs[i]['external_port_id'] ti = ti_outs[0].in_node() TensorIterator.cover_body_input_data_nodes_with_parameter_ops(ti) TensorIterator.cover_body_constant_data_nodes_with_const_ops(ti) TensorIterator.normalize_internal_ids(ti)
class IREngine(object): def __init__(self, path_to_xml: str, path_to_bin=None, precision="FP32", xml_tree=None): if not xml_tree and not os.path.exists(path_to_xml): raise AttributeError("File {} do not exists!".format(path_to_xml)) if path_to_bin and not os.path.exists(path_to_bin): raise AttributeError("File {} do not exists!".format(path_to_bin)) self.path_to_xml = str(path_to_xml) self.path_to_bin = str(path_to_bin) if path_to_bin else None self.xml_tree = xml_tree self.input_node = None self.ir_version = None self.meta_data = dict() if precision.upper() not in ['FP32', 'FP16']: raise AttributeError( "Precision {} is not supported!".format(precision)) self.__load_ir() def __load_xml(self): xml_tree = self.xml_tree or ET.parse(self.path_to_xml) xml_root = xml_tree.getroot() xml_layers = {} xml_edges = [] statistics = {} Edge = namedtuple('edge', ['from_layer', 'from_port', 'to_layer', 'to_port']) # Create graph with operations only self.graph = Graph() self.graph.graph['hashes'] = {} self.graph.graph['ir_version'] = int( xml_root.attrib['version']) if xml_root.attrib.get( 'version') is not None else None # NOTE: THis is MO internal attribute, it cannot be used for # defining graph input layout. We set it to NCHW as in MO back stage # during conversion for correct shape inference of layout specific # operations (ExtractImagePatches, SpaceToDepth, etc.) self.graph.graph['layout'] = 'NCHW' self.graph.name = xml_root.attrib['name'] if xml_root.attrib.get( 'name') is not None else None # Parse XML for child in xml_root: if child.tag == 'layers': for layer in child: layer_id, layer_attrs = self.__load_layer(layer) xml_layers.update({layer_id: layer_attrs}) elif child.tag == 'edges': for edge in child: xml_edges.append( Edge(edge.attrib['from-layer'], int(edge.attrib['from-port']), edge.attrib['to-layer'], int(edge.attrib['to-port']))) elif child.tag == 'statistics': layers = child.findall('layer') for layer in layers: statistics[layer.find('name').text] = { 'min': layer.find('min').text, 'max': layer.find('max').text } elif child.tag == 'meta_data': for elem in child: if elem.tag == 'cli_parameters': for det in elem: if det.tag != 'unset': value = det.attrib['value'] if value in ['True', 'False']: value = False if value == 'False' else True self.meta_data[det.tag] = value else: self.meta_data[det.tag] = det.attrib[ 'unset_cli_parameters'].split(',_') elif child.tag == 'quantization_parameters': # Section with Post Optimization Toolkit parameters self.meta_data['quantization_parameters'] = dict() for elem in child: if elem.tag == 'config': self.meta_data['quantization_parameters'][ 'config'] = elem.text elif elem.tag in ['version', 'cli_params']: self.meta_data['quantization_parameters'][ elem.tag] = elem.attrib['value'] self.graph.graph['cmd_params'] = Namespace( **self.meta_data) # TODO check what we need all this attrs if len(statistics): self.graph.graph['statistics'] = statistics for layer in xml_layers.keys(): self.graph.add_node(layer, **xml_layers[layer]) xml_edges.sort(key=lambda x: x.to_layer) for edge in xml_edges: self.graph.add_edges_from([(edge.from_layer, edge.to_layer, { 'from_port': edge.from_port, 'to_port': edge.to_port })]) # Insert data nodes between op nodes and insert data nodes with weights nodes = list(self.graph.nodes()) for node in nodes: out_edges = Node(self.graph, node).get_outputs() data_nodes = {} for port in self.graph.node[node]['ports']: data = self.graph.unique_id(prefix='data_') self.graph.add_node( data, **{ 'kind': 'data', 'shape': self.graph.node[node]['ports'][port][0], 'value': None }) self.graph.add_edges_from([(node, data, {'out': port})]) data_nodes.update({port: data}) for out_node, edge_attrs in out_edges: self.graph.remove_edge(node, out_node) if edge_attrs['from_port'] in data_nodes: data = data_nodes[edge_attrs['from_port']] else: raise RuntimeError( "SMTH wrong with IR! There is an edge from not existing port" ) self.graph.add_edges_from([(data, out_node, { 'in': edge_attrs['to_port'] })]) def __load_bin(self): bin_buff = np.fromfile(file=self.path_to_bin, dtype=np.uint8) graph = self.graph nodes = [node for node in graph.nodes()] hashes = defaultdict(dict) for node in nodes: for w in ['weights', 'biases', 'custom']: if w in graph.node[node]: data = graph.unique_id(prefix='data_') offset, size, in_port, precision = graph.node[node][w] if Node(graph, node).soft_get('type') == 'BinaryConvolution': precision = np.uint8 value = np.frombuffer(buffer=bin_buff, dtype=precision, count=size, offset=offset) hashes[graph.node[node]['name']][w] = hashlib.sha512( value.tobytes()).hexdigest() graph.add_node( data, **{ 'kind': 'data', 'value': value, 'shape': value.shape }) graph.add_edges_from([(data, node, {'in': in_port})]) self.graph.graph['hashes'].update(hashes) def __load_bin_hashes(self): graph = self.graph bin_hash_map = { name: blob_map.item(0) for name, blob_map in dict( np.load(self.path_to_bin, allow_pickle=True)).items() } for node in graph.nodes(): for w in ['weights', 'biases', 'custom']: if w in graph.node[node]: assert Node(graph, node).has_valid('name') node_name = Node(graph, node).name assert node_name in bin_hash_map and w in bin_hash_map[ node_name] graph.node[node]['hashes'] = bin_hash_map[node_name][w] def __load_ir(self): self.__load_xml() if not self.path_to_bin: return if self.path_to_bin.endswith('.bin.hashes.npz'): self.__load_bin_hashes() else: self.__load_bin() def __load_layer(self, layer): """ Layer example <layer id="1" name="862" precision="FP32" type="Convolution"> <data dilation-x="1" dilation-y="1" group="1" kernel-x="1" kernel-y="5" output="32" pad-b="0" pad-r="2" pad-x="2" pad-y="0" stride-x="1" stride-y="1"/> <input> <port id="0"> <dim>1</dim> <dim>3</dim> <dim>32</dim> <dim>32</dim> </port> </input> <output> <port id="3"> <dim>1</dim> <dim>32</dim> <dim>32</dim> <dim>32</dim> </port> </output> <blobs> <weights offset="0" size="1920"/> <biases offset="1920" size="128"/> </blobs> </layer> """ layer_id = layer.attrib['id'] layer_attrs = layer.attrib layer_attrs.update({ 'ports': {}, 'restored_input_ports': {}, 'kind': 'op' }) inputs_counter = 0 for attr in layer: if attr.tag == 'data': new_attrs = self.__normalize_attrs(attr.attrib) new_attrs['ir_data_attrs'] = attr.attrib if layer.attrib['type'] == 'Const': assert 'offset' in new_attrs and 'size' in new_attrs, \ 'Incorrect attributes for Const layer, {} instead of {}!'.format(new_attrs.keys(), ['offset', 'size']) precision = "" for item in layer: if item.tag == "output": precision = item[0].attrib["precision"] break new_attrs.update( self.__prepare_bin_attrs(layer, 0, 'custom', new_attrs['offset'], new_attrs['size'], precision)) layer_attrs.update(new_attrs) elif attr.tag == 'input': inputs_counter = len(attr) input = attr for port in input: port_id = int(port.attrib['id']) input_shape = [] port_rt_info = {} for dim in port: if dim.tag == "dim": input_shape.append(int(dim.text)) if dim.tag == 'rt_info': for attr in dim: port_rt_info.update( self.__read_rt_info_common(attr)) input_shape = shape_array([ d if d != -1 else dynamic_dimension_value for d in input_shape ]) in_tensor_names = None if 'names' in port.attrib: in_tensor_names = port.attrib['names'] # special attribute to pass information about operation input ports layer_attrs['restored_input_ports'].update({ port_id: (input_shape, in_tensor_names, port_rt_info) }) elif attr.tag == 'output': output = attr for port in output: port_id = int(port.attrib['id']) output_shape = [] port_rt_info = {} for dim in port: if dim.tag == "dim": output_shape.append(int(dim.text)) if dim.tag == 'rt_info': for attr in dim: port_rt_info.update( self.__read_rt_info_common(attr)) output_shape = shape_array([ d if d != -1 else dynamic_dimension_value for d in output_shape ]) out_tensor_names = None if 'names' in port.attrib: out_tensor_names = port.attrib['names'] # special attribute to pass information about operation input ports # NOTE: renaming or structure changing of this attribute may have big impact on tests layer_attrs['ports'].update({ port_id: (output_shape, out_tensor_names, port_rt_info) }) elif attr.tag == 'blobs': in_port = inputs_counter for blob_attr in attr: layer_attrs.update( self.__prepare_bin_attrs( layer, in_port, blob_attr.tag, blob_attr.attrib['offset'], blob_attr.attrib['size'], blob_attr.attrib.get('precision', None))) in_port += 1 elif attr.tag == 'body': xml_body_child = list(layer.iterfind('body')) assert len(xml_body_child) == 1 body_ir, input_port_map, output_port_map, input_layers = \ self.__read_subgraph(layer, layer_attrs, xml_body_child, 'port_map') body_ir.input_node = input_layers[0] layer_attrs.update({'body': body_ir}) layer_attrs.update({'input_port_map': input_port_map}) layer_attrs.update({'output_port_map': output_port_map}) xml_back_edges_map = list(layer.iterfind('back_edges')) if not len(xml_back_edges_map) == 1: log.warning( "TensorIterator body won\'t be compared due to missing back_edges section!" ) continue xml_back_edges_map = xml_back_edges_map[0] back_edges = [] for edge in xml_back_edges_map: back_edges.append(self.__normalize_attrs(edge.attrib)) layer_attrs.update({'back_edges': back_edges}) elif attr.tag == 'then_body' or attr.tag == 'else_body': assert layer.attrib['type'] == 'If', "Incorrect IR! The operation {0}" \ " has sub-graphs for If operation" layer_attrs = self.__read_if(layer, layer_attrs) continue elif attr.tag == 'rt_info': layer_attrs = self.__read_rt_info(layer, layer_attrs) continue return layer_id, layer_attrs @staticmethod def __prepare_bin_attrs(xml_layer, in_port, tag, offset, size, precision): layer_attrs = dict() if precision is None: precision = xml_layer.attrib['precision'] precision_map = { 'FP32': (4, np.float32), 'FP16': (2, np.float16), 'I64': (8, np.int64), 'I32': (4, np.int32), 'I8': (1, np.int8), 'U8': (1, np.uint8), 'U1': (1, np.uint8), 'U4': (1, np.uint8), 'I4': (1, np.uint8), 'BOOL': (1, np.bool), 'BIN': (1, np.uint8), 'U64': (8, np.uint64) } type_size, dtype = precision_map[precision] layer_attrs[tag] = (int(offset), int(size) // type_size, in_port, dtype) return layer_attrs @staticmethod def __normalize_attrs(attrs: dict): """ Normalize attributes for type 'data'. Replace " from values (not used right now) and make list of value with int, float or other types values. Example: {'order': '1,0,2'} -> {'order': [1, 0, 2]} {'order': '1'} -> {'order': 1} """ normalized_attrs = {} for attr, value in attrs.items(): value = value.replace('\"', '').replace(' ', '') value = value.split(',') n_value = [] for val in value: if IREngine.__isint(val): n_value.append(int(val)) elif IREngine.__isfloat(val): n_value.append(float(val)) elif val in ['True', 'False', 'true', 'false']: n_value.append(val in ['True', 'true']) else: n_value.append(val) if len(n_value) == 1: normalized_attrs.update({attr: n_value[0]}) else: normalized_attrs.update({attr: n_value}) return normalized_attrs @staticmethod def __isfloat(value): try: float(value) return True except ValueError: return False @staticmethod def __isint(value): is_signed = value.startswith('+') or value.startswith('-') other_chars_are_digits = value[1:].isdigit() all_chars_are_digits = value.isdigit() return all_chars_are_digits or (is_signed and other_chars_are_digits) @staticmethod def __find_input(graph): inputs = [] for node in sorted(graph.nodes()): node = Node(graph, node) if node.has_valid('type') and node.type in ('Input', 'Parameter'): inputs.append(node) if len(inputs) < 1: raise RuntimeError("Graph {} has less than one input node".format( graph.name)) return inputs def compare(self, ref_net): if not isinstance(ref_net, IREngine): ir_input = self.__find_input(self.graph)[0] ref_input = self.__find_input(ref_net)[0] ref_graph = ref_net else: ir_input = self.input_node or self.__find_input(self.graph)[0] ref_input = ref_net.input_node or ref_net.__find_input( ref_net.graph)[0] ref_graph = ref_net.graph # TODO check that ir_input[0].id and ref_input[0].id are the same result, stderr = compare_graphs(graph=self.graph, graph_ref=ref_graph, last_node=ir_input.id, last_node_ref=ref_input.id, check_op_attrs=True) return result, stderr def generate_bin_hashes_file(self, path_for_file=None): # This function creates file with extension '.bin.hashes.npz' where hashes of bin exists. # For creating this file in custom filder use attribute path_for_file. # Where directory for file should be existed graph = self.graph if path_for_file is None: path_for_file = str( Path(self.path_to_xml).with_suffix('.bin.hashes.npz')) assert 'hashes' in graph.graph, "Loaded IR graph doesn't contain `hashes`: {}".format( self.path_to_xml) np.savez_compressed(path_for_file, **graph.graph['hashes']) return path_for_file def get_inputs(self): # Function return input nodes in dictionary: {input_node_name: input_node_shape, ...} input_nodes = self.__find_input(self.graph) return { input_node.name: input_node.out_node().shape for input_node in input_nodes } def __eq__(self, other): # To call this function create two IREngine objects (IR1, IR2) and compare them IR1 == IR2 if not isinstance(other, IREngine): raise AttributeError( "IREngine can be compared only with IREngine object type") return self.compare(other)[0] def __read_subgraph(self, layer, layer_attrs, body_child, port_map_name): body_ir = IREngine(path_to_xml=None, path_to_bin=self.path_to_bin, xml_tree=ElementTree(body_child[0])) self.graph.graph['hashes'].update(body_ir.graph.graph['hashes']) xml_port_map = list(layer.iterfind(port_map_name)) assert not len(xml_port_map) != 1, "If then_body won\'t be compared due to missing {1} section in node {0}! " \ .format(layer_attrs['name'], port_map_name) xml_port_map = xml_port_map[0] input_layers = [] input_port_map = [] output_port_map = [] for port in xml_port_map: if port.tag == 'input': if 'internal_layer_id' not in port.attrib: log.warning( "internal_layer_id attrib not found in input section") else: input_layers.append( Node(body_ir.graph, port.attrib['internal_layer_id'])) input_port_map.append(self.__normalize_attrs(port.attrib)) elif port.tag == 'output': if 'internal_layer_id' not in port.attrib: log.warning( "internal_layer_id attrib not found in output section") else: output_port_map.append(self.__normalize_attrs(port.attrib)) return body_ir, input_port_map, output_port_map, input_layers def __read_if(self, layer, layer_attrs): xml_then_body_child = list(layer.iterfind('then_body')) xml_else_body_child = list(layer.iterfind('else_body')) assert len(xml_then_body_child) == 1 and len( xml_else_body_child) == 1, "If operation has only one subgraph" then_body_ir, then_input_port_map, then_output_port_map, _ = \ self.__read_subgraph(layer, layer_attrs, xml_then_body_child, 'then_port_map') layer_attrs.update({'then_graph': then_body_ir}) layer_attrs.update({'then_input_port_map': then_input_port_map}) layer_attrs.update({'then_output_port_map': then_output_port_map}) else_body_ir, else_input_port_map, else_output_port_map, _ = \ self.__read_subgraph(layer, layer_attrs, xml_else_body_child, 'else_port_map') layer_attrs.update({'else_graph': else_body_ir}) layer_attrs.update({'else_input_port_map': else_input_port_map}) layer_attrs.update({'else_output_port_map': else_output_port_map}) return layer_attrs def __read_rt_info(self, layer, layer_attrs): rt_info = RTInfo() xml_rt_info = list(layer.iterfind('rt_info'))[0] for attr in xml_rt_info: attr_name = attr.attrib['name'] if attr_name == 'old_api_map_order': rt_info.info.update( self.__read_old_api_map_order(attr, layer.attrib['type'])) elif attr_name == 'old_api_map_element_type': rt_info.info.update( self.__read_old_api_map_element_type( attr, layer.attrib['type'])) else: rt_info.info.update((self.__read_rt_info_common(attr))) layer_attrs.update({'rt_info': rt_info}) return layer_attrs @staticmethod def __read_old_api_map_order(attr, layer_type): version = int(attr.attrib['version']) order = list(map(int, attr.attrib['value'].split(','))) old_api_map = OldAPIMapOrder(version=version) if layer_type == 'Parameter': old_api_map.old_api_transpose_parameter(order) elif layer_type == 'Result': old_api_map.old_api_transpose_result(order) else: raise AttributeError( "Cannot read old_api_map for layer of type: {}".format( layer_type)) return {('old_api_map_order', version): old_api_map} @staticmethod def __read_old_api_map_element_type(attr, layer_type): version = int(attr.attrib['version']) element_type = destination_type_to_np_data_type(attr.attrib['value']) old_api_map = OldAPIMapElementType(version=version) old_api_map.set_legacy_type(element_type) return {('old_api_map_element_type', version): old_api_map} @staticmethod def __read_rt_info_common(attr): attr_name = attr.attrib['name'] version = int(attr.attrib['version']) rt_info = OrderedDict() for key in attr.attrib: if key not in ('name', 'version'): rt_info[key] = attr.attrib[key] return {(attr_name, version): rt_info}
def concat_convolutions(graph: Graph, start_node: Node, last_node: Node): """ This function converts group of convolutions into one """ # Check that concatenation makes in the same order conv_nodes = get_next_operation(start_node) assert len(conv_nodes) == len(last_node.in_nodes()) gconv = conv_nodes[0] for id in range(len(conv_nodes)): conv = conv_nodes[id] if conv.out_node().id != last_node.in_node(id).id: return False # Check that all convolutions have same weights shapes if not np.array_equal(conv.in_node(1).shape, gconv.in_node(1).shape): log.debug( 'Grouped convolutions fusion : convolutions have different weights shape' ) return False # Check that split and concat dims are valid channel_dim = gconv.channel_dims[0] split_axis = start_node.in_port(1).data.get_value() if channel_dim != split_axis or channel_dim != last_node.axis: log.debug( 'Grouped convolutions fusion : split or concat has weird axis!') return False # Check that all convolutions has the same parameters conv_attrs = ['pad', 'stride'] for attr in conv_attrs: for id in range(len(conv_nodes)): conv = conv_nodes[id] if not np.array_equal(gconv[attr], conv[attr]): log.debug( 'Grouped convolutions fusion : attrs {} doesn\'t match'. format(attr)) return False # Check that all Convolutions has biases (if exists) has_biases = False for id in range(len(conv_nodes)): conv = conv_nodes[id] if len(conv.in_nodes()) == 3: if not has_biases: has_biases = True elif has_biases: return False # All convolution mast have biases # Check that all biases have same shape if has_biases: for id in range(len(conv_nodes)): conv = conv_nodes[id] if conv.in_node(2).shape != gconv.in_node(2).shape: log.debug( 'Group convolutions fusion : convolutions have different biases shape {} and {}' .format(conv.in_node(2).shape, gconv.in_node(2).shape)) return False graph.remove_edge(gconv.in_node(0).id, gconv.id) graph.remove_edge(gconv.id, gconv.out_node().id) input = start_node.in_node(0) output = last_node.out_node() # Removing edges from data nodes to Split and Concat graph.remove_edge(input.id, start_node.id) graph.remove_edge(last_node.id, output.id) # Add edges to grouped convolution graph.add_edges_from([(input.id, gconv.id, { 'in': 0 }), (gconv.id, output.id, { 'out': 0 })]) # Concatenation of convolutions weights_node = gconv.in_node(1) bias_node = gconv.in_node(2) if has_biases else None weights_value = mo_array(weights_node.value) bias_value = mo_array(bias_node.value) if has_biases else None # gconv.get_weights_permute.perm contains permutation indices # where feature dimension is set to zero position, so 0 value # in gconv.get_weights_permute.inv indicates original feature dimension index feature_dim = np.where(gconv.get_weights_permute.inv == 0)[0][0] for conv in conv_nodes[1:]: weights_value = np.concatenate((weights_value, conv.in_node(1).value), axis=feature_dim) if has_biases: bias_value = np.concatenate((bias_value, conv.in_node(2).value), axis=-1) # Not validated weights_node.value = mo_array(weights_value) weights_node.shape = mo_array(weights_value.shape) if has_biases: bias_node.value = mo_array(bias_value) bias_node.shape = mo_array(bias_value.shape) log.debug('Start node : {} Last node : {} Nodes inside : {}'.format( start_node.id, last_node.id, len(start_node.out_nodes()))) log.debug('Output shape : {}'.format(weights_value.shape)) gconv.group = len(conv_nodes) gconv.output = weights_node.shape[feature_dim] gconv.output_shape[feature_dim] = weights_node.shape[feature_dim] return True
def add_reshape_after_data_node(graph: Graph, data_node_name: str): """ Adds reshape operation which changes shape of the tensor produced by TFSubgraphCall from 4D to real dimension of the tensor. The data_node_name node contains real dimensions of the tensor but they will be changed in the add_reshapes_for_tf_subgraph_calls function to a 4D because IE TF call layer supports output in 4D only. :param graph: graph to operate on. :param data_node_name: name of the data node to be reshaped to correct dimensions. :return: None """ data_node = Node(graph, data_node_name) # if the data node was previously marked as output then we need to mark as output new reshaped data node is_out_node = False if len(data_node.out_nodes()) == 1 and data_node.out_node().has( 'op') and data_node.out_node().op == 'Result': is_out_node = True graph.remove_node(data_node.out_node().id) # save old consumers nodes with edge attributes old_consumer_nodes_with_attrs = list() for index, out_op in enumerate(data_node.out_nodes()): edge_attrs = graph.get_edge_data(data_node_name, out_op.name)[0] old_consumer_nodes_with_attrs.append((out_op.name, edge_attrs)) # remove old consumers from the data node for out_op in list(data_node.out_nodes()): graph.remove_edge(data_node_name, out_op.name) # reshape operation node reshape_node_name = graph.unique_id("Reshape_") graph.add_node(reshape_node_name, kind='op', type='Reshape', name=reshape_node_name, op='Reshape', data_type=data_node['data_type']) update_ie_fields(graph.node[reshape_node_name]) # reshape shape data node reshape_shape_data_node_name = graph.unique_id("Reshape_shape_") graph.add_node(reshape_shape_data_node_name, kind='data', name=reshape_shape_data_node_name, value=mo_array(data_node['shape']), shape=[1]) # reshaped data node reshaped_value = None if data_node['value'] is not None: reshaped_value = mo_array(data_node['value']) reshaped_data_node_name = graph.unique_id("reshaped_data_") graph.add_node(reshaped_data_node_name, kind='data', name=reshaped_data_node_name, shape=mo_array(data_node['shape']), value=reshaped_value, nchw_layout=True) if is_out_node: add_opoutput(graph, reshaped_data_node_name, 0, False) graph.add_edges_from([ (data_node_name, reshape_node_name, { 'in': 0 }), (reshape_shape_data_node_name, reshape_node_name, { 'in': 1 }), (reshape_node_name, reshaped_data_node_name, { 'out': 0 }), ]) for out_node_name, edge_attrs in old_consumer_nodes_with_attrs: graph.add_edges_from([(reshaped_data_node_name, out_node_name, edge_attrs)])
def muladd_to_scaleshift_action(graph: Graph, match: dict): mul = match['mul'] add = match['add'] output = match['output'] # Pass works correctly only in case when node have only 1 output if len(mul.out_port(0).get_destinations()) > 1: return if mul.soft_get('can_be_scaleshift') is False or add.soft_get('can_be_scaleshift') is False: return mul_weights_id = get_value_id(mul) mul_input_id = get_tensor_id(mul) add_weights_id = get_value_id(add) if mul_weights_id is None: log.debug("Mul->Add to ScaleShift: Mul {} has no weights".format(mul.name)) return if mul_input_id is None: log.debug("Mul->Add to ScaleShift: Mul {} has no input".format(mul.name)) return if add_weights_id is None: log.debug("Mul->Add to ScaleShift: Add {} has no weights".format(add.name)) return input = mul.in_node(mul_input_id) weights = mul.in_node(mul_weights_id) bias = add.in_node(add_weights_id) # Transform values weights.value = np.squeeze(weights.value) weights.shape = int64_array(weights.value.shape) bias.value = np.squeeze(bias.value) bias.shape = int64_array(bias.value.shape) # Broadcast weights if they are scalar if weights.value.ndim == 0 and bias.value.ndim == 1: weights.value = np.full(bias.shape, weights.value.item(), dtype=weights.value.dtype) weights.shape = int64_array(weights.value.shape) if bias.shape != weights.shape: log.warning('Mul->Add to ScaleShift conversion stopped {} != {}'.format(weights.shape, bias.shape)) return if bias.value.ndim != weights.value.ndim or bias.value.size != weights.value.size: log.debug("Skipping Mul->Add to ScaleShift conversion for nodes {}, {} because of different weights " "and biases".format(mul.name, add.name)) return if bias.value.size == 1 and weights.value.size == 1: log.debug("Skipping Mul->Add to ScaleShift conversion for nodes {}, {}. Will be converted to Power" "".format(mul.name, add.name)) return op_name = "ScaleShift" log.debug("Fusing Mul->Add to {}. Input nodes: {} and {}, bias.shape = {}, weights.shape = {}" "".format(op_name, mul.id, add.id, bias.shape, weights.shape)) graph.remove_edge(input.node, mul.id) graph.remove_edge(weights.node, mul.id) graph.remove_edge(bias.node, add.id) graph.remove_edge(add.node, output.id) op_node = graph.unique_id(mul.name + '/Fused{}_'.format(op_name)) graph.add_node(op_node, **add_attrs_props(dict(kind='op', type=op_name, name=op_node, op=op_name, data_type=input.data_type))) scsh = Node(graph, op_node) scsh.add_input_port(0) scsh.add_input_port(1) scsh.add_input_port(2) scsh.add_output_port(0) update_ie_fields(graph.node[op_node]) graph.add_edges_from([ (input.node, op_node, {'in': 0}), (weights.node, op_node, {'in': 1, 'bin': 'weights'}), (bias.node, op_node, {'in': 2, 'bin': 'biases'}), (op_node, output.node, {'out': 0}) ]) return
def partial_infer(graph: Graph, start_node: str = None): """ Tries to execute constant parts of the graph and deduce as much as possible information following the data flow, e.g. calculate and propagate shapes and constant values. Partially or completely defined values are stored in data nodes (kind='data'). """ # We have to turn off strict mode due to above we add and remove edeges without attributes that is prohibited graph.strict_mode = False cycle_nodes = graph.get_nodes_with_attributes(is_cyclic=True) cycle_nodes = [Node(graph, node).out_node().id for node in cycle_nodes] ebunch_cyclic = list( graph.out_edges(nbunch=cycle_nodes, data=True, keys=True)) ebunch_reconnected = exit_bound_edges(graph, sources=cycle_nodes, end_node_attrs={'op': 'Exit'}) graph.remove_edges_from(ebunch_cyclic) graph.add_edges_from(ebunch_reconnected) try: nodes = list(nx.topological_sort(graph)) except: raise Error('Graph contains a cycle. Can not proceed. ' + refer_to_faq_msg(97)) graph.remove_edges_from(ebunch_reconnected) graph.add_edges_from(ebunch_cyclic) graph.strict_mode = True # Mark all nodes as not inferred yet if start_node is not None: start_index = nodes.index(start_node) 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) 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_and_set('infer'): node.infer(node) return graph