From 1e31c6472b9051f53119d318e7b0db7dfd0e08bd Mon Sep 17 00:00:00 2001 From: Gerard Gorman Date: Thu, 15 Oct 2026 02:03:34 +0000 Subject: [PATCH] compiler: Skip copy-backs only if no write to the guard intervenes The guard of the later copy-back is evaluated at the earlier one, so the skip is unsound if anything the guard reads, indices included, is written in between --- devito/passes/clusters/buffering.py | 15 +++++++++-- tests/test_buffering.py | 39 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) 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/tests/test_buffering.py b/tests/test_buffering.py index fa9b7d3562..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 ( @@ -1210,3 +1210,40 @@ def test_true_condition_single_buffer(): 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)