Skip to content

Commit db5eae4

Browse files
committed
Implement (almost) proper latency model; waiting for hwcomponents latency/bandwidth update
1 parent 847fbd0 commit db5eae4

7 files changed

Lines changed: 106 additions & 49 deletions

File tree

accelforge/frontend/arch/components.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,20 @@ class Network(Component, Leaf):
11741174
of the spatial nodes from top to bottom.
11751175
"""
11761176

1177+
total_latency: str | int | float = "max(max_hops*actions['hops'].latency, max_link_traffic/actions['hops'].latency)"
1178+
"""
1179+
Models latency as either:
1180+
- *Latency-bound*, which means that the latency of the route with the most number of
1181+
hops dominate the overall communication latency.
1182+
- *Bandwidth-bound*, which means that the traffic over the most congested link
1183+
dominates the overall communication latency.
1184+
1185+
Keywords:
1186+
- `max_hops` returns the number of hops in the longest route.
1187+
- `max_link_traffic` returns the amount of traffic (in bits) over the most congested
1188+
link.
1189+
"""
1190+
11771191
bits_per_value: EvalsTo[dict] = {}
11781192
"""
11791193
Sets the bits per value for tensors in this `TensorHolder`. Keys are evaluated as

accelforge/model/_looptree/latency/memory.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from accelforge.model._looptree.reuse.symbolic import BuffetStats
1616
from accelforge.util._eval_expressions import MATH_FUNCS, eval_expression
17-
from accelforge.util._sympy.broadcast_max import Max, Min
17+
from accelforge.util._sympy.broadcast_max import Max, Min, MaxGeqZero
1818
import symengine as se
1919

2020

@@ -47,6 +47,10 @@ def component_latency(
4747
component_to_actions: dict[str, dict[str, float]] = defaultdict(
4848
lambda: defaultdict(lambda: 0)
4949
)
50+
# Holds ``keywords" that do not map neatly to actions, e.g., max_hops for network
51+
component_to_keywords: dict[str, dict[str, float]] = defaultdict(
52+
lambda: defaultdict(lambda: 0)
53+
)
5054
name2component: dict[str, Component] = {node.name: node for node in flattened_arch}
5155

5256
compute_obj = flattened_arch[-1]
@@ -79,16 +83,28 @@ def component_latency(
7983
f"Component {component} is not a TensorHolder or Compute"
8084
)
8185

86+
network_to_max_link_traffic = defaultdict(lambda: defaultdict(lambda: 0))
87+
network_to_max_hops = defaultdict(lambda: [])
8288
for network, network_stats in looptree_results.network_stats.items():
8389
component = network.component
84-
actions = component_to_actions[component]
8590
if component not in name2component:
8691
raise ValueError(f"Component {component} found in mapping but not arch")
8792

88-
for action in name2component[component].actions:
89-
actions[f"{action.name}_actions"] += 0
93+
dim_traffic = network_to_max_link_traffic[component]
94+
for dim, max_traffic_in_dim in network_stats.max_traffic.items():
95+
dim_traffic[dim] += max_traffic_in_dim
9096

91-
actions["hops_actions"] += network_stats.max_hops
97+
network_to_max_hops[component].append(network_stats.max_hops)
98+
99+
for network, network_stats in looptree_results.network_stats.items():
100+
component = network.component
101+
keywords = component_to_keywords[component]
102+
keywords["max_link_traffic"] = MaxGeqZero(
103+
*network_to_max_link_traffic[component].values()
104+
)
105+
keywords["max_hops"] = MaxGeqZero(
106+
*network_to_max_hops[component]
107+
)
92108

93109
longest_compute_latency = Max(
94110
0, *[s.max_latency for s in looptree_results.compute_stats.values()]
@@ -126,14 +142,15 @@ def component_latency(
126142
"sum": se.Add,
127143
}
128144

129-
for component, actions in component_to_actions.items():
130-
component_obj = name2component[component]
145+
for component, component_obj in name2component.items():
146+
actions = component_to_actions[component]
131147
symbol_table = {
132148
"action2latency": component_to_action_latency[component],
133149
**symbol_table_base,
134150
**name2component[component].shallow_model_dump(include_None=True),
135151
**actions,
136152
**component_to_action_latency[component],
153+
**component_to_keywords[component],
137154
}
138155
if name2component[component].total_latency is not None:
139156
component_latency[component] = eval_expression(

accelforge/model/_looptree/reuse/symbolic/_network.py

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import copy
2-
from accelforge.frontend import arch
3-
from accelforge.frontend.arch import Network as NetworkSpec
41
from accelforge.frontend.mapping import (
5-
TensorHolder,
6-
TensorName
2+
Spatial
73
)
84
from accelforge.frontend._workload_isl._symbolic import (
95
compute_dense_tile_occupancy,
@@ -20,7 +16,7 @@
2016

2117
class NetworkAnalyzer:
2218
def __init__(self, network_stats):
23-
self.overall_max_hops = 0
19+
self.overall_max_hops: dict = {}
2420
self.network_stats = network_stats
2521

2622
def accumulate_child_result(
@@ -30,8 +26,9 @@ def accumulate_child_result(
3026
shape_repeats,
3127
einsum_name,
3228
child_shape,
33-
node,
29+
node: Spatial,
3430
):
31+
"""This function is called for every repeated shape."""
3532
flattened_arch = info.job.flattened_arch
3633

3734
for network, child_network_stats in child_result.network_stats.items():
@@ -40,44 +37,37 @@ def accumulate_child_result(
4037
self.network_stats[network] = NetworkStats()
4138
accumulated_network_stats = self.network_stats[network]
4239

43-
accumulated_network_stats.total_hops += (
44-
child_network_stats.total_hops * shape_repeats
45-
)
46-
accumulated_network_stats.max_hops = MaxGeqZero(
47-
accumulated_network_stats.max_hops,
48-
child_network_stats.max_hops,
49-
)
50-
projection = info.einsum_tensor_to_projection[(einsum_name, network.tensor)]
51-
component_object = flattened_arch[network.component]
52-
workload_bpv = info.job.einsum.tensor_accesses[
53-
network.tensor
54-
].bits_per_value
55-
bits_per_value = component_object.bits_per_value.get(
56-
network.tensor, workload_bpv
57-
)
58-
bits_per_action = component_object.bits_per_action
59-
if bits_per_action is not None:
60-
actions_per_value = bits_per_value / bits_per_action
61-
else:
62-
actions_per_value = bits_per_value
63-
volume = (
64-
compute_dense_tile_occupancy(projection, child_shape)
65-
* actions_per_value
66-
)
67-
40+
# We only need to update the summary if the spatial loop is for
41+
# a component higher than the network of interest
6842
if flattened_arch.is_above(node.component, network.component):
43+
accumulated_network_stats.total_hops += (
44+
child_network_stats.total_hops * shape_repeats
45+
)
46+
accumulated_network_stats.max_hops = MaxGeqZero(
47+
accumulated_network_stats.max_hops,
48+
child_network_stats.max_hops,
49+
)
50+
for k, v in child_network_stats.max_traffic.items():
51+
accumulated_network_stats.max_traffic[k] = MaxGeqZero(
52+
accumulated_network_stats.max_traffic.get(k, 0),
53+
v
54+
)
6955
continue
7056

57+
volume = self._get_data_volume(network, einsum_name, info, child_shape)
58+
7159
relevancy = info.tensor_to_relevancy[network.tensor][node.rank_variable]
7260

7361
# The fanout in this dimension in mapping nodes below, i.e., the stride
7462
last_fanout = child_result.fanout.get((node.component, einsum_name), {})
7563
last_fanout = last_fanout.get(node.name, 1)
7664
if isinstance(relevancy, Irrelevant):
65+
# The volume travels through link by link in one axis of the mesh
7766
# Distributed or not, the amount of total cost is the same.
7867
# However, the accesses now come from different physical memories
7968
total_cost = multicast_cost(shape_repeats, last_fanout)*volume
8069
max_hops = shape_repeats*last_fanout
70+
max_traffic = volume
8171
elif isinstance(relevancy, Relevant):
8272
# If distributed, then we bind data as locally as possible in the
8373
# physical buffers
@@ -99,26 +89,56 @@ def accumulate_child_result(
9989
*
10090
volume
10191
)
102-
max_hops = MinGeqZero(shape_repeats*last_fanout, physical_stride)
92+
max_hops = MinGeqZero((n_dsts_per_physical-1)*last_fanout, physical_stride)
93+
max_traffic = (n_dsts_per_physical-1)*volume
10394
else:
10495
total_cost = unicast_cost(shape_repeats, last_fanout)*volume
10596
max_hops = shape_repeats * last_fanout
97+
max_traffic = (shape_repeats-1)*volume
10698
elif isinstance(relevancy, PartiallyRelevant):
10799
raise NotImplementedError()
108100
else:
109101
raise RuntimeError(f"unhandled relevancy type {relevancy}")
110102

111-
# TODO: this is sketchy
112-
self.overall_max_hops += max_hops
103+
# Each subsequent call to this function (i.e., over different iterations of a spatial loop)
104+
# adds more to the max hops
105+
self.overall_max_hops[network] = self.overall_max_hops.get(network, 0) + max_hops
113106

114-
accumulated_network_stats.total_hops += total_cost
107+
accumulated_network_stats.total_hops += (
108+
total_cost + child_network_stats.total_hops*shape_repeats
109+
)
115110
accumulated_network_stats.max_hops = MaxGeqZero(
116111
accumulated_network_stats.max_hops,
117-
self.overall_max_hops + child_network_stats.max_hops,
112+
self.overall_max_hops[network] + child_network_stats.max_hops,
113+
)
114+
accumulated_network_stats.max_traffic[node.name] = MaxGeqZero(
115+
accumulated_network_stats.max_traffic.get(node.name, 0),
116+
max_traffic + child_network_stats.max_traffic.get(node.name, 0)
118117
)
119118

120119
return self.overall_max_hops
121120

121+
def _get_data_volume(self, network, einsum_name, info, child_shape):
122+
flattened_arch = info.job.flattened_arch
123+
projection = info.einsum_tensor_to_projection[(einsum_name, network.tensor)]
124+
component_object = flattened_arch[network.component]
125+
workload_bpv = info.job.einsum.tensor_accesses[
126+
network.tensor
127+
].bits_per_value
128+
bits_per_value = component_object.bits_per_value.get(
129+
network.tensor, workload_bpv
130+
)
131+
bits_per_action = component_object.bits_per_action
132+
if bits_per_action is not None:
133+
actions_per_value = bits_per_value / bits_per_action
134+
else:
135+
actions_per_value = bits_per_value
136+
volume = (
137+
compute_dense_tile_occupancy(projection, child_shape)
138+
* actions_per_value
139+
)
140+
return volume
141+
122142

123143
def multicast_cost(n_dsts, stride):
124144
"""Returns total hops of multicast along a dimension."""

accelforge/model/_looptree/reuse/symbolic/_stats.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
@dataclass
2222
class NetworkStats:
2323
total_hops: Any = field(default=0)
24+
"""Total number of hops overall. Useful to calculate energy."""
2425
max_hops: Any = field(default=0)
26+
"""Longest hops among all routes."""
27+
max_traffic: dict[int | str, Any] = field(default_factory=dict)
28+
"""Maximum traffic occuring on any single link along a dimension."""
2529

2630
def repeat(self, n_repeats):
2731
new = copy.copy(self)
@@ -32,10 +36,6 @@ def repeat(self, n_repeats):
3236
new.total_hops = new.total_hops * n_repeats
3337
return new
3438

35-
def combine(self, other: "NetworkStats"):
36-
self.total_hops += other.total_hops
37-
self.max_hops = max(self.max_hops, other.max_hops)
38-
3939

4040
@dataclass
4141
class BuffetStats:

accelforge/model/run_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def run_model(
4343
)
4444

4545
latency = component_latency(reuse, job.flattened_arch, pmapping, spec)
46+
print(latency)
4647
try:
4748
overall_latency = MaxGeqZero(*latency.values())
4849
except Exception as e:

tests/input_files/networked/hierarchical_1d.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ arch:
2424
name: PeArray
2525
area: 0
2626
leak_power: 0
27+
total_latency: "max_hops"
2728
actions:
28-
- {name: hops, energy: 1, latency: 0}
29+
- {name: hops, energy: 1, latency: 1}
2930

3031
- !Memory
3132
name: Scratchpad

tests/test_network.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def test_hierarchical_1d(self):
125125
* KN
126126
* BITS_PER_VALUE,
127127
)
128+
self.assertEqual(
129+
result.data["Total<SEP>latency"].iloc[0],
130+
4
131+
)
128132

129133
def test_hierarchical(self):
130134
M = 8

0 commit comments

Comments
 (0)