Skip to content

Commit 7eac827

Browse files
committed
compiler: Fix topofusion to honour the closest scope
1 parent 6257970 commit 7eac827

2 files changed

Lines changed: 101 additions & 83 deletions

File tree

devito/passes/clusters/fusion.py

Lines changed: 97 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import Counter, defaultdict
2+
from functools import cached_property
23
from itertools import groupby
34

45
from devito.finite_differences import IndexDerivative
@@ -8,7 +9,7 @@
89
)
910
from devito.symbolics import search
1011
from devito.tools import (
11-
DAG, as_tuple, flatten, frozendict, memoized_func, memoized_meth, timed_pass
12+
DAG, CacheInstances, as_tuple, flatten, frozendict, memoized_func, timed_pass
1213
)
1314

1415
__all__ = ['fuse']
@@ -50,32 +51,92 @@ def _fusion_hazards(scope0, scope1, prefix):
5051
return NO_HAZARD
5152

5253

53-
class Key(tuple):
54+
class Keys(CacheInstances):
5455

5556
"""
56-
A "fusion Key" for a Cluster (ClusterGroup) is a hashable tuple such that
57-
two Clusters (ClusterGroups) are topo-fusible if and only if their Key is
58-
identical.
59-
60-
A Key contains elements that can logically be split into two groups -- the
61-
`strict` and the `weak` components of the Key. Two Clusters (ClusterGroups)
62-
having same `strict` but different `weak` parts are, by definition, not
63-
fusible; however, since at least their `strict` parts match, they can at
64-
least be topologically reordered.
57+
Provide different kind of keys for Clusters (ClusterGroups) to be used in
58+
topological reordering and fusion.
6559
"""
6660

67-
def __new__(cls, itintervals, guards, syncs, weak):
68-
strict = [itintervals, guards, syncs]
69-
obj = super().__new__(cls, strict + weak)
61+
def __init__(self, c, fuse_tasks):
62+
self.c = c
63+
self.fuse_tasks = fuse_tasks
7064

71-
obj.itintervals = itintervals
72-
obj.guards = guards
73-
obj.syncs = syncs
65+
@property
66+
def first(self):
67+
return self.c[0]
7468

75-
obj.strict = tuple(strict)
76-
obj.weak = tuple(weak)
69+
@property
70+
def last(self):
71+
return self.c[-1]
7772

78-
return obj
73+
@cached_property
74+
def itintervals(self):
75+
return self.c.ispace.itintervals
76+
77+
@cached_property
78+
def guards(self):
79+
return self.c.guards if any(self.c.guards) else None
80+
81+
@cached_property
82+
def syncs(self):
83+
mapper = defaultdict(set)
84+
for d, v in self.c.syncs.items():
85+
for s in v:
86+
if isinstance(s, PrefetchUpdate):
87+
continue
88+
elif isinstance(s, WaitLock) and not self.fuse_tasks:
89+
# NOTE: A mix of Clusters w/ and w/o WaitLocks can safely
90+
# be fused, as in the worst case scenario the WaitLocks
91+
# get "hoisted" above the first Cluster in the sequence
92+
continue
93+
elif isinstance(s, (InitArray, SyncArray, WaitLock, ReleaseLock)):
94+
mapper[d].add(type(s))
95+
elif isinstance(s, WithLock) and self.fuse_tasks:
96+
# NOTE: Different WithLocks aren't fused unless the user
97+
# explicitly asks for it
98+
mapper[d].add(type(s))
99+
else:
100+
mapper[d].add(s)
101+
if d in mapper:
102+
mapper[d] = frozenset(mapper[d])
103+
return frozendict(mapper)
104+
105+
@cached_property
106+
def strict(self):
107+
return (self.itintervals, self.guards, self.syncs)
108+
109+
@cached_property
110+
def weak(self):
111+
c = self.c
112+
113+
# Clusters representing HaloTouches should get merged, if possible
114+
weak = [c.is_halo_touch]
115+
116+
# If there are writes to thread-shared object, make it part of the key.
117+
# This will promote fusion of non-adjacent Clusters writing to (some
118+
# form of) shared memory, which in turn will minimize the number of
119+
# necessary barriers. Same story for reads from thread-shared objects
120+
weak.extend([
121+
any(f._mem_shared for f in c.scope.writes),
122+
any(f._mem_shared for f in c.scope.reads)
123+
])
124+
weak.append(c.properties.is_core_init())
125+
126+
# Prefetchable Clusters should get merged, if possible
127+
weak.append(c.is_glb_load_to_mem_shared)
128+
129+
# Promoting adjacency of IndexDerivatives will maximize their reuse
130+
weak.append(any(search(c.exprs, IndexDerivative)))
131+
132+
# Promote adjacency of Clusters with same guard
133+
weak.append(c.guards)
134+
135+
return tuple(weak)
136+
137+
@cached_property
138+
def full(self):
139+
return self.strict + self.weak
79140

80141

81142
class Fusion(Queue):
@@ -91,7 +152,7 @@ def __init__(self, toposort, options=None):
91152
options = options or {}
92153

93154
self.toposort = toposort
94-
self.fusetasks = options.get('fuse-tasks', False)
155+
self.fuse_tasks = options.get('fuse-tasks', False)
95156

96157
super().__init__()
97158

@@ -111,8 +172,9 @@ def callback(self, cgroups, prefix):
111172
clusters = ClusterGroup(cgroups)
112173

113174
# Fusion
175+
key = lambda c: self._key(c).full
114176
processed = []
115-
for _, group in groupby(clusters, key=self._key):
177+
for _, group in groupby(clusters, key=key):
116178
g = list(group)
117179

118180
for maybe_fusible in self._apply_heuristics(g):
@@ -134,60 +196,8 @@ def callback(self, cgroups, prefix):
134196
else:
135197
return [ClusterGroup(processed, prefix)]
136198

137-
@memoized_meth
138199
def _key(self, c):
139-
itintervals = frozenset(c.ispace.itintervals)
140-
guards = c.guards if any(c.guards) else None
141-
142-
# We allow fusing Clusters/ClusterGroups even in presence of WaitLocks and
143-
# WithLocks, but not with any other SyncOps
144-
mapper = defaultdict(set)
145-
for d, v in c.syncs.items():
146-
for s in v:
147-
if isinstance(s, PrefetchUpdate):
148-
continue
149-
elif isinstance(s, WaitLock) and not self.fusetasks:
150-
# NOTE: A mix of Clusters w/ and w/o WaitLocks can safely
151-
# be fused, as in the worst case scenario the WaitLocks
152-
# get "hoisted" above the first Cluster in the sequence
153-
continue
154-
elif isinstance(s, (InitArray, SyncArray, WaitLock, ReleaseLock)):
155-
mapper[d].add(type(s))
156-
elif isinstance(s, WithLock) and self.fusetasks:
157-
# NOTE: Different WithLocks aren't fused unless the user
158-
# explicitly asks for it
159-
mapper[d].add(type(s))
160-
else:
161-
mapper[d].add(s)
162-
if d in mapper:
163-
mapper[d] = frozenset(mapper[d])
164-
syncs = frozendict(mapper)
165-
166-
# Clusters representing HaloTouches should get merged, if possible
167-
weak = [c.is_halo_touch]
168-
169-
# If there are writes to thread-shared object, make it part of the key.
170-
# This will promote fusion of non-adjacent Clusters writing to (some
171-
# form of) shared memory, which in turn will minimize the number of
172-
# necessary barriers. Same story for reads from thread-shared objects
173-
weak.extend([
174-
any(f._mem_shared for f in c.scope.writes),
175-
any(f._mem_shared for f in c.scope.reads)
176-
])
177-
weak.append(c.properties.is_core_init())
178-
179-
# Prefetchable Clusters should get merged, if possible
180-
weak.append(c.is_glb_load_to_mem_shared)
181-
182-
# Promoting adjacency of IndexDerivatives will maximize their reuse
183-
weak.append(any(search(c.exprs, IndexDerivative)))
184-
185-
# Promote adjacency of Clusters with same guard
186-
weak.append(c.guards)
187-
188-
key = Key(itintervals, guards, syncs, weak)
189-
190-
return key
200+
return Keys(c, self.fuse_tasks)
191201

192202
def _apply_heuristics(self, clusters):
193203
# We know at this point that `clusters` are potentially fusible since
@@ -232,6 +242,10 @@ def dump():
232242
return processed
233243

234244
def _toposort(self, cgroups, prefix):
245+
# If not enough ClusterGroups to do anything meaningful, don't waste time
246+
if len(cgroups) <= 2:
247+
return ClusterGroup(cgroups, prefix)
248+
235249
# Are there any ClusterGroups that could potentially be topologically
236250
# reordered? If not, do not waste time
237251
counter = Counter(self._key(cg).strict for cg in cgroups)
@@ -248,13 +262,17 @@ def choose_element(queue, scheduled):
248262
k = self._key(scheduled[-1])
249263
m = {i: self._key(i) for i in queue}
250264

251-
# Process the `strict` part of the key
252-
candidates = [i for i in queue if m[i].itintervals == k.itintervals]
265+
# First of all, ensure we preserve the integrity of the current scope
266+
candidates = [i for i in queue if k.last.ispace == m[i].first.ispace]
253267

254-
compatible = [i for i in candidates if m[i].guards == k.guards]
268+
compatible = [i for i in candidates if k.last.guards == m[i].first.guards]
255269
candidates = compatible or candidates
256270

257-
compatible = [i for i in candidates if m[i].syncs == k.syncs]
271+
# If the current scope is over, we maximize fusion
272+
fusible = [i for i in queue if k.itintervals == m[i].itintervals]
273+
candidates = candidates or fusible
274+
275+
compatible = [i for i in candidates if k.syncs == m[i].syncs]
258276
candidates = compatible or candidates
259277

260278
# Process the `weak` part of the key

tests/test_dimension.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2148,15 +2148,15 @@ def test_topofusion_w_subdims_conddims(self):
21482148
assert exprs[1].write is g
21492149

21502150
exprs = FindNodes(Expression).visit(bns['x1_blk0'])
2151-
assert len(exprs) == 1
2152-
assert exprs[0].write is h
2153-
2154-
exprs = FindNodes(Expression).visit(bns['x2_blk0'])
21552151
assert len(exprs) == 3
21562152
assert isinstance(exprs[0].expr.lhs, Temp)
21572153
assert exprs[1].write is fsave
21582154
assert exprs[2].write is gsave
21592155

2156+
exprs = FindNodes(Expression).visit(bns['x2_blk0'])
2157+
assert len(exprs) == 1
2158+
assert exprs[0].write is h
2159+
21602160
def test_topofusion_w_subdims_conddims_v2(self):
21612161
"""
21622162
Like `test_topofusion_w_subdims_conddims` but with more SubDomains,

0 commit comments

Comments
 (0)