1- from collections import OrderedDict , defaultdict , namedtuple
1+ from collections import defaultdict , namedtuple
22from contextlib import suppress
33from functools import cached_property , singledispatch
44
77from devito .exceptions import CompilationError
88from devito .ir .iet import (
99 AsyncCall , AsyncCallable , BlankLine , Block , BusyWait , Call , Callable , Conditional ,
10- DummyExpr , FindNodes , List , SyncSpot , Transformer , derive_parameters , make_callable
10+ DummyExpr , List , SyncSpot , Transformer , derive_parameters , make_callable
1111)
1212from devito .ir .iet .visitors import LazyVisitor
1313from devito .ir .support import (
1616from devito .passes .iet .engine import iet_pass
1717from devito .passes .iet .langbase import LangBB
1818from devito .symbolics import CondEq , CondNe
19- from devito .tools import DAG , as_mapper
19+ from devito .tools import as_mapper
2020from devito .types import HostLayer
2121
2222__all__ = ['Orchestrator' ]
2323
2424
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 ()
25+ Task = namedtuple ('Task' , 'spot guard sync_ops' )
26+ TaskGroupKey = namedtuple ('TaskGroupKey' , 'iteration gid optype in_snapshot' )
5127
5228
5329class Orchestrator :
@@ -65,42 +41,6 @@ def __init__(self, sregistry=None, options=None, **kwargs):
6541 self .sregistry = sregistry
6642 self .npthreads = (options or {}).get ('npthreads' )
6743
68- def _fuse_tasks (self , iet ):
69- """
70- Group compatible task SyncSpots into SyncSpotRegions.
71- """
72- if self .npthreads is None :
73- return iet
74-
75- insertions = defaultdict (list )
76- subs1 = {}
77-
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 ))
89- subs1 .update ({task .spot : SyncSpot (task .releases ) for task in tasks })
90-
91- if not subs1 :
92- return iet
93-
94- # These substitutions cannot be merged because a Transformer does not
95- # revisit a replacement, while task spots may be nested below an anchor
96- subs0 = {anchor : List (body = (anchor , * regions ))
97- for anchor , regions in insertions .items ()}
98-
99- iet = Transformer (subs0 ).visit (iet )
100- iet = Transformer (subs1 ).visit (iet )
101-
102- return iet
103-
10444 def _make_waitlock (self , iet , sync_ops , * args ):
10545 waitloop = List (
10646 body = BusyWait (Or (* [CondEq (s .handle , 0 ) for s in sync_ops ])),
@@ -126,8 +66,14 @@ def _make_releaselock(self, iet, sync_ops, *args):
12666
12767 return iet , [efunc ]
12868
129- def _make_withlock (self , iet , sync_ops , layer ):
130- body , prefix = withlock (layer , iet , sync_ops , self .langbb , self .sregistry )
69+ def _make_withlock (self , iet , sync_ops , layer , wrap = True ):
70+ return self ._make_async_task (withlock , iet , sync_ops , layer , wrap )
71+
72+ def _make_async_task (self , callback , iet , sync_ops , layer , wrap ):
73+ body , prefix = callback (layer , iet , sync_ops , self .langbb , self .sregistry )
74+
75+ if not wrap :
76+ return body , prefix
13177
13278 return self ._make_async_callable (body , prefix )
13379
@@ -172,105 +118,32 @@ def _make_syncarray(self, iet, sync_ops, layer):
172118
173119 return iet , []
174120
175- def _make_prefetchupdate (self , iet , sync_ops , layer ):
176- body , prefix = prefetchupdate (layer , iet , sync_ops , self .langbb , self .sregistry )
177-
178- return self ._make_async_callable (body , prefix )
179-
180- def _make_region (self , iet ):
181- """Lower a SyncSpotRegion into one asynchronous callable."""
182- sync_ops = tuple (op for spot in iet .sync_spots
183- for op in spot .sync_ops )
184- callback = {
185- PrefetchUpdate : prefetchupdate ,
186- WithLock : withlock
187- }[iet .optype ]
188-
189- layer = infer_sync_layer (sync_ops )
190-
191- body = []
192- for spot in iet .sync_spots :
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-
200- task_body , prefix = callback (
201- layer , List (body = task_body ), spot .sync_ops , self .langbb ,
202- self .sregistry
203- )
204- body .append (scope ._rebuild (task_body ))
205-
206- return self ._make_async_callable (body , prefix )
121+ def _make_prefetchupdate (self , iet , sync_ops , layer , wrap = True ):
122+ return self ._make_async_task (prefetchupdate , iet , sync_ops , layer , wrap )
207123
208124 @iet_pass
209125 def process (self , iet ):
210- # Group compatible task SyncSpots into SyncSpotRegions, if requested
211- # by the user
212- iet = self ._fuse_tasks (iet )
213-
214- # Lower regions first so their member SyncSpots are not processed
215- # independently by the generic SyncSpot lowering below
216- efuncs = []
217- subs = {}
218- for region in FindNodes (SyncSpotRegion ).visit (iet ):
219- call , efuncs1 = self ._make_region (region )
220- subs [region ] = call
221- efuncs .extend (efuncs1 )
222-
223- iet = Transformer (subs ).visit (iet )
224-
225126 # The SyncOps are to be processed in a given order
226- callbacks = OrderedDict ([
227- (WaitLock , self ._make_waitlock ),
228- (WithLock , self ._make_withlock ),
229- (SyncArray , self ._make_syncarray ),
230- (InitArray , self ._make_initarray ),
231- (SnapOut , self ._make_snapout ),
232- (SnapIn , self ._make_snapin ),
233- (PrefetchUpdate , self ._make_prefetchupdate ),
234- (ReleaseLock , self ._make_releaselock ),
235- ])
236- key = tuple (callbacks ).index
237-
238- # The SyncSpots may be nested, so we compute a topological ordering
239- # so that they are processed in a bottom-up fashion. This is necessary
240- # because e.g. an inner SyncSpot may generate new objects (e.g., a new
241- # Queue), which in turn must be visible to the outer SyncSpot to
242- # generate the correct parameters list
243- while True :
244- sync_spots = FindNodes (SyncSpot ).visit (iet )
245- if not sync_spots :
246- break
247-
248- n0 = ordered (sync_spots ).pop (0 )
249- mapper = as_mapper (n0 .sync_ops , type )
250-
251- subs = {}
252- for t in sorted (mapper , key = key ):
253- sync_ops = mapper [t ]
127+ callbacks = {
128+ WaitLock : self ._make_waitlock ,
129+ WithLock : self ._make_withlock ,
130+ SyncArray : self ._make_syncarray ,
131+ InitArray : self ._make_initarray ,
132+ SnapOut : self ._make_snapout ,
133+ SnapIn : self ._make_snapin ,
134+ PrefetchUpdate : self ._make_prefetchupdate ,
135+ ReleaseLock : self ._make_releaselock ,
136+ AsyncCallable : self ._make_async_callable
137+ }
254138
255- layer = infer_sync_layer (sync_ops )
139+ # Collect the task groups to lower atomically, if any
140+ task_groups = CollectTasks ().visit (iet ) if self .npthreads else ()
256141
257- n1 , v = callbacks [t ](subs .get (n0 , n0 ), sync_ops , layer )
142+ # Lower the SyncSpots in a single bottom-up traversal, atomically lowering
143+ lowerer = LowerSyncSpots (callbacks , task_groups )
144+ iet = lowerer .visit (iet )
258145
259- subs [n0 ] = n1
260- efuncs .extend (v )
261-
262- iet = Transformer (subs ).visit (iet )
263-
264- return iet , {'efuncs' : efuncs }
265-
266-
267- def ordered (sync_spots ):
268- dag = DAG (nodes = sync_spots )
269- for n0 in sync_spots :
270- for n1 in FindNodes (SyncSpot ).visit (n0 .body ):
271- dag .add_edge (n1 , n0 )
272-
273- return dag .topological_sort ()
146+ return iet , {'efuncs' : lowerer .efuncs }
274147
275148
276149class CollectTasks (LazyVisitor ):
@@ -284,7 +157,8 @@ def _post_visit(self, ret):
284157 groups [key ].append (task )
285158 # Activate the group after its last task in program order
286159 anchors [key ] = anchor
287- return tuple ((tasks , anchors [key ]) for key , tasks in groups .items ())
160+ return tuple ((tasks , anchors [key ], key .optype )
161+ for key , tasks in groups .items ())
288162
289163 def visit_Iteration (self , o , ** kwargs ):
290164 kwargs ['iteration' ] = o
@@ -315,8 +189,8 @@ def visit_SyncSpot(self, o, iteration=None, condition=None,
315189
316190 gid , = {i .gid for i in sync_ops }
317191 if gid is not None :
318- task = Task (o , guard , sync_ops , syncs [ ReleaseLock ] )
319- key = (iteration , gid , optype , in_snapshot )
192+ task = Task (o , guard , sync_ops )
193+ key = TaskGroupKey (iteration , gid , optype , in_snapshot )
320194 yield key , task , condition or o
321195
322196 break
@@ -331,6 +205,117 @@ def visit_SyncSpot(self, o, iteration=None, condition=None,
331205 )
332206
333207
208+ class LowerSyncSpots (Transformer ):
209+
210+ """
211+ Lower `SyncSpot`s in a single bottom-up traversal.
212+
213+ Compatible task `SyncSpot`s are lowered atomically into one asynchronous
214+ callable when their final anchor is reached. All other `SyncSpot`s are
215+ lowered individually.
216+
217+ Parameters
218+ ----------
219+ callbacks : mapping
220+ The callbacks used to lower `SyncOp`s and create asynchronous callables.
221+ task_groups : iterable
222+ The task groups to lower atomically.
223+ """
224+
225+ def __init__ (self , callbacks , task_groups ):
226+ super ().__init__ ({})
227+
228+ self ._callbacks = callbacks
229+
230+ self ._task_bodies = {}
231+ self ._anchors = defaultdict (list )
232+ for tasks , anchor , optype in task_groups :
233+ self ._task_bodies .update ((task .spot , None ) for task in tasks )
234+ self ._anchors [anchor ].append ((tasks , optype ))
235+
236+ self ._efuncs = []
237+
238+ @property
239+ def efuncs (self ):
240+ return self ._efuncs
241+
242+ @cached_property
243+ def _priority (self ):
244+ return tuple (self ._callbacks ).index
245+
246+ def visit_Node (self , o , ** kwargs ):
247+ iet = super ().visit_Node (o , ** kwargs )
248+ return self ._lower_task_groups (o , iet )
249+
250+ def visit_SyncSpot (self , o , ** kwargs ):
251+ body = self ._visit (o .body , ** kwargs )
252+
253+ if o in self ._task_bodies :
254+ # Retain the task body until the group's final anchor is visited
255+ self ._task_bodies [o ] = body
256+ releases = tuple (i for i in o .sync_ops
257+ if isinstance (i , ReleaseLock ))
258+ iet = self ._lower (SyncSpot (releases ), releases )
259+ else :
260+ iet = self ._lower (o ._rebuild (body = body ), o .sync_ops )
261+
262+ return self ._lower_task_groups (o , iet )
263+
264+ def _lower (self , iet , sync_ops ):
265+ mapper = as_mapper (sync_ops , type )
266+ for optype in sorted (mapper , key = self ._priority ):
267+ sync_ops = mapper [optype ]
268+ layer = infer_sync_layer (sync_ops )
269+
270+ iet , efuncs = self ._callbacks [optype ](iet , sync_ops , layer )
271+ self ._efuncs .extend (efuncs )
272+
273+ return iet
274+
275+ def _lower_task_groups (self , o , iet ):
276+ """
277+ Lower the task groups activated after `o`.
278+
279+ Examples
280+ --------
281+ A group of three guarded tasks is lowered schematically to::
282+
283+ AsyncCall(
284+ if guard0: task0
285+ if guard1: task1
286+ if guard2: task2
287+ )
288+
289+ The call is inserted after `o`; each task retains its own guard.
290+ """
291+ calls = []
292+ for tasks , optype in self ._anchors .get (o , ()):
293+ sync_ops = tuple (op for task in tasks for op in task .sync_ops )
294+ layer = infer_sync_layer (sync_ops )
295+
296+ body = []
297+ for task in tasks :
298+ task_body , prefix = self ._callbacks [optype ](
299+ List (body = self ._task_bodies .pop (task .spot )),
300+ task .sync_ops , layer , wrap = False
301+ )
302+ if task .guard is None :
303+ # Preserve the local scope of an unguarded task body
304+ scope = Block (body = task_body )
305+ else :
306+ scope = Conditional (task .guard , task_body )
307+ body .append (scope )
308+
309+ call , efuncs = self ._callbacks [AsyncCallable ](body , prefix )
310+ calls .append (call )
311+ self ._efuncs .extend (efuncs )
312+
313+ if calls :
314+ iet = List (body = (iet , * calls ))
315+
316+ return iet
317+
318+
334319# Task handlers
335320
336321layer_host = HostLayer ('host' )
0 commit comments