Skip to content
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)
22 changes: 21 additions & 1 deletion devito/ir/cgen/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from devito import configuration
from devito.arch.compiler import AOMPCompiler
from devito.symbolics.extended_sympy import BitwiseAnd
from devito.symbolics.inspection import has_integer_args, sympy_dtype
from devito.symbolics.queries import q_leaf
from devito.tools import (
Expand Down Expand Up @@ -255,7 +256,26 @@ def _print_RoundUp(self, expr):
return f'ROUND_UP({value}, {step})'

def _print_Mod(self, expr):
"""Print a Mod as a C-like %-based operation."""
"""
Print a Mod as an integer remainder or a power-of-two mask.

Python's `%` and SymPy's `Mod` give a nonnegative result for a positive
divisor, whereas C's `%` can be negative when the dividend is negative.
For example, `Mod(-1, 4) == 3`, but C's `-1 % 4 == -1`. For integer
operands and a positive power-of-two divisor `b`, emit `a & (b - 1)`
unless `a` is known nonnegative, preserving the Python/SymPy semantics.
"""
a, b = expr.args

# Unlike C's signed remainder, a mask preserves `Mod`'s nonnegative
# result for positive power-of-two divisors
if b.is_Integer and \
b > 0 and \
not (int(b) & (int(b) - 1)) and \
has_integer_args(a, b) and \
a.is_nonnegative is not True:
return f'({self._print(BitwiseAnd(a, b - 1))})'

args = [f'({self._print(a)})' for a in expr.args]
return '%'.join(args)

Expand Down
2 changes: 1 addition & 1 deletion devito/ir/iet/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ def expr_symbols(self):
with suppress(AttributeError):
ret.update(f.initvalue.free_symbols)
return tuple(ret)
elif f.is_Array and f.initvalue is not None:
elif f.is_ArrayLike and f.initvalue is not None:
# These are just a handful of values so it's OK to iterate them over
ret = set()
for i in f.initvalue:
Expand Down
4 changes: 2 additions & 2 deletions devito/ir/iet/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,11 @@ def _gen_value(self, obj, mode=1, masked=()):
except AttributeError:
pass

if obj.is_Array and obj.initvalue is not None and mode == 1:
if obj.is_ArrayLike and obj.initvalue is not None and mode == 1:
init = ListInitializer(obj.initvalue)
if not obj._mem_constant or init.is_numeric:
# printed at the Array's own precision, not the Operator's
value = c.Initializer(value, self.ccode(init, dtype=obj.dtype))
value = c.Initializer(value, self.ccode(init, dtype=obj.c0.dtype))
elif obj.is_LocalObject and obj.initvalue is not None and mode == 1:
value = c.Initializer(value, self.ccode(obj.initvalue))

Expand Down
200 changes: 110 additions & 90 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,87 @@ 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 `separated=True` satisfy
`L + R + space_order <= N`, checked against the full global parent extent
at `Operator.apply`. Their gap therefore accommodates stencil accesses;
larger shifts are still compared explicitly. If either SubDimension has
`separated=False`, 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

if not is_partition:
return MAYBE_OVERLAP

if not q_affine(e0, d0) or not q_affine(e1, d1):
return MAYBE_OVERLAP
continue

if it0.offsets != it1.offsets or it0.direction is not it1.direction:
return MAYBE_OVERLAP
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

e0 = e0._subs(d0, d0.root)
e1 = e1._subs(d1, d1.root)
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 len(bounds) == 2:
(m0, M0), (m1, M1) = bounds
mapper = {}

dl, dr = (it0.dim, it1.dim) if it0.dim.is_left else (it1.dim, it0.dim)
dlp, drp = dl.parent, dr.parent

if dl.is_left and dr.is_right and \
dl.separated and dr.separated and \
dlp is drp:
# Runtime validation guarantees N - L - R >= space_order
f = a0.function.c0
space_order = f.space_order if isinstance(f, Function) else 0
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 + space_order + gap - 1)

if (M0 - m1).subs(mapper).is_negative or \
(M1 - m0).subs(mapper).is_negative:
return True

if e0 - e1 == 0:
return DISJOINT
else:
return MAYBE_OVERLAP
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
3 changes: 3 additions & 0 deletions devito/symbolics/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ def q_positive(expr):
False simply means that it was not possible to determine an answer
to the query `X > 0`.
"""
if expr.is_Number:
return expr.is_positive is True

if not expr.is_Add:
return False

Expand Down
Loading
Loading