Skip to content

Commit 760a476

Browse files
authored
Merge pull request #3030 from devitocodes/hotfix-buffering-cpml
compiler: Fix buffering in presence of written HALO regions
2 parents caf6fc9 + 327c61f commit 760a476

8 files changed

Lines changed: 429 additions & 14 deletions

File tree

‎devito/ir/clusters/algorithms.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
from devito.ir.clusters.visitors import Queue, cluster_pass
1313
from devito.ir.equations import OpMax, OpMin, OpMinMax, identity_mapper
1414
from devito.ir.support import (
15-
Any, Backward, Forward, IterationSpace, Scope, erange, pull_dims
15+
Any, Backward, Forward, IterationSpace, Scope, detect_halo_writes, erange, pull_dims
1616
)
17+
from devito.logger import warning
1718
from devito.mpi.halo_scheme import HaloScheme, HaloTouch
1819
from devito.mpi.reduction_scheme import DistReduce
1920
from devito.symbolics import limits_mapper, retrieve_indexed, uxreplace, xreplace_indices
@@ -486,6 +487,8 @@ def communications(clusters):
486487
clusters = HaloComms().process(clusters)
487488
clusters = reduction_comms(clusters)
488489

490+
check_halo_writes(clusters)
491+
489492
return clusters
490493

491494

@@ -628,6 +631,26 @@ def _update(reductions):
628631
return processed
629632

630633

634+
def check_halo_writes(clusters):
635+
"""
636+
Warn about HALO writes along Dimensions not fixed to 1 in the Grid topology.
637+
"""
638+
for c in clusters:
639+
try:
640+
grid = c.grid
641+
except ValueError:
642+
grid = None
643+
644+
topology = {}
645+
if grid is not None and grid.topology is not None:
646+
topology = dict(zip(grid.dimensions, grid.topology, strict=True))
647+
648+
key = lambda d: d in c.dist_dimensions and topology.get(d) != 1 # noqa: B023
649+
if detect_halo_writes(c, key):
650+
warning("Writing to the HALO along potentially distributed Dimensions; "
651+
"set their Grid topology entries to 1")
652+
653+
631654
def normalize(clusters, sregistry=None, options=None, platform=None, **kwargs):
632655
clusters = normalize_nested_indexeds(clusters, sregistry)
633656
if options['mapify-reduce']:

‎devito/ir/support/basic.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,52 @@ def distance(self, other, logical=False):
492492

493493
return Vector(*ret)
494494

495+
def touched_nodomain(self, findex):
496+
"""
497+
Return a boolean 2-tuple, one entry for each ``findex`` DataSide. True
498+
means that the entire access lies outside the DOMAIN along that
499+
DataSide.
500+
501+
If containment outside the DOMAIN cannot be proven, return False for
502+
that DataSide. Unlike ``touched_halo``, this is a containment query and
503+
applies irrespective of whether ``findex`` is distributed.
504+
"""
505+
if not self.affine(findex):
506+
return (False, False)
507+
508+
d = self.aindices[findex]
509+
limits = []
510+
if d is not None:
511+
i = self.intervals[d]
512+
if i.is_Null:
513+
return (False, False)
514+
limits.append((d, d.symbolic_min + i.lower, d.symbolic_max + i.upper))
515+
516+
# Runtime DOMAIN bounds may select any part of the allocated extent
517+
for v in (findex.symbolic_min, findex.symbolic_max):
518+
if v.is_Symbol:
519+
limits.append((v, S.Zero, findex.symbolic_size - 1))
520+
521+
def outside(expr):
522+
# A negative maximum distance proves the entire access is outside
523+
expr = sympy.expand(expr)
524+
for symbol, lower, upper in limits:
525+
coefficient = expr.diff(symbol)
526+
if coefficient.has(symbol):
527+
return False
528+
elif coefficient.is_nonnegative:
529+
expr = expr.subs(symbol, upper)
530+
elif coefficient.is_nonpositive:
531+
expr = expr.subs(symbol, lower)
532+
else:
533+
return False
534+
535+
return expr.is_negative is True
536+
537+
index = self[findex] - self.function._size_nodomain[findex].left
538+
return (outside(index - findex.symbolic_min),
539+
outside(findex.symbolic_max - index))
540+
495541
def touched_halo(self, findex):
496542
"""
497543
Return a boolean 2-tuple, one entry for each ``findex`` DataSide. True

‎devito/ir/support/utils.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'Stencil',
1616
'bounded',
1717
'detect_accesses',
18+
'detect_halo_writes',
1819
'erange',
1920
'extrema',
2021
'maximum',
@@ -217,6 +218,24 @@ def detect_accesses(exprs):
217218
return mapper
218219

219220

221+
def detect_halo_writes(c, key):
222+
"""
223+
Return the write accesses in `c` proven entirely outside DOMAIN along at
224+
least one Dimension selected by `key`. Wild Clusters are ignored.
225+
"""
226+
writes = set()
227+
if c.is_wild:
228+
return writes
229+
230+
for w in c.scope.writes_gen():
231+
for d in w.findices:
232+
if key(d) and any(w.touched_nodomain(d)):
233+
writes.add(w)
234+
break
235+
236+
return writes
237+
238+
220239
def pull_dims(exprs, flag=True):
221240
"""
222241
Extract all Dimensions from one or more expressions. If `flag=True`

‎devito/passes/clusters/buffering.py‎

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from devito.exceptions import CompilationError
1010
from devito.ir import (
1111
Backward, Cluster, Forward, GuardBound, GuardFactor, InitArray, Interval,
12-
IntervalGroup, IterationSpace, Properties, Queue, Vector, lower_exprs, vmax, vmin
12+
IterationSpace, Properties, Queue, Vector, detect_halo_writes, lower_exprs, vmax, vmin
1313
)
1414
from devito.logger import warning
1515
from devito.passes.clusters.utils import is_memcpy
@@ -118,6 +118,10 @@ def key(f):
118118
# First we generate all the necessary buffers
119119
mapper = generate_buffers(clusters, key, sregistry, options)
120120

121+
# Take into account writes into the HALO regions so that the buffered
122+
# Functions can be populated accordingly
123+
clusters = expand_halo_transfers(clusters, mapper)
124+
121125
# Then we inject them into the Clusters. This involves creating the
122126
# initializing Clusters, and replacing the buffered Functions with the buffers
123127
clusters = InjectBuffers(mapper, sregistry, options).process(clusters)
@@ -485,6 +489,86 @@ def generate_buffers(clusters, key, sregistry, options, **kwargs):
485489
return mapper
486490

487491

492+
def expand_halo_transfers(clusters, mapper):
493+
"""
494+
Include the halo in buffered writes reading Functions with explicit HALO
495+
writes. For example, `usave` in `Eq(usave, u)` must eventually receive `u`'s
496+
populated HALO if a preceding `Eq` writes into `u`'s HALO.
497+
"""
498+
if not mapper:
499+
return clusters
500+
501+
# Get HALO writes along the buffered dimensions
502+
bdims = set()
503+
for b in mapper.values():
504+
bdims.update(d for d in b.dimensions if not isinstance(d, BufferDimension))
505+
506+
halo_writes = set()
507+
for c in clusters:
508+
for w in detect_halo_writes(c, bdims.__contains__):
509+
halo_writes.add(w.function)
510+
if not halo_writes:
511+
return clusters
512+
513+
# Expand the IterationSpace over the necessary amount of HALO; in doing so,
514+
# check the expanded footprint of every access, including shifted reads.
515+
# Writes must be pointwise so that the whole destination halo is filled
516+
buffered = {f for f, _ in mapper}
517+
processed = []
518+
for c in clusters:
519+
writes = c.scope.writes_tensor
520+
521+
if c.is_wild or \
522+
writes.isdisjoint(buffered) or \
523+
halo_writes.isdisjoint(c.scope.reads):
524+
processed.append(c)
525+
continue
526+
527+
if not writes <= buffered:
528+
raise CompilationError(
529+
"Cannot expand a mixed Cluster over the halo while buffering"
530+
)
531+
532+
ispace = c.ispace
533+
for f in writes:
534+
ispace = _include_halo(ispace, f)
535+
536+
for a in c.scope.accesses:
537+
f = a.function
538+
539+
for d in bdims.intersection(a.findices):
540+
size = f._size_nodomain[d]
541+
offset = simplify(a[d] - d - size.left)
542+
543+
if d not in ispace.dimensions or \
544+
not is_integer(offset) or \
545+
(a.is_write and offset != 0):
546+
raise CompilationError(
547+
f"Cannot expand access to `{f.name}` over the halo"
548+
)
549+
550+
i = ispace[d]
551+
if i.lower + offset < -size.left or \
552+
i.upper + offset > size.right:
553+
raise CompilationError(
554+
f"Insufficient halo for `{f.name}` in buffered write"
555+
)
556+
557+
processed.append(c.rebuild(ispace=ispace))
558+
559+
return processed
560+
561+
562+
def _include_halo(ispace, f):
563+
"""Extend `ispace` to include `f`'s HALO."""
564+
ihalo = [
565+
Interval(i.dim, -f._size_halo[i.dim].left, f._size_halo[i.dim].right, i.stamp)
566+
for i in ispace if i.dim in f.dimensions
567+
]
568+
569+
return IterationSpace.union(ispace, IterationSpace(ihalo))
570+
571+
488572
def map_buffered_functions(clusters, key):
489573
"""
490574
Map each candidate Function to the Clusters that access it.
@@ -641,12 +725,7 @@ def write_to(self):
641725
ispace = ispace.promote(lambda d: d.is_AbstractSub, mode='total')
642726

643727
# Analogous to the above, we need to include the halo region as well
644-
ihalo = IntervalGroup([
645-
Interval(i.dim, -h.left, h.right, i.stamp)
646-
for i, h in zip(ispace, self.b._size_halo, strict=False)
647-
])
648-
649-
ispace = IterationSpace.union(ispace, IterationSpace(ihalo))
728+
ispace = _include_halo(ispace, self.b)
650729

651730
return ispace
652731

‎devito/types/array.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ def initvalue(self):
520520
'_mem_rvalue', '__padding_dtype__', '_size_domain', '_size_halo',
521521
'_size_owned', '_size_padding', '_size_nopad', '_size_nodomain',
522522
'_offset_domain', '_offset_halo', '_offset_owned',
523-
'_dist_dimensions', '_C_get_field', 'grid',
523+
'_dist_dimensions', '_decomposition', '_C_get_field', 'grid',
524524
*AbstractFunction.__properties__):
525525
locals()[i] = property(lambda self, v=i: getattr(self.c0, v))
526526

‎tests/test_buffering.py‎

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
from conftest import skipif
66
from devito import (
7-
CondEq, ConditionalDimension, Constant, Dimension, Eq, Function, Grid, Operator,
8-
SparseTimeFunction, SubDimension, SubDomain, TimeFunction, configuration, switchconfig
7+
CondEq, ConditionalDimension, Constant, CustomDimension, Dimension, Eq, Function,
8+
Grid, Operator, SparseTimeFunction, SubDimension, SubDomain, TimeFunction,
9+
configuration, switchconfig
910
)
1011
from devito.arch.archinfo import AppleArm
1112
from devito.exceptions import CompilationError
12-
from devito.ir import FindSymbols, retrieve_iteration_tree
13+
from devito.ir import (
14+
Cluster, FindSymbols, Interval, IterationSpace, lower_exprs, retrieve_iteration_tree
15+
)
16+
from devito.passes.clusters.buffering import BufferDimension, expand_halo_transfers
17+
from devito.types import Array
1318

1419

1520
def test_read_write():
@@ -64,6 +69,100 @@ def test_write_only():
6469
assert np.all(v.data == v1.data)
6570

6671

72+
@pytest.mark.parametrize('forward', [False, True])
73+
def test_write_only_with_halo_source(forward):
74+
"""
75+
A buffered save of a Function with a populated halo must preserve that halo.
76+
"""
77+
nt = 5
78+
grid = Grid(shape=(17, 17))
79+
y = grid.dimensions[-1]
80+
81+
u = TimeFunction(name='u', grid=grid, space_order=8)
82+
usave = TimeFunction(name='usave', grid=grid, space_order=8, save=nt)
83+
84+
k = CustomDimension(name='k', parent=y, symbolic_min=1,
85+
symbolic_max=4, symbolic_size=4)
86+
87+
eqns = [Eq(u.forward, u + 1),
88+
Eq(u.forward._subs(y, -k), -u.forward._subs(y, k)),
89+
Eq(usave, u.forward if forward else u)]
90+
91+
op = Operator(eqns, opt='buffering', name='save_halo')
92+
op.apply(time_M=nt-2)
93+
94+
hx, hy = usave._size_halo.left[1:]
95+
for t in range(nt-1):
96+
assert np.all(usave.data[t] == t + forward)
97+
actual = usave.data_with_halo[t, hx:hx + grid.shape[0], hy-4:hy]
98+
assert np.all(actual == -(t + forward))
99+
100+
101+
@pytest.mark.parametrize('space_order, shift', [(0, 0), (8, -1), (8, 1), (10, 1)])
102+
@switchconfig(autopadding=False)
103+
def test_write_only_with_halo_source_bounds(space_order, shift):
104+
grid = Grid(shape=(17, 17))
105+
y = grid.dimensions[-1]
106+
107+
u = TimeFunction(name='u', grid=grid, space_order=8)
108+
v = TimeFunction(name='v', grid=grid, space_order=space_order)
109+
usave = TimeFunction(name='usave', grid=grid, space_order=8, save=5)
110+
111+
k = CustomDimension(name='k', parent=y, symbolic_min=1,
112+
symbolic_max=4, symbolic_size=4)
113+
114+
eqns = [Eq(u.forward, u + 1),
115+
Eq(u.forward._subs(y, -k), -u.forward._subs(y, k)),
116+
Eq(usave, u.forward + v.forward._subs(y, y + shift))]
117+
118+
if space_order == 10:
119+
# A wider halo accommodates the shifted read
120+
v.data_with_halo[:] = 2
121+
op = Operator(eqns, opt='buffering', name='save_shifted_halo')
122+
op.apply(time_M=3)
123+
assert np.all(usave.data[3] == 6)
124+
hx, hy = usave._size_halo.left[1:]
125+
assert np.all(usave.data_with_halo[3, hx:hx + grid.shape[0], hy-4:hy] == -2)
126+
else:
127+
with pytest.raises(CompilationError, match='Insufficient halo for `v`'):
128+
Operator(eqns, opt='buffering')
129+
130+
131+
@pytest.mark.parametrize('mixed', [False, True])
132+
def test_halo_transfers_non_time_dimension(mixed):
133+
s = Dimension(name='s')
134+
x = Dimension(name='x')
135+
u = Function(name='u', dimensions=(s, x), shape=(5, 17),
136+
halo=((0, 0), (4, 4)))
137+
usave = Function(name='usave', dimensions=(s, x), shape=(5, 17),
138+
halo=u.halo)
139+
db = BufferDimension('db', 0, 0, 1, s)
140+
b = Array(name='b', dimensions=(db, x), halo=usave.halo)
141+
k = CustomDimension(name='k', parent=x, symbolic_min=1,
142+
symbolic_max=4, symbolic_size=4)
143+
144+
mirror = Cluster(lower_exprs(Eq(u[s+1, -k], -u[s+1, k])),
145+
IterationSpace([Interval(s), Interval(k)]))
146+
eqns = [Eq(usave[s, x], u[s+1, x])]
147+
if mixed:
148+
eqns.append(Eq(u[s, x], 0))
149+
save = Cluster(lower_exprs(eqns), IterationSpace([Interval(s), Interval(x)]))
150+
mapper = {(usave, save.guards): b}
151+
152+
if mixed:
153+
with pytest.raises(CompilationError, match='mixed Cluster'):
154+
expand_halo_transfers([mirror, save], mapper)
155+
return
156+
157+
clusters = expand_halo_transfers([mirror, save], mapper)
158+
159+
assert clusters[0] is mirror
160+
assert clusters[1].ispace[x].offsets == (-4, 4)
161+
# The streaming axis is not part of the halo footprint, even with a shifted read
162+
assert clusters[1].ispace[s] == save.ispace[s]
163+
assert clusters[1].exprs[0].args == save.exprs[0].args
164+
165+
67166
def test_read_only():
68167
nt = 10
69168
grid = Grid(shape=(2, 2))

0 commit comments

Comments
 (0)