Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions devito/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def _normalize_kwargs(cls, **kwargs):
o['buf-async-degree'] = oo.pop('buf-async-degree', None)
o['buf-reuse'] = oo.pop('buf-reuse', None)

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking: might wanna throw a warning, or oo.pop('npthreads', oo.pop('fuse-tasks', None) for "safety"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any documentation that needs updating to reflect this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


# Flops minimization
o['cse-min-cost'] = oo.pop('cse-min-cost', cls.CSE_MIN_COST)
Expand Down
4 changes: 2 additions & 2 deletions devito/core/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def _normalize_kwargs(cls, **kwargs):
o['buf-async-degree'] = oo.pop('buf-async-degree', None)
o['buf-reuse'] = oo.pop('buf-reuse', None)

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

# Flops minimization
o['cse-min-cost'] = oo.pop('cse-min-cost', cls.CSE_MIN_COST)
Expand Down
9 changes: 9 additions & 0 deletions devito/core/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ def _check_kwargs(cls, **kwargs):
if oo['mpi'] and oo['mpi'] not in cls.MPI_MODES:
raise InvalidOperator(f"Unsupported MPI mode `{oo['mpi']}`")

npthreads = oo['npthreads']
if npthreads is not None and (
isinstance(npthreads, bool) or
not is_integer(npthreads) or npthreads <= 0
):
raise InvalidOperator(
"`npthreads` must be a positive integer"
)

if oo['cire-maxpar'] not in (False, 'basic', 'compact'):
raise InvalidOperator("Illegal `cire-maxpar` value")

Expand Down
3 changes: 1 addition & 2 deletions devito/ir/iet/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1511,8 +1511,7 @@ class BusyWait(While):
class SyncSpot(List):

"""
A node representing one or more synchronization operations, e.g., WaitLock,
withLock, etc.
A node coupling synchronization operations with an IET body.
"""

is_SyncSpot = True
Expand Down
42 changes: 38 additions & 4 deletions devito/ir/support/syncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,42 @@

class SyncOp(Pickable):

"""
Metadata for a synchronization operation attached to a `Cluster` or `SyncSpot`.

Parameters
----------
handle : object
The symbolic object identifying or controlling the operation, such as
an entry in a `Lock`. May be None when no handle is required.
target : AbstractFunction
The `Function` whose access is synchronized. For buffered data movements,
this is the compiler-generated buffer.
tindex : Expr, optional
The index into `target` involved in the operation.
function : AbstractFunction, optional
The original `Function` represented by a compiler-generated `target`. It
is the source of a `SyncCopyIn` and the destination of a `SyncCopyOut`.
findex : Expr, optional
The index into `function` corresponding to `tindex`.
dim : Dimension, optional
The `Dimension` along which `tindex` and `findex` are defined.
size : int, optional
The extent associated with the operation along `dim`. Defaults to 1.
origin : Indexed, optional
The original `Indexed` access from which the operation was derived.
gid : Stamp, optional
The `Stamp` identifying the asynchronous task group to which the operation
belongs.
"""

__rargs__ = ('handle', 'target')
__rkwargs__ = ('tindex', 'function', 'findex', 'dim', 'size', 'origin')
__rkwargs__ = (
'tindex', 'function', 'findex', 'dim', 'size', 'origin', 'gid'
)

def __init__(self, handle, target, tindex=None, function=None, findex=None,
dim=None, size=1, origin=None):
dim=None, size=1, origin=None, gid=None):
self.handle = handle
self.target = target

Expand All @@ -40,6 +71,7 @@ def __init__(self, handle, target, tindex=None, function=None, findex=None,
self.dim = dim
self.size = size
self.origin = origin
self.gid = gid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does gid stand for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a docstring, but basically group id (a la Linux)


def __eq__(self, other):
return (type(self) is type(other) and
Expand All @@ -50,11 +82,13 @@ def __eq__(self, other):
self.findex == other.findex and
self.dim is other.dim and
self.size == other.size and
self.origin == other.origin)
self.origin == other.origin and
self.gid == other.gid)

def __hash__(self):
return hash((self.__class__, self.handle, self.target, self.tindex,
self.function, self.findex, self.dim, self.size, self.origin))
self.function, self.findex, self.dim, self.size, self.origin,
self.gid))

def __repr__(self):
return f"{self.__class__.__name__}<{self.handle.name}>"
Expand Down
20 changes: 18 additions & 2 deletions devito/passes/clusters/asynchrony.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def memcpy_key(c):
return task_key, memcpy_key


def make_gid(ispace, d):
"""
Make a group ID from the stamps carried by the non-trigger Intervals.
"""
gids = {i.stamp for i in ispace if not i.dim._defines & d._defines}
if len(gids) != 1:
return None
else:
return gids.pop()


@timed_pass(name='tasking')
def tasking(clusters, key0, sregistry):
"""
Expand Down Expand Up @@ -147,6 +158,8 @@ def _schedule_waitlocks(self, c0, d, clusters, locks, syncs):
return protected

def _schedule_withlocks(self, c0, d, protected, locks, syncs):
gid = make_gid(c0.ispace, d)

for target in protected:
lock = locks[target]

Expand All @@ -169,7 +182,7 @@ def _schedule_withlocks(self, c0, d, protected, locks, syncs):
for i in indices:
syncs[c0][d].update([
ReleaseLock(lock[i], target),
WithLock(lock[i], target, i, function, findex, d)
WithLock(lock[i], target, i, function, findex, d, gid=gid)
])


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

gid = make_gid(ispace, d)

syncs = {d: [
ReleaseLock(handle, target),
PrefetchUpdate(handle, target, tindex, function, findex, d, 1, e.rhs)
PrefetchUpdate(handle, target, tindex, function, findex, d,
origin=e.rhs, gid=gid)
]}
syncs = {**c.syncs, **syncs}

Expand Down
72 changes: 63 additions & 9 deletions devito/passes/clusters/buffering.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
timed_pass
)
from devito.types import Array, CustomDimension, Eq, ModuloDimension
from devito.warnings import warn

__all__ = ['buffering']

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

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

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

return init + processed

def _optimize(self, clusters, descriptors):
npthreads = self.options['npthreads']

stamps = self._make_task_groups(descriptors)

for b, v in descriptors.items():
if v.is_writeonly:
# `b` might be written by multiple, potentially mutually
# exclusive, equations. For example, two equations that have or
# will have complementary guards, hence only one will be
# executed. In such a case, we can split the equations over
# separate IterationSpaces
key0 = lambda: Stamp()
key0 = lambda: stamps[b] if npthreads else Stamp() # noqa: B023
elif v.is_readonly:
# `b` is read multiple times -- this could just be the case of
# coupled equations, so we more cautiously perform a
# "buffer-wise" splitting of the IterationSpaces (i.e., only
# relevant if there are at least two read-only buffers)
stamp = Stamp()
key0 = lambda: stamp # noqa: B023
stamp_fixed = Stamp()
key0 = lambda: stamps[b] if npthreads else stamp_fixed # noqa: B023
else:
continue

processed = []
for c in clusters:
if b not in c.functions:
f = v.f if npthreads else b

if f not in c.functions:
processed.append(c)
continue

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

return clusters

def _make_task_groups(self, descriptors):
"""
Assign task buffers to at most `npthreads` balanced groups.
"""
npthreads = self.options['npthreads']

stamps = {}
if not npthreads:
return stamps

task_sets = (
[b for b, v in descriptors.items() if v.is_writeonly],
[b for b, v in descriptors.items() if v.is_readonly],
)

for tasks in task_sets:
if not tasks:
continue

# Calculate the number of groups and their sizes, so that the tasks
# are divided into balanced groups
ntasks = len(tasks)
ngroups = min(npthreads, ntasks)
base, remainder = divmod(ntasks, ngroups)
sizes = (base + 1,) * remainder + (base,) * (ngroups - remainder)

if npthreads > ntasks:
warn(
f"`npthreads={npthreads}` exceeds the {ntasks} available "
f"tasks; using `npthreads={ntasks}` instead"
)

# Create a unique Stamp for each group and assign it to the tasks
# in that group
start = 0
for n in sizes:
stamp = Stamp()
stamps.update({b: stamp for b in tasks[start:start + n]})
start += n

return stamps

def _reuse(self, init, clusters, descriptors):
"""
Reuse existing Buffers for buffering candidates.
"""
buf_reuse = self.options['buf-reuse']

if not buf_reuse:
return init, clusters

if callable(buf_reuse):
cbk = lambda v: [i for i in v if buf_reuse(descriptors[i])]
else:
Expand Down
2 changes: 1 addition & 1 deletion devito/passes/clusters/fusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def __init__(self, toposort, options=None):
options = options or {}

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

super().__init__()

Expand Down
Loading
Loading