Skip to content

Commit beaa385

Browse files
committed
compiler: Improve SubDimension DDA
1 parent 91540c5 commit beaa385

5 files changed

Lines changed: 109 additions & 48 deletions

File tree

‎devito/ir/support/basic.py‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,21 +1610,20 @@ def disjoint_subdims(a0, a1):
16101610
of the same Function.
16111611
16121612
Compare symbolic accessed bounds, including shifts and stencil points.
1613-
Block intervals are promoted to their logical SubDimensions. Bounds and
1614-
thicknesses remain symbolic: MPI decomposition and runtime overrides can
1615-
change their values independently of the defaults.
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.
16161617
1617-
For example, `xl = [m, m + L - 1]` and `xm = [m + L, M - R]` are
1618-
disjoint when they share the symbol `L`, whatever its runtime value.
1619-
Equal default thicknesses alone do not establish that relationship.
1618+
For example, a left slab of thickness 4 ends before a middle excluding 4
1619+
points, even when the two thickness symbols are distinct.
16201620
16211621
Opposite left/right slabs are assumed to form a valid partition: their
16221622
thicknesses satisfy `L + R <= N`. For translated stencil accesses, the
16231623
interior must also accommodate their combined inward reach. For example,
16241624
a pointwise left write and a right read at offset -4 require four interior
16251625
points. Runtime space_order checks cover explicit middle SubDimensions,
1626-
not arbitrary left/right pairs; no concrete domain size or thickness is
1627-
used here.
1626+
not arbitrary left/right pairs; no concrete domain size is used here.
16281627
16291628
Match data axes independently of the iteration nests. Return True if any
16301629
axis proves separation, False otherwise. Accesses over the same interval
@@ -1644,6 +1643,8 @@ def disjoint_subdims(a0, a1):
16441643
it0 != it1):
16451644
continue
16461645

1646+
thicknesses = {t: t.value for it in (it0, it1)
1647+
for t in it.dim.thickness if t.value is not None}
16471648
bounds = []
16481649
for e, d, it in ((e0, d0, it0), (e1, d1, it1)):
16491650
if not q_affine(e, d):
@@ -1658,8 +1659,8 @@ def disjoint_subdims(a0, a1):
16581659
M, m = it.symbolic_min, it.symbolic_max
16591660
else:
16601661
break
1661-
lower.append(v._subs(d, m))
1662-
upper.append(v._subs(d, M))
1662+
lower.append(v._subs(d, m.xreplace(thicknesses)))
1663+
upper.append(v._subs(d, M.xreplace(thicknesses)))
16631664
else:
16641665
bounds.append((sympy.Min(*lower), sympy.Max(*upper)))
16651666

@@ -1682,11 +1683,13 @@ def disjoint_subdims(a0, a1):
16821683
gap = sympy.Dummy(nonnegative=True)
16831684
if e0.diff(d0) == e1.diff(d1) == 1:
16841685
M, m = (M0, m1) if it0.dim.is_left else (M1, m0)
1685-
reach = (M - dl.symbolic_max - m + dr.symbolic_min).expand()
1686+
reach = (M - dl.symbolic_max.xreplace(thicknesses) - m +
1687+
dr.symbolic_min.xreplace(thicknesses)).expand()
16861688
if is_integer(reach):
16871689
gap += max(0, reach)
16881690

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

16911694
if (M0 - m1).subs(mapper).is_negative or \
16921695
(M1 - m0).subs(mapper).is_negative:

‎devito/types/dimension.py‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,12 @@ def _arg_check(self, *args, **kwargs):
554554
# the user
555555

556556
class Thickness(DataSymbol):
557-
"""A DataSymbol to represent a thickness of a SubDimension"""
557+
"""
558+
A SubDimension thickness, fixed at construction and localized by MPI.
559+
560+
Explicit runtime overrides are not supported: dependence analysis uses the
561+
declared thickness to determine the global region before MPI decomposition.
562+
"""
558563

559564
__rkwargs__ = DataSymbol.__rkwargs__ + ('root', 'side', 'local', 'value')
560565

@@ -589,11 +594,7 @@ def value(self):
589594
return self._value
590595

591596
def _arg_values(self, grid=None, **kwargs):
592-
# Allow override of thickness values to disable BCs
593-
# However, arguments from the user are considered global
594-
# So overriding the thickness to a nonzero value should not cause
595-
# boundaries to exist between ranks where they did not before
596-
rtkn = kwargs.get(self.name, self.value)
597+
rtkn = self.value
597598
if grid is not None and grid.is_distributed(self.root):
598599
# Get local thickness
599600
if self.local:
@@ -612,6 +613,15 @@ def _arg_values(self, grid=None, **kwargs):
612613

613614
return {self.name: tkn}
614615

616+
def _arg_check(self, args, *_args, **kwargs):
617+
# This module depends on Dimension, so importing it above would cycle
618+
from devito.mpi import mpi_raise # noqa: PLC0415
619+
620+
error = (f"Cannot override SubDimension thickness `{self.name}`"
621+
if self.name in kwargs else None)
622+
comm = args.comm if args.options['mpi'] else None
623+
mpi_raise(error, InvalidArgument, comm=comm)
624+
615625

616626
class AbstractSubDimension(DerivedDimension):
617627

@@ -840,7 +850,7 @@ def _arg_check(self, args, *_args, **kwargs):
840850
values = {**args,
841851
d.min_name: kwargs.get(d.min_name, 0),
842852
d.max_name: kwargs.get(d.max_name, kwargs.get(d.name, size - 1)),
843-
**{t.name: kwargs.get(t.name, t.value) for t in self.thickness}}
853+
**{t.name: t.value for t in self.thickness}}
844854
else:
845855
values = args
846856
size = int(subs_op_args(self.symbolic_size, values))

‎tests/test_dimension.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -794,17 +794,18 @@ def test_expandingbox_like(self, opt):
794794
grid = Grid(shape=(8, 8))
795795
x, y = grid.dimensions
796796

797+
# Declare the widest box; runtime root bounds control the active box
798+
xi = SubDimension.middle(name='xi', parent=x, thickness_left=0, thickness_right=0)
799+
yi = SubDimension.middle(name='yi', parent=y, thickness_left=0, thickness_right=0)
800+
797801
u = TimeFunction(name='u', grid=grid)
798-
xi = SubDimension.middle(name='xi', parent=x, thickness_left=2, thickness_right=2)
799-
yi = SubDimension.middle(name='yi', parent=y, thickness_left=2, thickness_right=2)
800802

801803
eqn = Eq(u.forward, u + 1)
802804
eqn = eqn.subs({x: xi, y: yi})
803805

804806
op = Operator(eqn, opt=opt)
805807

806-
op.apply(time=3, x_m=2, x_M=5, y_m=2, y_M=5,
807-
x_ltkn0=0, x_rtkn0=0, y_ltkn0=0, y_rtkn0=0)
808+
op.apply(time=3, x_m=2, x_M=5, y_m=2, y_M=5)
808809

809810
assert np.all(u.data[0, 2:-2, 2:-2] == 4.)
810811
assert np.all(u.data[1, 2:-2, 2:-2] == 3.)

‎tests/test_ir.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,6 @@ def test_subdimension_affine_bounds(self, shared_boundary, symbolic, slope, offs
562562
xm = SubDimension.middle('xm', x, 8, 40)
563563
if shared_boundary:
564564
xm = xm._rebuild(thickness=(xl.ltkn, xm.rtkn))
565-
else:
566-
expected = S.Infinity
567565
if symbolic:
568566
f = Array(name='f', dimensions=(x,))
569567
else:
@@ -1305,8 +1303,8 @@ def test_bundle_components(self):
13051303

13061304
@pytest.mark.parametrize('symbolic', [False, True])
13071305
@pytest.mark.parametrize('lower,upper,ndeps', [
1308-
(0, 31, (8, 32)), (0, 7, (0, 8)), (16, 31, (0, 16)),
1309-
(7, 8, (1, 2)), (15, 16, (1, 2))
1306+
(0, 31, (8, 24)), (0, 7, (0, 0)), (16, 31, (0, 16)),
1307+
(7, 8, (1, 1)), (15, 16, (1, 2))
13101308
])
13111309
def test_stencil_contains_producer(self, symbolic, lower, upper, ndeps):
13121310
grid = Grid(shape=(32,))
@@ -1317,6 +1315,8 @@ def test_stencil_contains_producer(self, symbolic, lower, upper, ndeps):
13171315
f = Function(name='f', grid=grid)
13181316
g = Function(name='g', grid=grid)
13191317
if symbolic:
1318+
# Thickness 8 fixes the left boundary; the parent upper bound remains
1319+
# symbolic, so stencil offsets >= 8 can still touch the producer
13201320
pi, ci = Interval(xm), Interval(xl)
13211321
else:
13221322
# Encode actual fixed iteration bounds, not runtime defaults

‎tests/test_subdomains.py‎

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,14 +1937,16 @@ def define(self, dimensions):
19371937
class TestSubDomainArguments:
19381938

19391939
@staticmethod
1940-
def _make_operator(left_shift=0, right_shift=0, grid=None, middle=False):
1940+
def _make_operator(left_shift=0, right_shift=0, grid=None, middle=False,
1941+
thickness=(8, 8)):
19411942
grid = grid or Grid(shape=(8, 32))
19421943
y = grid.dimensions[-1]
19431944

1944-
yl = SubDimension.left('yl', y, 8)
1945-
yr = SubDimension.right('yr', y, 8)
1945+
left, right = thickness
1946+
yl = SubDimension.left('yl', y, left)
1947+
yr = SubDimension.right('yr', y, right)
19461948
if middle:
1947-
yl = yr = SubDimension.middle('ym', y, 8, 8)
1949+
yl = yr = SubDimension.middle('ym', y, left, right)
19481950

19491951
u = TimeFunction(name='u', grid=grid, space_order=8)
19501952
v = TimeFunction(name='v', grid=grid, space_order=8)
@@ -1979,7 +1981,11 @@ def test_stencil_gap(self, middle, left_shift, right_shift, margin, override):
19791981
dl, = [d for d in op.dimensions if d.is_Sub and not d.is_right]
19801982
kwargs = {dl.ltkn.name: 32 - 8 - 8 - margin}
19811983

1982-
if middle and margin < 0:
1984+
if override == 'thickness':
1985+
with pytest.raises(InvalidArgument,
1986+
match='Cannot override SubDimension thickness'):
1987+
op.arguments(time_M=0, **kwargs)
1988+
elif middle and margin < 0:
19831989
with pytest.raises(InvalidArgument, match='at least 8 interior points'):
19841990
op.arguments(time_M=0, **kwargs)
19851991
else:
@@ -2007,13 +2013,11 @@ def test_runtime_space_order(self, middle, space_order, margin):
20072013

20082014
@pytest.mark.parametrize('side', ['left', 'right'])
20092015
def test_empty_slab(self, side):
2010-
op, _ = self._make_operator(right_shift=-4)
2011-
2012-
d, = [d for d in op.dimensions if d.is_Sub and getattr(d, f'is_{side}')]
2013-
thickness = d.ltkn if side == 'left' else d.rtkn
2016+
thickness = (0, 8) if side == 'left' else (8, 0)
2017+
op, _ = self._make_operator(right_shift=-4, thickness=thickness)
20142018

20152019
# The active slab fills the local domain; the opposite slab is absent
2016-
op.arguments(time_M=0, y_M=7, **{thickness.name: 0})
2020+
op.arguments(time_M=0, y_M=7)
20172021

20182022
def test_before_autotuning(self):
20192023
op, _ = self._make_operator(right_shift=-4, middle=True)
@@ -2032,32 +2036,27 @@ def test_before_autotuning(self):
20322036
def test_distributed_middle(self, left, mode):
20332037
grid = Grid(shape=(16, 32), topology=(1, 2))
20342038

2035-
op, _ = self._make_operator(grid=grid, middle=True)
2036-
2037-
d, = [d for d in op.dimensions if d.is_Sub]
2039+
op, _ = self._make_operator(grid=grid, middle=True, thickness=(left, 0))
20382040

20392041
# Rank 0 has one point or an empty middle (possibly with inverted bounds).
20402042
# Only the global size determines whether the middle is large enough
2041-
kwargs = {d.ltkn.name: left, d.rtkn.name: 0}
2042-
20432043
if left == 25:
20442044
with pytest.raises(InvalidArgument, match='at least 8 interior points'):
2045-
op.arguments(time_M=0, **kwargs)
2045+
op.arguments(time_M=0)
20462046
else:
2047-
op.arguments(time_M=0, **kwargs)
2047+
op.arguments(time_M=0)
20482048

20492049
@pytest.mark.parallel(mode=[(2, 'basic')])
20502050
def test_collective_rejection(self, mode):
20512051
grid = Grid(shape=(32, 32), topology=(2, 1))
20522052

20532053
op, _ = self._make_operator(right_shift=-4, grid=grid, middle=True)
20542054

2055-
dl, = [d for d in op.dimensions if d.is_Sub and d.is_middle]
2056-
left = 24 if grid.distributor.myrank == 0 else 0
2055+
upper = 22 if grid.distributor.myrank == 0 else 31
20572056

2058-
# Only rank 0 has an insufficient middle; its peer has 24 interior points
2057+
# Only rank 0 has an insufficient middle; its peer has 16 interior points
20592058
with pytest.raises(InvalidArgument, match='interior points'):
2060-
op.arguments(time_M=0, **{dl.ltkn.name: left})
2059+
op.arguments(time_M=0, y_M=upper)
20612060

20622061
def test_function_on_subdomain(self):
20632062
class Interior(SubDomain):
@@ -2086,4 +2085,52 @@ def define(self, dimensions):
20862085
assert concrete.ltkn.name in args
20872086

20882087
with pytest.raises(InvalidArgument, match='at least 8 interior points'):
2088+
op.arguments(y_M=22)
2089+
2090+
with pytest.raises(InvalidArgument,
2091+
match='Cannot override SubDimension thickness'):
20892092
op.arguments(**{concrete.ltkn.name: 17})
2093+
2094+
def test_thickness_overrides(self):
2095+
grid = Grid(shape=(32,))
2096+
x, = grid.dimensions
2097+
xi = SubDimension.left('xi', x, 4)
2098+
2099+
f = Function(name='f', grid=grid, space_order=0)
2100+
2101+
eq = Eq(f[xi], 1)
2102+
2103+
op = Operator(eq, name='thickness_overrides')
2104+
2105+
d, = [d for d in op.dimensions if d.is_Sub]
2106+
2107+
with pytest.raises(InvalidArgument,
2108+
match='Cannot override SubDimension thickness'):
2109+
op.apply(**{d.ltkn.name: 2})
2110+
2111+
assert np.all(f.data == 0)
2112+
2113+
@pytest.mark.parallel(mode=[(2, 'basic')])
2114+
def test_collective_thickness_rejection(self, mode):
2115+
grid = Grid(shape=(32,))
2116+
x, = grid.dimensions
2117+
xi = SubDimension.middle('xi', x, 4, 4)
2118+
2119+
f = Function(name='f', grid=grid, space_order=0)
2120+
2121+
eq = Eq(f[xi], 1)
2122+
2123+
op = Operator(eq, name='collective_thickness_rejection')
2124+
2125+
d, = [d for d in op.dimensions if d.is_Sub]
2126+
t = d.ltkn
2127+
2128+
# MPI clips the declared thicknesses without any explicit overrides
2129+
args = op.arguments()
2130+
assert set(grid.distributor.comm.allgather(args[t.name])) == {0, 4}
2131+
2132+
# Only rank 0 supplies an override; all ranks must reject it
2133+
kwargs = {t.name: 4} if grid.distributor.myrank == 0 else {}
2134+
with pytest.raises(InvalidArgument,
2135+
match='Cannot override SubDimension thickness'):
2136+
op.arguments(**kwargs)

0 commit comments

Comments
 (0)