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()
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
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
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
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