def create_op_to_quant_ops_dict(graph: tf.Graph, conn_graph: ConnectedGraph, ops_with_param_names: List[str], indices: List[int], activation_op_names: List[str]) -> OpToQuantOpsDictType: """ Create an op to quant ops dictionary mapping connected graph ops to a list consisting of the activation quantizer and a dictionary mapping param type string to param quantizers. :param graph: Tensorflow graph containing inserted quantizers :param conn_graph: Connected graph of the original unquantized model :param ops_with_param_names: List of tf operation names for which parameter quantizers were inserted for :param indices: Indices of tf operations of which parameter quantizers were inserted for :param activation_op_names: List of tf operation names for which activation quantizers were inserted for :return: Dictionary mapping connected graph ops to a list consisting of the activation quantizer and a dictionary mapping param type string to param quantizers. """ op_to_quant_ops_dict = {} for op_with_param_name, index in zip(ops_with_param_names, indices): op_with_param = graph.get_operation_by_name(op_with_param_name) conn_graph_op = conn_graph.get_op_from_module_name(op_with_param_name) param_type = 'weight' if op_with_param.type == 'BiasAdd': param_type = 'bias' param_quantizer = get_param_quantizer(op_with_param, index) assert param_quantizer.type in ['QcQuantize', 'QcQuantizeRecurrentParam'] add_op_to_quant_ops_dict_entry(param_quantizer, conn_graph_op, True, param_type, op_to_quant_ops_dict) for activation_op_name in activation_op_names: activation_op = graph.get_operation_by_name(activation_op_name) conn_graph_op = conn_graph.get_op_from_module_name(activation_op_name) activation_quantizer = \ [consumer for consumer in activation_op.outputs[0].consumers() if consumer.type == 'QcQuantize'] if len(activation_quantizer) != 1: _logger.error('Expected one activation quantizer but found %s', len(activation_quantizer)) raise AssertionError add_op_to_quant_ops_dict_entry(activation_quantizer[0], conn_graph_op, False, '', op_to_quant_ops_dict) return op_to_quant_ops_dict
def _mark_outputs_as_train_op(graph: tf.Graph, signature_def: SignatureDef) -> None: """Mark output nodes as training ops, so the optimizer ignores them""" train_op = GraphKeys.TRAIN_OP for _, tensor in signature_def.outputs.items(): name = _to_node_name(tensor.name) graph.add_to_collection(train_op, graph.get_operation_by_name(name))
def create_op_type_patterns_from_subgraph(subgraph: tf.Graph, additional_starting_ops: List[str]) -> \ List[graph_matcher.OpTypePattern]: """ Create and return a list of TensorFlow OpTypePattern objects for the given subgraph. The OpTypepatterns() are created in sequence from the input to the output of the subgraph. The last OpTypepattern() object in the returned list is for the Op under consideration. :param subgraph: The subgraph of an Op for which OpTypePattern is created. :param additional_starting_ops: Additional starting points for identifying valid ops to match with. Valid ops are defined as ops which can be traversed with both a dfs any input op as well as dfs backwards from any output op. Additional starting ops can be used when simply using default input and output ops gives a pattern easily matched by individual ops that are not actually of the desired matched type (BN_non_fused_keras_with_training_False would be matched with only a mul -> add, for example) :return: List of OpTypePattern() """ starting_op_names = ['aimet_input', 'aimet_constant', 'is_training' ] + additional_starting_ops ending_op_names = ['aimet_identity'] ops_from_ending_ops = set() op_list = [] valid_ops = get_valid_ops(subgraph, starting_op_names=starting_op_names, ending_op_names=ending_op_names) # DFS is done bottom up. # Reason: # If we do top down DFS, it becomes necessary to indicate a starting Op other than well known 'aimet_input' # For a Conv2D, for top down DFS, if only 'aimet_input' is given as starting Op for DFS, the kernel # input sub-graph for the Conv2D is missed. # This is not an issue for bottom up DFS since bottom up DFS looks at all inputs. # For building OpTypePattern() sequence, the dependent OpTypePattern() must be build first before using that # OpTypePattern() as an input in the next OpTypePattern() def dfs_upwards(curr_op): """ Function to perform DFS upwards starting at curr_op """ if curr_op in ops_from_ending_ops or curr_op.name in starting_op_names: # Do not process curr_op if we have seen it before, or if it is one of the starting ops return ops_from_ending_ops.add(curr_op) # List to hold inputs to curr_op input_ops = [] for inp in curr_op.inputs: input_ops.append(inp.op) dfs_upwards(inp.op) if curr_op.name not in ending_op_names and curr_op in valid_ops: op_list.append(curr_op) for name in ending_op_names: op = subgraph.get_operation_by_name(name) dfs_upwards(op) sub_patterns = get_op_type_patterns(op_list) return sub_patterns
def get_valid_ops(graph: tf.Graph, starting_op_names: List[str], ending_op_names: List[str]) -> Set[tf.Operation]: """ Get a set of valid ops. Valid ops are ops which can be reached both by a DFS from a starting op, as well as upward DFS from an ending op. If no ending ops are given, all ops reachable by DFS from starting ops are considered valid ops. DFS search both ways is needed since training ops will be seen by top down DFS but not bottom up, while parameters like weights and biases will be seen by bottom up search but not top down. Taking the intersection allows us to leave these ops out. :param graph: Graph to search for valid ops :param starting_op_names: Ops to start top down DFS search from :param ending_op_names: Ending ops to start bottom up DFS search from """ ops_from_starting_ops = set() ops_from_ending_ops = set() # For each starting op, do a DFS and add all child ops to ops_from_starting_ops set for name in starting_op_names: op = graph.get_operation_by_name(name) queue = [op] while queue: curr_op = queue.pop() ops_from_starting_ops.add(curr_op) for output in curr_op.outputs: for consumer in output.consumers(): if consumer not in ops_from_starting_ops: queue.append(consumer) # For each ending op, do a DFS upwards and add all parent ops to ops_from_ending_ops set for name in ending_op_names: op = graph.get_operation_by_name(name) queue = [op] while queue: curr_op = queue.pop() ops_from_ending_ops.add(curr_op) for inp in curr_op.inputs: if inp.op not in ops_from_ending_ops: queue.append(inp.op) # Only ops in the intersection of starting ops and ending ops sets are valid return ops_from_starting_ops.intersection(ops_from_ending_ops)
def get_op_input_indices(graph: tf.Graph, ops_with_param_names: List) -> List[int]: """ Get input indices of ops :param graph: Tensorflow graph as tf.Graph :param ops_with_param_names: List of op names with params to insert quantize ops for :return: list of indices of parameters for each op """ query = core.OpQuery(graph, ops_to_ignore=None) ops_with_params = [graph.get_operation_by_name(op_name) for op_name in ops_with_param_names] input_indices = query.get_weight_inputs(ops_with_params) if len(ops_with_param_names) != len(input_indices): _logger.error("Length of ops with params and input indices differ") raise AssertionError return input_indices
def get_ordered_ops(graph: tf.Graph, starting_op_names: List[str], output_op_names: List[str]) -> List[tf.Operation]: """ Function to get all the ops in graph based on occurrence by Depth First Traversal :param graph: tf.Graph :param starting_op_names: List of starting op names :param output_op_names: List of output op names of the model, used to help determine valid ops :return: ordered_ops: List of ops in order of occurrence """ def add_children_ops_before_parent_op(current_op: tf.Operation): """ Util function to add all the children ops in ordered_ops list before parent op using Depth First Traversal :param current_op: tf.Operation """ # Add current op to visited_ops set visited_ops.add(current_op) # iterate all the output tensors of current op for output_tensor in current_op.outputs: # iterate all the consumer ops of output tensor for consumer_op in output_tensor.consumers(): # add consumer op to visited_ops list if not added previously and recursively call if consumer_op not in visited_ops: add_children_ops_before_parent_op(consumer_op) # add to ordered_ops list only when all the children ops are traversed ordered_ops.append(current_op) # get set of valid operations in TF graph valid_ops = get_valid_ops(graph, starting_op_names, output_op_names) # Set of all ops that have been visited (to cut short duplicate traversals) visited_ops = set() # List of all ops in order of occurrence ordered_ops = [] for starting_op_name in starting_op_names: starting_op = graph.get_operation_by_name(starting_op_name) add_children_ops_before_parent_op(starting_op) # reverse the list because ops are in reverse order ordered_ops.reverse() # filter ordered ops for only valid ops ordered_ops = [op for op in ordered_ops if op in valid_ops] return ordered_ops
def _parse_graph_info(graph_def): """Parse GraphDef Fetch input tensors and output tensors name for reconstructing graph in uTensor Context object Argument ======== - graph_def <tf.GraphDef>: a GraphDef object Return ====== - graph_nodes <defaultdict>: a dict with key as operation name and value as a defaultdict with keys 'input_tensor' and 'output_tensor' which maps to a set of input/output tensor names respectively Note ==== - thought the output tensor names is irrelevent for TensorFlow, but it is neccessary for uTensor """ OperationInfo = namedtuple('OperationInfo', field_names=[ 'input_tensor', 'output_tensor', 'op_type', 'output_content', 'op_attr' ]) graph = Graph() with graph.as_default(): # pylint: disable=E1129 import_graph_def(graph_def, name="") graph_info = {} with Session(graph=graph): for node in graph_def.node: op = graph.get_operation_by_name(node.name) input_tensor = [(t.name, t.dtype, _parse_shape(t.shape)) for t in op.inputs] output_tensor = [(t.name, t.dtype, _parse_shape(t.shape)) for t in op.outputs] op_type = node.op output_content = {} op_attr = node.attr if node.op in ["Const"]: for tensor_name, _, _ in output_tensor: output_content[tensor_name] = make_ndarray( node.attr['value'].tensor) graph_info[node.name] = OperationInfo(input_tensor, output_tensor, op_type, output_content, op_attr) return graph_info
def _build_signature_def(graph: tf.Graph, input_nodes: list, output_nodes: list) -> SignatureDef: """Build model signature (input- and output descriptions) for a graph""" signature_def = SignatureDef() def add_tensor(nodes, info): nodes[info.name].name = info.name if info.dtype is not None: dtype = dtypes.as_dtype(info.dtype) shape = tf.TensorShape(info.shape) nodes[info.name].dtype = dtype.as_datatype_enum nodes[info.name].tensor_shape.CopyFrom(shape.as_proto()) for input_info in input_nodes: op = graph.get_operation_by_name(input_info.name) if op.type != c.TFJS_NODE_CONST_KEY: add_tensor(signature_def.inputs, input_info) for output_info in output_nodes: add_tensor(signature_def.outputs, output_info) return signature_def
def _parse_graph_nodes(graph_def: GraphDef) -> defaultdict: """Parse GraphDef Fetch input tensors and output tensors name for reconstructing graph in uTensor Context object Argument ======== - graph_def <tf.GraphDef>: a GraphDef object Return ====== - graph_nodes <defaultdict>: a dict with key as operation name and value as a defaultdict with keys 'input_tensor' and 'output_tensor' which maps to a set of input/output tensor names respectively Note ==== - thought the output tensor names is irrelevent for TensorFlow, but it is neccessary for uTensor """ graph = Graph() with graph.as_default(): # pylint: disable=E1129 import_graph_def(graph_def, name="") graph_info = defaultdict(lambda: {"output_content": {}}) with Session(graph=graph): for node in graph_def.node: op = graph.get_operation_by_name(node.name) op_info = graph_info[node.name] op_info["input_tensor"] = [(t.name, t.dtype, _parse_shape(t.shape)) for t in op.inputs] op_info["output_tensor"] = [ (t.name, t.dtype, _parse_shape(t.shape)) for t in op.outputs ] op_info["op_type"] = node.op if node.op in ["Const"]: for out_tensor, _, _ in op_info["output_tensor"]: tensor = graph.get_tensor_by_name(out_tensor) op_info["output_content"][tensor.name] = tensor.eval() return graph_info
def _create_placeholder_node_from_existing_node( node: tf.compat.v1.NodeDef, graph: tf.Graph) -> tf.compat.v1.NodeDef: """Creates a placeholder node to represent an existing node. Some partitioned subgraphs may require inputs that are loaded or computed previously. Hence, we replace the input nodes with placeholder nodes that share the same name, shape, and dtype. Now the inputs become placeholders inside partitioned subgraphs, and can be loaded by feed dicts at the runtime. Args: node: A `NodeDef` proto for the existing node. graph: A tf.Graph instance for the graph that contains the existing node. Returns: A `NodeDef` proto that stores a placeholder node. """ operation = graph.get_operation_by_name('import/%s' % (node.name)) output_tensor = operation.outputs[0] with tf.compat.v1.Session(graph=tf.Graph()) as sess: tf.compat.v1.placeholder( dtype=output_tensor.dtype, shape=output_tensor.shape, name=node.name) return sess.graph_def.node[0]
def from_dict(graph: tf.Graph, dictionary: dict) -> 'TrainSpec': return TrainSpec(loss=graph.get_tensor_by_name(dictionary['loss']), train_op=graph.get_operation_by_name( dictionary['train_op']), create_viz_op=False)
def children(op_name: str, graph: tf.Graph): op = graph.get_operation_by_name(op_name) return set(op for out in op.outputs for op in out.consumers())
def get_optimization_op(cls, graph: tf.Graph) -> tf.Operation: return graph.get_operation_by_name(_Labels.OPTIMIZATION_OP_LABEL)
def attach(self, tf_graph: tf.Graph) -> 'GlobalInitializer': return GlobalInitializer( tf_graph.get_operation_by_name(self.__op_name))