Skip to content

Commit 741e1b1

Browse files
committed
compiler: Revamp orchestration lowering
1 parent 8388947 commit 741e1b1

1 file changed

Lines changed: 147 additions & 162 deletions

File tree

devito/passes/iet/orchestration.py

Lines changed: 147 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections import OrderedDict, defaultdict, namedtuple
1+
from collections import defaultdict, namedtuple
22
from contextlib import suppress
33
from functools import cached_property, singledispatch
44

@@ -7,7 +7,7 @@
77
from devito.exceptions import CompilationError
88
from 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
)
1212
from devito.ir.iet.visitors import LazyVisitor
1313
from devito.ir.support import (
@@ -16,38 +16,14 @@
1616
from devito.passes.iet.engine import iet_pass
1717
from devito.passes.iet.langbase import LangBB
1818
from devito.symbolics import CondEq, CondNe
19-
from devito.tools import DAG, as_mapper
19+
from devito.tools import as_mapper
2020
from 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

5329
class 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

276149
class 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

336321
layer_host = HostLayer('host')

0 commit comments

Comments
 (0)