Skip to content

Commit 1f0d91a

Browse files
Network specification and start hops modeling (#31)
* Introduces a new node called `Array`, which can be used to create an array of components within a hierarchical environment, but also allows components to be in a flat organization. * Cleanup of some modeling code. * Create classes to track network statistics. --------- Co-authored-by: tanner-andrulis <andrulis@mit.edu>
1 parent b2d892f commit 1f0d91a

15 files changed

Lines changed: 1004 additions & 82 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ push-arm64:
6666
run-docker:
6767
docker-compose up
6868

69+
clean-notebooks:
70+
nb-clean clean notebooks/*.ipynb
71+
6972
.PHONY: generate-docs
7073
generate-docs:
7174
# pip install sphinx-autobuild sphinx_autodoc_typehints sphinx-copybutton pydata-sphinx-theme

accelforge/frontend/arch/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
Arch.model_rebuild()
1616
Fork.model_rebuild()
1717
Hierarchical.model_rebuild()
18+
Array.model_rebuild()
19+
Network.model_rebuild()
1820

1921
__all__ = [
2022
"Action",
@@ -27,6 +29,7 @@
2729
"Compute",
2830
"Container",
2931
"Fork",
32+
"Array",
3033
"Hierarchical",
3134
"Leaf",
3235
"Memory",

accelforge/frontend/arch/components.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
from accelforge.util._setexpressions import InvertibleSet, eval_set_expression
3030
from accelforge.frontend.renames import TensorName
3131
from accelforge.frontend.arch.constraints import Comparison
32-
from accelforge.frontend.arch.structure import ArchNode, Leaf
33-
from accelforge.frontend.arch.spatialable import Spatialable
32+
from accelforge.frontend.arch.structure import ArchNode, Branch, Leaf
33+
from accelforge.frontend.arch.spatialable import Spatial, Spatialable
3434

3535
from accelforge.util._basetypes import _uninstantiable
3636

@@ -998,3 +998,11 @@ def _render_node_shape(self) -> str:
998998

999999
def _render_node_color(self) -> str:
10001000
return "#E0EEFF"
1001+
1002+
1003+
class Network(Component, Leaf):
1004+
def _render_node_shape(self) -> str:
1005+
return "Msquare"
1006+
1007+
def _render_node_color(self) -> str:
1008+
return "#FAF8C8"

accelforge/frontend/arch/spatialable.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,9 @@ class Spatialable(EvalableModel):
8181
def get_fanout(self) -> int:
8282
"""The spatial fanout of this node."""
8383
return int(math.prod(x.fanout for x in self.spatial))
84+
85+
def _spatial_str(self, include_newline=True) -> str:
86+
if not self.spatial:
87+
return ""
88+
result = ", ".join(f"{s.fanout}× {s.name}" for s in self.spatial)
89+
return f"\n[{result}]" if include_newline else result

accelforge/frontend/arch/structure.py

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import deepcopy
12
import math
23
from typing import (
34
Any,
@@ -19,6 +20,8 @@
1920

2021
from accelforge.util.exceptions import EvaluationError
2122

23+
from accelforge.frontend.arch.spatialable import Spatialable
24+
2225
from pydantic import Discriminator
2326
from accelforge.util._basetypes import _uninstantiable
2427
from accelforge.util.parallel import _SVGJupyterRender
@@ -68,6 +71,29 @@ def find(self, name: str, default: Any = _FIND_SENTINEL) -> Union["Leaf", Any]:
6871
return default
6972
raise ValueError(f"Leaf {name} not found in {self}")
7073

74+
def iterate_hierarchically(self, _parents=None):
75+
"""
76+
Iterates over all Leaf nodes while also yielding the list of all Leaf
77+
nodes that are hierarchical parents over the current node.
78+
"""
79+
if _parents is None:
80+
_parents = []
81+
82+
if isinstance(self, Leaf):
83+
yield self, _parents
84+
_parents.append(self)
85+
return
86+
87+
assert isinstance(self, Branch)
88+
if isinstance(self, (Fork, Array)):
89+
for node in self.nodes:
90+
yield from node.iterate_hierarchically(list(_parents))
91+
elif isinstance(self, Hierarchical):
92+
for node in self.nodes:
93+
yield from node.iterate_hierarchically(_parents)
94+
else:
95+
raise RuntimeError("unhandled structure type")
96+
7197
def _render_node_name(self) -> str:
7298
"""The name for a Pydot node."""
7399
return f"{self.__class__.__name__}_{id(self)}"
@@ -137,12 +163,6 @@ def __str__(self) -> str:
137163
result = f"{result} [{spatial_str}]"
138164
return result
139165

140-
def _spatial_str(self, include_newline=True) -> str:
141-
if not self.spatial:
142-
return ""
143-
result = ", ".join(f"{s.fanout}× {s.name}" for s in self.spatial)
144-
return f"\n[{result}]" if include_newline else result
145-
146166
def _render_node_label(self) -> str:
147167
return f"{self.name}" + self._spatial_str()
148168

@@ -162,7 +182,9 @@ class Branch(ArchNode):
162182
Annotated["Memory", Tag("Memory")],
163183
Annotated["Toll", Tag("Toll")],
164184
Annotated["Container", Tag("Container")],
185+
Annotated["Network", Tag("Network")],
165186
Annotated["Hierarchical", Tag("Hierarchical")],
187+
Annotated["Array", Tag("Array")],
166188
Annotated["Fork", Tag("Fork")],
167189
],
168190
Discriminator(_get_tag),
@@ -235,6 +257,94 @@ def _power_gating(
235257
return result, non_power_gated_porp
236258

237259

260+
class Array(Branch, Spatialable):
261+
def model_post_init(self, __context__=None) -> None:
262+
for node in self.nodes:
263+
if isinstance(node, Fork):
264+
raise EvaluationError("cannot have fork inside array")
265+
266+
def _flatten(
267+
self,
268+
compute_node: str,
269+
fanout: int = 1,
270+
return_fanout: bool = False,
271+
):
272+
from accelforge.frontend.arch.components import Compute
273+
nodes = []
274+
275+
for node in self.nodes:
276+
try:
277+
if isinstance(node, Branch):
278+
raise RuntimeError("do not put branches inside array")
279+
elif isinstance(node, Leaf):
280+
fanout *= node.get_fanout()
281+
node = deepcopy(node)
282+
node.spatial = EvalableList()
283+
nodes.append(node)
284+
else:
285+
raise RuntimeError(f"unhandled structure type {node}")
286+
except EvaluationError as e:
287+
e.add_field(node)
288+
raise e
289+
290+
if return_fanout:
291+
return nodes, fanout
292+
return nodes
293+
294+
def _render_node_label(self) -> str:
295+
return f"Array {self._spatial_str()}"
296+
297+
def _render_node_color(self) -> str:
298+
return "#FCC2FC"
299+
300+
def _parent2child_names(
301+
self, parent_name: str = None
302+
) -> tuple[list[tuple[str, str, str]], str]:
303+
from accelforge.frontend.arch.components import Compute
304+
305+
edges = []
306+
current_parent_name = parent_name
307+
308+
for node in self.nodes:
309+
if isinstance(node, Branch):
310+
raise EvaluationError("do not put branch in array")
311+
elif isinstance(node, Compute):
312+
# Compute nodes branch off to the side like a Fork
313+
if current_parent_name is not None:
314+
edges.append((current_parent_name, node._render_node_name(), "dashed"))
315+
else:
316+
if current_parent_name is not None:
317+
edges.append((current_parent_name, node._render_node_name(), "dashed"))
318+
return edges, self._render_node_name()
319+
320+
def _render_make_children(self) -> list[pydot.Node]:
321+
"""Renders only children, not the Hierarchical node itself."""
322+
result = [self._render_node()]
323+
for node in self.nodes:
324+
children = node._render_make_children()
325+
result.extend(child for child in children if child is not None)
326+
return result
327+
328+
def render(self) -> str:
329+
"""Renders the architecture as a Pydot graph."""
330+
graph = _pydot_graph()
331+
332+
# Render all nodes (Hierarchical nodes return None and are filtered out)
333+
for node in self._render_make_children():
334+
if node is not None:
335+
graph.add_node(node)
336+
337+
# Add edges
338+
edges, _ = self._parent2child_names()
339+
for parent_name, child_name in edges:
340+
graph.add_edge(pydot.Edge(parent_name, child_name))
341+
342+
return _SVGJupyterRender(graph.create_svg(prog="dot").decode("utf-8"))
343+
344+
def _repr_svg_(self) -> str:
345+
return self.render()
346+
347+
238348
class Hierarchical(Branch):
239349
def _flatten(
240350
self,
@@ -265,6 +375,18 @@ def _flatten(
265375
):
266376
break
267377
assert not isinstance(node, Fork)
378+
elif isinstance(node, Array):
379+
new_nodes = node._flatten(
380+
compute_node, fanout, return_fanout=False
381+
)
382+
nodes.extend(new_nodes)
383+
nodes.append(node)
384+
fanout *= node.get_fanout()
385+
if any(
386+
isinstance(n, Compute) and n.name == compute_node
387+
for n in new_nodes
388+
):
389+
break
268390
elif isinstance(node, Compute):
269391
if node.name == compute_node:
270392
fanout *= node.get_fanout()
@@ -299,23 +421,27 @@ def _parent2child_names(
299421
edges.extend(child_edges)
300422
if last_child_name is not None:
301423
current_parent_name = last_child_name
424+
elif isinstance(node, Array):
425+
child_edges, last_child_name = node._parent2child_names(
426+
node._render_node_name()
427+
)
428+
edges.extend(child_edges)
429+
edges.append((current_parent_name, last_child_name, "solid"))
430+
if last_child_name is not None:
431+
current_parent_name = last_child_name
302432
elif isinstance(node, Compute):
303433
# Compute nodes branch off to the side like a Fork
304434
if current_parent_name is not None:
305-
edges.append((current_parent_name, node._render_node_name()))
435+
edges.append((current_parent_name, node._render_node_name(), "solid"))
306436
else:
307437
if current_parent_name is not None:
308-
edges.append((current_parent_name, node._render_node_name()))
438+
edges.append((current_parent_name, node._render_node_name(), "solid"))
309439

310440
# Update parent for next iteration
311441
current_parent_name = node._render_node_name()
312442

313443
return edges, current_parent_name
314444

315-
def _render_node(self) -> pydot.Node:
316-
"""Hierarchical nodes should not be rendered."""
317-
return None
318-
319445
def _render_make_children(self) -> list[pydot.Node]:
320446
"""Renders only children, not the Hierarchical node itself."""
321447
result = []
@@ -335,8 +461,8 @@ def render(self) -> str:
335461

336462
# Add edges
337463
edges, _ = self._parent2child_names()
338-
for parent_name, child_name in edges:
339-
graph.add_edge(pydot.Edge(parent_name, child_name))
464+
for parent_name, child_name, style in edges:
465+
graph.add_edge(pydot.Edge(parent_name, child_name, style=style))
340466

341467
return _SVGJupyterRender(graph.create_svg(prog="dot").decode("utf-8"))
342468

accelforge/frontend/spec.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from accelforge.frontend.mapper import FFM
44
from accelforge.frontend.renames import EinsumName, Renames
55
from accelforge.util._eval_expressions import EvaluationError, ParseExpressionsContext
6-
from accelforge.frontend.arch import Compute, Leaf, Component, Arch, Container
6+
from accelforge.frontend.arch import Compute, Leaf, Component, Arch, Container, Spatialable
77

88
from accelforge.frontend.workload import Workload
99
from accelforge.frontend.variables import Variables
@@ -194,7 +194,6 @@ def calculate_component_area_energy_latency_leak(
194194
if einsum_name is None and len(self.workload.einsums) > 0:
195195
einsum_name = self.workload.einsums[0].name
196196

197-
components = set()
198197
try:
199198
if not getattr(self, "_evaluated", False):
200199
self = self._spec_eval_expressions(einsum_name=einsum_name)
@@ -211,36 +210,37 @@ def calculate_component_area_energy_latency_leak(
211210
)
212211
raise
213212

214-
for arch in self._get_flattened_architecture():
215-
fanout = 1
216-
for component in arch:
217-
fanout *= component.get_fanout()
218-
if component.name in components or isinstance(component, Container):
219-
continue
220-
assert isinstance(component, Component)
221-
components.add(component.name)
222-
orig: Component = self.arch.find(component.name)
223-
c = component
224-
if area:
225-
c = c.calculate_area(models)
226-
orig.area = c.area
227-
orig.total_area = c.area * fanout
228-
if energy:
229-
c = c.calculate_action_energy(models)
230-
for a in c.actions:
231-
orig_action = orig.actions[a.name]
232-
orig_action.energy = a.energy
233-
if latency:
234-
c = c.calculate_action_latency(models)
235-
for a in c.actions:
236-
orig_action = orig.actions[a.name]
237-
orig_action.latency = a.latency
238-
if leak:
239-
c = c.calculate_leak_power(models)
240-
orig.leak_power = c.leak_power
241-
orig.total_leak_power = c.leak_power * fanout
242-
orig.component_modeling_log.extend(c.component_modeling_log)
243-
orig.component_model = c.component_model
213+
for leaf, parents in self.arch.iterate_hierarchically():
214+
if not isinstance(leaf, Component):
215+
continue
216+
217+
global_fanout = 1
218+
for p in parents:
219+
if isinstance(p, Spatialable):
220+
global_fanout *= p.get_fanout()
221+
222+
orig: Component = self.arch.find(leaf.name)
223+
c = leaf
224+
if area:
225+
c = c.calculate_area(models)
226+
orig.area = c.area
227+
orig.total_area = c.area * global_fanout
228+
if energy:
229+
c = c.calculate_action_energy(models)
230+
for a in c.actions:
231+
orig_action = orig.actions[a.name]
232+
orig_action.energy = a.energy
233+
if latency:
234+
c = c.calculate_action_latency(models)
235+
for a in c.actions:
236+
orig_action = orig.actions[a.name]
237+
orig_action.latency = a.latency
238+
if leak:
239+
c = c.calculate_leak_power(models)
240+
orig.leak_power = c.leak_power
241+
orig.total_leak_power = c.leak_power * global_fanout
242+
orig.component_modeling_log.extend(c.component_modeling_log)
243+
orig.component_model = c.component_model
244244

245245
return self
246246

accelforge/mapper/FFM/_join_pmappings/join_pmappings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,8 @@ def no_match_lookahead_error(
618618
# ======================================================================
619619
# If we delayed the mapping merging, do it now.
620620
# ======================================================================
621+
import copy
622+
prev_combined = copy.deepcopy(combined)
621623
if DELAY:
622624
mappings = parallel(
623625
[c.mappings for c in combined],

0 commit comments

Comments
 (0)