def merge_nodes(graph: Graph, nodes_to_merge_names: list, inputs_desc: list = None, outputs_desc: list = None): """ Merges nodes specified in the set 'nodes_to_merge_names' into one mega-node, creating new edges between mega-node and inputs/outputs nodes of the mega-node. The added edges contain name of input/output nodes which will be used for generation of placeholders and will be saved to the IR xml so IE plug-in know how to map input/output data for the layer. Also the function adds protobufs of the nodes of the sub-graph and 'Const' ops consumed by nodes in the sub-graph to the node's attribute 'pbs'. :param graph: the graph object to operate on. :param nodes_to_merge_names: list of nodes names that should be merged into a single node. :param inputs_desc: optional list describing input nodes order. :param outputs_desc: optional list describing output nodes order. """ if not is_connected_component(graph, nodes_to_merge_names): log.warning( "The following nodes do not form connected sub-graph: {}".format( nodes_to_merge_names)) # graph.dump_graph_for_graphviz(nodes_to_dump=nodes_to_merge_names) new_node_name = graph.unique_id("TFSubgraphCall_") log.info("Create new node with name '{}' for nodes '{}'".format( new_node_name, ', '.join(nodes_to_merge_names))) graph.add_node(new_node_name) new_node_attrs = graph.node[new_node_name] new_node_attrs['name'] = new_node_name set_tf_custom_call_node_attrs(new_node_attrs) new_node = Node(graph, new_node_name) added_input_tensors_names = set( ) # set of tensors that are were added as input to the sub-graph added_new_node_output_tensors = dict( ) # key - tensor name, value - out port for node_name in nodes_to_merge_names: node = Node(graph, node_name) add_node_pb_if_not_yet_added(node, new_node) # TODO: any improvements? for in_node_name, edge_attrs in Node(graph, node_name).get_inputs(): in_node = Node(graph, in_node_name) # internal edges between nodes of the sub-graph if in_node_name in nodes_to_merge_names: add_node_pb_if_not_yet_added(in_node, new_node) continue # edge outside of sub-graph into sub-graph if in_node_name not in nodes_to_merge_names: # we cannot use the 'in_node_name' as a protobuf operation name here # because the 'in_node_name' could be a sub-graph matched before. input_tensor_name = node.pb.input[edge_attrs['in']] if input_tensor_name not in added_input_tensors_names: if not new_node.has_port('in', edge_attrs['in']): new_node.add_input_port(edge_attrs['in']) graph.add_edge( in_node_name, new_node_name, **merge_edge_props( { 'in': find_input_port(new_node, inputs_desc, node_name, edge_attrs['in']), 'out': edge_attrs['out'], 'internal_input_node_name': input_tensor_name, 'original_dst_node_name': node_name, 'original_dst_port': edge_attrs['in'], 'in_attrs': [ 'in', 'internal_input_node_name', 'original_dst_node_name', 'original_dst_port', 'placeholder_name' ], 'out_attrs': ['out'] }, edge_attrs)) log.debug( "Creating edge from outside of sub-graph to inside sub-graph: {} -> {}" .format(in_node_name, new_node_name)) added_input_tensors_names.add(input_tensor_name) # edge from inside sub-graph to outside sub-graph for out_node_name, edge_attrs in Node(graph, node_name).get_outputs(): if out_node_name not in nodes_to_merge_names: log.debug( "Creating edge from inside of sub-graph to outside sub-graph: {} -> {}" .format(new_node_name, out_node_name)) out_name = internal_output_name_for_node( node_name, edge_attrs['out']) if out_name not in added_new_node_output_tensors.keys(): added_new_node_output_tensors[out_name] = find_output_port( new_node, outputs_desc, node_name, edge_attrs['out']) if not new_node.has_port( 'out', added_new_node_output_tensors[out_name]): new_node.add_output_port( added_new_node_output_tensors[out_name]) graph.add_edge( new_node_name, out_node_name, **merge_edge_props( { 'in': edge_attrs['in'], 'out': added_new_node_output_tensors[out_name], 'internal_output_node_name': out_name, 'in_attrs': ['in', 'internal_input_node_name'], 'out_attrs': ['out', 'internal_output_node_name'] }, edge_attrs)) new_node['output_tensors_names'] = [ val for val in {v: k for k, v in added_new_node_output_tensors.items()}.values() ] # add nodes using the same order as in initial GraphDef so we can dump them to IR in "correct" order new_node['nodes_order'] = [ node for node in graph.graph['initial_nodes_order'] if node in new_node['pbs'].keys() ] for n in nodes_to_merge_names: if graph.has_node( n): # check if not deleted by another (similar) pattern graph.remove_node(n) return Node(graph, new_node_name)
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 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 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 protobuf2nx(graph: Graph, pb): """ Convert proto message with ONNX model to equivalent NX representation. All nodes and edges are restored here as ONNX model has op/data representation, that means that nodes are connected via tensor names. Name of tensors are defined on demand in nodes, so we have a code similar to Caffe here. :param graph: the Graph object to load the graph into :param pb: the ONNX file protobuf message :return: None """ # maps a tensor name to a node produced it and the node port: str -> (node_id, node_port) data_nodes_map = {} graph_pb = pb.graph add_initializers_and_inputs_to_graph(graph, graph_pb, data_nodes_map) output_ids = [] for outp in graph_pb.output: name = str(outp.name) if graph.has_node(name): log.error( 'Name {} of output node already exists in graph. Ignoring this output. If the output is required,' ' please rename it.'.format(name), extra={'is_warning': True}) continue else: # add fake node on output graph.add_node(name, kind='op', op='FakeOutput', pb=outp) output_ids.append(name) # Go through all nodes in the original model order (because data nodes are defined on-the-fly and order is # important) for node in graph_pb.node: # create an NX node fw_name = node_id(node) id = graph.unique_id(fw_name) graph.add_node(id, pb=node, kind='op') if hasattr(graph, 'op_names_statistic') and hasattr(node, 'op_type'): graph.op_names_statistic[node.op_type] += 1 # add incoming edges based on data_nodes_map for dst_port, inp in enumerate(node.input): # should add edge inp --> id if inp not in data_nodes_map: if inp == '': # input is omitted; most likely it corresponds to an optional input for an operator continue else: raise Error( 'Reference to {} is not satisfied. A node refer not existing data tensor. ONNX model is not ' 'consistent. Protobuf fragment: {}', inp, node) src_id, src_port = data_nodes_map[inp] assert (graph.has_node(src_id)) edge_attrs = { 'out': src_port, 'in': dst_port, 'name': inp, 'fw_tensor_debug_info': [(src_id, inp)], 'in_attrs': ['in', 'name'], 'out_attrs': ['out', 'name'], 'data_attrs': ['fw_tensor_debug_info'] } graph.add_edge(src_id, id, **edge_attrs) # add outgoing edges to data_nodes_map for src_port, out in enumerate(node.output): if out in output_ids: edge_attrs = { 'out': src_port, 'in': 0, 'name': out, 'fw_tensor_debug_info': [(fw_name, out)], 'in_attrs': ['in', 'name'], 'out_attrs': ['out', 'name'], 'data_attrs': ['fw_tensor_debug_info'] } graph.add_edge(id, out, **edge_attrs) if out in data_nodes_map: log.debug("Detected reuse of blob {}.".format(out)) data_nodes_map[out] = (id, src_port) graph.graph[ 'tensor_mapping'] = data_nodes_map # save main graph tensor names mapping for Loop op parsing
def extract(cls, loop_node): Loop.update_node_stat(loop_node, {}) body_graph_proto = onnx_attr(loop_node, 'body', 'g', None) main_graph = loop_node.graph # create a Graph object for the body and take graph attributes from the main graph body_graph = Graph() main_graph_attrs_copy = {} for attr_key, attr_value in main_graph.graph.items(): if attr_key not in ['tensor_mapping', 'parent_node']: main_graph_attrs_copy[attr_key] = copy.deepcopy(attr_value) body_graph.graph.update(main_graph_attrs_copy) loop_node['body'] = body_graph # save parent node for nested loops to know which node contains body (and which graph is on upper level) body_graph.graph['parent_node'] = loop_node # maps a tensor name to a node produced it and the node port: str -> (node_id, node_port) data_nodes_map = {} body_graph.graph['tensor_mapping'] = data_nodes_map # save mapping for possible Loop inside the Loop body_parameters = add_initializers_and_inputs_to_graph(body_graph, body_graph_proto, data_nodes_map) external_edges = [] # (src_node, src_out_port), dest_body_parameter_node # save additional edges information for graph on each level, the first one is the deepest additional_params = [] # (src_node, src_out_port) -> parameter_node (for manually added Parameters) # Go through all nodes in the original model order because data nodes are defined on-the-fly and order matters for pb_node in body_graph_proto.node: # create an NX node id = body_graph.unique_id(node_id(pb_node)) body_graph.add_node(id, pb=pb_node, kind='op') if hasattr(body_graph, 'op_names_statistic') and hasattr(pb_node, 'op_type'): body_graph.op_names_statistic[pb_node.op_type] += 1 # add incoming edges based on data_nodes_map for dst_port, inp in enumerate(pb_node.input): # should add edge src_internal_id --> dst_id if inp not in data_nodes_map: if inp == '': # input is omitted; most likely it corresponds to an optional input for an operator continue else: is_finished = create_cross_body_edge(body_graph, external_edges, additional_params, inp, id, dst_port) if not is_finished: raise Error( 'Reference to "{}" is not satisfied. A node refer not existing data tensor. ONNX ' 'model is not consistent. Protobuf fragment: {}', inp, pb_node) else: src_id, src_port = data_nodes_map[inp] create_edge_with_attrs(body_graph, inp, src_id, src_port, id, dst_port) # add outgoing edges to data_nodes_map for src_port, out in enumerate(pb_node.output): if out in data_nodes_map: log.debug("Detected reuse of blob {}.".format(out)) data_nodes_map[out] = (id, src_port) body_results = [] for output in body_graph_proto.output: tensor_name = str(output.name) node_name, output_port = data_nodes_map[tensor_name] assert body_graph.has_node(node_name), 'The body graph does not contain output with name "{}"'.format( node_name) body_results.append(Node(body_graph, add_opoutput(body_graph, node_name, output_port, False))) # add 'internal_layer_id' attribute which is a must have attribute for the loop body node for idx, body_node in enumerate(body_graph.get_op_nodes()): body_node['internal_layer_id'] = idx loop_carried_dependencies_count = len(body_graph_proto.input) - 2 scan_outputs_count = len(body_graph_proto.output) - 1 - loop_carried_dependencies_count # Loop inputs: # 0 - trip count # 1 - execution condition # 2 .. - loop carried dependencies # Loop outputs: # 0 .. loop_carried_dependencies_count - 1 - loop carried dependencies # loop_carried_dependencies_count .. - scan outputs # Body inputs: # 0 - iteration number # 1 - execution condition # 2 .. - loop carried dependencies # Body outputs: # 0 - execution condition # 1 .. loop_carried_dependencies_count - loop carried dependencies # loop_carried_dependencies_count + 1 .. - scan outputs # some of the inputs/outputs may not be connected but the normalization transformation will take care of it # connection Loop body nodes with external input edges next_loop_input_port_idx = sorted(loop_node.in_edges().keys())[-1] + 1 cur_graph = body_graph for external_edges_subg in external_edges: if 'parent_node' not in cur_graph.graph: continue cur_loop_node = cur_graph.graph['parent_node'] parent_graph = cur_loop_node.graph for (src_node, src_port), body_node, tensor_name in external_edges_subg: create_edge_with_attrs(parent_graph, tensor_name, src_node, src_port, cur_loop_node.id, next_loop_input_port_idx) Loop.connect_body_input(cur_loop_node, next_loop_input_port_idx, body_node) next_loop_input_port_idx += 1 cur_graph = parent_graph # mark current iteration input Parameter node Loop.mark_current_iteration_parameter_node(loop_node, body_parameters[0]) # connect initial value for "execution condition" input of the loop Loop.connect_body_input(loop_node, 1, body_parameters[1]) # add back edge with "execution condition" Loop.add_back_edge(loop_node, body_parameters[1], body_results[0]) # mark "execution condition" Result node Loop.mark_execution_condition_result_node(loop_node, body_results[0]) # connect initial value for "loop carried" dependencies variables for idx in range(loop_carried_dependencies_count): Loop.connect_body_input(loop_node, idx + 2, body_parameters[idx + 2]) # add back edge for "loop carried" dependencies variables for idx in range(loop_carried_dependencies_count): Loop.add_back_edge(loop_node, body_parameters[idx + 2], body_results[idx + 1]) # connect final value for "loop carried" dependencies variables for idx in range(loop_carried_dependencies_count): Loop.connect_body_output(loop_node, idx, body_results[idx + 1]) # connect "scan outputs" and mark axis for concatenation for idx in range(loop_carried_dependencies_count, loop_carried_dependencies_count + scan_outputs_count): Loop.connect_body_output(loop_node, idx, body_results[idx + 1], axis=0) # run function to parse body nodes attributes similar to the main graph extract_node_attrs(body_graph, lambda node: onnx_op_extractor(node, check_for_duplicates(onnx_op_extractors))) return cls.enabled
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}