Skip to content

Commit 35f711f

Browse files
Merge pull request #2975 from devitocodes/fuse-tasks-groups
compiler: Revamp fuse-task optoption
2 parents 78741f4 + 30041af commit 35f711f

11 files changed

Lines changed: 496 additions & 101 deletions

File tree

devito/core/cpu.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def _normalize_kwargs(cls, **kwargs):
4747
o['buf-async-degree'] = oo.pop('buf-async-degree', None)
4848
o['buf-reuse'] = oo.pop('buf-reuse', None)
4949

50-
# Fusion
51-
o['fuse-tasks'] = oo.pop('fuse-tasks', False)
50+
# Tasking
51+
o['npthreads'] = oo.pop('npthreads', None)
5252

5353
# Flops minimization
5454
o['cse-min-cost'] = oo.pop('cse-min-cost', cls.CSE_MIN_COST)

devito/core/gpu.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ def _normalize_kwargs(cls, **kwargs):
6161
o['buf-async-degree'] = oo.pop('buf-async-degree', None)
6262
o['buf-reuse'] = oo.pop('buf-reuse', None)
6363

64-
# Fusion
65-
o['fuse-tasks'] = oo.pop('fuse-tasks', False)
64+
# Tasking
65+
o['npthreads'] = oo.pop('npthreads', None)
6666

6767
# Flops minimization
6868
o['cse-min-cost'] = oo.pop('cse-min-cost', cls.CSE_MIN_COST)

devito/core/operator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,15 @@ def _check_kwargs(cls, **kwargs):
247247
if oo['mpi'] and oo['mpi'] not in cls.MPI_MODES:
248248
raise InvalidOperator(f"Unsupported MPI mode `{oo['mpi']}`")
249249

250+
npthreads = oo['npthreads']
251+
if npthreads is not None and (
252+
isinstance(npthreads, bool) or
253+
not is_integer(npthreads) or npthreads <= 0
254+
):
255+
raise InvalidOperator(
256+
"`npthreads` must be a positive integer"
257+
)
258+
250259
if oo['cire-maxpar'] not in (False, 'basic', 'compact'):
251260
raise InvalidOperator("Illegal `cire-maxpar` value")
252261

devito/ir/iet/nodes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,8 +1511,7 @@ class BusyWait(While):
15111511
class SyncSpot(List):
15121512

15131513
"""
1514-
A node representing one or more synchronization operations, e.g., WaitLock,
1515-
withLock, etc.
1514+
A node coupling synchronization operations with an IET body.
15161515
"""
15171516

15181517
is_SyncSpot = True

devito/ir/support/syncs.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,42 @@
2626

2727
class SyncOp(Pickable):
2828

29+
"""
30+
Metadata for a synchronization operation attached to a `Cluster` or `SyncSpot`.
31+
32+
Parameters
33+
----------
34+
handle : object
35+
The symbolic object identifying or controlling the operation, such as
36+
an entry in a `Lock`. May be None when no handle is required.
37+
target : AbstractFunction
38+
The `Function` whose access is synchronized. For buffered data movements,
39+
this is the compiler-generated buffer.
40+
tindex : Expr, optional
41+
The index into `target` involved in the operation.
42+
function : AbstractFunction, optional
43+
The original `Function` represented by a compiler-generated `target`. It
44+
is the source of a `SyncCopyIn` and the destination of a `SyncCopyOut`.
45+
findex : Expr, optional
46+
The index into `function` corresponding to `tindex`.
47+
dim : Dimension, optional
48+
The `Dimension` along which `tindex` and `findex` are defined.
49+
size : int, optional
50+
The extent associated with the operation along `dim`. Defaults to 1.
51+
origin : Indexed, optional
52+
The original `Indexed` access from which the operation was derived.
53+
gid : Stamp, optional
54+
The `Stamp` identifying the asynchronous task group to which the operation
55+
belongs.
56+
"""
57+
2958
__rargs__ = ('handle', 'target')
30-
__rkwargs__ = ('tindex', 'function', 'findex', 'dim', 'size', 'origin')
59+
__rkwargs__ = (
60+
'tindex', 'function', 'findex', 'dim', 'size', 'origin', 'gid'
61+
)
3162

3263
def __init__(self, handle, target, tindex=None, function=None, findex=None,
33-
dim=None, size=1, origin=None):
64+
dim=None, size=1, origin=None, gid=None):
3465
self.handle = handle
3566
self.target = target
3667

@@ -40,6 +71,7 @@ def __init__(self, handle, target, tindex=None, function=None, findex=None,
4071
self.dim = dim
4172
self.size = size
4273
self.origin = origin
74+
self.gid = gid
4375

4476
def __eq__(self, other):
4577
return (type(self) is type(other) and
@@ -50,11 +82,13 @@ def __eq__(self, other):
5082
self.findex == other.findex and
5183
self.dim is other.dim and
5284
self.size == other.size and
53-
self.origin == other.origin)
85+
self.origin == other.origin and
86+
self.gid == other.gid)
5487

5588
def __hash__(self):
5689
return hash((self.__class__, self.handle, self.target, self.tindex,
57-
self.function, self.findex, self.dim, self.size, self.origin))
90+
self.function, self.findex, self.dim, self.size, self.origin,
91+
self.gid))
5892

5993
def __repr__(self):
6094
return f"{self.__class__.__name__}<{self.handle.name}>"

devito/passes/clusters/asynchrony.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ def memcpy_key(c):
4545
return task_key, memcpy_key
4646

4747

48+
def make_gid(ispace, d):
49+
"""
50+
Make a group ID from the stamps carried by the non-trigger Intervals.
51+
"""
52+
gids = {i.stamp for i in ispace if not i.dim._defines & d._defines}
53+
if len(gids) != 1:
54+
return None
55+
else:
56+
return gids.pop()
57+
58+
4859
@timed_pass(name='tasking')
4960
def tasking(clusters, key0, sregistry):
5061
"""
@@ -147,6 +158,8 @@ def _schedule_waitlocks(self, c0, d, clusters, locks, syncs):
147158
return protected
148159

149160
def _schedule_withlocks(self, c0, d, protected, locks, syncs):
161+
gid = make_gid(c0.ispace, d)
162+
150163
for target in protected:
151164
lock = locks[target]
152165

@@ -169,7 +182,7 @@ def _schedule_withlocks(self, c0, d, protected, locks, syncs):
169182
for i in indices:
170183
syncs[c0][d].update([
171184
ReleaseLock(lock[i], target),
172-
WithLock(lock[i], target, i, function, findex, d)
185+
WithLock(lock[i], target, i, function, findex, d, gid=gid)
173186
])
174187

175188

@@ -274,9 +287,12 @@ def _actions_from_update_memcpy(c, d, clusters, actions, sregistry):
274287
guard1 = GuardBoundNext(function.indices[d], direction)
275288
guards = c.guards.impose(d, guard0 & guard1)
276289

290+
gid = make_gid(ispace, d)
291+
277292
syncs = {d: [
278293
ReleaseLock(handle, target),
279-
PrefetchUpdate(handle, target, tindex, function, findex, d, 1, e.rhs)
294+
PrefetchUpdate(handle, target, tindex, function, findex, d,
295+
origin=e.rhs, gid=gid)
280296
]}
281297
syncs = {**c.syncs, **syncs}
282298

devito/passes/clusters/buffering.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
timed_pass
1919
)
2020
from devito.types import Array, CustomDimension, Eq, ModuloDimension
21+
from devito.warnings import warn
2122

2223
__all__ = ['buffering']
2324

@@ -39,7 +40,7 @@ def buffering(clusters, key, sregistry, options, **kwargs):
3940
The symbol registry, to create unique names for buffers and Dimensions.
4041
options : dict
4142
The optimization options.
42-
Accepted: ['buf-async-degree'].
43+
Accepted: ['buf-async-degree', 'buf-reuse', 'npthreads'].
4344
* 'buf-async-degree': Specify the size of the buffer. By default, the
4445
buffer size is the minimal one, inferred from the memory accesses in
4546
the ``clusters`` themselves. An asynchronous degree equals to `k`
@@ -49,6 +50,9 @@ def buffering(clusters, key, sregistry, options, **kwargs):
4950
implemented by other passes).
5051
* 'buf-reuse': If True, the pass will try to reuse existing Buffers for
5152
different buffered Functions. By default, False.
53+
* 'npthreads': Number of pthreads for asynchronous tasks. The tasks are
54+
divided into this many balanced groups. By default, None, which uses
55+
one pthread per task.
5256
**kwargs
5357
Additional compilation options.
5458
Accepted: ['opt_init_onwrite', 'opt_buffer'].
@@ -252,36 +256,41 @@ def callback(self, clusters, prefix):
252256
processed.append(Cluster(expr, ispace, guards, properties, syncs))
253257

254258
# Lift {write,read}-only buffers into separate IterationSpaces
255-
if not self.options['fuse-tasks']:
256-
processed = self._optimize(processed, descriptors)
259+
processed = self._optimize(processed, descriptors)
257260

258-
if self.options['buf-reuse']:
259-
init, processed = self._reuse(init, processed, descriptors)
261+
# Reuse existing Buffers for buffering candidates, if requested
262+
init, processed = self._reuse(init, processed, descriptors)
260263

261264
return init + processed
262265

263266
def _optimize(self, clusters, descriptors):
267+
npthreads = self.options['npthreads']
268+
269+
stamps = self._make_task_groups(descriptors)
270+
264271
for b, v in descriptors.items():
265272
if v.is_writeonly:
266273
# `b` might be written by multiple, potentially mutually
267274
# exclusive, equations. For example, two equations that have or
268275
# will have complementary guards, hence only one will be
269276
# executed. In such a case, we can split the equations over
270277
# separate IterationSpaces
271-
key0 = lambda: Stamp()
278+
key0 = lambda: stamps[b] if npthreads else Stamp() # noqa: B023
272279
elif v.is_readonly:
273280
# `b` is read multiple times -- this could just be the case of
274281
# coupled equations, so we more cautiously perform a
275282
# "buffer-wise" splitting of the IterationSpaces (i.e., only
276283
# relevant if there are at least two read-only buffers)
277-
stamp = Stamp()
278-
key0 = lambda: stamp # noqa: B023
284+
stamp_fixed = Stamp()
285+
key0 = lambda: stamps[b] if npthreads else stamp_fixed # noqa: B023
279286
else:
280287
continue
281288

282289
processed = []
283290
for c in clusters:
284-
if b not in c.functions:
291+
f = v.f if npthreads else b
292+
293+
if f not in c.functions:
285294
processed.append(c)
286295
continue
287296

@@ -294,12 +303,57 @@ def _optimize(self, clusters, descriptors):
294303

295304
return clusters
296305

306+
def _make_task_groups(self, descriptors):
307+
"""
308+
Assign task buffers to at most `npthreads` balanced groups.
309+
"""
310+
npthreads = self.options['npthreads']
311+
312+
stamps = {}
313+
if not npthreads:
314+
return stamps
315+
316+
task_sets = (
317+
[b for b, v in descriptors.items() if v.is_writeonly],
318+
[b for b, v in descriptors.items() if v.is_readonly],
319+
)
320+
321+
for tasks in task_sets:
322+
if not tasks:
323+
continue
324+
325+
# Calculate the number of groups and their sizes, so that the tasks
326+
# are divided into balanced groups
327+
ntasks = len(tasks)
328+
ngroups = min(npthreads, ntasks)
329+
base, remainder = divmod(ntasks, ngroups)
330+
sizes = (base + 1,) * remainder + (base,) * (ngroups - remainder)
331+
332+
if npthreads > ntasks:
333+
warn(
334+
f"`npthreads={npthreads}` exceeds the {ntasks} available "
335+
f"tasks; using `npthreads={ntasks}` instead"
336+
)
337+
338+
# Create a unique Stamp for each group and assign it to the tasks
339+
# in that group
340+
start = 0
341+
for n in sizes:
342+
stamp = Stamp()
343+
stamps.update({b: stamp for b in tasks[start:start + n]})
344+
start += n
345+
346+
return stamps
347+
297348
def _reuse(self, init, clusters, descriptors):
298349
"""
299350
Reuse existing Buffers for buffering candidates.
300351
"""
301352
buf_reuse = self.options['buf-reuse']
302353

354+
if not buf_reuse:
355+
return init, clusters
356+
303357
if callable(buf_reuse):
304358
cbk = lambda v: [i for i in v if buf_reuse(descriptors[i])]
305359
else:

devito/passes/clusters/fusion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def __init__(self, toposort, options=None):
152152
options = options or {}
153153

154154
self.toposort = toposort
155-
self.fuse_tasks = options.get('fuse-tasks', False)
155+
self.fuse_tasks = options.get('npthreads') is not None
156156

157157
super().__init__()
158158

0 commit comments

Comments
 (0)