Skip to content

Commit

Permalink
floogen: Use unique IDs for endpoints (#78)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fischeti authored Oct 7, 2024
1 parent 944e529 commit b139479
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions floogen/model/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand Down
75 changes: 50 additions & 25 deletions floogen/model/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#
# Author: Tim Fischer <[email protected]>

import re
from typing import List, Tuple

import networkx as nx
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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")
67 changes: 38 additions & 29 deletions floogen/model/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,48 +304,54 @@ 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):
if self.graph.nodes[neighbor].get("id") is not None:
# 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 = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion floogen/model/network_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion floogen/model/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion floogen/model/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion floogen/templates/floo_axi_chimney.sv.mako
Original file line number Diff line number Diff line change
@@ -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) %>\
Expand Down
2 changes: 1 addition & 1 deletion floogen/templates/floo_axi_router.sv.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion floogen/templates/floo_nw_chimney.sv.mako
Original file line number Diff line number Diff line change
@@ -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) %>\
Expand Down
2 changes: 1 addition & 1 deletion floogen/templates/floo_nw_router.sv.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>\
Expand Down

0 comments on commit b139479

Please sign in to comment.