Skip to content

Commit 4440234

Browse files
committed
compiler: Don't collect aliases guarded by a Dimension they don't span
An alias 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 alias 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. Discard those aliases in `collect`, as the pivot is formed and before its distance vectors and cost are computed. A SEQUENTIAL Dimension is exempt: its loop encloses the alias' own, so the guard is evaluated outside of them in any case, and requiring the alias to span it would cost the hoisting of a time invariant guarded by a subsampled time Dimension (`test_invariants_with_conditional`). 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 `collect` saw a different type depending on which CIRE variant called it. `Properties` is a `frozendict` itself, so preserving it there costs nothing.
1 parent 436199c commit 4440234

4 files changed

Lines changed: 78 additions & 11 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: 10 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,14 @@ 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 {d for v in self.values() for d in pull_dims(v, flag=False)}
291+
283292
def has(self, d, cls):
284293
"""
285294
True if the guard registered for `d` contains an instance of `cls`.

‎devito/passes/clusters/aliases.py‎

Lines changed: 28 additions & 5 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, pull_dims, relax_properties,
14+
unbounded, vmax, 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,
@@ -343,7 +344,7 @@ def callback(self, clusters, prefix, xtracted=None):
343344
def _lookup_key(self, c, d):
344345
ispace = c.ispace.reset()
345346
intervals = c.ispace.intervals.drop(d).reset()
346-
properties = frozendict({d: relax_properties(v) for d, v in c.properties.items()})
347+
properties = Properties({d: relax_properties(v) for d, v in c.properties.items()})
347348

348349
return AliasKey(ispace, intervals, c.dtype, c.guards, properties)
349350

@@ -566,7 +567,22 @@ def collect(extracted, meta, minstorage):
566567
* a[i] + c[i] : because at least one of the operands differs
567568
* a[i+2] - b[i+2] : because at least one operation differs
568569
* a[i+2] + b[i] : because the distances along ``i`` differ (+2 and +0)
570+
571+
An aliasing expression is discarded if it does not span all of the
572+
Dimensions its guard reads, since it would then be computed in a loop
573+
nest that does not define its guard.
569574
"""
575+
# The Dimensions an alias must span for its guard to be evaluated within
576+
# the alias' own loops: those the guards read, but for the SEQUENTIAL ones,
577+
# along which the guard is evaluated outside of them anyway. Taken at their
578+
# root, an alias and a guard not necessarily using the same derived
579+
# Dimensions -- the alias' own are pulled with their ancestors below
580+
if meta.guards:
581+
guard_dims = {d.root for d in meta.guards.dimensions
582+
if not meta.properties.is_sequential(d._defines)}
583+
else:
584+
guard_dims = set()
585+
570586
# Find the potential aliases
571587
found = []
572588
for expr in extracted:
@@ -657,6 +673,13 @@ def collect(extracted, meta, minstorage):
657673
for i, b, v in zip(c.indexeds, c.bases, offsets, strict=True)}
658674
pivot = uxreplace(c.expr, subs)
659675

676+
# An alias is scheduled over the Dimensions it spans, but it
677+
# inherits its guard as it is, so a guard reading a Dimension the
678+
# alias does not span would be evaluated in a loop nest that does
679+
# not define it
680+
if not guard_dims <= pull_dims(pivot):
681+
continue
682+
660683
# Distance of each aliased expression from the basis alias
661684
aliaseds = []
662685
distances = []

‎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)