Skip to content

Commit 02e8d9e

Browse files
committed
compiler: Don't extract candidates guarded by a Dimension they don't span
An extraction is scheduled over the Dimensions it spans, but the guard it inherits from its Cluster is kept as it is. When the guard reads a Dimension the extraction itself does not span, the temporary is computed in a loop nest that does not define it, and the generated code does not compile: for (int x = ...) if (sdf(x, y) >= 0) /* y is not iterated here */ r0[y] = ... which is what an expression guarded by a ConditionalDimension over a Function of all the Dimensions produces -- an immersed boundary condition, say -- as soon as a sub-expression of it depends on fewer Dimensions than the guard. The added test is an MFE of exactly that, and fails to compile with "'x' undeclared" without this change. Rule those candidates out in `_do_generate`, next to the extractions that would break a data dependency, so that they are never generated to begin with. They cannot go in `exclude` itself, whose entries reject a candidate that *contains* them, whereas here a candidate is rejected for what it *lacks*: excluding `x` above would take the whole `x, y` extraction with it. A SEQUENTIAL Dimension is exempt: its loop encloses the extraction's own, so the guard is evaluated outside of them in any case, and requiring the extraction to span it would cost the hoisting of a time invariant guarded by a subsampled time Dimension (`test_invariants_with_conditional`). The requirement is carried on the transformer rather than passed down through `_generate`, which is overridden downstream and whose signature is therefore not ours to change. The Dimensions a guard reads are what `Guards` is asked for here, so give it a `dimensions` property rather than walking its values from the outside. `Cluster.guards_dimensions` was already doing that walk by hand and now defers to it. The property returns the Dimensions as they are, not their roots: `guards_dimensions` reaches `expose_tuning_knobs` through `used_dimensions`, which tests them for `is_Block`, and a root is never a BlockDimension. Asking `Properties` whether a Dimension is SEQUENTIAL requires it to still be one: `CireInvariants._lookup_key` was rebuilding it as a plain `frozendict`, losing the class and its API, where the other `_lookup_key` passes it through untouched -- so the two CIRE variants disagreed on the type. `Properties` is a `frozendict` itself, so preserving it there costs nothing.
1 parent 436199c commit 02e8d9e

4 files changed

Lines changed: 84 additions & 17 deletions

File tree

‎devito/ir/clusters/cluster.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,12 @@ def exprs_dimensions(self):
118118
dims_implicit = {d for e in self.exprs for d in e.implicit_dims}
119119
return dims_explicit | dims_implicit
120120

121-
@cached_property
121+
@property
122122
def guards_dimensions(self):
123123
"""
124124
The Dimensions that appear explicitly in the guards.
125125
"""
126-
syms_guards = {d for e in self.guards.values() for d in e.free_symbols}
127-
dims_guards = {i for i in syms_guards if i.is_Dimension}
128-
return dims_guards
126+
return self.guards.dimensions
129127

130128
@cached_property
131129
def used_dimensions(self):

‎devito/ir/support/guards.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
"""
66

77
from collections import Counter, defaultdict
8-
from functools import singledispatch
8+
from functools import cached_property, singledispatch
99
from operator import ge, gt, le, lt
1010

1111
import numpy as np
1212
from sympy import And, Expr, Ge, Gt, Le, Lt, Mul, true
1313
from sympy.logic.boolalg import BooleanFunction
1414

1515
from devito.ir.support.space import Forward, IterationDirection
16+
from devito.ir.support.utils import pull_dims
1617
from devito.symbolics import CondEq, CondNe, IntDiv, search
1718
from devito.symbolics.manipulation import _uxreplace_handle, _uxreplace_registry
1819
from devito.tools import Pickable, as_tuple, frozendict, split
@@ -280,6 +281,15 @@ class Guards(frozendict):
280281
def get(self, d, v=true):
281282
return super().get(d, v)
282283

284+
@cached_property
285+
def dimensions(self):
286+
"""
287+
The Dimensions the guards read, that is those a guarded object must
288+
be evaluated within.
289+
"""
290+
return frozenset({d for v in self.values()
291+
for d in pull_dims(v, flag=False)})
292+
283293
def has(self, d, cls):
284294
"""
285295
True if the guard registered for `d` contains an instance of `cls`.

‎devito/passes/clusters/aliases.py‎

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from devito.finite_differences import EvalDerivative, IndexDerivative, Weights
1010
from devito.ir import (
1111
PARALLEL_IF_PVT, SEPARABLE, SEQUENTIAL, Cluster, ClusterGroup, ExprGeometry, Forward,
12-
Interval, IntervalGroup, IterationSpace, LabeledVector, Queue, Vector, extrema,
13-
maximum, minimum, normalize_properties, relax_properties, unbounded, vmax, vmin
12+
Interval, IntervalGroup, IterationSpace, LabeledVector, Properties, Queue, Vector,
13+
extrema, maximum, minimum, normalize_properties, relax_properties, unbounded, vmax,
14+
vmin
1415
)
1516
from devito.passes.clusters.cse import _cse
1617
from devito.passes.clusters.utils import expose_tuning_knobs
@@ -19,8 +20,8 @@
1920
uxreplace
2021
)
2122
from devito.tools import (
22-
Reconstructable, Stamp, as_mapper, as_tuple, flatten, frozendict, generator,
23-
is_integer, split, timed_pass
23+
Reconstructable, Stamp, as_mapper, as_tuple, flatten, generator, is_integer, split,
24+
timed_pass
2425
)
2526
from devito.types import (
2627
CustomDimension, Eq, Hyperplane, IncrDimension, Indexed, ModuloDimension, Size,
@@ -288,6 +289,7 @@ def _do_generate(self, exprs, exclude, cbk_search, cbk_compose=None):
288289
free_symbols = i.free_symbols
289290
if {a.function for a in free_symbols} & exclude:
290291
continue
292+
291293
mapper.add(i, make, terms)
292294

293295
return mapper
@@ -304,6 +306,31 @@ def __init__(self, sregistry, options, platform):
304306
def process(self, clusters):
305307
return self._process_fatd(clusters, 1, xtracted=[])
306308

309+
@classmethod
310+
def _make_exclude(cls, clusters, d, p):
311+
"""
312+
The symbols an extraction must not touch.
313+
"""
314+
# Rule out extractions that would break data dependencies
315+
exclude = set().union(*[c.scope.writes for c in clusters])
316+
317+
# Rule out extractions that depend on the Dimension currently investigated,
318+
# as they clearly wouldn't be invariants
319+
exclude.update({d, *p.sub_iterators})
320+
321+
# An extraction is hoisted out of `d`, but it inherits its guard as it
322+
# is, so it must not be hoisted past any Dimension the guard reads, or
323+
# the guard would be evaluated where it is not defined. Excluding those
324+
# Dimensions keeps the extraction inside the loops defining them. A
325+
# SEQUENTIAL Dimension is exempt, its loop enclosing the extraction's own
326+
for c in clusters:
327+
exclude.update(
328+
i for i in c.guards.dimensions
329+
if not c.properties.is_sequential(i._defines)
330+
)
331+
332+
return exclude
333+
307334
def callback(self, clusters, prefix, xtracted=None):
308335
if not prefix:
309336
return clusters
@@ -314,12 +341,7 @@ def callback(self, clusters, prefix, xtracted=None):
314341
if d.is_Virtual:
315342
return clusters
316343

317-
# Rule out extractions that would break data dependencies
318-
exclude = set().union(*[c.scope.writes for c in clusters])
319-
320-
# Rule out extractions that depend on the Dimension currently investigated,
321-
# as they clearly wouldn't be invariants
322-
exclude.update({d, *p.sub_iterators})
344+
exclude = self._make_exclude(clusters, d, p)
323345

324346
key = lambda c: self._lookup_key(c, d)
325347
processed = list(clusters)
@@ -343,7 +365,7 @@ def callback(self, clusters, prefix, xtracted=None):
343365
def _lookup_key(self, c, d):
344366
ispace = c.ispace.reset()
345367
intervals = c.ispace.intervals.drop(d).reset()
346-
properties = frozendict({d: relax_properties(v) for d, v in c.properties.items()})
368+
properties = Properties({d: relax_properties(v) for d, v in c.properties.items()})
347369

348370
return AliasKey(ispace, intervals, c.dtype, c.guards, properties)
349371

‎tests/test_dse.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from devito import ( # noqa
1212
NODE, Abs, ConditionalDimension, Constant, DefaultDimension, Derivative, Dimension,
13-
Eq, Function, Ge, Grid, Inc, Lt, Operator, SparseTimeFunction, SubDimension,
13+
Eq, Function, Ge, Grid, Inc, Lt, Max, Operator, SparseTimeFunction, SubDimension,
1414
TimeFunction, configuration, cos, dimensions, div, exp, first_derivative, floor, grad,
1515
norm, sin, solve, sqrt, switchconfig, transpose
1616
)
@@ -2512,6 +2512,43 @@ def test_contraction_with_conditional(self):
25122512
assert len(FindNodes(Conditional).visit(op)) == 1
25132513
assert np.all(u.data[6:] == 1.42)
25142514

2515+
def test_no_extraction_guarded_by_unspanned_dimension(self):
2516+
"""
2517+
An alias is scheduled over the Dimensions it spans, but it inherits its
2518+
guard as it is. A guard reading a Dimension the alias does not span
2519+
would then be evaluated in a loop nest that does not define it, giving
2520+
code that does not compile.
2521+
2522+
Here `sin(...)` depends on `y` alone while the condition reads both `x`
2523+
and `y`, which is what an immersed boundary condition produces.
2524+
"""
2525+
grid = Grid(shape=(16, 16))
2526+
x, y = grid.dimensions
2527+
2528+
sdf = Function(name='sdf', grid=grid)
2529+
sdf.data[:] = 1.
2530+
2531+
cond = ConditionalDimension(name='inside', parent=y, condition=Ge(sdf, 0))
2532+
2533+
u = TimeFunction(name='u', grid=grid, space_order=4)
2534+
u.data[:] = 1.
2535+
2536+
# Expensive enough to be extracted, and `y`-only
2537+
prof = sin(Max(0., 1. - y)) + sin(Max(0., 1. + y))
2538+
2539+
eqn = Eq(u.forward, u.laplace + prof*u, implicit_dims=[cond])
2540+
2541+
op = Operator(eqn, opt=('advanced', {'cire-mingain': 0, 'openmp': False}))
2542+
2543+
# No temporary may be created over fewer Dimensions than the guard reads
2544+
for i in FindSymbols().visit(op):
2545+
if i.is_Array:
2546+
assert {x, y}.issubset(set(i.dimensions))
2547+
2548+
# Used to fail to compile with "'x' undeclared"
2549+
op.apply(time_M=2)
2550+
assert np.all(np.isfinite(u.data[:]))
2551+
25152552
def test_collection_from_conditional(self):
25162553
nt = 10
25172554
grid = Grid(shape=(10, 10))

0 commit comments

Comments
 (0)