diff --git a/pacman/operations/router_algorithms/__init__.py b/pacman/operations/router_algorithms/__init__.py index d493f8ce1..08b967b81 100644 --- a/pacman/operations/router_algorithms/__init__.py +++ b/pacman/operations/router_algorithms/__init__.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .basic_dijkstra_routing import basic_dijkstra_routing -from .ner_route import ner_route, ner_route_traffic_aware from .application_router import route_application_graph -__all__ = ['basic_dijkstra_routing', 'ner_route', 'ner_route_traffic_aware', - 'route_application_graph'] +__all__ = ['route_application_graph'] diff --git a/pacman/operations/router_algorithms/basic_dijkstra_routing.py b/pacman/operations/router_algorithms/basic_dijkstra_routing.py deleted file mode 100644 index e34fc62f6..000000000 --- a/pacman/operations/router_algorithms/basic_dijkstra_routing.py +++ /dev/null @@ -1,501 +0,0 @@ -# Copyright (c) 2014 The University of Manchester -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import sys -from collections import defaultdict -from typing import Dict, Tuple, Set, Optional, List, Iterable -from spinn_utilities.log import FormatAdapter -from spinn_utilities.progress_bar import ProgressBar -from spinn_utilities.ordered_set import OrderedSet -from spinn_machine import Chip, Link -from pacman.data import PacmanDataView -from pacman.exceptions import PacmanRoutingException -from pacman.model.routing_table_by_partition import ( - MulticastRoutingTableByPartition, MulticastRoutingTableByPartitionEntry) -from pacman.utilities.algorithm_utilities.routing_algorithm_utilities import ( - get_app_partitions, vertex_xy_and_route) -from pacman.model.graphs.application import ( - ApplicationVertex, ApplicationEdgePartition) -from pacman.model.graphs.machine import MachineVertex -_OptInt = Optional[int] - -logger = FormatAdapter(logging.getLogger(__name__)) -infinity = float("inf") - -#: Assumed bandwidth cost per entry. -BW_PER_ROUTE_ENTRY = 0.01 -#: Maximum bandwidth of a link. -MAX_BW = 250 -# pylint: disable=wrong-spelling-in-comment - - -class _NodeInfo(object): - """ - :ivar list(~spinn_machine.Link) neighbours: - :ivar list(float) weights: - """ - __slots__ = ("neighbours", "weights") - - def __init__(self) -> None: - self.neighbours: List[Optional[Link]] = list() - self.weights: List[float] = list() - - @property - def neighweights(self) -> Iterable[Tuple[Optional[Link], float]]: - """ - Zip of neighbours and their weights - - :rtype: iterable(link or None, float) - """ - return zip(self.neighbours, self.weights) - - -class _DijkstraInfo(object): - __slots__ = ("activated", "cost") - - def __init__(self) -> None: - self.activated = False - self.cost: Optional[float] = None - - -class _DestInfo: - __slots__ = ("cores", "links") - - def __init__(self) -> None: - self.cores: Set[int] = set() - self.links: Set[int] = set() - - -def basic_dijkstra_routing(): - """ - Find routes between the edges with the allocated information, - placed in the given places - - :return: The discovered routes - :rtype: MulticastRoutingTables - :raise PacmanRoutingException: - If something goes wrong with the routing - """ - router = _BasicDijkstraRouting() - return router.route_all_partitions() - - -class _BasicDijkstraRouting(object): - """ - A routing algorithm that can find routes for edges between vertices - in a machine graph that have been placed on a machine by the use of a - Dijkstra shortest path algorithm. - """ - - __slots__ = ( - # the routing path objects used to be returned to the work flow - "_routing_paths", ) - - def __init__(self) -> None: - # set up basic data structures - self._routing_paths = MulticastRoutingTableByPartition() - - def route_all_partitions(self) -> MulticastRoutingTableByPartition: - """ - Find routes between the edges with the allocated information, - placed in the given places - - :param bool use_progress_bar: whether to show a progress bar - :return: The discovered routes - :rtype: MulticastRoutingTableByPartition - :raise PacmanRoutingException: - If something goes wrong with the routing - """ - - nodes_info = self._initiate_node_info() - tables = self._initiate_dijkstra_tables() - self._update_all_weights(nodes_info) - - partitions = get_app_partitions() - progress = ProgressBar(len(partitions), "Creating routing entries") - - for partition in progress.over(partitions): - self._route(partition, nodes_info, tables) - return self._routing_paths - - @staticmethod - def __vertex_and_route(tgt) -> Tuple[ - Chip, Tuple[MachineVertex, _OptInt, _OptInt]]: - xy, details = vertex_xy_and_route(tgt) - return PacmanDataView.get_chip_at(*xy), details - - def _route(self, partition: ApplicationEdgePartition, - node_info: Dict[Chip, _NodeInfo], - tables: Dict[Chip, _DijkstraInfo]): - """ - :param ApplicationEdgePartition partition: - :param dict(Chip,_NodeInfo) node_info: - :param dict(Chip,_DijkstraInfo) tables: - """ - # pylint: disable=too-many-arguments - source = partition.pre_vertex - - # Destination (dst, core, link) by source machine vertices - destinations: Dict[MachineVertex, Dict[Chip, _DestInfo]] = \ - defaultdict(lambda: defaultdict(_DestInfo)) - dest_chips: Dict[MachineVertex, Set[Chip]] = defaultdict(set) - - for edge in partition.edges: - target = edge.post_vertex - target_vertices = \ - target.splitter.get_source_specific_in_coming_vertices( - source, partition.identifier) - - for tgt, srcs in target_vertices: - dst, (m_vertex, core, link) = self.__vertex_and_route(tgt) - for src in srcs: - if isinstance(src, ApplicationVertex): - for s in src.splitter.get_out_going_vertices( - partition.identifier): - if core is not None: - destinations[s][dst].cores.add(core) - if link is not None: - destinations[s][dst].links.add(link) - dest_chips[s].add(dst) - else: - if core is not None: - destinations[src][dst].cores.add(core) - if link is not None: - destinations[src][dst].links.add(link) - dest_chips[src].add(dst) - - outgoing: OrderedSet[MachineVertex] = OrderedSet( - source.splitter.get_out_going_vertices(partition.identifier)) - for in_part in source.splitter.get_internal_multicast_partitions(): - if in_part.identifier == partition.identifier: - outgoing.add(in_part.pre_vertex) - for edge in in_part.edges: - dst, (_tgt, core, link) = self.__vertex_and_route( - edge.post_vertex) - if core is not None: - destinations[in_part.pre_vertex][dst].cores.add(core) - if link is not None: - destinations[in_part.pre_vertex][dst].links.add(link) - dest_chips[in_part.pre_vertex].add(dst) - - for m_vertex in outgoing: - src, (m_vertex, core, link) = self.__vertex_and_route(m_vertex) - if dest_chips[m_vertex]: - self._update_all_weights(node_info) - self._reset_tables(tables) - tables[src].activated = True - tables[src].cost = 0 - self._propagate_costs_until_reached_destinations( - tables, node_info, dest_chips[m_vertex], src) - - for dst, info in destinations[m_vertex].items(): - self._retrace_back_to_source( - dst, info, tables, node_info, core, - link, m_vertex, partition.identifier) - - def _initiate_node_info(self) -> Dict[Chip, _NodeInfo]: - """ - Set up a dictionary which contains data for each chip in the - machine. - - :return: nodes_info dictionary - :rtype: dict(Chip,_NodeInfo) - """ - nodes_info = dict() - for chip in PacmanDataView.get_machine().chips: - # get_neighbours should return a list of - # dictionaries of 'x' and 'y' values - node = _NodeInfo() - for source_id in range(6): - node.neighbours.append(chip.router.get_link(source_id)) - node.weights.append(infinity) - nodes_info[chip] = node - return nodes_info - - def _initiate_dijkstra_tables(self) -> Dict[Chip, _DijkstraInfo]: - """ - Set up the Dijkstra's table which includes if you've reached a - given node. - - :return: the Dijkstra's table dictionary - :rtype: dict(Chip,_DijkstraInfo) - """ - # Holds all the information about nodes within one full run of - # Dijkstra's algorithm - return { - chip: _DijkstraInfo() - for chip in PacmanDataView.get_machine().chips} - - def _update_all_weights(self, nodes_info: Dict[Chip, _NodeInfo]): - """ - Change the weights of the neighbouring nodes. - - :param dict(Chip,_NodeInfo) nodes_info: - the node info dictionary - """ - for key in nodes_info: - if nodes_info[key] is not None: - self._update_neighbour_weights(nodes_info, key) - - def _update_neighbour_weights( - self, nodes_info: Dict[Chip, _NodeInfo], key: Chip): - """ - Change the weights of the neighbouring nodes. - - :param dict(Chip,_NodeInfo) nodes_info: - the node info dictionary - :param Chip key: - the identifier to the object in `nodes_info` - """ - for n, neighbour in enumerate(nodes_info[key].neighbours): - if neighbour is not None: - nodes_info[key].weights[n] = 1 - - @staticmethod - def _reset_tables(tables: Dict[Chip, _DijkstraInfo]): - """ - Reset the Dijkstra tables for a new path search. - - :param dict(Chip,_DijkstraInfo) tables: - the dictionary object for the Dijkstra-tables - """ - for key in tables: - tables[key] = _DijkstraInfo() - - def _propagate_costs_until_reached_destinations( - self, tables: Dict[Chip, _DijkstraInfo], - nodes_info: Dict[Chip, _NodeInfo], dest_chips: Set[Chip], - source: Chip): - """ - Propagate the weights till the destination nodes of the source - nodes are retraced. - - :param dict(Chip,_DijkstraInfo) tables: - the dictionary object for the Dijkstra-tables - :param dict(Chip,_NodeInfo) nodes_info: - the dictionary object for the nodes inside a route scope - :param set(Chip) dest_chips: - :param Chip source: - :raise PacmanRoutingException: - when the destination node could not be reached from this source - node - """ - dest_chips_to_find = set(dest_chips) - dest_chips_to_find.discard(source) - - current = source - - # Iterate only if the destination node hasn't been activated - while dest_chips_to_find: - # PROPAGATE! - for neighbour, weight in nodes_info[current].neighweights: - # "neighbours" is a list of 6 links or None objects. There is - # a None object where there is no connection to that neighbour - if neighbour is not None and not ( - neighbour.destination_x == source.x and - neighbour.destination_y == source.y): - # These variables change with every look at a new neighbour - self._update_neighbour( - tables, neighbour, current, source, weight) - - # Set the next activated node as the deactivated node with the - # lowest current cost - current = self._minimum(tables) - tables[current].activated = True - dest_chips_to_find.discard(current) - - @staticmethod - def _minimum(tables: Dict[Chip, _DijkstraInfo]) -> Chip: - """ - :param dict(Chip,_DijkstraInfo) tables: - :rtype: tuple(int,int) - """ - # This is the lowest cost across ALL deactivated nodes in the graph. - lowest_cost: float = sys.maxsize - lowest: Optional[Chip] = None - - # Find the next node to be activated - for key in tables: - cost = tables[key].cost - # Don't continue if the node hasn't even been touched yet - if (cost is not None and not tables[key].activated - and cost < lowest_cost): - lowest_cost, lowest = cost, key - - # If there were no deactivated nodes with costs, but the destination - # was not reached this iteration, raise an exception - if lowest is None: - raise PacmanRoutingException( - "Destination could not be activated, ending run") - - return lowest - - @staticmethod - def __get_neighbour_destination(neighbour: Link) -> Optional[Chip]: - return PacmanDataView.get_machine().get_chip_at( - neighbour.destination_x, neighbour.destination_y) - - def _update_neighbour( - self, tables: Dict[Chip, _DijkstraInfo], neighbour: Link, - current: Chip, source: Chip, weight: float): - """ - Update the lowest cost for each neighbouring chip of a node. - - :param dict(Chip,_DijkstraInfo) tables: - :param ~spinn_machine.Link neighbour: - :param Chip current: - :param Chip source: - :param float weight: - :raise PacmanRoutingException: when the algorithm goes to a node that - doesn't exist in the machine or the node's cost was set too low. - """ - neighbour_chip = self.__get_neighbour_destination(neighbour) - if not neighbour_chip or neighbour_chip not in tables: - raise PacmanRoutingException( - f"Tried to propagate to ({neighbour.destination_x}, " - f"{neighbour.destination_y}), which is not in the" - " graph: remove non-existent neighbours") - - chip_cost = tables[current].cost - assert chip_cost is not None - neighbour_cost = tables[neighbour_chip].cost - - # Only try to update if the neighbour_chip is within the graph and the - # cost if the node hasn't already been activated and the lowest cost - # if the new cost is less, or if there is no current cost. - new_weight = float(chip_cost + weight) - if not tables[neighbour_chip].activated and ( - neighbour_cost is None or new_weight < neighbour_cost): - # update Dijkstra table - tables[neighbour_chip].cost = new_weight - - if tables[neighbour_chip].cost == 0 and neighbour_chip != source: - raise PacmanRoutingException( - f"!!!Cost of non-source node ({neighbour.destination_x}, " - f"{neighbour.destination_y}) was set to zero!!!") - - def _retrace_back_to_source( - self, dest: Chip, dest_info: _DestInfo, - tables: Dict[Chip, _DijkstraInfo], - nodes_info: Dict[Chip, _NodeInfo], - source_processor: _OptInt, source_link: _OptInt, - pre_vertex, partition_id) -> None: - """ - :param Placement dest: Destination placement - :param _DestInfo dest_info: Information for building an entry - :param dict(Chip,_DijkstraInfo) tables: - :param MachineEdge edge: - :param dict(Chip,_NodeInfo) nodes_info: - :param int source_processor: - :param int source_link: - :raise PacmanRoutingException: - when the algorithm doesn't find a next point to search from. AKA, - the neighbours of a chip do not have a cheaper cost than the node - itself, but the node is not the destination or when the algorithm - goes to a node that's not considered in the weighted search. - """ - entry = MulticastRoutingTableByPartitionEntry( - dest_info.links, dest_info.cores) - self._routing_paths.add_path_entry( - entry, dest.x, dest.y, pre_vertex, partition_id) - prev_entry = entry - - while tables[dest].cost != 0: - for idx, neighbour in enumerate(nodes_info[dest].neighbours): - if neighbour is not None: - n = self.__get_neighbour_destination(neighbour) - - # Only check if it can be a preceding node if it actually - # exists - if not n or n not in tables: - raise PacmanRoutingException( - "Tried to trace back to node not in " - "graph: remove non-existent neighbours") - - if tables[n].cost is not None: - dest, prev_entry, added = self._create_routing_entry( - n, tables, idx, nodes_info, dest, - prev_entry, pre_vertex, partition_id) - if added: - break - else: - raise PacmanRoutingException( - "Iterated through all neighbours of tracking node but" - " did not find a preceding node! Consider increasing " - "acceptable discrepancy between sought traceback cost" - " and actual cost at node. Terminating...") - if source_processor is not None: - prev_entry.incoming_processor = source_processor - if source_link is not None: - prev_entry.incoming_link = source_link - - def _create_routing_entry( - self, neighbour: Chip, tables: Dict[Chip, _DijkstraInfo], - neighbour_index: int, nodes_info: Dict[Chip, _NodeInfo], - dest: Chip, previous_entry: MulticastRoutingTableByPartitionEntry, - pre_vertex, partition_id) -> Tuple[ - Chip, MulticastRoutingTableByPartitionEntry, bool]: - """ - Create a new routing entry. - - :param Chip neighbour: - :param dict(Chip,_DijkstraInfo) tables: - :param int neighbour_index: - :param dict(Chip,_NodeInfo) nodes_info: - :param Chip dest: - :param MulticastRoutingTableByPartitionEntry previous_entry: - :return: dest, previous_entry, made_an_entry - :rtype: tuple(Chip, MulticastRoutingTableByPartitionEntry, bool) - :raise PacmanRoutingException: - when the bandwidth of a router is beyond expected parameters - """ - # Set the direction of the routing other_entry as that which is from - # the preceding node to the current tracking node. - # neighbour is the 'old' chip since it is from the preceding node. - # dest is the 'new' chip since it is where the router should send the - # packet to. - dec_direction = (3, 4, 5, 0, 1, 2)[neighbour_index] - made_an_entry = False - - neighbour_weight = nodes_info[neighbour].weights[dec_direction] - chip_sought_cost = tables[dest].cost - assert chip_sought_cost is not None - chip_sought_cost -= neighbour_weight - neighbours_lowest_cost = tables[neighbour].cost - - if neighbours_lowest_cost is not None and ( - self._close_enough(neighbours_lowest_cost, chip_sought_cost)): - # build the multicast entry - entry = MulticastRoutingTableByPartitionEntry(dec_direction, None) - previous_entry.incoming_link = neighbour_index - # add entry for next hop going backwards into path - self._routing_paths.add_path_entry( - entry, neighbour.x, neighbour.y, pre_vertex, partition_id) - previous_entry = entry - made_an_entry = True - - # Finally move the tracking node - dest = neighbour - - return dest, previous_entry, made_an_entry - - @staticmethod - def _close_enough(v1: float, v2: float, delta=0.00000000001) -> bool: - """ - :param float v1: - :param float v2: - :param float delta: How close values have to be to be "equal" - """ - return abs(v1 - v2) < delta diff --git a/pacman/operations/router_algorithms/ner_route.py b/pacman/operations/router_algorithms/ner_route.py deleted file mode 100644 index cb4d8dd4e..000000000 --- a/pacman/operations/router_algorithms/ner_route.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright (c) 2017 The University of Manchester -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Neighbour Exploring Routing (NER) algorithm from J. Navaridas et al. - -Algorithm reference: J. Navaridas et al. SpiNNaker: Enhanced multicast routing, -Parallel Computing (2014). - -`https://dx.doi.org/10.1016/j.parco.2015.01.002` - -Based on -https://github.com/project-rig/rig/blob/master/rig/place_and_route/route/ner.py -https://github.com/project-rig/rig/blob/master/rig/geometry.py -https://github.com/project-rig/rig/blob/master/rig/place_and_route/route/utils.py -""" - -from collections import defaultdict -import functools -from typing import Callable, Dict, Final, Iterable, List, Tuple - -from typing_extensions import TypeAlias - -from spinn_utilities.progress_bar import ProgressBar -from spinn_utilities.ordered_set import OrderedSet -from spinn_utilities.typing.coords import XY - -from pacman.data import PacmanDataView -from pacman.model.routing_table_by_partition import ( - MulticastRoutingTableByPartition) -from pacman.utilities.algorithm_utilities.routing_algorithm_utilities import ( - route_has_dead_links, avoid_dead_links, convert_a_route, - longest_dimension_first, nodes_to_trees, vertex_xy, get_targets_by_chip, - least_busy_dimension_first, get_app_partitions, vertex_xy_and_route) -from pacman.model.graphs.application import ApplicationVertex -from pacman.utilities.algorithm_utilities.routing_tree import RoutingTree -from pacman.model.graphs.machine import MachineVertex - -_Vec: TypeAlias = Tuple[int, int, int] -_V2N: TypeAlias = Callable[[_Vec, XY], List[Tuple[int, XY]]] -_Inf: Final = float("inf") -# pylint: disable=wrong-spelling-in-comment - - -def _ner_net(src: XY, destinations: Iterable[XY], - vector_to_nodes: _V2N) -> RoutingTree: - """ - Produce a shortest path tree for a given net using NER. - - This is the kernel of the NER algorithm. - - :param tuple(int,int) source: - The coordinate (x, y) of the source vertex. - :param iterable(tuple(int,int)) destinations: - The coordinates of destination vertices. - :param vector_to_nodes: How to get the nodes from a machine vector - :return: - A RoutingTree is produced rooted at the source and visiting all - destinations but which does not contain any vertices etc. - :rtype: RoutingTree - """ - machine = PacmanDataView.get_machine() - - # The radius to check for neighbours, and the total number of chips that - # could appear in the radius - radius = 20 - n_nodes_radius = 1261 - - # Map from (x, y) to RoutingTree objects - route = {src: RoutingTree(src)} - - # Handle each destination, sorted by distance from the source, closest - # first. - sorted_dest = sorted( - destinations, key=(lambda dest: machine.get_vector_length(src, dest))) - for destination in sorted_dest: - # We shall attempt to find our nearest neighbouring placed node. - neighbour = None - - # Try to find a nearby (within radius hops) node in the routing tree - # that we can route to (falling back on just routing to the source). - if len(route) / 3 > n_nodes_radius: - # This implementation scans potential neighbours in an expanding - # radius; this is ~3x faster per iteration than the one below. - for candidate in machine.concentric_xys(radius, destination): - if candidate in route: - neighbour = candidate - break - else: - # This implementation scans the list of all route nodes created so - # far and finds the closest node which is < radius hops. This is - # ~3x slower per iteration than the one above. - neighbour_distance = _Inf - for candidate_neighbour in route: - distance = machine.get_vector_length( - candidate_neighbour, destination) - if distance <= radius and ( - neighbour is None or distance < neighbour_distance): - neighbour = candidate_neighbour - neighbour_distance = distance - - # Fall back on routing directly to the source if no nodes within radius - # hops of the destination was found. - if neighbour is None: - neighbour = src - - # Find the shortest vector from the neighbour to this destination - vector: _Vec = machine.get_vector(neighbour, destination) - - # The route may inadvertently pass through an - # already connected node. If the route is allowed to pass through that - # node it would create a cycle in the route which would be VeryBad(TM). - # As a result, we work backward through the route and truncate it at - # the first point where the route intersects with a connected node. - nodes = vector_to_nodes(vector, neighbour) - i = len(nodes) - for _direction, (x, y) in reversed(nodes): - i -= 1 - if (x, y) in route: - # We've just bumped into a node which is already part of the - # route, this becomes our new neighbour and we truncate the LDF - # route. (Note ldf list is truncated just after the current - # position since it gives (direction, destination) pairs). - neighbour = (x, y) - nodes = nodes[i + 1:] - break - - # Take the longest dimension first route. - nodes_to_trees(nodes, neighbour, route) - - return route[src] - - -def _do_route(source_xy: XY, post_vertexes: Iterable[MachineVertex], - vector_to_nodes: _V2N) -> RoutingTree: - """ - Routing algorithm based on Neighbour Exploring Routing (NER). - - Algorithm reference: J. Navaridas et al. SpiNNaker: Enhanced multicast - routing, Parallel Computing (2014). - https://dx.doi.org/10.1016/j.parco.2015.01.002 - - This algorithm attempts to use NER to generate routing trees for all nets - and routes around broken links using A* graph search. If the system is - fully connected, this algorithm will always succeed though no consideration - of congestion or routing-table usage is attempted. - - :param tuple(int,int) source_xy: - :param iterable(MachineVertex) post_vertexes: - :param vector_to_nodes: - :return: - :rtype: RoutingTree - """ - destinations = frozenset( - vertex_xy(post_vertex) for post_vertex in post_vertexes) - # Generate routing tree (assuming a perfect machine) - root = _ner_net(source_xy, destinations, vector_to_nodes) - - # Fix routes to avoid dead chips/links - if route_has_dead_links(root): - root = avoid_dead_links(root) - - return root - - -def _ner_route(vector_to_nodes: _V2N) -> MulticastRoutingTableByPartition: - """ - Performs routing using rig algorithm. - - :param vector_to_nodes: - :return: the routing tables - :rtype: MulticastRoutingTableByPartition - """ - routing_tables = MulticastRoutingTableByPartition() - - partitions = get_app_partitions() - - progress_bar = ProgressBar(len(partitions), "Routing") - - for partition in progress_bar.over(partitions): - source = partition.pre_vertex - post_vertices_by_source: Dict[ - MachineVertex, OrderedSet[MachineVertex]] = defaultdict(OrderedSet) - for edge in partition.edges: - splitter = edge.post_vertex.splitter - for tgt, srcs in splitter.get_source_specific_in_coming_vertices( - source, partition.identifier): - for src in srcs: - if isinstance(src, ApplicationVertex): - for s in src.splitter.get_out_going_vertices( - partition.identifier): - post_vertices_by_source[s].add(tgt) - - outgoing: OrderedSet[MachineVertex] = OrderedSet( - source.splitter.get_out_going_vertices(partition.identifier)) - for in_part in source.splitter.get_internal_multicast_partitions(): - if in_part.identifier == partition.identifier: - outgoing.add(in_part.pre_vertex) - for edge in in_part.edges: - post_vertices_by_source[in_part.pre_vertex].add( - edge.post_vertex) - - for m_vertex in outgoing: - post_vertexes = post_vertices_by_source[m_vertex] - source_xy, (m_vertex, core, link) = vertex_xy_and_route(m_vertex) - routing_tree = _do_route(source_xy, post_vertexes, vector_to_nodes) - targets = get_targets_by_chip(post_vertexes) - convert_a_route( - routing_tables, m_vertex, partition.identifier, - core, link, routing_tree, targets) - - return routing_tables - - -def ner_route() -> MulticastRoutingTableByPartition: - """ - basic NER router. - - :return: a routing table by partition - :rtype: MulticastRoutingTableByPartition - """ - return _ner_route(longest_dimension_first) - - -def ner_route_traffic_aware() -> MulticastRoutingTableByPartition: - """ - traffic-aware NER router. - - :return: a routing table by partition - :rtype: MulticastRoutingTableByPartition - """ - traffic: Dict[XY, int] = defaultdict(int) - return _ner_route(functools.partial(least_busy_dimension_first, traffic)) diff --git a/pacman/utilities/algorithm_utilities/routing_algorithm_utilities.py b/pacman/utilities/algorithm_utilities/routing_algorithm_utilities.py index aa7c971e4..b1cb01216 100644 --- a/pacman/utilities/algorithm_utilities/routing_algorithm_utilities.py +++ b/pacman/utilities/algorithm_utilities/routing_algorithm_utilities.py @@ -12,26 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import deque, defaultdict -import heapq -import itertools -from typing import Deque, Dict, Iterable, List, Optional, Set, Tuple +from typing import List, Optional, Tuple from spinn_utilities.typing.coords import XY -from spinn_machine import Machine, Chip +from spinn_machine import Chip from pacman.data import PacmanDataView -from pacman.exceptions import MachineHasDisconnectedSubRegion -from pacman.model.routing_table_by_partition import ( - MulticastRoutingTableByPartitionEntry, - MulticastRoutingTableByPartition) -from pacman.model.graphs import AbstractVertex, AbstractVirtual +from pacman.model.graphs import AbstractVirtual from pacman.model.graphs.application import ( ApplicationEdgePartition, ApplicationVertex) from pacman.model.graphs.machine import MachineVertex -from .routing_tree import RoutingTree - def get_app_partitions() -> List[ApplicationEdgePartition]: """ @@ -72,335 +63,6 @@ def get_app_partitions() -> List[ApplicationEdgePartition]: return partitions -def route_has_dead_links(root: RoutingTree) -> bool: - """ - Quickly determine if a route uses any dead links. - - :param RoutingTree root: - The root of the RoutingTree which contains nothing but RoutingTrees - (i.e. no vertices and links). - :return: True if the route uses any dead/missing links, False otherwise. - :rtype: bool - """ - for _, (x, y), routes in root.traverse(): - chip = PacmanDataView.get_chip_at(x, y) - for route in routes: - if chip is None: - return True - if not chip.router.is_link(route): - return True - return False - - -def avoid_dead_links(root: RoutingTree) -> RoutingTree: - """ - Modify a RoutingTree to route-around dead links in a Machine. - - Uses A* to reconnect disconnected branches of the tree (due to dead links - in the machine). - - :param RoutingTree root: - The root of the RoutingTree which contains nothing but RoutingTrees - (i.e. no vertices and links). - :return: - A new RoutingTree is produced rooted as before. - :rtype: RoutingTree - """ - # Make a copy of the RoutingTree with all broken parts disconnected - root, lookup, broken_links = _copy_and_disconnect_tree(root) - - # For each disconnected subtree, use A* to connect the tree to *any* other - # disconnected subtree. Note that this process will eventually result in - # all disconnected subtrees being connected, the result is a fully - # connected tree. - for parent, child in broken_links: - child_chips = frozenset( - c.chip for c in lookup[child] if isinstance(c, RoutingTree)) - - # Try to reconnect broken links to any other part of the tree - # (excluding this broken subtree itself since that would create a - # cycle). - path = a_star(child, parent, set(lookup).difference(child_chips)) - - # Add new RoutingTree nodes to reconnect the child to the tree. - last_node = lookup[path[0][1]] - last_direction = path[0][0] - for direction, (x, y) in path[1:]: - if (x, y) not in child_chips: - # This path segment traverses new ground so we must create a - # new RoutingTree for the segment. - new_node = RoutingTree((x, y)) - # A* will not traverse anything but chips in this tree so this - # assert is a sanity check that this occurred correctly. - assert (x, y) not in lookup, "Cycle created." - lookup[(x, y)] = new_node - else: - # This path segment overlaps part of the disconnected tree - # (A* doesn't know where the disconnected tree is and thus - # doesn't avoid it). To prevent cycles being introduced, this - # overlapped node is severed from its parent and merged as part - # of the A* path. - new_node = lookup[(x, y)] - - # Find the node's current parent and disconnect it. - for node in lookup[child]: # pragma: no branch - if not isinstance(node, RoutingTree): - continue - dn = [(d, n) for d, n in node.children if n == new_node] - assert len(dn) <= 1 - if dn: - node.remove_child(dn[0]) - # A node can only have one parent so we can stop now. - break - last_node.append_child((last_direction, new_node)) - last_node = new_node - last_direction = direction - last_node.append_child((last_direction, lookup[child])) - - return root - - -def _copy_and_disconnect_tree(root: RoutingTree) -> Tuple[ - RoutingTree, Dict[XY, RoutingTree], Set[Tuple[XY, XY]]]: - """ - Copy a RoutingTree (containing nothing but RoutingTrees), disconnecting - nodes which are not connected in the machine. - - .. note:: - If a dead chip is part of the input RoutingTree, no corresponding node - will be included in the copy. The assumption behind this is that the - only reason a tree would visit a dead chip is because a route passed - through the chip and wasn't actually destined to arrive at that chip. - This situation is impossible to confirm since the input routing trees - have not yet been populated with vertices. The caller is responsible - for being sensible. - - :param RoutingTree root: - The root of the RoutingTree that contains nothing but RoutingTrees - (i.e. no children which are vertices or links). - :return: (root, lookup, broken_links) - Where: - * `root` is the new root of the tree - :py:class:`~.RoutingTree` - * `lookup` is a dict {(x, y): - :py:class:`~.RoutingTree`, ...} - * `broken_links` is a set ([(parent, child), ...]) containing all - disconnected parent and child (x, y) pairs due to broken links. - :rtype: tuple(RoutingTree, dict(tuple(int,int),RoutingTree), - set(tuple(tuple(int,int),tuple(int,int)))) - """ - new_root: Optional[RoutingTree] = None - - # Lookup for copied routing tree {(x, y): RoutingTree, ...} - new_lookup: Dict[XY, RoutingTree] = {} - - # List of missing connections in the copied routing tree [(new_parent, - # new_child), ...] - broken_links: Set[Tuple[XY, XY]] = set() - - # A queue [(new_parent, direction, old_node), ...] - to_visit: Deque[ - Tuple[Optional[RoutingTree], Optional[int], RoutingTree]] = deque( - ((None, None, root), )) - machine = PacmanDataView.get_machine() - while to_visit: - new_parent, direction, old_node = to_visit.popleft() - if machine.is_chip_at(old_node.chip[0], old_node.chip[1]): - # Create a copy of the node - new_node = RoutingTree(old_node.chip) - new_lookup[new_node.chip] = new_node - else: - # This chip is dead, move all its children into the parent node - assert new_parent is not None, \ - "Net cannot be sourced from a dead chip." - new_node = new_parent - - if new_parent is None: - # This is the root node - new_root = new_node - elif new_node is not new_parent: - assert direction is not None - # If this node is not dead, check connectivity to parent - # node (no reason to check connectivity between a dead node - # and its parent). - if _is_linked(new_parent.chip, new_node.chip, direction, machine): - # Is connected via working link - new_parent.append_child((direction, new_node)) - else: - # Link to parent is dead (or original parent was dead and - # the new parent is not adjacent) - broken_links.add((new_parent.chip, new_node.chip)) - - # Copy children - for child_direction, child in old_node.children: - if isinstance(child, RoutingTree): - to_visit.append((new_node, child_direction, child)) - - assert new_root is not None - return new_root, new_lookup, broken_links - - -def a_star(sink: XY, heuristic_source: XY, - sources: Set[XY]) -> List[Tuple[int, XY]]: - """ - Use A* to find a path from any of the sources to the sink. - - .. note:: - The heuristic means that the search will proceed towards - heuristic_source without any concern for any other sources. This means - that the algorithm may miss a very close neighbour in order to pursue - its goal of reaching heuristic_source. This is not considered a problem - since 1) the heuristic source will typically be in the direction of the - rest of the tree and near by and often the closest entity 2) it - prevents us accidentally forming loops in the rest of the tree since - we'll stop as soon as we touch any part of it. - - :param tuple(int,int) sink: (x, y) - :param tuple(int,int) heuristic_source: (x, y) - An element from `sources` which is used as a guiding heuristic for the - A* algorithm. - :param set(tuple(int,int)) sources: set([(x, y), ...]) - :return: [(int, (x, y)), ...] - A path starting with a coordinate in `sources` and terminating at - connected neighbour of `sink` (i.e. the path does not include `sink`). - The direction given is the link down which to proceed from the given - (x, y) to arrive at the next point in the path. - :rtype: list(tuple(int,tuple(int,int))) - """ - machine = PacmanDataView.get_machine() - # Select the heuristic function to use for distances - # pylint: disable=unnecessary-lambda-assignment - heuristic = (lambda the_node: machine.get_vector_length( - the_node, heuristic_source)) - - # A dictionary {node: (direction, previous_node}. An entry indicates that - # 1) the node has been visited and 2) which node we hopped from (and the - # direction used) to reach previous_node. This is a dummy value if the - # node is the sink. - visited: Dict[XY, Tuple[int, XY]] = {sink: (-1, (-1, -1))} - - # The node which the tree will be reconnected to - selected_source = None - - # A heap (accessed via heapq) of (distance, (x, y)) where distance is the - # distance between (x, y) and heuristic_source and (x, y) is a node to - # explore. - to_visit = [(heuristic(sink), sink)] - while to_visit: - _, node = heapq.heappop(to_visit) - - # Terminate if we've found the destination - if node in sources: - selected_source = node - break - - # Try all neighbouring locations. - for neighbour_link in range(6): # Router.MAX_LINKS_PER_ROUTER - # Note: link identifiers are from the perspective of the neighbour, - # not the current node! - neighbour = machine.xy_over_link( - # Same as Router.opposite - node[0], node[1], (neighbour_link + 3) % 6) - - # Skip links which are broken - if not machine.is_link_at( - neighbour[0], neighbour[1], neighbour_link): - continue - - # Skip neighbours who have already been visited - if neighbour in visited: - continue - - # Explore all other neighbours - visited[neighbour] = (neighbour_link, node) - heapq.heappush(to_visit, (heuristic(neighbour), neighbour)) - - # Fail of no paths exist - if selected_source is None: - raise MachineHasDisconnectedSubRegion( - f"Could not find path from {sink} to {heuristic_source}") - - # Reconstruct the discovered path, starting from the source we found and - # working back until the sink. - path = [(visited[selected_source][0], selected_source)] - while visited[path[-1][1]][1] != sink: - node = visited[path[-1][1]][1] - direction = visited[node][0] - path.append((direction, node)) - - return path - - -def _is_linked( - source: XY, target: XY, direction: int, machine: Machine) -> bool: - """ - :param tuple(int,int) source: - :param tuple(int,int) target: - :param int direction: - :param ~spinn_machine.Machine machine: - :rtype: bool - """ - s_chip = machine.get_chip_at(source[0], source[1]) - if s_chip is None: - return False - link = s_chip.router.get_link(direction) - if link is None: - return False - if link.destination_x != target[0]: - return False - if link.destination_y != target[1]: - return False - return True - - -def convert_a_route( - routing_tables: MulticastRoutingTableByPartition, - source_vertex: AbstractVertex, partition_id: str, - incoming_processor: Optional[int], incoming_link: Optional[int], - route_tree: RoutingTree, - targets_by_chip: Dict[XY, Tuple[Set[int], Set[int]]]): - """ - Converts the algorithm specific partition_route back to standard SpiNNaker - and adds it to the `routing_tables`. - - :param MulticastRoutingTableByPartition routing_tables: - SpiNNaker-format routing tables - :param AbstractSingleSourcePartition partition: - Partition this route applies to - :param incoming_processor: processor this link came from - :type incoming_processor: int or None - :param incoming_link: link this link came from - :type incoming_link: int or None - :param RoutingTree route_tree: algorithm specific format of the route - :param dict((int,int),(list(int),list(int))) targets_by_chip: - Target cores and links of things on the route that are final end points - """ - x, y = route_tree.chip - - next_hops: List[Tuple[RoutingTree, int]] = list() - processor_ids: List[int] = list() - link_ids: List[int] = list() - for (route, next_hop) in route_tree.children: - if route is not None: - link_ids.append(route) - next_incoming_link = (route + 3) % 6 - if isinstance(next_hop, RoutingTree): - next_hops.append((next_hop, next_incoming_link)) - if (x, y) in targets_by_chip: - cores, links = targets_by_chip[x, y] - processor_ids.extend(cores) - link_ids.extend(links) - - entry = MulticastRoutingTableByPartitionEntry( - link_ids, processor_ids, incoming_processor, incoming_link) - routing_tables.add_path_entry(entry, x, y, source_vertex, partition_id) - - for next_hop, next_incoming_link in next_hops: - convert_a_route( - routing_tables, source_vertex, partition_id, None, - next_incoming_link, next_hop, targets_by_chip) - - def longest_dimension_first( vector: Tuple[int, int, int], start: XY) -> List[Tuple[int, XY]]: """ @@ -421,46 +83,6 @@ def longest_dimension_first( start) -def least_busy_dimension_first( - traffic: Dict[XY, int], vector: Tuple[int, int, int], - start: XY) -> List[Tuple[int, XY]]: - """ - List the (x, y) steps on a route that goes through the least busy - routes first. - - :param traffic: A dictionary of (x, y): count of routes - :type traffic: dict(tuple(int,int), int) - :param vector: (x, y, z) - The vector which the path should cover. - :type vector: tuple(int, int, int) - :param start: (x, y) - The coordinates from which the path should start. - - .. note:: - This is a 2D coordinate. - :type start: tuple(int, int) - :return: min route - :rtype: list(tuple(int,tuple(int, int))) - """ - - # Go through and find the sum of traffic depending on the route taken - min_sum = 0 - min_route = None - for order in itertools.permutations([0, 1, 2]): - dm_vector = [(i, vector[i]) for i in order] - route = vector_to_nodes(dm_vector, start) - sum_traffic = sum(traffic[x, y] for _, (x, y) in route) - if min_route is None or min_sum > sum_traffic: - min_sum = sum_traffic - min_route = route - - assert min_route is not None - for _, (x, y) in min_route: - traffic[x, y] += 1 - - return min_route - - def vector_to_nodes(dm_vector: List[XY], start: XY) -> List[Tuple[int, XY]]: """ Convert a vector to a set of nodes. @@ -517,74 +139,6 @@ def vector_to_nodes(dm_vector: List[XY], start: XY) -> List[Tuple[int, XY]]: return out -def nodes_to_trees( - nodes: List[Tuple[int, XY]], start: XY, route: Dict[XY, RoutingTree]): - """ - Convert a list of nodes into routing trees, adding them to existing routes. - - :param list(tuple(int,tuple(int,int))) nodes: - The list of (link_id, (target_x, target_y)) nodes on the route - :param tuple(int,int) start: The start of the route - :param dict(tuple(int,int),RoutingTree) route: - Existing routing trees, with key (x, y) coordinates of the chip of the - routes. - """ - last_node = route.get(start) - if last_node is None: - last_node = RoutingTree(start) - route[start] = last_node - for direction, (x, y) in nodes: - this_node = RoutingTree((x, y)) - route[(x, y)] = this_node - - last_node.append_child((direction, this_node)) - last_node = this_node - - -def most_direct_route(source: XY, dest: XY, machine: Machine) -> RoutingTree: - """ - Find the most direct route from source to target on the machine. - - :param tuple(int,int) source: The source x, y coordinates - :param tuple(int,int) dest: The destination x, y coordinates - :param ~spinn_machine.Machine machine: The machine on which to route - :rtype: RoutingTree - """ - vector = machine.get_vector(source, dest) - nodes = longest_dimension_first(vector, source) - route: Dict[XY, RoutingTree] = dict() - nodes_to_trees(nodes, source, route) - root = route[source] - if route_has_dead_links(root): - root = avoid_dead_links(root) - return root - - -def get_targets_by_chip( - vertices: Iterable[MachineVertex]) -> Dict[ - XY, Tuple[Set[int], Set[int]]]: - """ - Get the target links and cores on the relevant chips. - - :param list(MachineVertex) vertices: The vertices to target - :return: A dict of (x, y) to target (cores, links) - :rtype: dict(tuple(int, int), tuple(set(int), set(int))) - """ - by_chip: Dict[XY, Tuple[Set[int], Set[int]]] = defaultdict( - lambda: (set(), set())) - for vertex in vertices: - coords = vertex_xy(vertex) - if isinstance(vertex, AbstractVirtual): - # Sinks with route-to-endpoint restriction must be routed - # in the according directions. - link = route_to_endpoint(vertex) - by_chip[coords][1].add(link) - else: - core = PacmanDataView.get_placement_of_vertex(vertex).p - by_chip[coords][0].add(core) - return by_chip - - def vertex_xy(vertex: MachineVertex) -> XY: """ :param MachineVertex vertex: @@ -630,13 +184,3 @@ def vertex_xy_and_route(vertex: MachineVertex) -> Tuple[ link_data = vertex.get_link_data(machine) return ((link_data.connected_chip_x, link_data.connected_chip_y), (vertex, None, link_data.connected_link)) - - -def route_to_endpoint(vertex: AbstractVirtual) -> int: - """ - :param MachineVertex vertex: - :rtype: int - """ - machine = PacmanDataView.get_machine() - link_data = vertex.get_link_data(machine) - return link_data.connected_link diff --git a/unittests/operations_tests/router_algorithms_tests/test_routers.py b/unittests/operations_tests/router_algorithms_tests/test_routers.py index a254c1c68..00e204ef6 100644 --- a/unittests/operations_tests/router_algorithms_tests/test_routers.py +++ b/unittests/operations_tests/router_algorithms_tests/test_routers.py @@ -28,10 +28,6 @@ place_application_graph) from pacman.operations.router_algorithms.application_router import ( route_application_graph, _path_without_errors) -from pacman.operations.router_algorithms.basic_dijkstra_routing import ( - basic_dijkstra_routing) -from pacman.operations.router_algorithms.ner_route import ( - ner_route, ner_route_traffic_aware) from pacman.utilities.algorithm_utilities.routing_algorithm_utilities import ( longest_dimension_first, get_app_partitions, vertex_xy, vertex_xy_and_route) @@ -48,10 +44,7 @@ @pytest.fixture(params=[ - (route_application_graph, 10, 50), - (basic_dijkstra_routing, 10, 10), - (ner_route, 10, 50), - (ner_route_traffic_aware, 10, 50)]) + (route_application_graph, 10, 50)]) def params(request): return request.param