Skip to content

Commit 1069c63

Browse files
Merge pull request #3033 from devitocodes/tweak-graph-construction-final
compiler: Revamp DDA for SubDimensions
2 parents f976f24 + 867e30d commit 1069c63

27 files changed

Lines changed: 1494 additions & 190 deletions

‎devito/exceptions.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,19 @@ class ExecutionError(DevitoError):
5454
* Device shared memory or registers (e.g., too many threads per block);
5555
* etc.
5656
"""
57+
58+
59+
def mpi_raise(error, exception=ValueError, comm=None):
60+
"""
61+
Raise `exception` with the first non-None error message in rank order.
62+
63+
All ranks in `comm` must call this routine, including those with no local
64+
error (`error=None`). This prevents a rank-local exception from stranding
65+
peers in subsequent MPI calls. With no communicator or `MPI.COMM_NULL`,
66+
only the local error is checked.
67+
"""
68+
# A null MPI communicator is false, like None
69+
if comm:
70+
error = next((i for i in comm.allgather(error) if i is not None), None)
71+
if error is not None:
72+
raise exception(error)

‎devito/finite_differences/differentiable.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@ def _gather_for_diff(self):
563563

564564
# Bypass useless expensive SymPy _eval_ methods, for which we either already
565565
# know or don't care about the answer, because it'd have ~zero impact on our
566-
# average expressions
566+
# average expressions. Sign inference may also call `diff`, which here
567+
# constructs finite differences rather than symbolic derivatives
567568

568569
def _eval_is_even(self):
569570
return None
@@ -580,12 +581,18 @@ def _eval_is_negative(self):
580581
def _eval_is_extended_negative(self):
581582
return None
582583

584+
def _eval_is_extended_nonpositive(self):
585+
return None
586+
583587
def _eval_is_positive(self):
584588
return None
585589

586590
def _eval_is_extended_positive(self):
587591
return None
588592

593+
def _eval_is_extended_nonnegative(self):
594+
return None
595+
589596
def _eval_is_zero(self):
590597
return None
591598

‎devito/ir/cgen/printer.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from devito import configuration
1818
from devito.arch.compiler import AOMPCompiler
19+
from devito.symbolics.extended_sympy import BitwiseAnd
1920
from devito.symbolics.inspection import has_integer_args, sympy_dtype
2021
from devito.symbolics.queries import q_leaf
2122
from devito.tools import (
@@ -255,7 +256,26 @@ def _print_RoundUp(self, expr):
255256
return f'ROUND_UP({value}, {step})'
256257

257258
def _print_Mod(self, expr):
258-
"""Print a Mod as a C-like %-based operation."""
259+
"""
260+
Print a Mod as an integer remainder or a power-of-two mask.
261+
262+
Python's `%` and SymPy's `Mod` give a nonnegative result for a positive
263+
divisor, whereas C's `%` can be negative when the dividend is negative.
264+
For example, `Mod(-1, 4) == 3`, but C's `-1 % 4 == -1`. For integer
265+
operands and a positive power-of-two divisor `b`, emit `a & (b - 1)`
266+
unless `a` is known nonnegative, preserving the Python/SymPy semantics.
267+
"""
268+
a, b = expr.args
269+
270+
# Unlike C's signed remainder, a mask preserves `Mod`'s nonnegative
271+
# result for positive power-of-two divisors
272+
if b.is_Integer and \
273+
b > 0 and \
274+
not (int(b) & (int(b) - 1)) and \
275+
has_integer_args(a, b) and \
276+
a.is_nonnegative is not True:
277+
return f'({self._print(BitwiseAnd(a, b - 1))})'
278+
259279
args = [f'({self._print(a)})' for a in expr.args]
260280
return '%'.join(args)
261281

‎devito/ir/iet/nodes.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,7 @@ def expr_symbols(self):
10781078
with suppress(AttributeError):
10791079
ret.update(f.initvalue.free_symbols)
10801080
return tuple(ret)
1081-
elif f.is_Array and f.initvalue is not None:
1081+
elif f.is_ArrayLike and f.initvalue is not None:
10821082
# These are just a handful of values so it's OK to iterate them over
10831083
ret = set()
10841084
for i in f.initvalue:

‎devito/ir/iet/visitors.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,11 @@ def _gen_value(self, obj, mode=1, masked=()):
360360
except AttributeError:
361361
pass
362362

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

‎devito/ir/support/basic.py‎

Lines changed: 123 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sympy import Expr, S
88

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

361+
if disjoint_subdims(self, other):
362+
return Vector(S.ImaginaryUnit)
363+
361364
ret = []
362365
for sit, oit in zip(self.itintervals, other.itintervals, strict=False):
363366
n = len(ret)
@@ -369,20 +372,14 @@ def distance(self, other, logical=False):
369372
# E.g., `self=R<f,[x]>` and `self.itintervals=(x, i)`
370373
break
371374

372-
# If over SubDimensions, check disjointness
373-
test = disjoint_subdims(self[n], other[n], sai, oai, sit, oit)
374-
if test == DISJOINT:
375-
return Vector(S.ImaginaryUnit)
376-
elif test == MAYBE_OVERLAP:
377-
ret.append(S.Infinity)
378-
continue
379-
380375
try:
381376
if not (sit == oit and sai.root is oai.root):
382377
# E.g., `self=R<f,[x + 2]>` and `other=W<f,[i + 1]>`
383378
# E.g., `self=R<f,[x]>`, `other=W<f,[x + 1]>`,
384379
# `self.itintervals=(x<0>,)`, `other.itintervals=(x<1>,)`
385-
return vinf(ret)
380+
# Keep looking: a later axis may prove disjointness
381+
ret.append(S.Infinity)
382+
continue
386383
except AttributeError:
387384
# E.g., `self=R<f,[cy]>` and `self.itintervals=(y,)` => `sai=None`
388385
pass
@@ -1152,7 +1149,29 @@ def reads_smart_gen(self, f):
11521149
"""
11531150
Generate all read accesses to a given function.
11541151
1155-
StencilDimensions, if any, are replaced with their extrema.
1152+
StencilDimensions, if any, are replaced with:
1153+
1154+
* in presence of SubDimensions: the range of points they span;
1155+
* in all other cases: just their extrema, since it suffices to
1156+
capture all possible dependencies.
1157+
1158+
The reason SubDimensions must be treated specially -- with a full set
1159+
of TimedAccess objects getting generated -- is to handle the special
1160+
case of SubDimensions thinner than the stencil’s reach. For example, consider
1161+
the following scenario:
1162+
1163+
* A SubDimension with just two points, 10 and 11;
1164+
* One equation writes `F[10]` and `F[11]`;
1165+
* Another equation runs over the same SubDimension reading the stencil
1166+
`F[x-4] ... F[x+4]`.
1167+
1168+
If we examine only the two extreme stencil offsets:
1169+
1170+
* `F[x-4]` reads points 6–7: no overlap.
1171+
* `F[x+4]` reads points 14–15: no overlap.
1172+
1173+
But interior offsets certainly overlap -- for instance, `F[x-1]` reads
1174+
9–10, which includes the producer’s point 10.
11561175
11571176
Notes
11581177
-----
@@ -1163,9 +1182,13 @@ def reads_smart_gen(self, f):
11631182
be found. For example, a DiscreteFunction would never appear among
11641183
the iteration symbols.
11651184
"""
1185+
uses_subdims = lambda i: any(d.is_Sub for d in i.ispace.dimensions)
1186+
11661187
if isinstance(f, (Function, Temp, TempArray, TBArray)):
11671188
for i in self.getreads(f):
1168-
for j in extrema(i.access):
1189+
expand = erange if uses_subdims(i) else extrema
1190+
1191+
for j in expand(i.access):
11691192
yield TimedAccess(j, i.mode, i.timestamp, i.ispace)
11701193

11711194
else:
@@ -1581,90 +1604,103 @@ def skippable_interval(d, ispace, it):
15811604
return d is None or (d in ispace and not d._defines & it.dim._defines)
15821605

15831606

1584-
# Possible return values for `disjoint_subdims`
1585-
INAPPLICABLE = 0
1586-
DISJOINT = 1
1587-
MAYBE_OVERLAP = 2
1607+
def disjoint_subdims(a0, a1):
1608+
"""
1609+
Determine whether two TimedAccesses touch disjoint SubDimension regions
1610+
of the same Function.
1611+
1612+
Compare symbolic accessed bounds, including shifts and stencil points.
1613+
Block intervals are promoted to their logical SubDimensions. Declared
1614+
thicknesses determine the global regions: explicit overrides are forbidden,
1615+
while MPI clips these regions to each rank. Parent bounds and access offsets
1616+
remain symbolic; only iteration bounds use the declared thicknesses.
1617+
1618+
For example, a left SubDimension of thickness 4 ends before a middle
1619+
SubDimension excluding 4 points, even when the two thickness symbols are distinct.
1620+
1621+
Left/right SubDimensions of the same parent with `separated=True` satisfy
1622+
`L + R + space_order <= N`, checked against the full global parent extent
1623+
at `Operator.apply`. Their gap therefore accommodates stencil accesses;
1624+
larger shifts are still compared explicitly. If either SubDimension has
1625+
`separated=False`, no minimum separation is assumed.
1626+
1627+
Match data axes independently of the iteration nests. Return True if any
1628+
axis proves separation, False otherwise. Accesses over the same interval
1629+
use the general distance analysis.
1630+
"""
1631+
for e0, e1, d0, d1 in zip(a0, a1, a0.aindices, a1.aindices, strict=False):
1632+
it0 = a0.intervals[d0]
1633+
it1 = a1.intervals[d1]
1634+
if it0.is_Null or it1.is_Null:
1635+
continue
1636+
1637+
it0 = it0.promote(lambda d: d.is_Incr)
1638+
it1 = it1.promote(lambda d: d.is_Incr)
1639+
if not (it0.dim.is_Sub and
1640+
it1.dim.is_Sub and
1641+
it0.dim.root is it1.dim.root and
1642+
it0 != it1):
1643+
continue
1644+
1645+
f = a0.function.c0
1646+
space_order = f.space_order if isinstance(f, Function) else 0
1647+
if disjoint_subdims_axis(e0, e1, d0, d1, it0, it1, space_order):
1648+
return True
1649+
1650+
return False
15881651

15891652

1590-
def disjoint_subdims(e0, e1, d0, d1, it0, it1):
1653+
@memoized_func(scope='build')
1654+
def disjoint_subdims_axis(e0, e1, d0, d1, it0, it1, space_order):
15911655
"""
1592-
Determine whether two accesses span distinct pieces of the same
1593-
SubDimension decomposition.
1594-
1595-
Consider a root Dimension `x` with bounds `x_m` and `x_M`. A valid
1596-
left/middle/right decomposition with thicknesses `L` and `R` is::
1597-
1598-
xl = [x_m, x_m + L - 1]
1599-
xm = [x_m + L, x_M - R]
1600-
xr = [x_M - R + 1, x_M]
1601-
1602-
These intervals are pairwise disjoint. Replacing `xl`, `xm`, or `xr`
1603-
with `x` in an affine access removes the choice of partition piece while
1604-
retaining the relative access. If two such normalized accesses have zero
1605-
distance, they apply the same affine map to disjoint intervals and therefore
1606-
cannot refer to the same data point. The apparent dependence is imaginary.
1607-
1608-
For example, `f[xl]` and `f[xm]` normalize to `f[x]` and `f[x]`;
1609-
they are independent. The same holds for `f[xl + 1]` and `f[xm + 1]`
1610-
when their iteration intervals have equal offsets. By contrast, `f[xl]`
1611-
and `f[xm - 1]` normalize to different accesses, and the latter may reach
1612-
into the left piece, so they must be treated conservatively.
1613-
1614-
This proof requires distinct pieces of the same root, compatible declared
1615-
thicknesses, affine accesses, and iteration intervals with equal offsets and
1616-
directions. Runtime bounds are assumed to preserve the declared partition.
1617-
Return DISJOINT if disjointness is proven, and MAYBE_OVERLAP if the
1618-
intervals are aligned SubDimensions but are not proven disjoint. In
1619-
particular, two declarations of the same left, right, or middle piece
1620-
overlap along this Dimension. MAYBE_OVERLAP lets the caller record an
1621-
infinite distance and inspect later Dimensions, which may still prove the
1622-
multidimensional accesses disjoint. Return INAPPLICABLE if this test does not
1623-
apply, so that the general distance analysis can classify the dependence.
1656+
Test separation along one data axis of two SubDimension accesses.
1657+
1658+
The result depends on the indices, intervals and stencil order, rather than
1659+
the access timestamps, modes or other axes. Cache it across the many
1660+
TimedAccess pairs and Scopes that reuse the same one-dimensional regions.
1661+
The cache is cleared at the end of Operator construction.
16241662
"""
1625-
try:
1626-
# E.g., `f[xl]` over `(xl,)` and `f[xm]` over `(xm,)` need this
1627-
# special test, while accesses over the same `(xl,)` should use general
1628-
# distance analysis, so we can return immediately in such a case
1629-
if not (d0.is_Sub and
1630-
d1.is_Sub and
1631-
d0.root is d1.root and
1632-
it0.dim.root is d0.root and
1633-
it1.dim.root is d1.root and
1634-
it0 != it1):
1635-
return INAPPLICABLE
1636-
except AttributeError:
1637-
return INAPPLICABLE
1638-
1639-
if (d0.is_left and d1.is_middle) or \
1640-
(d0.is_middle and d1.is_left):
1641-
is_partition = d0.ltkn.value == d1.ltkn.value
1642-
elif (d0.is_middle and d1.is_right) or \
1643-
(d0.is_right and d1.is_middle):
1644-
is_partition = d0.rtkn.value == d1.rtkn.value
1645-
elif d0.is_left and d1.is_right:
1646-
is_partition = d0.ltkn.value is not None and d1.rtkn.value is not None
1647-
elif d0.is_right and d1.is_left:
1648-
is_partition = d0.rtkn.value is not None and d1.ltkn.value is not None
1649-
else:
1650-
is_partition = False
1663+
thicknesses = {t: t.value for it in (it0, it1)
1664+
for t in it.dim.thickness if t.value is not None}
1665+
bounds = []
1666+
for e, d, it in ((e0, d0, it0), (e1, d1, it1)):
1667+
if not q_affine(e, d):
1668+
break
1669+
1670+
lower, upper = [], []
1671+
for v in erange(e):
1672+
slope = v.diff(d)
1673+
if slope.is_nonnegative:
1674+
m, M = it.symbolic_min, it.symbolic_max
1675+
elif slope.is_nonpositive:
1676+
M, m = it.symbolic_min, it.symbolic_max
1677+
else:
1678+
break
1679+
lower.append(v._subs(d, m.xreplace(thicknesses)))
1680+
upper.append(v._subs(d, M.xreplace(thicknesses)))
1681+
else:
1682+
bounds.append((sympy.Min(*lower), sympy.Max(*upper)))
16511683

1652-
if not is_partition:
1653-
return MAYBE_OVERLAP
1684+
if len(bounds) == 2:
1685+
(m0, M0), (m1, M1) = bounds
1686+
mapper = {}
16541687

1655-
if not q_affine(e0, d0) or not q_affine(e1, d1):
1656-
return MAYBE_OVERLAP
1688+
dl, dr = (it0.dim, it1.dim) if it0.dim.is_left else (it1.dim, it0.dim)
1689+
dlp, drp = dl.parent, dr.parent
16571690

1658-
if it0.offsets != it1.offsets or it0.direction is not it1.direction:
1659-
return MAYBE_OVERLAP
1691+
if dl.is_left and dr.is_right and \
1692+
dl.separated and dr.separated and \
1693+
dlp is drp:
1694+
# Runtime validation guarantees N - L - R >= space_order
1695+
gap = sympy.Dummy(nonnegative=True)
1696+
mapper[dlp.symbolic_max] = (dlp.symbolic_min + dl.ltkn.value +
1697+
dr.rtkn.value + space_order + gap - 1)
16601698

1661-
e0 = e0._subs(d0, d0.root)
1662-
e1 = e1._subs(d1, d1.root)
1699+
if (M0 - m1).subs(mapper).is_negative or \
1700+
(M1 - m0).subs(mapper).is_negative:
1701+
return True
16631702

1664-
if e0 - e1 == 0:
1665-
return DISJOINT
1666-
else:
1667-
return MAYBE_OVERLAP
1703+
return False
16681704

16691705

16701706
def disjoint_test(e0, e1, d, it):

0 commit comments

Comments
 (0)