Skip to content

Commit 9b38f49

Browse files
committed
compiler: Refactor fuse-tasks lowering
1 parent 26b03bc commit 9b38f49

3 files changed

Lines changed: 171 additions & 39 deletions

File tree

devito/ir/iet/algorithms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def iet_build(stree):
5858
body = HaloSpot(None, i.halo_scheme)
5959

6060
elif i.is_Sync:
61-
body = SyncSpot((i.sync_ops,), body=queues.pop(i, None))
61+
body = SyncSpot(i.sync_ops, body=queues.pop(i, None))
6262

6363
queues.setdefault(i.parent, []).append(body)
6464

devito/ir/iet/nodes.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
'Section',
6262
'Switch',
6363
'SyncSpot',
64+
'SyncSpotRegion',
6465
'TimedList',
6566
'Transfer',
6667
'Using',
@@ -92,6 +93,7 @@ class Node(Signer):
9293
is_HaloSpot = False
9394
is_ExpressionBundle = False
9495
is_SyncSpot = False
96+
is_SyncSpotRegion = False
9597

9698
_traversable = []
9799
"""
@@ -1512,19 +1514,16 @@ class SyncSpot(List):
15121514

15131515
"""
15141516
A node coupling synchronization operations with an IET body.
1515-
1516-
`ops` contains either one group applying to the whole `body`, or one group
1517-
per body item, with `ops[i]` applying to `body[i]`.
15181517
"""
15191518

15201519
is_SyncSpot = True
15211520

1522-
def __init__(self, ops, body=None):
1521+
def __init__(self, sync_ops, body=None):
15231522
super().__init__(body=body)
1524-
self.ops = tuple(tuple(i) for i in ops)
1523+
self.sync_ops = sync_ops
15251524

15261525
def __repr__(self):
1527-
return f"<SyncSpot ({','.join(str(i) for i in flatten(self.ops))})>"
1526+
return f"<SyncSpot ({','.join(str(i) for i in self.sync_ops)})>"
15281527

15291528
@property
15301529
def is_async_op(self):
@@ -1533,15 +1532,37 @@ def is_async_op(self):
15331532
If False, the SyncSpot may for example represent a wait on a lock.
15341533
"""
15351534
return any(isinstance(s, (WithLock, PrefetchUpdate))
1536-
for s in flatten(self.ops))
1535+
for s in self.sync_ops)
15371536

15381537
@property
15391538
def functions(self):
1540-
ret = [(s.lock, s.function, s.target) for s in flatten(self.ops)]
1539+
ret = [(s.lock, s.function, s.target) for s in self.sync_ops]
15411540
ret = tuple(filter_ordered(f for f in flatten(ret) if f is not None))
15421541
return ret
15431542

15441543

1544+
class SyncSpotRegion(List):
1545+
1546+
"""A sequence of SyncSpots treated as one unit during orchestration."""
1547+
1548+
is_SyncSpotRegion = True
1549+
1550+
def __init__(self, body):
1551+
body = as_tuple(body)
1552+
assert body and all(isinstance(i, SyncSpot) for i in body)
1553+
super().__init__(body=body)
1554+
1555+
@property
1556+
def sync_spots(self):
1557+
return self.body
1558+
1559+
@cached_property
1560+
def optype(self):
1561+
"""The common type of the synchronization operations in this region."""
1562+
optype, = {type(j) for i in self.sync_spots for j in i.sync_ops}
1563+
return optype
1564+
1565+
15451566
class CBlankLine(List):
15461567

15471568
def __init__(self, **kwargs):

devito/passes/iet/orchestration.py

Lines changed: 141 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,85 @@
1-
from collections import OrderedDict
1+
from collections import OrderedDict, defaultdict, namedtuple
22
from contextlib import suppress
33
from functools import singledispatch
44

55
from sympy import Or
66

77
from devito.exceptions import CompilationError
88
from 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
1214
from devito.ir.support import (
1315
InitArray, PrefetchUpdate, ReleaseLock, SnapIn, SnapOut, SyncArray, WaitLock, WithLock
1416
)
1517
from devito.passes.iet.engine import iet_pass
1618
from devito.passes.iet.langbase import LangBB
1719
from devito.symbolics import CondEq, CondNe
18-
from devito.tools import DAG, as_mapper, as_tuple, flatten
20+
from devito.tools import DAG, as_mapper
1921
from 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+
2431
class 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

186297
layer_host = HostLayer('host')

0 commit comments

Comments
 (0)