Skip to content

Commit 99f0a9e

Browse files
committed
compiler: fix buffering corner case such as interp
1 parent f2815a7 commit 99f0a9e

4 files changed

Lines changed: 69 additions & 37 deletions

File tree

devito/ir/stree/algorithms.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,15 @@ def preprocess(clusters, options=None, **kwargs):
169169
for c in clusters:
170170
if c.is_halo_touch:
171171
hs = HaloScheme.union(e.rhs.halo_scheme for e in c.exprs)
172-
queue.append(c.rebuild(exprs=[], halo_scheme=hs))
172+
# Peel syncs (e.g. a WaitLock) off, they must survive an unbound halo.
173+
# Restrict the IterationSpace to the syncs' prefix so we don't carry
174+
# inner Dimensions (e.g. a layer's VirtualDimension) that have no
175+
# content left, which would yield empty guarded/iteration nodes
176+
if c.syncs:
177+
key = lambda d: any(d in s._defines for s in c.syncs) # noqa: B023
178+
ispace = c.ispace.prefix(key)
179+
processed.append(c.rebuild(exprs=[], ispace=ispace))
180+
queue.append(c.rebuild(exprs=[], syncs={}, halo_scheme=hs))
173181

174182
elif c.is_dist_reduce:
175183
processed.append(c)

devito/ir/support/guards.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ class GuardFactor(Guard, CondEq, Pickable):
6565

6666
__rargs__ = ('d',)
6767

68-
def __new__(cls, d, **kwargs):
68+
def __new__(cls, *args, **kwargs):
69+
if len(args) != 1:
70+
# Reconstruction with relational args (e.g. via sympy `_subs`): the
71+
# factor semantics no longer hold, so degrade to a plain relational
72+
base = CondNe if issubclass(cls, CondNe) else CondEq
73+
return base(*args, **kwargs)
74+
75+
d, = args
6976
assert d.is_Conditional
7077

7178
obj = super().__new__(cls, d.parent % d.symbolic_factor, 0)

devito/passes/clusters/asynchrony.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -330,20 +330,22 @@ def _actions_from_update_memcpy(c, d, bounds, clusters, actions, sregistry):
330330

331331
pc = c.rebuild(exprs=expr, ispace=ispace, guards=guards, syncs=syncs)
332332

333-
# Since we're turning `e` into a prefetch, we need to:
334-
# 1) attach a WaitLock SyncOp to the first Cluster accessing `target`
335-
# 2) insert the prefetch Cluster right after the last Cluster accessing `target`
336-
# 3) drop the original Cluster performing a memcpy-like fetch
337-
n = clusters.index(c)
338-
first = None
339-
last = None
340-
for c1 in clusters[n+1:]:
341-
if target in c1.scope.reads:
342-
if first is None:
343-
first = c1
344-
last = c1
345-
assert first is not None
346-
assert last is not None
333+
# Wait before the first, prefetch after the last access to `target`, then
334+
# drop the memcpy `c`. `c` may be toposorted amid the readers, so scan them
335+
# all; count only reads over the streamed `pd`, not the buffer-init loop.
336+
reads = [c1 for c1 in clusters
337+
if c1 is not c and target in c1.scope.reads and pd in c1.ispace.itdims]
338+
assert reads
339+
first = reads[0]
340+
last = reads[-1]
341+
342+
# Advance `last` past its loop nest so the prefetch follows it rather than
343+
# splitting it, e.g. severing an interpolation's store from its point loop
344+
nest_dims = set(last.ispace.itdims) - set(d._defines)
345+
for c1 in clusters[clusters.index(last)+1:]:
346+
if not nest_dims & set(c1.ispace.itdims):
347+
break
348+
last = c1
347349

348350
actions[first].syncs[d].append(WaitLock(handle, target))
349351
actions[last].insert.append(pc)

devito/passes/clusters/buffering.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,11 @@ def _optimize(self, clusters, descriptors):
303303
processed.append(c)
304304
continue
305305

306-
key1 = lambda d: not d._defines & v.dim._defines # noqa: B023
306+
# Lift only along the buffer's own spatial Dimensions, not
307+
# unrelated ones (e.g. sparse points of an indirect read)
308+
key1 = lambda d: (not d._defines & v.dim._defines and # noqa: B023
309+
any(d._defines & bd._defines # noqa: B023
310+
for bd in v.bdims)) # noqa: B023
307311
dims = c.ispace.project(key1).itdims
308312
ispace = c.ispace.lift(dims, key0())
309313
processed.append(c.rebuild(ispace=ispace))
@@ -465,30 +469,37 @@ def itdims(self):
465469
def ispace(self):
466470
# The IterationSpace within which the buffer will be accessed
467471

468-
# NOTE: The `key` is to avoid Clusters including `f` but not directly
469-
# using it in an expression, such as HaloTouch Clusters
470-
def key(c):
471-
bufferdim = any(i in c.ispace.dimensions for i in self.bdims)
472-
xd_only = all(d._defines & self.xd._defines for d in c.ispace.dimensions)
473-
return bufferdim or xd_only
474-
475472
ispaces = set()
473+
indirect = set()
476474
for c in self.clusters:
477-
if not key(c):
478-
continue
479-
480-
# Skip wild clusters (e.g. HaloTouch Clusters)
475+
# Skip wild clusters (e.g. HaloTouch Clusters), which include `f`
476+
# but do not directly use it in an expression
481477
if c.is_wild:
482478
continue
483479

484-
# Iterations space and buffering dims
485-
edims = [d for d in self.bdims if d not in c.ispace.dimensions]
486-
if not edims:
487-
ispaces.add(c.ispace)
488-
else:
489-
# Add all missing buffering dimensions and reorder to
490-
# avoid duplicates with different ordering
491-
ispaces.add(c.ispace.insert(self.dim, edims).reorder())
480+
bufferdim = any(i in c.ispace.dimensions for i in self.bdims)
481+
xd_only = all(d._defines & self.xd._defines for d in c.ispace.dimensions)
482+
483+
if bufferdim or xd_only:
484+
# `c` iterates (at least some of) the buffer's spatial dims
485+
edims = [d for d in self.bdims if d not in c.ispace.dimensions]
486+
if not edims:
487+
ispaces.add(c.ispace)
488+
else:
489+
# Add all missing buffering dimensions and reorder to
490+
# avoid duplicates with different ordering
491+
ispaces.add(c.ispace.insert(self.dim, edims).reorder())
492+
elif ((self.f in c.scope.reads or self.f in c.scope.writes) and
493+
self.dim.root in c.ispace.dimensions):
494+
# `c` accesses `f` indirectly (e.g. interpolation), so it doesn't
495+
# iterate `bdims`; span the buffer's own Dimensions instead
496+
tispace = c.ispace.project(lambda i: i._defines & self.dim.root._defines)
497+
indirect.add(tispace.insert(self.dim, list(self.bdims)))
498+
499+
# Indirect accessors define the ispace only for a read-only streamed
500+
# buffer, where nothing iterates the buffer's own Dimensions directly
501+
if not ispaces:
502+
ispaces = indirect
492503

493504
if len(ispaces) > 1:
494505
# Best effort to make buffering work in the presence of multiple
@@ -609,7 +620,11 @@ def last_idx(self):
609620
mapper = {}
610621
func = vmax if self.is_forward_buffering else vmin
611622
for c in self.lastwrite + self.firstread:
612-
indices = extract_indices(self.f, self.dim, [c])
623+
# Consider all Clusters sharing `c`'s guards, so the leading edge is
624+
# found even when `f` is accessed at different offsets across them
625+
# (e.g. `f` and `f.forward` in separate Eqs)
626+
group = [c1 for c1 in self.clusters if c1.guards == c.guards]
627+
indices = extract_indices(self.f, self.dim, group)
613628
idx = func(*[Vector(i) for i in indices])[0]
614629
mapper[c] = idx
615630

0 commit comments

Comments
 (0)