1- from collections import OrderedDict
1+ from collections import OrderedDict , defaultdict , namedtuple
22from contextlib import suppress
33from functools import singledispatch
44
55from sympy import Or
66
77from devito .exceptions import CompilationError
88from devito .ir .iet import (
9- AsyncCall , AsyncCallable , BlankLine , BusyWait , Call , Callable , DummyExpr , FindNodes ,
10- List , SyncSpot , Transformer , derive_parameters , make_callable
9+ AsyncCall , AsyncCallable , BlankLine , BusyWait , Call , Callable , Conditional ,
10+ DummyExpr , FindNodes , List , SyncSpot , SyncSpotRegion , Transformer ,
11+ derive_parameters , make_callable
1112)
13+ from devito .ir .iet .visitors import LazyVisitor
1214from devito .ir .support import (
1315 InitArray , PrefetchUpdate , ReleaseLock , SnapIn , SnapOut , SyncArray , WaitLock , WithLock
1416)
1517from devito .passes .iet .engine import iet_pass
1618from devito .passes .iet .langbase import LangBB
1719from devito .symbolics import CondEq , CondNe
18- from devito .tools import DAG , as_mapper , as_tuple , flatten
20+ from devito .tools import DAG , as_mapper
1921from devito .types import HostLayer
2022
2123__init__ = ['Orchestrator' ]
2224
2325
26+ _Task = namedtuple (
27+ '_Task' , 'spot condition anchor iteration gid task_type sync_ops releases'
28+ )
29+
30+
2431class Orchestrator :
2532
2633 """
27- Lower the SyncSpot in IET for efficient host-device asynchronous computation.
34+ Lower synchronization nodes for efficient host-device asynchronous computation.
2835 """
2936
3037 langbb = LangBB
3138 """
3239 The language used to implement host-device data movements.
3340 """
3441
35- def __init__ (self , sregistry = None , ** kwargs ):
42+ def __init__ (self , sregistry = None , options = None , ** kwargs ):
3643 self .sregistry = sregistry
44+ self .npthreads = (options or {}).get ('npthreads' )
45+
46+ def _fuse_tasks (self , iet ):
47+ """Group compatible task SyncSpots into SyncSpotRegions."""
48+ mapper = as_mapper (
49+ _FindTasks ().visit (iet ),
50+ lambda task : (task .iteration , task .gid , task .task_type ,
51+ isinstance (task .anchor , SyncSpot ))
52+ )
53+
54+ insertions = defaultdict (list )
55+ subs1 = {}
56+
57+ for tasks in mapper .values ():
58+ spots = [SyncSpot (task .sync_ops ,
59+ body = Conditional (task .condition .condition ,
60+ task .spot .body ))
61+ for task in tasks ]
62+ insertions [tasks [- 1 ].anchor ].append (SyncSpotRegion (spots ))
63+
64+ for task in tasks :
65+ subs1 [task .spot ] = SyncSpot (task .releases )
66+
67+ if not subs1 :
68+ return iet
69+
70+ # These substitutions cannot be merged because a Transformer does not
71+ # revisit a replacement, while task spots may be nested below an anchor
72+ subs0 = {}
73+ for anchor , regions in insertions .items ():
74+ if isinstance (anchor , SyncSpot ):
75+ subs0 [anchor ] = anchor ._rebuild (body = anchor .body + tuple (regions ))
76+ else :
77+ subs0 [anchor ] = List (body = (anchor , * regions ))
78+
79+ iet = Transformer (subs0 ).visit (iet )
80+ iet = Transformer (subs1 ).visit (iet )
81+
82+ return iet
3783
3884 def _make_waitlock (self , iet , sync_ops , * args ):
3985 waitloop = List (
@@ -53,24 +99,25 @@ def _make_releaselock(self, iet, sync_ops, *args):
5399 parameters = derive_parameters (pre , ordering = 'canonical' )
54100 efunc = Callable (name , pre , 'void' , parameters , 'static' )
55101
102+ if isinstance (iet , SyncSpot ) and not iet .body :
103+ iet = List ()
104+
56105 iet = List (body = [Call (name , efunc .parameters )] + [iet ])
57106
58107 return iet , [efunc ]
59108
60109 def _make_withlock (self , iet , sync_ops , layer ):
61110 body , prefix = withlock (layer , iet , sync_ops , self .langbb , self .sregistry )
62111
63- # Turn `iet` into an AsyncCallable so that subsequent passes know
64- # that we're happy for this Callable to be executed asynchronously
112+ return self ._make_async_callable (body , prefix )
113+
114+ def _make_async_callable (self , body , prefix ):
65115 name = self .sregistry .make_name (prefix = prefix )
66116 body = List (body = body )
67117 parameters = derive_parameters (body , ordering = 'canonical' )
68118 efunc = AsyncCallable (name , body , parameters = parameters )
69119
70- # The corresponding AsyncCall
71- iet = AsyncCall (name , efunc .parameters )
72-
73- return iet , [efunc ]
120+ return AsyncCall (name , efunc .parameters ), [efunc ]
74121
75122 def _make_callable (self , name , iet , * args ):
76123 name = self .sregistry .make_name (prefix = name )
@@ -108,23 +155,36 @@ def _make_syncarray(self, iet, sync_ops, layer):
108155 def _make_prefetchupdate (self , iet , sync_ops , layer ):
109156 body , prefix = prefetchupdate (layer , iet , sync_ops , self .langbb , self .sregistry )
110157
111- # Turn `iet` into an AsyncCallable so that subsequent passes know
112- # that we're happy for this Callable to be executed asynchronously
113- name = self .sregistry .make_name (prefix = prefix )
114- body = List (body = body )
115- parameters = derive_parameters (body , ordering = 'canonical' )
116- efunc = AsyncCallable (name , body , parameters = parameters )
158+ return self ._make_async_callable (body , prefix )
117159
118- # The corresponding AsyncCall
119- iet = AsyncCall (name , efunc .parameters )
160+ def _make_region (self , iet ):
161+ """Lower a SyncSpotRegion into one asynchronous callable."""
162+ sync_ops = tuple (j for i in iet .sync_spots for j in i .sync_ops )
163+ callback = {
164+ PrefetchUpdate : prefetchupdate ,
165+ WithLock : withlock
166+ }[iet .optype ]
120167
121- return iet , [efunc ]
168+ layers = {infer_layer (i .function ) for i in sync_ops }
169+ if len (layers ) != 1 :
170+ raise CompilationError ("Unsupported streaming case" )
171+ layer = layers .pop ()
172+
173+ body = []
174+ for spot in iet .sync_spots :
175+ condition , = spot .body
176+ task_body , prefix = callback (
177+ layer , List (body = condition .then_body ), spot .sync_ops , self .langbb ,
178+ self .sregistry
179+ )
180+ body .append (condition ._rebuild (then_body = task_body ))
181+
182+ return self ._make_async_callable (body , prefix )
122183
123184 @iet_pass
124185 def process (self , iet ):
125- sync_spots = FindNodes (SyncSpot ).visit (iet )
126- if not sync_spots :
127- return iet , {}
186+ if self .npthreads is not None :
187+ iet = self ._fuse_tasks (iet )
128188
129189 # The SyncOps are to be processed in a given order
130190 callbacks = OrderedDict ([
@@ -139,19 +199,32 @@ def process(self, iet):
139199 ])
140200 key = lambda s : list (callbacks ).index (s )
141201
202+ # A region consumes its immediate SyncSpots as one unit, so lower all
203+ # regions before looking for ordinary SyncSpots
204+ efuncs = []
205+ while True :
206+ sync_regions = FindNodes (SyncSpotRegion ).visit (iet )
207+ if not sync_regions :
208+ break
209+
210+ n0 = ordered (sync_regions ).pop (0 )
211+ n1 , v = self ._make_region (n0 )
212+
213+ iet = Transformer ({n0 : n1 }).visit (iet )
214+ efuncs .extend (v )
215+
142216 # The SyncSpots may be nested, so we compute a topological ordering
143217 # so that they are processed in a bottom-up fashion. This is necessary
144218 # because e.g. an inner SyncSpot may generate new objects (e.g., a new
145219 # Queue), which in turn must be visible to the outer SyncSpot to
146220 # generate the correct parameters list
147- efuncs = []
148221 while True :
149222 sync_spots = FindNodes (SyncSpot ).visit (iet )
150223 if not sync_spots :
151224 break
152225
153226 n0 = ordered (sync_spots ).pop (0 )
154- mapper = as_mapper (flatten ( n0 .ops ), lambda i : type ( i ) )
227+ mapper = as_mapper (n0 .sync_ops , type )
155228
156229 subs = {}
157230 for t in sorted (mapper , key = key ):
@@ -172,15 +245,53 @@ def process(self, iet):
172245 return iet , {'efuncs' : efuncs }
173246
174247
175- def ordered (sync_spots ):
176- dag = DAG (nodes = sync_spots )
177- for n0 in sync_spots :
178- for n1 in as_tuple ( FindNodes (SyncSpot ) .visit (n0 .body ) ):
248+ def ordered (sync_nodes ):
249+ dag = DAG (nodes = sync_nodes )
250+ for n0 in sync_nodes :
251+ for n1 in FindNodes (type ( n0 )) .visit (n0 .body ):
179252 dag .add_edge (n1 , n0 )
180253
181254 return dag .topological_sort ()
182255
183256
257+ class _FindTasks (LazyVisitor ):
258+
259+ """Find guarded asynchronous SyncSpots and their structural context."""
260+
261+ def visit_Iteration (self , o , ** kwargs ):
262+ kwargs ['iteration' ] = o
263+ yield from self ._visit (o .children , ** kwargs )
264+
265+ def visit_Conditional (self , o , ** kwargs ):
266+ kwargs ['condition' ] = o
267+ yield from self ._visit (o .children , ** kwargs )
268+
269+ def visit_SyncSpot (self , o , iteration = None , condition = None , anchor = None ):
270+ if iteration is not None and condition is not None and not condition .else_body :
271+ syncs = as_mapper (o .sync_ops , type )
272+ sync_types = set (syncs )
273+ for task_type in (PrefetchUpdate , WithLock ):
274+ if sync_types != {task_type , ReleaseLock }:
275+ continue
276+
277+ sync_ops = syncs [task_type ]
278+ gids = {i .gid for i in sync_ops }
279+
280+ if len (gids ) == 1 and None not in gids :
281+ gid , = gids
282+ yield _Task (o , condition , anchor or condition , iteration ,
283+ gid , task_type , sync_ops , syncs [ReleaseLock ])
284+ break
285+
286+ if any (isinstance (i , SnapOut ) for i in o .sync_ops ):
287+ # Keep composite task calls inside the `SnapOut` scope
288+ anchor = o
289+
290+ yield from self ._visit (
291+ o .children , iteration = iteration , condition = condition , anchor = anchor
292+ )
293+
294+
184295# Task handlers
185296
186297layer_host = HostLayer ('host' )
0 commit comments