Skip to content

Commit bef9642

Browse files
committed
compiler: prevent schedule from breaking loop over subiterators
1 parent 59b562b commit bef9642

2 files changed

Lines changed: 78 additions & 7 deletions

File tree

‎devito/ir/clusters/algorithms.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,12 @@ def callback(self, clusters, prefix, backlog=None, known_break=None):
153153
# `scope.d_anti == {}` or because the few anti dependences are not carried
154154
# in any Dimension. We exploit this observation so that we only compute
155155
# `d_flow`, which instead may be expensive, when strictly necessary
156+
# Note: a Dimension with sub-iterators is SEQUENTIAL (see `Parallelism`),
157+
# so its Clusters run interleaved within one loop; scheduling them over
158+
# separate IterationSpaces would reorder them
156159
maybe_break = scope.d_anti.cause & candidates
157-
if len(clusters) > 1 and maybe_break:
160+
is_sub = any(c.sub_iterators[dim] for c in clusters)
161+
if len(clusters) > 1 and maybe_break and not is_sub:
158162
require_break = scope.d_flow.cause & maybe_break
159163
if require_break:
160164
backlog = [clusters[-1]] + backlog

‎tests/test_operator.py‎

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from conftest import assert_structure, skipif
1818

1919
from devito import ( # noqa
20-
CELL, NODE, Buffer, Constant, Dimension, Eq, Function, Ge, Grid, Gt, Inc, Le, Lt,
21-
Operator, SpaceDimension, SparseFunction, SparseTimeFunction, TensorFunction,
20+
CELL, NODE, Buffer, CondEq, Constant, Dimension, Eq, Function, Ge, Grid, Gt, Inc, Le,
21+
Lt, Operator, SpaceDimension, SparseFunction, SparseTimeFunction, TensorFunction,
2222
TensorTimeFunction, TimeFunction, VectorFunction, VectorTimeFunction, configuration,
2323
dimensions, div, error, exp, grad, sin, switchconfig
2424
)
@@ -1975,10 +1975,11 @@ def test_scheduling_sparse_functions(self):
19751975
op = Operator([eqn1] + eqn2 + [eqn3] + eqn4, opt=('noop', {'openmp': False}))
19761976
trees = retrieve_iteration_tree(op)
19771977
assert len(trees) == 5
1978-
# Time loop not shared due to the WAR
1979-
assert trees[0][0].dim is time and trees[0][0] is trees[1][0] # this IS shared
1980-
assert trees[1][0] is not trees[3][0]
1981-
assert trees[3][0].dim is time and trees[3][0] is trees[4][0] # this IS shared
1978+
# Single, shared time loop despite the WAR: splitting it would run the
1979+
# injection into `u1[time]` to completion before eqn3 reads
1980+
# `u1[time + 1]`, reversing the very dependence it stems from
1981+
assert trees[0][0].dim is time
1982+
assert all(trees[0][0] is i[0] for i in trees)
19821983

19831984
# Now single, shared time loop expected
19841985
eqn2 = sf1.inject(u1.forward, expr=sf1)
@@ -1987,6 +1988,72 @@ def test_scheduling_sparse_functions(self):
19871988
assert len(trees) == 5
19881989
assert all(trees[0][0] is i[0] for i in trees)
19891990

1991+
def test_no_time_fission_reversing_war(self):
1992+
"""
1993+
The time loop must not be split to disambiguate iteration directions
1994+
when doing so reverses a WAR carried in time.
1995+
"""
1996+
nt = 6
1997+
grid = Grid((3, 3))
1998+
u1 = TimeFunction(name='u1', grid=grid, save=nt + 2, time_order=2)
1999+
u2 = TimeFunction(name='u2', grid=grid, time_order=2)
2000+
2001+
# `Inc(u1, ...)` writes the current time slot, which eqn3 reads one
2002+
# step ahead as `u1[time + 1]`
2003+
eqns = [Eq(u1.forward, u1 + 2.0 - u1.backward),
2004+
Inc(u1, 100.0),
2005+
Eq(u2.forward, u2 + 2 * u2.backward - u1.dt2)]
2006+
2007+
op = Operator(eqns, opt=('noop', {'openmp': False}))
2008+
assert len({i[0] for i in retrieve_iteration_tree(op)}) == 1
2009+
2010+
op.apply(time_m=1, time_M=nt, dt=1.0)
2011+
2012+
# Reference: the same equations, in order, one time step at a time
2013+
r1 = np.zeros((nt + 2, 3, 3), dtype=grid.dtype)
2014+
r2 = np.zeros((3, 3, 3), dtype=grid.dtype)
2015+
for t in range(1, nt + 1):
2016+
r1[t + 1] = r1[t] + 2.0 - r1[t - 1]
2017+
r1[t] += 100.0
2018+
tm1, t0, tp1 = (t - 1) % 3, t % 3, (t + 1) % 3
2019+
r2[tp1] = r2[t0] + 2 * r2[tm1] - (r1[t + 1] - 2 * r1[t] + r1[t - 1])
2020+
2021+
assert np.allclose(u1.data, r1)
2022+
assert np.allclose(u2.data, r2)
2023+
2024+
def test_no_time_fission_splitting_temporaries(self):
2025+
"""
2026+
The time loop must not be split between a sparse function's position
2027+
temporaries and the accesses that use them.
2028+
"""
2029+
grid = Grid((11, 11))
2030+
time = grid.time_dim
2031+
2032+
u = TimeFunction(name='u', grid=grid, space_order=2, save=Buffer(2))
2033+
v = TimeFunction(name='v', grid=grid, space_order=2, save=Buffer(2))
2034+
sf = SparseTimeFunction(name='sf', grid=grid, npoint=1, nt=10)
2035+
sf.coordinates.data[:] = 0.5
2036+
2037+
# `u` is stepped for `n2` steps, then `v` for `n1`, and so on, with the
2038+
# two live time slots handed over at each phase boundary
2039+
n1, n2 = 2, 3
2040+
mod = time % (n1 + n2)
2041+
cv = ConditionalDimension(name='cv', parent=time, condition=Lt(mod, n1))
2042+
cu = ConditionalDimension(name='cu', parent=time, condition=Ge(mod, n1))
2043+
cuv = ConditionalDimension(name='cuv', parent=time, condition=CondEq(mod, 0))
2044+
cvu = ConditionalDimension(name='cvu', parent=time, condition=CondEq(mod, n1))
2045+
2046+
eqns = ([Eq(u.forward, u.laplace + u, implicit_dims=cu)] +
2047+
sf.inject(u.forward, expr=sf, implicit_dims=cu) +
2048+
[Eq(v.forward, v, implicit_dims=cuv), Eq(v, u, implicit_dims=cuv)] +
2049+
[Eq(v.forward, v.laplace + v, implicit_dims=cv)] +
2050+
sf.inject(v.forward, expr=sf, implicit_dims=cv) +
2051+
[Eq(u.forward, v.forward, implicit_dims=cvu),
2052+
Eq(u, v, implicit_dims=cvu)])
2053+
2054+
# Would raise if a position temporary were left behind in another loop
2055+
Operator(eqns).cfunction # noqa B018
2056+
19902057
def test_scheduling_with_free_dims(self):
19912058
"""Tests loop scheduling in presence of free dimensions."""
19922059
grid = Grid((4, 4))

0 commit comments

Comments
 (0)