11from collections import Counter , defaultdict
2+ from functools import cached_property
23from itertools import groupby
34
45from devito .finite_differences import IndexDerivative
89)
910from devito .symbolics import search
1011from 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
81142class 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
0 commit comments