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
16 changes: 16 additions & 0 deletions devito/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ class ExecutionError(DevitoError):
* Device shared memory or registers (e.g., too many threads per block);
* etc.
"""


def mpi_raise(error, exception=ValueError, comm=None):
"""
Raise `exception` with the first non-None error message in rank order.

All ranks in `comm` must call this routine, including those with no local
error (`error=None`). This prevents a rank-local exception from stranding
peers in subsequent MPI calls. With no communicator or `MPI.COMM_NULL`,
only the local error is checked.
"""
# A null MPI communicator is false, like None
if comm:
error = next((i for i in comm.allgather(error) if i is not None), None)
if error is not None:
raise exception(error)
193 changes: 105 additions & 88 deletions devito/ir/support/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sympy import Expr, S

from devito.ir.support.space import Backward, null_ispace
from devito.ir.support.utils import AccessMode, extrema
from devito.ir.support.utils import AccessMode, erange, extrema
from devito.ir.support.vector import LabeledVector, Vector
from devito.symbolics import (
compare_ops, q_affine, q_comp_acc, q_constant, retrieve_indexed, search
Expand Down Expand Up @@ -358,6 +358,9 @@ def distance(self, other, logical=False):
# E.g., `uv(x).x` and `uv(x).y` -- not a real dependence!
return Vector(S.ImaginaryUnit)

if disjoint_subdims(self, other):
return Vector(S.ImaginaryUnit)

ret = []
for sit, oit in zip(self.itintervals, other.itintervals, strict=False):
n = len(ret)
Expand All @@ -369,20 +372,14 @@ def distance(self, other, logical=False):
# E.g., `self=R<f,[x]>` and `self.itintervals=(x, i)`
break

# If over SubDimensions, check disjointness
test = disjoint_subdims(self[n], other[n], sai, oai, sit, oit)
if test == DISJOINT:
return Vector(S.ImaginaryUnit)
elif test == MAYBE_OVERLAP:
ret.append(S.Infinity)
continue

try:
if not (sit == oit and sai.root is oai.root):
# E.g., `self=R<f,[x + 2]>` and `other=W<f,[i + 1]>`
# E.g., `self=R<f,[x]>`, `other=W<f,[x + 1]>`,
# `self.itintervals=(x<0>,)`, `other.itintervals=(x<1>,)`
return vinf(ret)
# Keep looking: a later axis may prove disjointness
ret.append(S.Infinity)
continue
except AttributeError:
# E.g., `self=R<f,[cy]>` and `self.itintervals=(y,)` => `sai=None`
pass
Expand Down Expand Up @@ -1152,7 +1149,29 @@ def reads_smart_gen(self, f):
"""
Generate all read accesses to a given function.

StencilDimensions, if any, are replaced with their extrema.
StencilDimensions, if any, are replaced with:

* in presence of SubDimensions: the range of points they span;
* in all other cases: just their extrema, since it suffices to
capture all possible dependencies.

The reason SubDimensions must be treated specially -- with a full set
of TimedAccess objects getting generated -- is to handle the special
case of SubDimensions thinner than the stencil’s reach. For example, consider
the following scenario:

* A SubDimension with just two points, 10 and 11;
* One equation writes `F[10]` and `F[11]`;
* Another equation runs over the same SubDimension reading the stencil
`F[x-4] ... F[x+4]`.

If we examine only the two extreme stencil offsets:

* `F[x-4]` reads points 6–7: no overlap.
* `F[x+4]` reads points 14–15: no overlap.

But interior offsets certainly overlap -- for instance, `F[x-1]` reads
9–10, which includes the producer’s point 10.

Notes
-----
Expand All @@ -1163,9 +1182,13 @@ def reads_smart_gen(self, f):
be found. For example, a DiscreteFunction would never appear among
the iteration symbols.
"""
uses_subdims = lambda i: any(d.is_Sub for d in i.ispace.dimensions)

if isinstance(f, (Function, Temp, TempArray, TBArray)):
for i in self.getreads(f):
for j in extrema(i.access):
expand = erange if uses_subdims(i) else extrema

for j in expand(i.access):
yield TimedAccess(j, i.mode, i.timestamp, i.ispace)

else:
Expand Down Expand Up @@ -1581,90 +1604,84 @@ def skippable_interval(d, ispace, it):
return d is None or (d in ispace and not d._defines & it.dim._defines)


# Possible return values for `disjoint_subdims`
INAPPLICABLE = 0
DISJOINT = 1
MAYBE_OVERLAP = 2


def disjoint_subdims(e0, e1, d0, d1, it0, it1):
def disjoint_subdims(a0, a1):
"""
Determine whether two accesses span distinct pieces of the same
SubDimension decomposition.

Consider a root Dimension `x` with bounds `x_m` and `x_M`. A valid
left/middle/right decomposition with thicknesses `L` and `R` is::

xl = [x_m, x_m + L - 1]
xm = [x_m + L, x_M - R]
xr = [x_M - R + 1, x_M]

These intervals are pairwise disjoint. Replacing `xl`, `xm`, or `xr`
with `x` in an affine access removes the choice of partition piece while
retaining the relative access. If two such normalized accesses have zero
distance, they apply the same affine map to disjoint intervals and therefore
cannot refer to the same data point. The apparent dependence is imaginary.

For example, `f[xl]` and `f[xm]` normalize to `f[x]` and `f[x]`;
they are independent. The same holds for `f[xl + 1]` and `f[xm + 1]`
when their iteration intervals have equal offsets. By contrast, `f[xl]`
and `f[xm - 1]` normalize to different accesses, and the latter may reach
into the left piece, so they must be treated conservatively.

This proof requires distinct pieces of the same root, compatible declared
thicknesses, affine accesses, and iteration intervals with equal offsets and
directions. Runtime bounds are assumed to preserve the declared partition.
Return DISJOINT if disjointness is proven, and MAYBE_OVERLAP if the
intervals are aligned SubDimensions but are not proven disjoint. In
particular, two declarations of the same left, right, or middle piece
overlap along this Dimension. MAYBE_OVERLAP lets the caller record an
infinite distance and inspect later Dimensions, which may still prove the
multidimensional accesses disjoint. Return INAPPLICABLE if this test does not
apply, so that the general distance analysis can classify the dependence.
Determine whether two TimedAccesses touch disjoint SubDimension regions
of the same Function.

Compare symbolic accessed bounds, including shifts and stencil points.
Block intervals are promoted to their logical SubDimensions. Declared
thicknesses determine the global regions: explicit overrides are forbidden,
while MPI clips these regions to each rank. Parent bounds and access offsets
remain symbolic; only iteration bounds use the declared thicknesses.

For example, a left SubDimension of thickness 4 ends before a middle
SubDimension excluding 4 points, even when the two thickness symbols are distinct.

Left/right SubDimensions of the same parent with `overlap=False` satisfy
`L + R <= N`, checked against the full global parent extent at runtime.
Their regions may be adjacent; stencil accesses extending from one into
the other may still induce dependences. If either SubDimension allows
overlap, no minimum separation is assumed.

Match data axes independently of the iteration nests. Return True if any
axis proves separation, False otherwise. Accesses over the same interval
use the general distance analysis.
"""
try:
# E.g., `f[xl]` over `(xl,)` and `f[xm]` over `(xm,)` need this
# special test, while accesses over the same `(xl,)` should use general
# distance analysis, so we can return immediately in such a case
if not (d0.is_Sub and
d1.is_Sub and
d0.root is d1.root and
it0.dim.root is d0.root and
it1.dim.root is d1.root and
for e0, e1, d0, d1 in zip(a0, a1, a0.aindices, a1.aindices, strict=False):
it0 = a0.intervals[d0]
it1 = a1.intervals[d1]
if it0.is_Null or it1.is_Null:
continue

it0 = it0.promote(lambda d: d.is_Incr)
it1 = it1.promote(lambda d: d.is_Incr)
if not (it0.dim.is_Sub and
it1.dim.is_Sub and
it0.dim.root is it1.dim.root and
it0 != it1):
return INAPPLICABLE
except AttributeError:
return INAPPLICABLE

if (d0.is_left and d1.is_middle) or \
(d0.is_middle and d1.is_left):
is_partition = d0.ltkn.value == d1.ltkn.value
elif (d0.is_middle and d1.is_right) or \
(d0.is_right and d1.is_middle):
is_partition = d0.rtkn.value == d1.rtkn.value
elif d0.is_left and d1.is_right:
is_partition = d0.ltkn.value is not None and d1.rtkn.value is not None
elif d0.is_right and d1.is_left:
is_partition = d0.rtkn.value is not None and d1.ltkn.value is not None
else:
is_partition = False
continue

thicknesses = {t: t.value for it in (it0, it1)
for t in it.dim.thickness if t.value is not None}
bounds = []
for e, d, it in ((e0, d0, it0), (e1, d1, it1)):
if not q_affine(e, d):
break

if not is_partition:
return MAYBE_OVERLAP
lower, upper = [], []
for v in erange(e):
slope = v.diff(d)
if slope.is_nonnegative:
m, M = it.symbolic_min, it.symbolic_max
elif slope.is_nonpositive:
M, m = it.symbolic_min, it.symbolic_max
else:
break
lower.append(v._subs(d, m.xreplace(thicknesses)))
upper.append(v._subs(d, M.xreplace(thicknesses)))
else:
bounds.append((sympy.Min(*lower), sympy.Max(*upper)))

if not q_affine(e0, d0) or not q_affine(e1, d1):
return MAYBE_OVERLAP
if len(bounds) == 2:
(m0, M0), (m1, M1) = bounds
mapper = {}

if it0.offsets != it1.offsets or it0.direction is not it1.direction:
return MAYBE_OVERLAP
dl, dr = (it0.dim, it1.dim) if it0.dim.is_left else (it1.dim, it0.dim)
dlp, drp = dl.parent, dr.parent

e0 = e0._subs(d0, d0.root)
e1 = e1._subs(d1, d1.root)
if dl.is_left and dr.is_right and dlp is drp and \
not (dl.overlap or dr.overlap):
# The runtime partition check guarantees a nonnegative gap only
gap = sympy.Dummy(nonnegative=True)

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.

?

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.

dropped, revamped, simplified

mapper[dlp.symbolic_max] = (dlp.symbolic_min + dl.ltkn.value +
dr.rtkn.value + gap - 1)

if e0 - e1 == 0:
return DISJOINT
else:
return MAYBE_OVERLAP
if (M0 - m1).subs(mapper).is_negative or \
(M1 - m0).subs(mapper).is_negative:
return True

return False


def disjoint_test(e0, e1, d, it):
Expand Down
6 changes: 4 additions & 2 deletions devito/operator/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
split, timed_pass, timed_region
)
from devito.types import Buffer, Evaluable, device_layer, disk_layer, host_layer
from devito.types.dimension import Thickness
from devito.types.dimension import SubDimension, Thickness
from devito.warnings import warn

__all__ = ['Operator']
Expand Down Expand Up @@ -716,7 +716,9 @@ def _prepare_arguments(self, autotune=None, estimate_memory=False, **kwargs):
except AttributeError:
pass
if d.is_Derived:
d._arg_check(args)
d._arg_check(args, **kwargs)

SubDimension._arg_check_thickness(self.dimensions, args)

# Turn arguments into a format suitable for the generated code
# E.g., instead of NumPy arrays for Functions, the generated code expects
Expand Down
10 changes: 6 additions & 4 deletions devito/passes/iet/orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ def _make_syncarray(self, iet, sync_ops, layer):
def _make_prefetchupdate(self, iet, sync_ops, layer, wrap=True):
return self._make_async_task(prefetchupdate, iet, sync_ops, layer, wrap)

@iet_pass
def process(self, iet):
callbacks = {
@property
def _callbacks(self):
return {
WaitLock: self._make_waitlock,
WithLock: self._make_withlock,
SyncArray: self._make_syncarray,
Expand All @@ -139,13 +139,15 @@ def process(self, iet):
AsyncCallable: self._make_async_callable
}

@iet_pass
def process(self, iet):
# Collect the compatible asynchronous task groups, if any
task_groups = TaskGroups()
if self.npthreads:
CollectTasks(task_groups).visit(iet)

# Lower the SyncSpots in a single bottom-up traversal, atomically lowering
lowerer = LowerSyncSpots(callbacks, task_groups, self.sregistry)
lowerer = LowerSyncSpots(self._callbacks, task_groups, self.sregistry)
iet = lowerer.visit(iet)

return iet, {'efuncs': lowerer.efuncs}
Expand Down
Loading
Loading