def convert(self, logical_plan: LogicalPlan) -> None:
     nodes_to_skip = set()
     while True:  # repeat until the logical_graph converges
         input_nodes = logical_plan.logical_graph.get_nodes_by_type("_inputs")
         # _PseudoOperation(type_name="_inputs"))
         root_node = None
         for node in input_nodes:
             if node in nodes_to_skip:
                 continue
             root_node = node
             break
         if root_node == None:
             break  # end of convert
         else:
             nodes_to_dedup = []
             for node in input_nodes:
                 if node in nodes_to_skip:
                     continue
                 if self._check_deduplicate_by_node(root_node, node):
                     nodes_to_dedup.append(node)
             assert(len(nodes_to_dedup) >= 1)
             if len(nodes_to_dedup) == 1:
                 assert(nodes_to_dedup[0] == root_node)
                 nodes_to_skip.add(root_node)
             else:
                 dedup_node = DedupInputNode(logical_plan.logical_graph, uid(), nodes_to_dedup)._register()
                 for edge in logical_plan.logical_graph.edges:
                     if edge.head in nodes_to_dedup:
                         edge.head = dedup_node
                     if edge.tail in nodes_to_dedup:
                         edge.tail = dedup_node
                 for node in nodes_to_dedup:
                     node.remove()
示例#2
0
def extract_mutation_from_pt_module(
        pytorch_model: nn.Module) -> Tuple[Model, Optional[List[Mutator]]]:
    model = Model(_internal=True)
    graph = Graph(model, uid(), '_model', _internal=True)._register()
    model.python_class = pytorch_model.__class__
    if len(inspect.signature(model.python_class.__init__).parameters) > 1:
        if not is_model_wrapped(pytorch_model):
            raise ValueError(
                'Please annotate the model with @model_wrapper decorator in python execution mode '
                'if your model has init parameters.')
        model.python_init_params = cast(dict, pytorch_model.trace_kwargs)
    else:
        model.python_init_params = {}

    # hyper-parameter choice
    namespace: ModelNamespace = cast(ModelNamespace,
                                     pytorch_model._model_namespace)
    for param_spec in namespace.parameter_specs:
        assert param_spec.categorical and param_spec.type == 'choice'
        node = graph.add_node(f'param_spec_{param_spec.name}',
                              'ModelParameterChoice',
                              {'candidates': param_spec.values})
        node.label = param_spec.name

    for name, module in pytorch_model.named_modules():
        # tricky case: value choice that serves as parameters are stored in traced arguments
        if is_basic_unit(module):
            trace_kwargs = cast(Dict[str, Any], module.trace_kwargs)
            for key, value in trace_kwargs.items():
                if isinstance(value, ValueChoiceX):
                    for i, choice in enumerate(value.inner_choices()):
                        node = graph.add_node(
                            f'{name}.init.{key}.{i}', 'ValueChoice',
                            {'candidates': choice.candidates})
                        node.label = choice.label

        if isinstance(module, (LayerChoice, InputChoice, ValueChoice)):
            # TODO: check the label of module and warn if it's auto-generated
            pass
        if isinstance(module, LayerChoice):
            node = graph.add_node(name, 'LayerChoice',
                                  {'candidates': module.names})
            node.label = module.label
        if isinstance(module, InputChoice):
            node = graph.add_node(name, 'InputChoice', {
                'n_candidates': module.n_candidates,
                'n_chosen': module.n_chosen
            })
            node.label = module.label
        if isinstance(module, ValueChoiceX):
            for i, choice in enumerate(module.inner_choices()):
                node = graph.add_node(f'{name}.{i}', 'ValueChoice',
                                      {'candidates': choice.candidates})
                node.label = choice.label
        if isinstance(module, NasBench101Cell):
            node = graph.add_node(name, 'NasBench101Cell',
                                  {'max_num_edges': module.max_num_edges})
            node.label = module.label
        if isinstance(module, Placeholder):
            raise NotImplementedError(
                'Placeholder is not supported in python execution mode.')

    model.status = ModelStatus.Frozen
    if not graph.hidden_nodes:
        return model, None

    mutators = []
    mutators_final = []
    for nodes in _group_by_label_and_type(graph.hidden_nodes):
        label = nodes[0].label
        assert label is not None, f'label of {nodes[0]} can not be None.'
        assert _is_all_equal(map(lambda n: n.operation.type, nodes)), \
            f'Node with label "{label}" does not all have the same type.'
        assert _is_all_equal(map(lambda n: n.operation.parameters, nodes)), \
            f'Node with label "{label}" does not agree on parameters.'
        if nodes[0].operation.type == 'NasBench101Cell':
            # The mutation of Nas-bench-101 is special, and has to be done lastly.
            mutators_final.append(NasBench101Mutator(label))
        else:
            mutators.append(ManyChooseManyMutator(label))
    return model, mutators + mutators_final
示例#3
0
    def assemble(self, multi_model_placement: Dict[Model, Device]) \
            -> Tuple[Model, Dict[Node, Device]]:
        """
        Given a set of models to be formed in a physical model and their device placement,
        this function replaces all the logical node in this LogicalPlan with executable physical nodes
        for the physical model.

        Parameters
        ----------
        multi_model_placement : dict
            a dict of models and device placement.
            These models will be assembled into the same physical model to run.

        Returns
        -------
        phy_model : Model
            the physical model formed by models in `multi_model_placement`
            all logical node are replaced by physical nodes
        node_placements : dict
            the device placement of the nodes in `phy_model`
        """
        phy_model = Model(_internal=True)
        phy_graph = self.lp_model.root_graph._fork_to(phy_model)
        phy_graph._rename_graph(phy_graph.name, "_model")

        # merge sub-graphs
        for model in multi_model_placement:
            if phy_model.evaluator is None and model.evaluator is not None:
                phy_model.evaluator = model.evaluator
            for graph_name in model.graphs:
                if graph_name != model._root_graph_name:
                    new_graph = model.graphs[graph_name]._fork_to(
                        phy_model, name_prefix=f'M_{model.model_id}_')

                    # prefix of M_ of hidden_nodes name in non-root graphs is added here
                    for new_node in new_graph.hidden_nodes:
                        if isinstance(new_node.operation, Cell):
                            old_cell_name = new_node.operation.cell_name
                            new_node.operation = copy.deepcopy(
                                new_node.operation)
                            new_node.operation.cell_name = f'M_{model.model_id}_{old_cell_name}'

        assert (phy_model.evaluator is not None)

        # When replace logical nodes, merge the training configs when
        # input/output nodes are replaced.
        evaluator_slot = {}  # Model ID -> Slot ID
        input_slot_mapping = {}
        output_slot_mapping = {}
        # Replace all logical nodes to executable physical nodes
        hidden_nodes = phy_graph.hidden_nodes.copy()
        node_placements = {}

        added_models = []

        for node in hidden_nodes:
            if isinstance(node, OriginNode):
                model_id = node.original_graph.model.model_id
                if node.original_graph.model not in multi_model_placement:
                    for edge in node.incoming_edges:
                        edge.remove()
                    for edge in node.outgoing_edges:
                        edge.remove()
                    node.remove()
                    continue

            if isinstance(node, AbstractLogicalNode):
                new_node, placement = node.assemble(multi_model_placement)
                if isinstance(new_node.operation, _IOPseudoOperation):
                    model_id = new_node.graph.model.model_id
                    if model_id not in evaluator_slot:
                        added_models.append(model_id)
                        evaluator_slot[model_id] = len(added_models) - 1
                        slot = evaluator_slot[model_id]
                    else:
                        slot = evaluator_slot[model_id]
                    # If a model's inputs/outputs are not used in the multi-model
                    # the codegen and trainer should not generate and use them
                    # "use_input" and "use_output" are used to mark whether
                    # an input/output of a model is used in a multi-model
                    if new_node.operation.type == '_inputs':
                        input_slot_mapping[new_node] = slot
                    if new_node.operation.type == '_outputs':
                        output_slot_mapping[new_node] = slot

                self.node_replace(node, new_node)

                # name prefix of M_ of cells in hidden_nodes of root graphs is added here
                # FIXME: merge this rename with non-root graph, only do once.
                if isinstance(new_node.operation, Cell):
                    old_cell_name = new_node.operation.cell_name
                    new_node.operation = copy.deepcopy(new_node.operation)
                    new_node.operation.cell_name = f'M_{model_id}_{old_cell_name}'

                # input should be at CPU, move it to GPU first if necessary
                if isinstance(new_node.operation, _IOPseudoOperation
                              ) and new_node.operation.type == '_inputs':
                    # hack: only support single_server
                    node_placements[new_node] = CPUDevice(
                        node_id=placement.node_id)
                else:
                    node_placements[new_node] = placement

                node.remove()

        # If two nodes are placed on different devices, use ToDevice op to copy the node
        # TODO: when copying one node to multiple devices, broadcast is more efficient than P2P communication
        existing_edges = phy_graph.edges.copy()
        # Avoid a node is copied multiple times on the same device
        copied_op: Dict[Tuple(Node, Device), Node] = {}
        for edge in existing_edges:
            head_placement = node_placements[edge.head]
            tail_placement = node_placements[edge.tail]
            if head_placement != tail_placement:
                if head_placement.node_id != tail_placement.node_id:
                    raise ValueError(
                        'Cross-server placement is not supported.')
                # Same server different devices
                if (edge.head, tail_placement) in copied_op:
                    to_node = copied_op[(edge.head, tail_placement)]
                else:
                    dst_name = edge.head.name + "_to_" + edge.tail.name
                    to_operation = Operation.new(
                        'ToDevice', {
                            "device": tail_placement,
                            "src": (edge.head.name, edge.head_slot),
                            "dst": dst_name
                        })
                    to_node = Node(phy_graph, uid(), dst_name,
                                   to_operation)._register()
                    Edge((edge.head, edge.head_slot), (to_node, None),
                         _internal=True)._register()
                    copied_op[(edge.head, tail_placement)] = to_node
                    node_placements[to_node] = head_placement
                edge.head = to_node
                edge.head_slot = None

        # merge all input nodes into one with multiple slots
        input_nodes = []
        for node in phy_graph.hidden_nodes:
            if isinstance(
                    node.operation,
                    _IOPseudoOperation) and node.operation.type == '_inputs':
                input_nodes.append(node)

        for edge in phy_graph.edges:
            if edge.head in input_nodes:
                edge.head_slot = input_slot_mapping[edge.head]
                edge.head = phy_graph.input_node

        # merge all output nodes into one with multiple slots
        output_nodes = []
        for node in phy_graph.hidden_nodes:
            if isinstance(
                    node.operation,
                    _IOPseudoOperation) and node.operation.type == '_outputs':
                output_nodes.append(node)

        for edge in phy_graph.edges:
            if edge.tail in output_nodes:
                edge.tail_slot = output_slot_mapping[edge.tail]
                edge.tail = phy_graph.output_node

        for node in input_nodes:
            node.remove()
        for node in output_nodes:
            node.remove()

        return phy_model, node_placements
示例#4
0
    def assemble(self, multi_model_placement: Dict[Model, PhysicalDevice]) \
            -> Tuple[Model, Dict[Node, PhysicalDevice], List[Model]]:
        phy_model = Model(_internal=True)  # self.lp_model.fork()
        phy_graph = self.lp_model.root_graph._fork_to(phy_model)

        # Add a flag to mark multi-model in graph json.
        # Multi-model has a list of training configs in kwargs['model_kwargs']
        if len(multi_model_placement) > 1:
            phy_model.training_config.kwargs['is_multi_model'] = True
            phy_model.training_config.kwargs['model_cls'] = phy_graph.name
            phy_model.training_config.kwargs['model_kwargs'] = []
            # FIXME: allow user to specify
            phy_model.training_config.module = 'nni.retiarii.trainer.PyTorchMultiModelTrainer'

        # merge sub-graphs
        for model in multi_model_placement:
            for graph_name in model.graphs:
                if graph_name != model._root_graph_name:
                    model.graphs[graph_name]._fork_to(
                        phy_model, name_prefix=f'M_{model.model_id}_')

        # When replace logical nodes, merge the training configs when
        # input/output nodes are replaced.
        training_config_slot = {}  # Model ID -> Slot ID
        input_slot_mapping = {}
        output_slot_mapping = {}
        # Replace all logical nodes to executable physical nodes
        hidden_nodes = phy_graph.hidden_nodes.copy()
        node_placements = {}
        for node in hidden_nodes:
            if isinstance(node, OriginNode):
                model_id = node.original_graph.model.model_id
                if node.original_graph.model not in multi_model_placement:
                    for edge in node.incoming_edges:
                        edge.remove()
                    for edge in node.outgoing_edges:
                        edge.remove()
                    node.remove()
                    continue

            if isinstance(node, AbstractLogicalNode):
                new_node, placement = node.assemble(multi_model_placement)
                if isinstance(new_node.operation, _IOPseudoOperation):
                    model_id = new_node.graph.model.model_id
                    if model_id not in training_config_slot:
                        phy_model.training_config.kwargs['model_kwargs'].append(
                            new_node.graph.model.training_config.kwargs.copy())
                        training_config_slot[model_id] = len(
                            phy_model.training_config.kwargs['model_kwargs']
                        ) - 1
                        slot = training_config_slot[model_id]
                        phy_model.training_config.kwargs['model_kwargs'][slot][
                            'model_id'] = model_id
                        phy_model.training_config.kwargs['model_kwargs'][slot][
                            'use_input'] = False
                        phy_model.training_config.kwargs['model_kwargs'][slot][
                            'use_output'] = False
                    else:
                        slot = training_config_slot[model_id]
                    # If a model's inputs/outputs are not used in the multi-model
                    # the codegen and trainer should not generate and use them
                    # "use_input" and "use_output" are used to mark whether
                    # an input/output of a model is used in a multi-model
                    if new_node.operation.type == '_inputs':
                        input_slot_mapping[new_node] = slot
                        phy_model.training_config.kwargs['model_kwargs'][slot][
                            'use_input'] = True
                    if new_node.operation.type == '_outputs':
                        output_slot_mapping[new_node] = slot
                        phy_model.training_config.kwargs['model_kwargs'][slot][
                            'use_output'] = True

                self.node_replace(node, new_node)

                if isinstance(new_node.operation, Cell):
                    old_cell_name = new_node.operation.cell_name
                    new_node.operation = copy.deepcopy(new_node.operation)
                    new_node.operation.cell_name = f'M_{model_id}_{old_cell_name}'
                node_placements[new_node] = placement

                node.remove()

        # If two nodes are placed on different devices, use ToDevice op to copy the node
        existing_edges = phy_graph.edges.copy()
        # Avoid a node is copied multiple times on the same device
        copied_op: Dict[Tuple(Node, PhysicalDevice), Node] = {}
        for edge in existing_edges:
            head_placement = node_placements[edge.head]
            tail_placement = node_placements[edge.tail]
            if head_placement != tail_placement:
                if head_placement.server != tail_placement.server:
                    raise ValueError(
                        'Cross-server placement is not supported.')
                # Same server different devices
                if (edge.head, tail_placement) in copied_op:
                    to_node = copied_op[(edge.head, tail_placement)]
                else:
                    to_operation = Operation.new(
                        'ToDevice', {"device": tail_placement.device})
                    to_node = Node(phy_graph, uid(),
                                   edge.head.name + "_to_" + edge.tail.name,
                                   to_operation)._register()
                    Edge((edge.head, edge.head_slot), (to_node, None),
                         _internal=True)._register()
                    copied_op[(edge.head, tail_placement)] = to_node
                edge.head = to_node
                edge.head_slot = None

        # merge all input nodes into one with multiple slots
        input_nodes = []
        for node in phy_graph.hidden_nodes:
            if isinstance(
                    node.operation,
                    _IOPseudoOperation) and node.operation.type == '_inputs':
                input_nodes.append(node)

        for edge in phy_graph.edges:
            if edge.head in input_nodes:
                edge.head_slot = input_slot_mapping[edge.head]
                edge.head = phy_graph.input_node

        # merge all output nodes into one with multiple slots
        output_nodes = []
        for node in phy_graph.hidden_nodes:
            if isinstance(
                    node.operation,
                    _IOPseudoOperation) and node.operation.type == '_outputs':
                output_nodes.append(node)

        for edge in phy_graph.edges:
            if edge.tail in output_nodes:
                edge.tail_slot = output_slot_mapping[edge.tail]
                edge.tail = phy_graph.output_node

        for node in input_nodes:
            node.remove()
        for node in output_nodes:
            node.remove()

        return phy_model, node_placements
示例#5
0
def extract_mutation_from_pt_module(
        pytorch_model: nn.Module) -> Tuple[Model, Optional[List[Mutator]]]:
    model = Model(_internal=True)
    graph = Graph(model, uid(), '_model', _internal=True)._register()
    model.python_class = pytorch_model.__class__
    if len(inspect.signature(model.python_class.__init__).parameters) > 1:
        if not getattr(pytorch_model, '_nni_model_wrapper', False):
            raise ValueError(
                'Please annotate the model with @model_wrapper decorator in python execution mode '
                'if your model has init parameters.')
        model.python_init_params = pytorch_model.trace_kwargs
    else:
        model.python_init_params = {}

    for name, module in pytorch_model.named_modules():
        # tricky case: value choice that serves as parameters are stored in traced arguments
        if is_basic_unit(module):
            for key, value in module.trace_kwargs.items():
                if isinstance(value, ValueChoice):
                    node = graph.add_node(name + '.init.' + key, 'ValueChoice',
                                          {'candidates': value.candidates})
                    node.label = value.label

        if isinstance(module, (LayerChoice, InputChoice, ValueChoice)):
            # TODO: check the label of module and warn if it's auto-generated
            pass
        if isinstance(module, LayerChoice):
            node = graph.add_node(name, 'LayerChoice',
                                  {'candidates': module.names})
            node.label = module.label
        if isinstance(module, InputChoice):
            node = graph.add_node(name, 'InputChoice', {
                'n_candidates': module.n_candidates,
                'n_chosen': module.n_chosen
            })
            node.label = module.label
        if isinstance(module, ValueChoice):
            node = graph.add_node(name, 'ValueChoice',
                                  {'candidates': module.candidates})
            node.label = module.label
        if isinstance(module, Repeat) and module.min_depth <= module.max_depth:
            node = graph.add_node(name, 'Repeat', {
                'candidates':
                list(range(module.min_depth, module.max_depth + 1))
            })
            node.label = module.label
        if isinstance(module, NasBench101Cell):
            node = graph.add_node(name, 'NasBench101Cell',
                                  {'max_num_edges': module.max_num_edges})
            node.label = module.label
        if isinstance(module, Placeholder):
            raise NotImplementedError(
                'Placeholder is not supported in python execution mode.')

    model.status = ModelStatus.Frozen
    if not graph.hidden_nodes:
        return model, None

    mutators = []
    mutators_final = []
    for nodes in _group_by_label_and_type(graph.hidden_nodes):
        assert _is_all_equal(map(lambda n: n.operation.type, nodes)), \
            f'Node with label "{nodes[0].label}" does not all have the same type.'
        assert _is_all_equal(map(lambda n: n.operation.parameters, nodes)), \
            f'Node with label "{nodes[0].label}" does not agree on parameters.'
        if nodes[0].operation.type == 'NasBench101Cell':
            mutators_final.append(NasBench101Mutator(nodes[0].label))
        else:
            mutators.append(ManyChooseManyMutator(nodes[0].label))
    return model, mutators + mutators_final