Esempio n. 1
0
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
Esempio n. 2
0
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))
Esempio n. 3
0
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
Esempio n. 4
0
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)
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
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
Esempio n. 8
0
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
Esempio n. 9
0
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
Esempio n. 10
0
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]
Esempio n. 11
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)
Esempio n. 12
0
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())
Esempio n. 13
0
 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))