diff --git a/devito/ir/clusters/algorithms.py b/devito/ir/clusters/algorithms.py index f594d35203..ed640e29ed 100644 --- a/devito/ir/clusters/algorithms.py +++ b/devito/ir/clusters/algorithms.py @@ -366,6 +366,9 @@ def guard(clusters): else: guards = {d: mode(*v, evaluate=False) for d, v in guards.items()} + # A condition folded to `true` (e.g., `time >= 0`) imposes no guard + guards = {d: v for d, v in guards.items() if v is not sympy.true} + # Construct a guarded Cluster processed.append(c.rebuild(exprs=exprs, guards=guards)) diff --git a/devito/ir/support/guards.py b/devito/ir/support/guards.py index 8a3842ddfd..909a5e59e1 100644 --- a/devito/ir/support/guards.py +++ b/devito/ir/support/guards.py @@ -55,7 +55,7 @@ def negated(self): # *** GuardFactor -class GuardFactor(Guard, CondEq, Pickable): +class GuardFactor(Guard): """ A guard for factor-based ConditionalDimensions. @@ -86,11 +86,14 @@ def _args_rebuild(self): return (self.d,) -class GuardFactorEq(GuardFactor, CondEq): +# The relational bases are mixed in by the leaves only, or `CondEq` would +# precede `CondNe` in the MRO of `GuardFactorNe`, making it an equality. E.g., +# `Not(GuardFactorEq(d))` would then be printed as `time % k == 0` +class GuardFactorEq(GuardFactor, CondEq, Pickable): pass -class GuardFactorNe(GuardFactor, CondNe): +class GuardFactorNe(GuardFactor, CondNe, Pickable): pass diff --git a/devito/passes/clusters/buffering.py b/devito/passes/clusters/buffering.py index 18826dff2d..b1a0215bc2 100644 --- a/devito/passes/clusters/buffering.py +++ b/devito/passes/clusters/buffering.py @@ -14,7 +14,9 @@ ) from devito.logger import warning from devito.passes.clusters.utils import is_memcpy -from devito.symbolics import CondEq, IntDiv, retrieve_functions, uxreplace +from devito.symbolics import ( + CondEq, IntDiv, retrieve_functions, retrieve_terminals, uxreplace +) from devito.tools import ( Stamp, as_mapper, as_tuple, filter_ordered, flatten, frozendict, is_integer, timed_pass @@ -1064,7 +1066,16 @@ def _overwritten(v, c, idxf, descriptors, order, d): # The later copy-back fires under all of its guards, which may be # keyed on Dimensions derived from `d` (e.g., a layer Dimension) - conditions.append(And(*c1.guards.values(), coincide)) + condition = And(*c1.guards.values(), coincide) + + # The condition is evaluated at the earlier copy-back, so nothing + # it reads, indices included, may be written in between + reads = {i.function for i in retrieve_terminals(condition, deep=True)} + between = [c2 for c2, n in order.items() if order[c] < n <= order[c1]] + if any(not reads.isdisjoint(c2.scope.writes) for c2 in between): + continue + + conditions.append(condition) if not conditions: return None diff --git a/devito/passes/iet/languages/C.py b/devito/passes/iet/languages/C.py index c5ac413ac4..0f687280ac 100644 --- a/devito/passes/iet/languages/C.py +++ b/devito/passes/iet/languages/C.py @@ -89,6 +89,13 @@ class CPrinter(BasePrinter, C99CodePrinter): def _print_ImaginaryUnit(self, expr): return '_Complex_I' + # C99 has no `true`/`false` without `stdbool.h`, which is never included + def _print_BooleanTrue(self, expr): + return '1' + + def _print_BooleanFalse(self, expr): + return '0' + def _print_ListInitializer(self, expr): li = super()._print_ListInitializer(expr) if expr.dtype: diff --git a/tests/test_buffering.py b/tests/test_buffering.py index 29c864e848..da6d160b13 100644 --- a/tests/test_buffering.py +++ b/tests/test_buffering.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from sympy import Not, Or +from sympy import And, Not, Or from conftest import skipif from devito import ( @@ -1179,3 +1179,71 @@ def test_buffering_multi_cond(factor): # The last sample is on the subsampled grid, hence no extra slot expected[-2:] = [ntmod - 1, 0] assert np.allclose(f_all.data[:, 11, 11], expected) + + +def test_true_condition_single_buffer(): + """ + A condition folded to `true` (`time` is nonnegative, so `time >= 0` is + `true`) imposes no guard. Reads of `f` under it and unguarded reads of `f` + must thus share one buffer. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + nt = 4 + + f = TimeFunction(name='f', grid=grid, time_order=0, space_order=0, save=nt) + g = TimeFunction(name='g', grid=grid, time_order=0, space_order=0) + h = TimeFunction(name='h', grid=grid, time_order=0, space_order=0) + for i in range(nt): + f.data[i] = i + + ctrue = ConditionalDimension(name='ctrue', parent=time, condition=time >= 0, + relation='strict') + + eqs = [Eq(g, g + f, implicit_dims=ctrue), Eq(h, h + 2*f)] + + op = Operator(eqs, opt='buffering', name='TrueCondition') + + buffers = [i for i in FindSymbols().visit(op.body) if i.is_Array and i._mem_heap] + assert len(buffers) == 1 + + op.apply(time_m=0, time_M=nt-1) + assert np.allclose(g.data[0], 6.) + assert np.allclose(h.data[0], 12.) + + +@pytest.mark.parametrize('case', ['mutable', 'indirect']) +def test_buffering_same_slot_skip(case): + """ + A later guard reads data updated between two potential writes. The update + makes that guard false, so the only firing write must still be copied back. + Every Function has at most one firing write per step, slot and point. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + x, _ = grid.dimensions + + f = TimeFunction(name='f', grid=grid, time_order=0, space_order=0, save=4) + T = TimeFunction(name='T', grid=grid, time_order=0, space_order=0) + + if case == 'mutable': + cm = ConditionalDimension(name='cm', parent=time, relation='strict', + condition=And(CondEq(time, 3), T[0, 0, 0] < 3.5)) + eqs = [Eq(f, T), Eq(T, T + 1), Eq(f, T + 1, implicit_dims=cm)] + expected = [0, 1, 2, 3] + else: + I = TimeFunction(name='I', grid=grid, time_order=0, space_order=0, + dtype=np.int32) + ci = ConditionalDimension(name='ci', parent=time, condition=CondEq(time, 3), + relation='strict') + cm = ConditionalDimension(name='cm', parent=time, relation='strict', + condition=And(CondEq(time, 3), + T[0, I[0, 0, 0], 0] < 3.5)) + eqs = [Eq(T, time + 2*x), Eq(f, T), Eq(I, 1, implicit_dims=ci), + Eq(f, T + 1, implicit_dims=cm)] + expected = [2, 3, 4, 5] + + op = Operator(eqs, opt='buffering', name='SameSlotSkip') + op.apply(time_m=0, time_M=3) + + assert np.all(f.data[:, 1, 1] == expected) diff --git a/tests/test_dimension.py b/tests/test_dimension.py index 3e28210fe5..a58301abff 100644 --- a/tests/test_dimension.py +++ b/tests/test_dimension.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from sympy import And, Or +from sympy import And, Not, Or, false from conftest import assert_blocking, assert_structure, opts_tiling, skipif from devito import ( # noqa @@ -13,7 +13,7 @@ configuration, dimensions, floor, norm, sin, sum, switchconfig ) from devito.exceptions import InvalidArgument -from devito.ir import SymbolRegistry +from devito.ir import GuardFactor, SymbolRegistry from devito.ir.equations.algorithms import concretize_subdims from devito.ir.iet import ( Conditional, Expression, FindNodes, FindSymbols, Iteration, retrieve_iteration_tree @@ -2128,6 +2128,46 @@ def test_blocking_w_guard(self): 't,x0_blk0,y0_blk0,x,y,z'], 't,x0_blk0,y0_blk0,x,y,z,z') + def test_false_condition(self): + """ + A condition that SymPy folds to `false` (`time` is nonnegative) must + still be printed as valid C. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + + condition = time < 0 + assert condition is false + ct = ConditionalDimension(name='ct', parent=time, condition=condition) + + T = TimeFunction(name='T', grid=grid, time_order=0) + f = TimeFunction(name='f', grid=grid, time_order=0, save=2) + + op = Operator([Eq(f, T + 1, implicit_dims=ct), Eq(T, T + 1)], + name='FalseCondition') + op.apply(time_M=1) + + assert np.all(f.data[:, 1, 1] == [0, 0]) + + def test_negated_factor_condition(self): + """ + Negating a subsampling guard must select the timesteps off the + subsampled grid. + """ + grid = Grid(shape=(4, 4)) + time = grid.time_dim + + ct = ConditionalDimension(name='ct', parent=time, factor=2) + cn = ConditionalDimension(name='cn', parent=time, + condition=Not(GuardFactor(ct)), relation='strict') + + f = TimeFunction(name='f', grid=grid, time_order=0, save=4) + + op = Operator(Eq(f, time + 1, implicit_dims=cn), name='NegatedFactor') + op.apply(time_M=3, ctf=2) + + assert np.all(f.data[:, 1, 1] == [0, 2, 0, 4]) + class TestCustomDimension: