def find_next_nodes_not_of_types(graph: NNCFGraph, nncf_node: NNCFNode, types: List[str]) -> List[NNCFNode]: """ Traverse nodes in the graph from nncf node to find first nodes that aren't of type from types list. First nodes with some condition mean nodes: - for which this condition is true - reachable from `nncf_node` such that on the path from `nncf_node` to this nodes there are no other nodes with fulfilled condition :param graph: Graph to work with. :param nncf_node: NNCFNode to start search. :param types: List of types. :return: List of next nodes for nncf_node of type not from types list. """ visited = {node_id: False for node_id in graph.get_all_node_ids()} partial_traverse_function = partial(traverse_function, type_check_fn=lambda x: x not in types, visited=visited) nncf_nodes = [nncf_node] if nncf_node.node_type not in types: nncf_nodes = graph.get_next_nodes(nncf_node) next_nodes = [] for node in nncf_nodes: next_nodes.extend(graph.traverse_graph(node, partial_traverse_function)) return next_nodes
def get_next_nodes_of_types(graph: NNCFGraph, nncf_node: NNCFNode, types: List[str]) -> List[NNCFNode]: """ Looking for nodes with type from types list from `nncf_node` such that there is path from `nncf_node` to this node and on this path no node has one of types type. :param graph: Graph to work with. :param nncf_node: NNCFNode to start search. :param types: List of types to find. :return: List of next nodes of nncf_node with type from types list. """ sources_types = types visited = {node_id: False for node_id in graph.get_all_node_ids()} partial_traverse_function = partial( traverse_function, type_check_fn=lambda x: x in sources_types, visited=visited) nncf_nodes = [nncf_node] if nncf_node.node_type in sources_types: nncf_nodes = graph.get_next_nodes(nncf_node) next_nodes = [] for node in nncf_nodes: next_nodes.extend(graph.traverse_graph(node, partial_traverse_function)) return next_nodes
def get_sources_of_node(nncf_node: NNCFNode, graph: NNCFGraph, sources_types: List[str]) -> List[NNCFNode]: """ Source is a node of source such that there is path from this node to `nncf_node` and on this path no node has one of `sources_types` type. :param sources_types: List of sources types. :param nncf_node: NNCFNode to get sources. :param graph: NNCF graph to work with. :return: List of all sources nodes. """ visited = {node_id: False for node_id in graph.get_all_node_ids()} partial_traverse_function = partial( traverse_function, type_check_fn=lambda x: x in sources_types, visited=visited) nncf_nodes = [nncf_node] if nncf_node.node_type in sources_types: nncf_nodes = graph.get_previous_nodes(nncf_node) source_nodes = [] for node in nncf_nodes: source_nodes.extend( graph.traverse_graph(node, partial_traverse_function, False)) return source_nodes
def _get_input_preprocessing_nodes( self, nncf_graph: NNCFGraph, model: tf.keras.Model) -> List[NNCFNode]: retval = [] def traverse_fn( node: NNCFNode, preprocessing_nodes: List[NNCFNode] ) -> Tuple[bool, List[NNCFNode]]: is_finished = True successors = nncf_graph.get_next_nodes(node) if len(successors) == 1: successor = next(iter(successors)) # It is necessary to determine the number of input nodes from the model # in order to correctly count the duplicated edges original_name, _ = get_original_name_and_instance_idx( successor.node_name) layer = model.get_layer(name=original_name) num_previous_nodes = len(layer.input) if isinstance( layer.input, list) else 1 if successor.metatype in ELEMENTWISE_LAYER_METATYPES and num_previous_nodes == 1: preprocessing_nodes.append(successor) is_finished = False return is_finished, preprocessing_nodes for nncf_node in nncf_graph.get_input_nodes(): preprocessing_nodes_for_this_input = nncf_graph.traverse_graph( nncf_node, traverse_fn) retval += preprocessing_nodes_for_this_input return retval
def _get_quantized_nodes_for_output( self, nncf_graph: NNCFGraph, insertion_points: List[str], node_key: str, quantized_nodes_for_output: List[NNCFNode] = None ) -> List[NNCFNode]: nncf_node = nncf_graph.get_node_by_key(node_key) if quantized_nodes_for_output is None: if node_key in insertion_points: return [nncf_node] quantized_nodes_for_output = [] for predecessor in nncf_graph.get_previous_nodes(nncf_node): pred_node_key = nncf_graph.get_node_key_by_id(predecessor.node_id) if len(nncf_graph.get_next_nodes(predecessor)) > 1: logger.warning( 'Removing of FakeQuantize after layer {} ' 'with multiple outputs is not fully supported'.format( predecessor.node_name)) if predecessor.metatype in LAYER_METATYPES_AGNOSTIC_TO_DATA_PRECISION: self._get_quantized_nodes_for_output( nncf_graph, insertion_points, pred_node_key, quantized_nodes_for_output) elif nncf_graph.get_node_key_by_id( predecessor.node_id) in insertion_points: quantized_nodes_for_output.append(predecessor) return quantized_nodes_for_output
def _get_default_pre_hook_ip_list( nncf_graph: NNCFGraph) -> List[PreHookInsertionPoint]: # Pre-hook all input ports of all nodes allowed_pre_hook_insertion_points = [] for nncf_node in nncf_graph.get_all_nodes(): pred_nodes = nncf_graph.get_previous_nodes(nncf_node) for pred_node in pred_nodes: input_edge = nncf_graph.get_edge(pred_node, nncf_node) allowed_pre_hook_insertion_points.append( PreHookInsertionPoint(nncf_node.node_name, input_edge.input_port_id)) return allowed_pre_hook_insertion_points
def init_output_masks_in_graph(graph: NNCFGraph, nodes: List): """ Initialize masks in groph for mask propagation algorithm :param graph: NNCFNetwork :param nodes: list with pruned nodes """ for node in graph.get_all_nodes(): node.data.pop('input_masks', None) node.data.pop('output_mask', None) for minfo in nodes: mask = minfo.operand.binary_filter_pruning_mask nncf_node = graph.get_node_by_id(minfo.nncf_node_id) nncf_node.data['output_mask'] = PTNNCFTensor(mask)
def get_cluster_next_nodes( graph: NNCFGraph, pruned_groups_info: Clusterization[PrunedLayerInfoBase], prunable_types: List[str]) -> Dict[int, List[NNCFNodeName]]: """ Finds nodes of `prunable_types` types that receive the output of a pruned cluster as input. :param graph: NNCFGraph. :param pruned_groups_info: `Clusterization` of pruning groups. :param prunable_types: Types of nodes that will be returned. :return Dictionary of next node names by cluster {cluster_id: [node_name]}. """ next_nodes = {} for cluster in pruned_groups_info.get_all_clusters(): next_nodes_cluster = set() cluster_nodes = set() for pruned_layer_info in cluster.elements: nncf_cluster_node = graph.get_node_by_id( pruned_layer_info.nncf_node_id) cluster_nodes.add(nncf_cluster_node.node_name) curr_next_nodes = get_next_nodes_of_types(graph, nncf_cluster_node, prunable_types) next_nodes_idxs = [n.node_name for n in curr_next_nodes] next_nodes_cluster = next_nodes_cluster.union(next_nodes_idxs) next_nodes[cluster.id] = list(next_nodes_cluster - cluster_nodes) return next_nodes
def _get_quantizable_weighted_layer_nodes( self, nncf_graph: NNCFGraph) -> List[QuantizableWeightedLayerNode]: nodes_with_weights = [] for node in nncf_graph.get_all_nodes(): metatype = node.metatype if metatype in OUTPUT_NOOP_METATYPES: continue if not (metatype in QUANTIZATION_LAYER_METATYPES and should_consider_scope( node.node_name, ignored_scopes=self.ignored_scopes_per_group[ QuantizerGroup.WEIGHTS], target_scopes=None)): continue assert issubclass(metatype, TFLayerWithWeightsMetatype) nodes_with_weights.append(node) scope_overrides_dict = self._get_algo_specific_config_section().get( 'scope_overrides', {}) weighted_node_and_qconf_lists = assign_qconfig_lists_to_modules( nodes_with_weights, self.DEFAULT_QCONFIG, self.global_quantizer_constraints[QuantizerGroup.WEIGHTS], scope_overrides_dict, hw_config=self.hw_config) return [ QuantizableWeightedLayerNode(node, qconf_list) for node, qconf_list in weighted_node_and_qconf_lists.items() ]
def generate_output_mask( cls, node: NNCFNode, graph: NNCFGraph, tensor_processor: Type[NNCFPruningBaseTensorProcessor] ) -> Optional[NNCFTensor]: """ Generate output mask from input masks with all None replaced by identity masks. If all input masks is None return None. :param node: Node to determine it's sources. :param graph: NNCF graph to work with. :param tensor_processor: Interface with tensor processing methods. :return: Filled input masks. """ input_edges = graph.get_input_edges(node) previous_nodes = [edge.from_node for edge in input_edges] input_masks = [ input_node.data['output_mask'] for input_node in previous_nodes ] not_empty_masks = [mask for mask in input_masks if mask is not None] # type: List[NNCFTensor] if not not_empty_masks: return None first_non_empty_mask = not_empty_masks[0] device = first_non_empty_mask.device filled_input_masks = [] for i, mask in enumerate(input_masks): if mask is None: concat_axis = node.layer_attributes.axis concat_dim = input_edges[i].tensor_shape[concat_axis] mask = tensor_processor.ones(concat_dim, device) filled_input_masks.append(mask) result_mask = tensor_processor.concatenate(filled_input_masks, 0) return result_mask
def get_bn_node_for_conv(graph: NNCFGraph, conv_node: NNCFNode) -> Optional[NNCFNode]: successors = graph.get_next_nodes(conv_node) for succ in successors: if succ.node_type == 'batch_norm': return succ return None
def _get_default_post_hook_ip_list( nncf_graph: NNCFGraph) -> List[PostHookInsertionPoint]: # Post-hook all nodes, post hook applies to the entire op output allowed_post_hook_insertion_points = [] for nncf_node in nncf_graph.get_all_nodes(): allowed_post_hook_insertion_points.append( PostHookInsertionPoint(nncf_node.node_name)) return allowed_post_hook_insertion_points
def _get_related_batchnorms(self, layer_name: str, group: Cluster, graph: NNCFGraph) -> List[NNCFNode]: """ Returns List of batchnorm elements related to the layer. Note: Single node per layer for shared bactchnorm layers """ layer_nodes = [node_ for node_ in group.elements if node_.layer_name == layer_name] bn_nodes = [] bn_layer_names = [] for layer_node in layer_nodes: for next_node in graph.get_next_nodes(layer_node): for bn_node in graph.traverse_graph(next_node, self._get_bn_for_node): bn_layer_name = get_layer_identifier(bn_node) if bn_layer_name not in bn_layer_names: bn_layer_names.append(bn_layer_name) bn_nodes.append(bn_node) return bn_nodes
def _get_custom_layer_node_names( self, nncf_graph: NNCFGraph, converter: TFModelConverter) -> List[NNCFNodeName]: retval = [] for node in nncf_graph.get_all_nodes(): metatype = node.metatype if metatype in OUTPUT_NOOP_METATYPES: continue is_custom, _ = converter.get_layer_info_for_node(node.node_name) if is_custom: retval.append(node.node_name) return retval
def get_first_nodes_of_type(graph: NNCFGraph, op_types: List[str]) -> List[NNCFNode]: """ Looking for first node in graph with type in `op_types`. First == layer with type in `op_types`, that there is a path from the input such that there are no other operations with type in `op_types` on it. :param op_types: Types of modules to track. :param graph: Graph to work with. :return: List of all first nodes with type in `op_types`. """ graph_roots = graph.get_input_nodes() # NNCFNodes here visited = {node_id: False for node_id in graph.get_all_node_ids()} partial_traverse_function = partial(traverse_function, type_check_fn=lambda x: x in op_types, visited=visited) first_nodes_of_type = [] for root in graph_roots: first_nodes_of_type.extend(graph.traverse_graph(root, partial_traverse_function)) return first_nodes_of_type
def cluster_special_ops(graph: NNCFGraph, special_types: List[str], identity_types: List[str]) -> Clusterization[NNCFNode]: """ This model will cluster all operations with type from special_types. Connected nodes is nodes that: 1. Have path between nodes with only identity type nodes on it 2. Have common input (identity type nodes can be on path from this input) :param graph: Graph to work with. :param special_types: List of types that should be grouped to groups of dependent nodes. :return: Clusterization of `special_types` nodes to the dependent groups. """ topologically_sorted_nodes = graph.topological_sort() all_special_nodes = [ node for node in graph.get_all_nodes() if node.node_type in special_types ] # 0. Initially all nodes is a separate clusters clusterization = Clusterization[NNCFNode](lambda x: x.node_id) for i, node in enumerate(all_special_nodes): cluster = Cluster[NNCFNode]( i, [node], [get_position(topologically_sorted_nodes, node.node_id)]) clusterization.add_cluster(cluster) for node in topologically_sorted_nodes: if node.node_type in identity_types: continue all_outputs = find_next_nodes_not_of_types(graph, node, identity_types) all_output_special_nodes = [ node for node in all_outputs if node.node_type in special_types ] if node.node_type in special_types: all_output_special_nodes.append(node) merge_clusters_for_nodes(all_output_special_nodes, clusterization) return clusterization
def get_input_masks(node: NNCFNode, graph: NNCFGraph) -> List[Optional[NNCFTensor]]: """ Returns input masks for all inputs of given NNCFNode. :param node: Given NNCFNode. :param graph: Graph to work with. :return: Input masks. """ input_masks = [ input_node.data['output_mask'] for input_node in graph.get_previous_nodes(node) ] return input_masks
def _handle_quantize_inputs_option( self, quantizer_setup: SingleConfigQuantizerSetup, nncf_graph: NNCFGraph) -> SingleConfigQuantizerSetup: qp_ids_to_discard = [] for qp_id, qp in quantizer_setup.quantization_points.items(): if qp.is_activation_quantization_point(): insertion_point = qp.insertion_point target_node = nncf_graph.get_node_by_name( insertion_point.target_node_name) if not self.quantize_inputs and target_node.metatype in INPUT_NOOP_METATYPES: qp_ids_to_discard.append(qp_id) for qp_id in qp_ids_to_discard: quantizer_setup.discard(qp_id, keep_shared_input_qps=True) return quantizer_setup
def get_last_nodes_of_type(graph: NNCFGraph, op_types: List[str]) -> List[NNCFNode]: """ Looking for last node in graph with type in `op_types`. Last == layer with type in `op_types`, that there is a path from this layer to the model output such that there are no other operations with type in `op_types` on it. :param op_types: Types of modules to track. :param graph: Graph to work with. :return: List of all last pruned nodes. """ graph_outputs = graph.get_output_nodes() # NNCFNodes here visited = {node_id: False for node_id in graph.get_all_node_ids()} partial_traverse_function = partial(traverse_function, type_check_fn=lambda x: x in op_types, visited=visited) last_nodes_of_type = [] for output in graph_outputs: last_nodes_of_type.extend( graph.traverse_graph(output, partial_traverse_function, False)) return last_nodes_of_type
def get_ip_graph_for_test(nncf_graph: NNCFGraph, weighted_node_names: List[NNCFNodeName] = None) -> InsertionPointGraph: pre_hooks = [] post_hooks = [] for node in nncf_graph.get_all_nodes(): in_edges = nncf_graph.get_input_edges(node) for in_edge in in_edges: ip = PreHookInsertionPoint(node.node_name, in_edge.input_port_id) pre_hooks.append(ip) if issubclass(node.metatype, PTSplitMetatype): continue ip = PostHookInsertionPoint(node.node_name) post_hooks.append(ip) weighted_target_points = None if weighted_node_names is not None: weighted_target_points = [] for name in weighted_node_names: weighted_target_points.append(name) ip_graph = InsertionPointGraph(nncf_graph, weight_modifiable_node_names=weighted_target_points, allowed_pre_hook_insertion_points=pre_hooks, allowed_post_hook_insertion_points=post_hooks) return ip_graph
def _add_custom_layer_subgraph(self, nncf_graph: NNCFGraph, custom_layer_name: str) -> NNCFGraph: # TODO (vshampor): filter meaningless ops such as Identity, resource read etc. custom_layer_info = self._custom_layer_infos[custom_layer_name] node_name_vs_nncf_node_ids = {} # type: Dict[NNCFNodeName, int] for node_info in custom_layer_info.node_infos.values(): weight_node_name = node_info.weight_node_name is_shared = False if weight_node_name is not None: shared_node_dict = custom_layer_info.shared_weight_node_names_vs_weighted_op_node_names is_shared = len(shared_node_dict[weight_node_name]) > 1 nncf_node = nncf_graph.add_nncf_node( node_name=node_info.target_node_name, node_type=node_info.node_type, node_metatype=node_info.node_metatype, # TODO (vshampor): collect layer attributes for custom nodes layer_attributes=None, layer_name=node_info.weight_node_name, # sic! is_shared=is_shared, ignored_algorithms=['quantization']) node_name_vs_nncf_node_ids[ node_info.target_node_name] = nncf_node.node_id self._nncf_node_names_vs_custom_layer_name[ node_info.target_node_name] = custom_layer_name for edge, edge_data in custom_layer_info.edge_infos.items(): from_node_name, to_node_name = edge from_node_id = node_name_vs_nncf_node_ids[from_node_name] to_node_id = node_name_vs_nncf_node_ids[to_node_name] nncf_graph.add_edge_between_nncf_nodes( from_node_id, to_node_id, tensor_shape=edge_data.tensor_shape, input_port_id=edge_data.input_port_id, output_port_id=edge_data.output_port_id, dtype=edge_data.dtype) return nncf_graph
def _paint_activation_quantizer_node(self, nncf_graph: NNCFGraph, quantizer_id: NonWeightQuantizerId, quantizer_info: NonWeightQuantizerInfo, bitwidth_color_map: Dict[int, str], groups_of_adjacent_quantizers: GroupsOfAdjacentQuantizers): # pylint:disable=too-many-branches affected_insertion_points_list = quantizer_info.affected_insertions for target_point in affected_insertion_points_list: nncf_node_name = target_point.target_node_name nncf_node = nncf_graph.get_node_by_name(nncf_node_name) node_id = nncf_node.node_id input_port_id = target_point.input_port_id if input_port_id is None: # Post-hooking used for activation quantization # Currently only a single post-hook can immediately follow an operation succs = list(nncf_graph.get_next_nodes(nncf_node)) assert len(succs) == 1 target_nncf_node_key = nncf_graph.get_node_key_by_id(succs[0].node_id) else: # Pre-hooking used for activation quantization previous_nodes = nncf_graph.get_previous_nodes(nncf_node) target_node = None for prev_node in previous_nodes: prev_edge = nncf_graph.get_nx_edge(prev_node, nncf_node) if prev_edge[NNCFGraph.INPUT_PORT_ID_EDGE_ATTR] == input_port_id: target_node = prev_node break assert target_node is not None, "Could not find a pre-hook quantizer node for a specific " \ "input port!" target_nncf_node_id = target_node.node_id target_nncf_node_key = nncf_graph.get_node_key_by_id(target_nncf_node_id) activation_fq_node = self._nx_graph.nodes[target_nncf_node_key] bitwidth = quantizer_info.quantizer_module_ref.num_bits activation_fq_node['color'] = bitwidth_color_map[bitwidth] activation_fq_node['style'] = 'filled' node_id = activation_fq_node[NNCFGraph.ID_NODE_ATTR] activation_fq_node['label'] = 'AFQ_[{}]_#{}'.format( quantizer_info.quantizer_module_ref.get_quantizer_config(), str(node_id)) grouped_mode = bool(groups_of_adjacent_quantizers) if grouped_mode: group_id_str = 'UNDEFINED' group_id = groups_of_adjacent_quantizers.get_group_id_for_quantizer(quantizer_id) if node_id is None: nncf_logger.error('No group for activation quantizer: {}'.format(target_nncf_node_key)) else: group_id_str = str(group_id) activation_fq_node['label'] += "_G" + group_id_str
def _get_multiforward_nodes(self, graph: NNCFGraph) -> List[List[NNCFNode]]: """ Groups nodes based on their `layer_name` property to determine groups of nodes belonging to a single weighted layer object in the model, i.e. the group of operations in the graph that reuse one and the same set of weights, and returns the groups that have more than one element. :return: List of lists of nodes; each list corresponds to a group of nodes united by the common underlying layer object of the original model. """ ret = defaultdict(list) for node in graph.get_nodes_by_types(self._prune_operations_types): ret[node.layer_name].append(node) return [ ret[module_identifier] for module_identifier in ret if len(ret[module_identifier]) > 1 ]
def count_filters_num(graph: NNCFGraph, op_metatypes: List[Type[OperatorMetatype]], output_channels: Dict[NNCFNodeName, int] = None) -> int: """ Counts filters of `op_metatypes` layers taking into account new output channels number. :param graph: Graph to work with. :param op_metatypes: List of metatypes defining convolution operations. :param output_channels: A dictionary of output channels number in pruned model. :return: Current number of filters according to given graph and output channels. """ filters_num = 0 output_channels = output_channels or {} for node in graph.get_nodes_by_metatypes(op_metatypes): filters_num += output_channels.get(node.node_name, node.layer_attributes.out_channels) return filters_num
def get_conv_in_out_channels(graph: NNCFGraph): """ Collects the number of input and output channels for each convolution in the graph. :param graph: NNCFGraph :return Dictionary with the number of input channels to convolution layers: {node_name: input_channels_num} Dictionary with the number of output channels from convolution layers: {node_name: output_channels_num} """ in_channels, out_channels = {}, {} for node in graph.get_all_nodes(): if isinstance(node.layer_attributes, ConvolutionLayerAttributes): name = node.node_name if name in in_channels and name in out_channels: continue in_channels[name] = node.layer_attributes.in_channels out_channels[name] = node.layer_attributes.out_channels return in_channels, out_channels
def _calculate_output_shape(self, graph: NNCFGraph, node: NNCFNode) -> Tuple[int, ...]: """ Calculates output shape of convolution layer by input edge. :param graph: the model graph :param node: node from NNCF graph :return: output shape """ in_edge = graph.get_input_edges(node)[0] shape = list(in_edge.tensor_shape)[2:] attrs = node.layer_attributes assert isinstance(attrs, ConvolutionLayerAttributes) for i, _ in enumerate(shape): if attrs.transpose: shape[i] = (shape[i] - 1) * attrs.stride[ i] - 2 * attrs.padding_values[i] + attrs.kernel_size[i] else: shape[i] = (shape[i] + 2 * attrs.padding_values[i] - attrs.kernel_size[i]) // attrs.stride[i] + 1 return tuple(shape)
def get_weight_node_name(graph: NNCFGraph, node_name: NNCFNodeName) -> NNCFNodeName: node = graph.get_node_by_name(node_name) while list(graph.get_previous_nodes(node)): node = list(graph.get_previous_nodes(node))[-1] return node.node_name
def convert(self) -> NNCFGraph: nncf_graph = NNCFGraph() producer_layer_id = None model_config = self._model.get_config() layer_name = None for layer_config in model_config['layers']: layer_name = layer_config['config']['name'] if layer_name in self._custom_layer_infos: nncf_graph = self._add_custom_layer_subgraph( nncf_graph, layer_name) continue layer_type = self._get_layer_type(layer_config) layer_dtype = self._get_layer_dtype(layer_config) data_format = layer_config['config'].get('data_format') model_layer = self._get_layer(layer_name) layer_metatype = get_keras_layer_metatype(model_layer) attrs = dict(type=layer_type, dtype=layer_dtype, metatype=layer_metatype, data_format=data_format, in_ports=[0], out_ports=[0], is_shared=False) layer_attributes = None if layer_metatype in DEPTHWISE_CONV_LAYER_METATYPES: layer_attributes = _get_conv_layer_attributes( self._get_layer(layer_name), is_depthwise=True) elif layer_metatype in GENERAL_CONV_LAYER_METATYPES: layer_attributes = _get_conv_layer_attributes( self._get_layer(layer_name), is_depthwise=False) elif layer_metatype in LAYER_METATYPES_AGNOSTIC_TO_DATA_PRECISION_WITH_MULTIPLE_INPUTS: layer_attributes = _get_multiple_input_layer_attributes( model_layer) elif layer_metatype in RESHAPE_METATYPES: layer_attributes = _get_reshape_layer_attributes(model_layer) if layer_attributes is not None: attrs.update({NNCFGraph.LAYER_ATTRIBUTES: layer_attributes}) node_name = layer_name nncf_node = nncf_graph.add_nncf_node( node_name, node_type=layer_type, node_metatype=layer_metatype, layer_attributes=layer_attributes, layer_name=layer_name, is_shared=False) input_shapes = self._prepare_shape(model_layer.input_shape) output_shapes = self._prepare_shape(model_layer.output_shape) self._node_info[node_name] = { 'layer_name': layer_name, 'target_node_name': layer_name, 'inbound_node_idx': None, 'input_shapes': input_shapes, 'output_shapes': output_shapes, } if producer_layer_id is not None: input_shapes = self._prepare_shape( self._model.get_layer(layer_name).input_shape) nncf_graph.add_edge_between_nncf_nodes( producer_layer_id, nncf_node.node_id, tensor_shape=input_shapes[0], input_port_id=0, output_port_id=0, dtype=Dtype.FLOAT ) # TODO(vshampor): determine from keras layers producer_layer_id = nncf_node.node_id if layer_name is not None: last_producer_layer_name = layer_name last_producer_layer_id = producer_layer_id output_model_layer = self._model.get_layer( last_producer_layer_name) output_aux_node_name = PREFIX_AUXILIARY_OUTPUT_NODE + '_0' output_node = nncf_graph.add_nncf_node( node_name=output_aux_node_name, node_type=NNCFGraphNodeType.OUTPUT_NODE, node_metatype=OutputNoopMetatype) nncf_graph.add_edge_between_nncf_nodes( last_producer_layer_id, output_node.node_id, tensor_shape=self._prepare_shape( output_model_layer.output_shape), input_port_id=0, output_port_id=0, dtype=Dtype.FLOAT) return nncf_graph
def convert(self) -> NNCFGraph: nncf_graph = NNCFGraph() node_name_vs_nncf_node_ids = {} # type: Dict[str, int] output_node_id_vs_model_output_idx = {} # type: Dict[int, int] # Regular nodes for node_name, node_info in self._node_info.items(): layer_name = node_info['layer_name'] node_name_vs_nncf_node_ids[layer_name] = [] layer_info = self._layer_info[layer_name] metatype = layer_info['metatype'] layer = self._get_layer(layer_name) if metatype in DEPTHWISE_CONV_LAYER_METATYPES: layer_attributes = _get_conv_layer_attributes( self._get_layer(layer_name), is_depthwise=True) elif metatype in GENERAL_CONV_LAYER_METATYPES: layer_attributes = _get_conv_layer_attributes( self._get_layer(layer_name), is_depthwise=False) elif metatype in LAYER_METATYPES_AGNOSTIC_TO_DATA_PRECISION_WITH_MULTIPLE_INPUTS: layer_attributes = _get_multiple_input_layer_attributes(layer) elif metatype in RESHAPE_METATYPES: layer_attributes = _get_reshape_layer_attributes(layer) else: layer_attributes = None is_shared = len(self._layer_name_to_node_names[layer_name]) > 1 nncf_node = nncf_graph.add_nncf_node( node_name=node_name, node_type=layer_info['type'], node_metatype=metatype, layer_attributes=layer_attributes, layer_name=layer_name, is_shared=is_shared) node_name_vs_nncf_node_ids[node_name] = nncf_node.node_id #pylint:disable=protected-access if layer in self._model._output_layers: output_idx = self._model._output_layers.index(layer) output_node_id_vs_model_output_idx[ nncf_node.node_id] = output_idx # Regular edges for edge, edge_info in self._edge_info.items(): from_node_name, to_node_name = edge from_node_id = node_name_vs_nncf_node_ids[from_node_name] to_node_id = node_name_vs_nncf_node_ids[to_node_name] nncf_graph.add_edge_between_nncf_nodes( from_node_id, to_node_id, tensor_shape=edge_info['tensor_shape'], input_port_id=edge_info['to_node_input_port_id'], output_port_id=edge_info['from_node_output_port_id'], dtype=edge_info['dtype']) # Custom nodes and edges for custom_layer_name in self._custom_layer_infos: nncf_graph = self._add_custom_layer_subgraph( nncf_graph, custom_layer_name) # TODO (vshampor): connect the subgraphs with the rest of the graph for output_node_id, model_output_idx in output_node_id_vs_model_output_idx.items( ): # Ticket: 56853 # We won't add an NNCF output auxiliary node for all of the NNCF nodes corresponding to real # model output, but only for the nodes that do not serve as a tensor source for any other node. # The reason is that current TF capabilities do not allow to insert post-hooks after TF functional model # output nodes without changing the name of the corresponding output, which won't be obvious to the user. nncf_node = nncf_graph.get_node_by_id(output_node_id) if not nncf_graph.get_next_nodes(nncf_node): output_aux_node_name = f'{nncf_node.node_name}_{PREFIX_AUXILIARY_OUTPUT_NODE}_{model_output_idx}' output_node = nncf_graph.add_nncf_node( node_name=output_aux_node_name, node_type=NNCFGraphNodeType.OUTPUT_NODE, node_metatype=OutputNoopMetatype) node_info = self._node_info[ nncf_node. node_name] # works if _node_info keys are identical to node_names nncf_graph.add_edge_between_nncf_nodes( nncf_node.node_id, output_node.node_id, tensor_shape=node_info['output_shapes'][0], input_port_id=0, output_port_id=0, dtype=Dtype.FLOAT) return nncf_graph
def create_pruning_groups(self, graph: NNCFGraph) -> Clusterization[NNCFNode]: """ This function groups ALL nodes with pruning types to groups that should be pruned together. 1. Create clusters for special ops (eltwises) that should be pruned together 2. Create groups of nodes that should be pruned together (taking into account clusters of special ops) 3. Add remaining single nodes 4. Unite clusters for Conv + Depthwise conv (should be pruned together too) 5. Checks for groups (all nodes in group can prune or all group can't be pruned) Return groups of modules that should be pruned together. :param graph: Graph to work with and their initialization parameters as values. :return: Clusterization of pruned nodes. """ # pylint:disable=too-many-branches all_nodes_to_prune = graph.get_nodes_by_types( self._prune_operations_types) # NNCFNodes here # 1. Clusters for special ops identity_like_types = self._identity_mask_propagation_op_metatype.get_all_op_aliases( ) special_ops_clusterization = cluster_special_ops( graph, self._grouping_operations_types, identity_like_types) pruned_nodes_clusterization = Clusterization[NNCFNode]( lambda x: x.node_id) # 2. Clusters for nodes that should be pruned together (taking into account clusters for special ops) for i, cluster in enumerate( special_ops_clusterization.get_all_clusters()): all_pruned_inputs = {} clusters_to_merge = [] for node in cluster.elements: sources = get_sources_of_node(node, graph, self._prune_operations_types) for source_node in sources: if pruned_nodes_clusterization.is_node_in_clusterization( source_node.node_id): # Merge clusters if some node already added in another cluster cluster = pruned_nodes_clusterization.get_cluster_containing_element( source_node.node_id) clusters_to_merge.append(cluster.id) elif source_node.node_id not in all_pruned_inputs: all_pruned_inputs[source_node.node_id] = source_node if all_pruned_inputs: cluster = Cluster[NNCFNode](i, all_pruned_inputs.values(), all_pruned_inputs.keys()) clusters_to_merge.append(cluster.id) pruned_nodes_clusterization.add_cluster(cluster) # Merge clusters if one source node in several special ops clusters pruned_nodes_clusterization.merge_list_of_clusters( clusters_to_merge) last_cluster_idx = len(special_ops_clusterization.get_all_clusters()) # 3. Add remaining single nodes as separate clusters for node in all_nodes_to_prune: if not pruned_nodes_clusterization.is_node_in_clusterization( node.node_id): cluster = Cluster[NNCFNode](last_cluster_idx, [node], [node.node_id]) pruned_nodes_clusterization.add_cluster(cluster) last_cluster_idx += 1 stop_propagation_ops = self._stop_propagation_op_metatype.get_all_op_aliases( ) # 4. Merge clusters for Conv + Depthwise conv (should be pruned together too) for node in all_nodes_to_prune: cluster_id = pruned_nodes_clusterization.get_cluster_containing_element( node.node_id).id if is_prunable_depthwise_conv(node): previous_convs = get_previous_convs( graph, node, self._prune_operations_types, stop_propagation_ops) previous_clusters = [ pruned_nodes_clusterization.get_cluster_containing_element( node.node_id).id for node in previous_convs ] pruned_nodes_clusterization.merge_list_of_clusters( [cluster_id] + previous_clusters) # 5. Merge nodes into one cluster if some module forwards several times multiforward_nodes = self._get_multiforward_nodes(graph) for list_of_nodes in multiforward_nodes: clusters_to_merge = [ pruned_nodes_clusterization.get_cluster_containing_element( node.node_id).id for node in list_of_nodes ] pruned_nodes_clusterization.merge_list_of_clusters( clusters_to_merge) # Merge previous convolutions into one cluster all_previous_convs = [] for node in list_of_nodes: nncf_node = graph.get_node_by_id(node.node_id) previous_convs = get_previous_convs( graph, nncf_node, self._prune_operations_types, stop_propagation_ops) # Check if previous node isn't multiforward, # in case of multiforward nodes cycle for previous_conv in previous_convs: if previous_conv not in list_of_nodes: all_previous_convs.append(previous_conv) previous_clusters = [ pruned_nodes_clusterization.get_cluster_containing_element( node.node_id).id for node in all_previous_convs ] pruned_nodes_clusterization.merge_list_of_clusters( previous_clusters) # 6. Checks for groups (all nodes in group can be pruned or all group can't be pruned). model_analyser = ModelAnalyzer(graph, self._pruning_operator_metatypes, is_prunable_depthwise_conv) can_prune_analysis = model_analyser.analyse_model_before_pruning() can_prune_and_should_prune_analysis = self._should_prune_groups_analysis( graph, pruned_nodes_clusterization, can_prune_analysis) can_prune_final_analysis = self._pruning_dimensions_analysis( graph, can_prune_and_should_prune_analysis) self._filter_groups(pruned_nodes_clusterization, can_prune_final_analysis) return pruned_nodes_clusterization