From b139479646143d6415bfab24a0614246df707edd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 7 Oct 2024 16:51:54 +0200 Subject: [PATCH] floogen: Use unique IDs for endpoints (#78) * floogen(routing)!: Rename `id_offset` to `xy_id_offset` An ID offset is only needed for XY Routing * floogen(routing): Remove support for ID offset for non-XY-Routing * floogen(routing): Introduce unique `uid` for enum generation * floogen(graph): Make it possible to get node ID with object * floogen(graph): Make it possible to return either only object or name of a node * floogen(routing): More reasonable ordering of endpoint name * docs: Update CHANGELOG --- CHANGELOG.md | 1 + floogen/model/endpoint.py | 4 +- floogen/model/graph.py | 75 ++++++++++++++-------- floogen/model/network.py | 67 ++++++++++--------- floogen/model/network_interface.py | 3 +- floogen/model/router.py | 2 +- floogen/model/routing.py | 2 +- floogen/templates/floo_axi_chimney.sv.mako | 2 +- floogen/templates/floo_axi_router.sv.mako | 2 +- floogen/templates/floo_nw_chimney.sv.mako | 2 +- floogen/templates/floo_nw_router.sv.mako | 2 +- 11 files changed, 99 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca67d054..683ae44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - All examples were adapted to reflect those changes. - A FlooGen configuration file now requires a `network_type` field, to determine the type of network to generate. The options are `axi` for single-AXI networks and `narrow-wide` for the narrow-wide AXI configurations. - The system address map `Sam` is now sorted correctly and can be indexed with `ep_id_e` values. +- `id_offset` was renamed to `xy_id_offset`, since this is now only applicable in `XYRouting` networks. An ID offset does not make sense for other types of routing algorithms. The use of `id_offset` is anyway not recommended anymore, since the direction of the connections can be specified in the `connections` schema. ### Fixed diff --git a/floogen/model/endpoint.py b/floogen/model/endpoint.py index 42bcde72..7fa51348 100644 --- a/floogen/model/endpoint.py +++ b/floogen/model/endpoint.py @@ -22,7 +22,7 @@ class EndpointDesc(BaseModel): description: Optional[str] = "" array: Optional[Union[Tuple[int], Tuple[int, int]]] = None addr_range: Optional[AddrRange] = None - id_offset: Optional[Id] = None + xy_id_offset: Optional[Id] = None mgr_port_protocol: Optional[List[str]] = None sbr_port_protocol: Optional[List[str]] = None @@ -34,7 +34,7 @@ def int_to_tuple(cls, v): return (v,) return v - @field_validator("id_offset", mode="before") + @field_validator("xy_id_offset", mode="before") @classmethod def dict_to_coord_obj(cls, v): """Convert dict to Coord object.""" diff --git a/floogen/model/graph.py b/floogen/model/graph.py index 9a1caf5e..cf6138b0 100644 --- a/floogen/model/graph.py +++ b/floogen/model/graph.py @@ -5,6 +5,7 @@ # # Author: Tim Fischer +import re from typing import List, Tuple import networkx as nx @@ -87,66 +88,70 @@ def is_link_edge(self, edge): """Return whether the edge is a link edge.""" return self.edges[edge]["type"] == "link" - def get_nodes(self, filters=None, with_name=False): + def get_nodes(self, filters=None, with_obj=True, with_name=False): """Filter the nodes from the graph.""" nodes = self.nodes if filters is not None: for flt in filters: nodes = list(filter(flt, nodes)) - if with_name: + if with_obj and with_name: return [(node, self.get_node_obj(node)) for node in nodes] - return [self.get_node_obj(node) for node in nodes] + if with_obj: + return [self.get_node_obj(node) for node in nodes] + return nodes - def get_edges(self, filters=None, with_name=False): + def get_edges(self, filters=None, with_obj=True, with_name=False): """Filter the edges from the graph.""" edges = self.edges if filters is not None: for flt in filters: edges = list(filter(flt, edges)) - if with_name: + if with_obj and with_name: return [(edge, self.get_edge_obj(edge)) for edge in edges] - return [self.get_edge_obj(edge) for edge in edges] + if with_obj: + return [self.get_edge_obj(edge) for edge in edges] + return edges - def get_edges_from(self, node, filters=None, with_name=False): + def get_edges_from(self, node, filters=None, with_obj=True, with_name=False): """Return the outgoing edges from the node.""" if filters is None: filters = [] filters = [lambda e: e[0] == node] + filters - return self.get_edges(filters=filters, with_name=with_name) + return self.get_edges(filters=filters, with_obj=with_obj, with_name=with_name) - def get_edges_to(self, node, filters=None, with_name=False): + def get_edges_to(self, node, filters=None, with_obj=True, with_name=False): """Return the incoming edges to the node.""" if filters is None: filters = [] filters = [lambda e: e[1] == node] + filters - return self.get_edges(filters=filters, with_name=with_name) + return self.get_edges(filters=filters, with_obj=with_obj, with_name=with_name) - def get_edges_of(self, node, filters=None, with_name=False): + def get_edges_of(self, node, filters=None, with_obj=True, with_name=False): """Return the edges of the node.""" if filters is None: filters = [] filters = [lambda e: node in e] + filters - return self.get_edges(filters=filters, with_name=with_name) + return self.get_edges(filters=filters, with_obj=with_obj, with_name=with_name) - def get_ni_nodes(self, with_name=False): + def get_ni_nodes(self, with_obj=True, with_name=False): """Return the ni nodes.""" - return self.get_nodes(filters=[self.is_ni_node], with_name=with_name) + return self.get_nodes(filters=[self.is_ni_node], with_obj=with_obj, with_name=with_name) - def get_rt_nodes(self, with_name=False): + def get_rt_nodes(self, with_obj=True, with_name=False): """Return the router nodes.""" - return self.get_nodes(filters=[self.is_rt_node], with_name=with_name) + return self.get_nodes(filters=[self.is_rt_node], with_obj=with_obj, with_name=with_name) - def get_ep_nodes(self, with_name=False): + def get_ep_nodes(self, with_obj=True, with_name=False): """Return the endpoint nodes.""" - return self.get_nodes(filters=[self.is_ep_node], with_name=with_name) + return self.get_nodes(filters=[self.is_ep_node], with_obj=with_obj, with_name=with_name) - def get_prot_edges(self, with_name=False): + def get_prot_edges(self, with_obj=True, with_name=False): """Return the protocol edges.""" - return self.get_edges(filters=[self.is_prot_edge], with_name=with_name) + return self.get_edges(filters=[self.is_prot_edge], with_obj=with_obj, with_name=with_name) - def get_link_edges(self, with_name=False): + def get_link_edges(self, with_obj=True, with_name=False): """Return the link edges.""" - return self.get_edges(filters=[self.is_link_edge], with_name=with_name) + return self.get_edges(filters=[self.is_link_edge], with_obj=with_obj, with_name=with_name) def get_nodes_from_range(self, node: str, rng: List[Tuple[int]]): """Return the nodes from the range.""" @@ -282,8 +287,28 @@ def add_nodes_as_array( def create_unique_ep_id(self, node) -> int: """Return the endpoint id.""" ep_nodes = [name for name, _ in self.get_ep_nodes(with_name=True)] - return sorted(ep_nodes).index(node) - def get_node_id(self, node): + # Custom sorting function: extract numeric part after letters + def extract_number(name): + match = re.search(r"(\d+)$", name) + return int(match.group(1)) if match else -1 + + return sorted( + ep_nodes, key=lambda name: (re.sub(r"\d+$", "", name), extract_number(name)) + ).index(node) + + def get_node_id(self, node_name=None, node_obj=None): """Return the node id.""" - return self.nodes[node]["id"] + if node_name is not None: + return self.nodes[node_name]["id"] + if node_obj is not None: + return self.nodes[node_obj.name]["id"] + raise ValueError("Node name or object not provided") + + def get_node_uid(self, node_name=None, node_obj=None): + """Return the unique node id.""" + if node_name is not None: + return self.nodes[node_name]["uid"] + if node_obj is not None: + return self.nodes[node_obj.name]["uid"] + raise ValueError("Node name or object not provided") diff --git a/floogen/model/network.py b/floogen/model/network.py index 01eca474..81024141 100644 --- a/floogen/model/network.py +++ b/floogen/model/network.py @@ -304,15 +304,16 @@ def get_ni_of_ep(ep): def compile_ids(self): """Infer the id type from the network.""" + # Add XY coordinates to the nodes match self.routing.route_algo: case RouteAlgo.XY: # 1st stage: Get all router nodes for node_name, node in self.graph.get_rt_nodes(with_name=True): x, y = self.graph.get_node_arr_idx(node_name) - node_id = Coord(x=x, y=y) - if node.id_offset is not None: - node_id += node.id_offset - self.graph.nodes[node_name]["id"] = node_id + node_xy_id = Coord(x=x, y=y) + if node.xy_id_offset is not None: + node_xy_id += node.xy_id_offset + self.graph.nodes[node_name]["id"] = node_xy_id for node_name, node in self.graph.get_ni_nodes(with_name=True): # Search for a neighbor node *with* an array index for neighbor in self.graph.neighbors(node_name): @@ -320,32 +321,37 @@ def compile_ids(self): # If it has a directed edge, we can derive the coordinate from there edge = self.graph.edges[(node_name, neighbor)] if edge["dst_dir"] is not None: - node_id = self.graph.nodes[neighbor]["id"] + XYDirections.to_coords( - edge["dst_dir"] - ) + node_xy_id = self.graph.nodes[neighbor][ + "id" + ] + XYDirections.to_coords(edge["dst_dir"]) break edge = self.graph.edges[(neighbor, node_name)] if edge["src_dir"] is not None: - node_id = self.graph.nodes[neighbor]["id"] + XYDirections.to_coords( - edge["src_dir"] - ) + node_xy_id = self.graph.nodes[neighbor][ + "id" + ] + XYDirections.to_coords(edge["src_dir"]) break - assert node_id is not None - if node.id_offset is not None: - node_id += node.id_offset - self.graph.nodes[node_name]["id"] = node_id + assert node_xy_id is not None + if node.xy_id_offset is not None: + node_xy_id += node.xy_id_offset + self.graph.nodes[node_name]["id"] = node_xy_id case RouteAlgo.ID | RouteAlgo.SRC: for ep_name, ep in self.graph.get_ep_nodes(with_name=True): node_id = SimpleId(id=self.graph.create_unique_ep_id(ep_name)) - if ep.id_offset is not None: - node_id += ep.id_offset ni_name = ep.get_ni_name(ep_name) self.graph.nodes[ep_name]["id"] = node_id self.graph.nodes[ni_name]["id"] = node_id + # Add unique IDs (uid's) to the nodes + for ep_name, ep in self.graph.get_ep_nodes(with_name=True): + node_id = SimpleId(id=self.graph.create_unique_ep_id(ep_name)) + ni_name = ep.get_ni_name(ep_name) + self.graph.nodes[ep_name]["uid"] = node_id + self.graph.nodes[ni_name]["uid"] = node_id + def compile_links(self): """Infer the link type from the network.""" - for edge, _ in self.graph.get_link_edges(with_name=True): + for edge in self.graph.get_link_edges(with_obj=False, with_name=True): # Check if link is bidirectional is_bidirectional = self.graph.has_edge(edge[1], edge[0]) link = { @@ -484,7 +490,8 @@ def compile_nis(self): "endpoint": ep_desc, "routing": self.routing, "addr_range": ep_desc.addr_range.model_copy() if ep_desc.addr_range else None, - "id": self.graph.get_node_id(ni_name).model_copy(), + "id": self.graph.get_node_id(node_name=ni_name).model_copy(), + "uid": self.graph.get_node_uid(node_name=ni_name).model_copy(), } assert ep_desc @@ -603,7 +610,7 @@ def gen_xy_routing_info(self): xy_routing_info["num_x_bits"] = clog2(max_x - min_x + 1) xy_routing_info["num_y_bits"] = clog2(max_y - min_y + 1) xy_routing_info["addr_offset_bits"] = clog2(max_address) - xy_routing_info["id_offset"] = Coord(x=min_x, y=min_y) + xy_routing_info["xy_id_offset"] = Coord(x=min_x, y=min_y) return xy_routing_info def gen_routes(self): @@ -645,8 +652,8 @@ def gen_sam(self): ni_sbr_nodes = reversed([ni for ni in self.graph.get_ni_nodes() if ni.is_sbr()]) for ni in ni_sbr_nodes: dest = ni.id - if self.routing.id_offset is not None: - dest -= self.routing.id_offset + if self.routing.xy_id_offset is not None: + dest -= self.routing.xy_id_offset addr_range = ni.addr_range addr_rule = RouteMapRule(dest=dest, addr_range=addr_range, desc=ni.name) addr_table.append(addr_rule) @@ -713,7 +720,11 @@ def render_routers(self): def render_ni_tables(self): """Render the network interfaces tables in the generated code.""" string = "" - sorted_ni_list = sorted(self.graph.get_ni_nodes(), key=lambda ni: ni.id.id, reverse=True) + sorted_ni_list = sorted( + self.graph.get_ni_nodes(), + key=lambda ni: self.graph.get_node_id(node_obj=ni), + reverse=True + ) for ni in sorted_ni_list: string += ni.table.render( num_route_bits=self.routing.num_route_bits, no_decl=True) @@ -735,11 +746,9 @@ def render_nis(self): def render_ep_enum(self): """Render the endpoint enum in the generated code.""" - match self.routing.route_algo: - case RouteAlgo.XY: - fields_dict = {ep.name: i for i, ep in enumerate(self.graph.get_ni_nodes())} - case RouteAlgo.ID | RouteAlgo.SRC: - fields_dict = {ep.name: ep.id.id for ep in self.graph.get_ni_nodes()} + fields_dict = { + ep.name: self.graph.get_node_uid(node_obj=ep).id for ep in self.graph.get_ni_nodes() + } fields_dict = dict(sorted(fields_dict.items(), key=lambda item: item[1])) fields_dict["num_endpoints"] = len(fields_dict) return sv_enum_typedef(name="ep_id_e", fields_dict=fields_dict) @@ -750,8 +759,8 @@ def render_network(self): def visualize(self, savefig=True, filename: pathlib.Path = "network.png"): """Visualize the network graph.""" - ni_nodes = [name for name, _ in self.graph.get_ni_nodes(with_name=True)] - router_nodes = [name for name, _ in self.graph.get_rt_nodes(with_name=True)] + ni_nodes = self.graph.get_ni_nodes(with_obj=False, with_name=True) + router_nodes = self.graph.get_rt_nodes(with_obj=False, with_name=True) filtered_graph = self.graph.subgraph(ni_nodes + router_nodes) nx.draw(filtered_graph, with_labels=True) if savefig: diff --git a/floogen/model/network_interface.py b/floogen/model/network_interface.py index 60c2f1ea..82b60c4e 100644 --- a/floogen/model/network_interface.py +++ b/floogen/model/network_interface.py @@ -11,7 +11,7 @@ from pydantic import BaseModel from mako.lookup import Template -from floogen.model.routing import Id, AddrRange, Routing, RouteMap +from floogen.model.routing import Id, SimpleId, AddrRange, Routing, RouteMap from floogen.model.protocol import AXI4 from floogen.model.link import NarrowWideLink, AxiLink from floogen.model.endpoint import EndpointDesc @@ -27,6 +27,7 @@ class NetworkInterface(BaseModel): routing: Routing table: Optional[RouteMap] = None id: Optional[Id] = None + uid: Optional[SimpleId] = None arr_idx: Optional[Id] = None addr_range: Optional[AddrRange] = None diff --git a/floogen/model/router.py b/floogen/model/router.py index da7901b8..0c5c8581 100644 --- a/floogen/model/router.py +++ b/floogen/model/router.py @@ -26,7 +26,7 @@ class RouterDesc(BaseModel): name: str array: Optional[Union[Tuple[int], Tuple[int, int]]] = None tree: Optional[List[int]] = None - id_offset: Optional[Id] = None + xy_id_offset: Optional[Id] = None auto_connect: Optional[bool] = True degree: Optional[int] = None diff --git a/floogen/model/routing.py b/floogen/model/routing.py index 25707a2d..20bb73d5 100644 --- a/floogen/model/routing.py +++ b/floogen/model/routing.py @@ -440,7 +440,7 @@ class Routing(BaseModel): sam: Optional[RouteMap] = None table: Optional[RouteMap] = None addr_offset_bits: Optional[int] = None - id_offset: Optional[Id] = None + xy_id_offset: Optional[Id] = None num_endpoints: Optional[int] = None num_id_bits: Optional[int] = None num_x_bits: Optional[int] = None diff --git a/floogen/templates/floo_axi_chimney.sv.mako b/floogen/templates/floo_axi_chimney.sv.mako index a6952e79..ef6f4d12 100644 --- a/floogen/templates/floo_axi_chimney.sv.mako +++ b/floogen/templates/floo_axi_chimney.sv.mako @@ -1,5 +1,5 @@ <%! from floogen.utils import snake_to_camel, bool_to_sv %>\ -<% actual_xy_id = ni.id - ni.routing.id_offset if ni.routing.id_offset is not None else ni.id %>\ +<% actual_xy_id = ni.id - ni.routing.xy_id_offset if ni.routing.xy_id_offset is not None else ni.id %>\ <% in_prot = next((prot for prot in noc.protocols if prot.direction == "input"), None) %>\ <% out_prot = next((prot for prot in noc.protocols if prot.direction == "output"), None) %>\ diff --git a/floogen/templates/floo_axi_router.sv.mako b/floogen/templates/floo_axi_router.sv.mako index be082255..ac5b88ea 100644 --- a/floogen/templates/floo_axi_router.sv.mako +++ b/floogen/templates/floo_axi_router.sv.mako @@ -4,7 +4,7 @@ <% def camelcase(s): return ''.join(x.capitalize() or '_' for x in s.split('_')) %>\ -<% offset_xy_id = router.id - network.routing.id_offset if network.routing.id_offset is not None else router.id %>\ +<% offset_xy_id = router.id - network.routing.xy_id_offset if network.routing.xy_id_offset is not None else router.id %>\ <% req_type = next(d for d in router.incoming if d is not None).req_type %>\ <% rsp_type = next(d for d in router.incoming if d is not None).rsp_type %>\ % if router.route_algo == RouteAlgo.ID: diff --git a/floogen/templates/floo_nw_chimney.sv.mako b/floogen/templates/floo_nw_chimney.sv.mako index 8482007c..0a21e6f9 100644 --- a/floogen/templates/floo_nw_chimney.sv.mako +++ b/floogen/templates/floo_nw_chimney.sv.mako @@ -1,5 +1,5 @@ <%! from floogen.utils import snake_to_camel, bool_to_sv %>\ -<% actual_xy_id = ni.id - ni.routing.id_offset if ni.routing.id_offset is not None else ni.id %>\ +<% actual_xy_id = ni.id - ni.routing.xy_id_offset if ni.routing.xy_id_offset is not None else ni.id %>\ <% narrow_in_prot = next((prot for prot in noc.protocols if prot.type == "narrow" and prot.direction == "input"), None) %>\ <% narrow_out_prot = next((prot for prot in noc.protocols if prot.type == "narrow" and prot.direction == "output"), None) %>\ <% wide_in_prot = next((prot for prot in noc.protocols if prot.type == "wide" and prot.direction == "input"), None) %>\ diff --git a/floogen/templates/floo_nw_router.sv.mako b/floogen/templates/floo_nw_router.sv.mako index 28f21d5f..34b0c4a3 100644 --- a/floogen/templates/floo_nw_router.sv.mako +++ b/floogen/templates/floo_nw_router.sv.mako @@ -4,7 +4,7 @@ <% def camelcase(s): return ''.join(x.capitalize() or '_' for x in s.split('_')) %>\ -<% offset_xy_id = router.id - network.routing.id_offset if network.routing.id_offset is not None else router.id %>\ +<% offset_xy_id = router.id - network.routing.xy_id_offset if network.routing.xy_id_offset is not None else router.id %>\ <% req_type = next(d for d in router.incoming if d is not None).req_type %>\ <% rsp_type = next(d for d in router.incoming if d is not None).rsp_type %>\ <% wide_type = next(d for d in router.incoming if d is not None).wide_type %>\