11from collections import OrderedDict , defaultdict , namedtuple
22from contextlib import suppress
3- from functools import singledispatch
3+ from functools import cached_property , singledispatch
44
55from sympy import Or
66
77from devito .exceptions import CompilationError
88from devito .ir .iet import (
99 AsyncCall , AsyncCallable , BlankLine , Block , BusyWait , Call , Callable , Conditional ,
10- DummyExpr , FindNodes , List , SyncSpot , SyncSpotRegion , Transformer , derive_parameters ,
11- make_callable
10+ DummyExpr , FindNodes , List , SyncSpot , Transformer , derive_parameters , make_callable
1211)
1312from devito .ir .iet .visitors import LazyVisitor
1413from devito .ir .support import (
2322__all__ = ['Orchestrator' ]
2423
2524
26- Task = namedtuple ('Task' , 'spot guard anchor sync_ops releases' )
25+ Task = namedtuple ('Task' , 'spot guard sync_ops releases' )
26+
27+
28+ class SyncSpotRegion (List ):
29+
30+ """A sequence of SyncSpots treated as one unit during orchestration."""
31+
32+ def __init__ (self , body ):
33+ super ().__init__ (body = body )
34+ assert self .body and all (isinstance (i , SyncSpot ) for i in self .body )
35+
36+ @property
37+ def sync_spots (self ):
38+ return self .body
39+
40+ @cached_property
41+ def optype (self ):
42+ """
43+ The type of the synchronization operation in this region.
44+ """
45+ optypes = {type (op ) for spot in self .sync_spots for op in spot .sync_ops }
46+ assert len (optypes ) == 1 , (
47+ "Expected a SyncSpotRegion to contain exactly one type of "
48+ "synchronization operation"
49+ )
50+ return optypes .pop ()
2751
2852
2953class Orchestrator :
@@ -48,39 +72,29 @@ def _fuse_tasks(self, iet):
4872 if self .npthreads is None :
4973 return iet
5074
51- groups = CollectTasks ().visit (iet )
52-
5375 insertions = defaultdict (list )
5476 subs1 = {}
5577
56- for tasks in groups .values ():
78+ for tasks , anchor in CollectTasks ().visit (iet ):
79+ spots = []
80+ for task in tasks :
81+ if task .guard is None :
82+ # Preserve a local scope when there is no Conditional
83+ scope = Block (body = task .spot .body )
84+ else :
85+ scope = Conditional (task .guard , task .spot .body )
86+ spots .append (SyncSpot (task .sync_ops , body = scope ))
87+
88+ insertions [anchor ].append (SyncSpotRegion (spots ))
5789 subs1 .update ({task .spot : SyncSpot (task .releases ) for task in tasks })
5890
59- # Unguarded tasks form separate groups and can share one SyncSpot
60- if tasks [0 ].guard is None :
61- sync_ops = tuple (op for task in tasks for op in task .sync_ops )
62- sync_ops += tuple (tasks [- 1 ].releases )
63- # Blocks preserve the local scope of each task body
64- body = [Block (body = task .spot .body ) for task in tasks ]
65- subs1 [tasks [- 1 ].spot ] = SyncSpot (sync_ops , body = body )
66- continue
67-
68- spots = [SyncSpot (task .sync_ops ,
69- body = Conditional (task .guard , task .spot .body ))
70- for task in tasks ]
71- insertions [tasks [- 1 ].anchor ].append (SyncSpotRegion (spots ))
72-
7391 if not subs1 :
7492 return iet
7593
7694 # These substitutions cannot be merged because a Transformer does not
7795 # revisit a replacement, while task spots may be nested below an anchor
78- subs0 = {}
79- for anchor , regions in insertions .items ():
80- if isinstance (anchor , SyncSpot ):
81- subs0 [anchor ] = anchor ._rebuild (body = anchor .body + tuple (regions ))
82- else :
83- subs0 [anchor ] = List (body = (anchor , * regions ))
96+ subs0 = {anchor : List (body = (anchor , * regions ))
97+ for anchor , regions in insertions .items ()}
8498
8599 iet = Transformer (subs0 ).visit (iet )
86100 iet = Transformer (subs1 ).visit (iet )
@@ -172,19 +186,22 @@ def _make_region(self, iet):
172186 WithLock : withlock
173187 }[iet .optype ]
174188
175- layers = {infer_layer (i .function ) for i in sync_ops }
176- if len (layers ) != 1 :
177- raise CompilationError ("Unsupported streaming case" )
178- layer = layers .pop ()
189+ layer = infer_sync_layer (sync_ops )
179190
180191 body = []
181192 for spot in iet .sync_spots :
182- condition , = spot .body
193+ scope , = spot .body
194+ if isinstance (scope , Conditional ):
195+ task_body = scope .then_body
196+ else :
197+ assert isinstance (scope , Block )
198+ task_body = scope .body
199+
183200 task_body , prefix = callback (
184- layer , List (body = condition . then_body ), spot .sync_ops , self .langbb ,
201+ layer , List (body = task_body ), spot .sync_ops , self .langbb ,
185202 self .sregistry
186203 )
187- body .append (condition ._rebuild (then_body = task_body ))
204+ body .append (scope ._rebuild (task_body ))
188205
189206 return self ._make_async_callable (body , prefix )
190207
@@ -235,10 +252,7 @@ def process(self, iet):
235252 for t in sorted (mapper , key = key ):
236253 sync_ops = mapper [t ]
237254
238- layers = {infer_layer (s .function ) for s in sync_ops }
239- if len (layers ) != 1 :
240- raise CompilationError ("Unsupported streaming case" )
241- layer = layers .pop ()
255+ layer = infer_sync_layer (sync_ops )
242256
243257 n1 , v = callbacks [t ](subs .get (n0 , n0 ), sync_ops , layer )
244258
@@ -265,9 +279,12 @@ class CollectTasks(LazyVisitor):
265279
266280 def _post_visit (self , ret ):
267281 groups = defaultdict (list )
268- for key , task in ret :
282+ anchors = {}
283+ for key , task , anchor in ret :
269284 groups [key ].append (task )
270- return groups
285+ # Activate the group after its last task in program order
286+ anchors [key ] = anchor
287+ return tuple ((tasks , anchors [key ]) for key , tasks in groups .items ())
271288
272289 def visit_Iteration (self , o , ** kwargs ):
273290 kwargs ['iteration' ] = o
@@ -277,7 +294,8 @@ def visit_Conditional(self, o, **kwargs):
277294 kwargs ['condition' ] = o
278295 yield from self ._visit (o .children , ** kwargs )
279296
280- def visit_SyncSpot (self , o , iteration = None , condition = None , anchor = None ):
297+ def visit_SyncSpot (self , o , iteration = None , condition = None ,
298+ in_snapshot = False ):
281299 if iteration is not None :
282300 syncs = as_mapper (o .sync_ops , type )
283301
@@ -297,20 +315,19 @@ def visit_SyncSpot(self, o, iteration=None, condition=None, anchor=None):
297315
298316 gid , = {i .gid for i in sync_ops }
299317 if gid is not None :
300- task = Task (o , guard , anchor or condition ,
301- sync_ops , syncs [ReleaseLock ])
302- key = (iteration , gid , optype , anchor is not None ,
303- condition is not None )
304- yield key , task
318+ task = Task (o , guard , sync_ops , syncs [ReleaseLock ])
319+ key = (iteration , gid , optype , in_snapshot )
320+ yield key , task , condition or o
305321
306322 break
307323
308324 if any (isinstance (i , SnapOut ) for i in o .sync_ops ):
309- # Keep composite task calls inside the `SnapOut` scope
310- anchor = o
325+ # Do not mix composite tasks with other compatible groups
326+ in_snapshot = True
311327
312328 yield from self ._visit (
313- o .children , iteration = iteration , condition = condition , anchor = anchor
329+ o .children , iteration = iteration , condition = condition ,
330+ in_snapshot = in_snapshot
314331 )
315332
316333
@@ -327,6 +344,20 @@ def infer_layer(f):
327344 return layer_host
328345
329346
347+ def infer_sync_layer (sync_ops ):
348+ """
349+ Infer the unique storage layer used by a sequence of SyncOps.
350+ """
351+ layers = {infer_layer (i .function ) for i in sync_ops }
352+ if len (layers ) != 1 :
353+ found = ', ' .join (sorted (str (i ) for i in layers )) or 'none'
354+ raise CompilationError (
355+ "Expected synchronization operations to use exactly one storage "
356+ f"layer, but found: { found } "
357+ )
358+ return layers .pop ()
359+
360+
330361@singledispatch
331362def withlock (layer , iet , sync_ops , lang , sregistry ):
332363 raise NotImplementedError
0 commit comments