Skip to content

Commit 418f94a

Browse files
Merge pull request #3028 from devitocodes/sparse-local-sum
compiler: Introduce SparseLocalSum
2 parents cf24a2b + 531cd55 commit 418f94a

17 files changed

Lines changed: 414 additions & 64 deletions

File tree

‎devito/finite_differences/differentiable.py‎

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
from devito.finite_differences.tools import coeff_priority, make_shift_x0
2222
from devito.logger import warning
2323
from devito.tools import (
24-
Tag, as_tuple, extract_dtype, filter_ordered, flatten, frozendict, infer_dtype,
25-
is_integer, is_number, memoized_func, split
24+
Pickable, Tag, as_tuple, extract_dtype, filter_ordered, flatten, frozendict,
25+
infer_dtype, is_integer, is_number, memoized_func, split
2626
)
2727
from devito.types import Array, DimensionTuple, Evaluable, StencilDimension
2828
from devito.types.basic import AbstractFunction, Indexed
@@ -35,6 +35,7 @@
3535
'Imag',
3636
'IndexDerivative',
3737
'IndexDerivativeProperty',
38+
'LocalSum',
3839
'Real',
3940
'Weights',
4041
]
@@ -940,13 +941,106 @@ def _evaluate(self, **kwargs):
940941
terms.append(expr.xreplace(mapper))
941942
return sum(terms)
942943

944+
@property
945+
def bound_symbols(self):
946+
return set(self.dimensions)
947+
943948
@property
944949
def free_symbols(self):
945-
return super().free_symbols - set(self.dimensions)
950+
return super().free_symbols - self.bound_symbols
946951

947952
func = DifferentiableOp._rebuild
948953

949954

955+
class LocalSum(IndexSum, Pickable):
956+
957+
"""
958+
A zero-initialized sum over guarded local dimensions.
959+
960+
`cdims` are guarded ConditionalDimensions, retained with their original
961+
parents and conditions. `dimensions` exposes the parent iteration dimensions.
962+
Masked points contribute zero. The sum remains symbolic until Cluster lowering
963+
chooses its implementation.
964+
965+
Examples
966+
--------
967+
For bilinear interpolation, `posx` and `posy` are the grid indices of sparse
968+
point `p`, and `wx` and `wy` hold its interpolation weights::
969+
970+
i = CustomDimension('i', 0, 1, 2)
971+
j = CustomDimension('j', 0, 1, 2)
972+
ci = ConditionalDimension('i', i, indirect=True,
973+
condition=And(posx + i >= x_m, posx + i <= x_M))
974+
cj = ConditionalDimension('j', j, indirect=True,
975+
condition=And(posy + j >= y_m, posy + j <= y_M))
976+
value = LocalSum(
977+
wx[p, ci]*wy[p, cj]*f[posx + ci, posy + cj],
978+
cdims=(ci, cj)
979+
)
980+
Eq(rcv[p], value)
981+
982+
The scalar lowering has the following semantics (pseudocode)::
983+
984+
acc = 0
985+
for i in range(2):
986+
for j in range(2):
987+
if x_m <= posx + i <= x_M and y_m <= posy + j <= y_M:
988+
acc += wx[p, i]*wy[p, j]*f[posx + i, posy + j]
989+
rcv[p] = acc
990+
991+
The guarded indices and their parents are local to the sum; `p` remains an
992+
outer iteration dimension.
993+
If every tap is masked, `rcv[p]` receives zero.
994+
"""
995+
996+
__rargs__ = ('expr',)
997+
__rkwargs__ = ('cdims', 'dtype')
998+
999+
def __new__(cls, expr, cdims=(), dtype=None, **kwargs):
1000+
obj = sympy.Expr.__new__(cls, expr)
1001+
1002+
obj._expr = expr
1003+
obj._cdims = as_tuple(cdims)
1004+
obj._dtype = dtype
1005+
1006+
return obj
1007+
1008+
def _hashable_content(self):
1009+
return super()._hashable_content() + (self.cdims, self.dtype)
1010+
1011+
@property
1012+
def cdims(self):
1013+
return self._cdims
1014+
1015+
@cached_property
1016+
def dtype(self):
1017+
if self._dtype is None:
1018+
return extract_dtype(self.expr)
1019+
return self._dtype
1020+
1021+
@cached_property
1022+
def dimensions(self):
1023+
return tuple(d.parent for d in self.cdims)
1024+
1025+
@cached_property
1026+
def conditionals(self):
1027+
return frozendict({d: d.condition for d in self.cdims})
1028+
1029+
@property
1030+
def bound_symbols(self):
1031+
return super().bound_symbols | set(self.cdims)
1032+
1033+
@property
1034+
def free_symbols(self):
1035+
symbols = self.expr.free_symbols.union(*[d.free_symbols for d in self.cdims])
1036+
return symbols - self.bound_symbols
1037+
1038+
def _evaluate(self, **kwargs):
1039+
return self._rebuild(*self._evaluate_args(**kwargs))
1040+
1041+
__reduce_ex__ = Pickable.__reduce_ex__
1042+
1043+
9501044
class WeightsIndexed(Indexed):
9511045

9521046
@property

‎devito/ir/clusters/cluster.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ def dist_dimensions(self):
148148
ret.update(f._dist_dimensions)
149149
return frozenset(ret)
150150

151+
@cached_property
152+
def local_sums(self):
153+
"""
154+
The local sums in equation order, retaining occurrences across equations.
155+
"""
156+
return tuple(s for e in self.exprs for s in e.local_sums)
157+
151158
@cached_property
152159
def scope(self):
153160
return Scope(self.exprs)
@@ -674,6 +681,10 @@ def rebuild(self, **kwargs):
674681
def exprs(self):
675682
return flatten(c.exprs for c in self)
676683

684+
@cached_property
685+
def local_sums(self):
686+
return tuple(s for c in self for s in c.local_sums)
687+
677688
@cached_property
678689
def scope(self):
679690
return Scope(exprs=self.exprs)

‎devito/ir/equations/algorithms.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from devito.data.allocators import DataReference
55
from devito.finite_differences.differentiable import diff2sympy
6-
from devito.ir.support import GuardFactor
6+
from devito.ir.support import GuardFactor, bounded
77
from devito.logger import warning
88
from devito.symbolics import (
99
IntDiv, retrieve_dimensions, retrieve_functions, retrieve_indexed, uxreplace
@@ -28,12 +28,17 @@ def dimension_sort(expr):
2828
appear within Indexeds.
2929
"""
3030

31+
# Bound Dimensions do not impact the order of the enclosing iteration space
32+
bound = bounded(expr)
33+
3134
def handle_indexed(indexed):
3235
relation = []
3336
for i in indexed.indices:
3437
try:
3538
# Assume it's an AffineIndexAccessFunction...
36-
relation.append(i.d)
39+
# It may contain only a scalar offset and bound stencil indices
40+
if i.d:
41+
relation.append(i.d)
3742
except AttributeError:
3843
# It's not! Maybe there are some nested Indexeds (e.g., the
3944
# situation is A[B[i]])
@@ -46,9 +51,9 @@ def handle_indexed(indexed):
4651
# what the user is attempting to do
4752
relation.extend(filter_sorted(i.atoms(Dimension)))
4853

49-
# StencilDimensions are lowered subsequently through special compiler
54+
# Bound Dimensions are lowered subsequently through special compiler
5055
# passes, so they can be ignored here
51-
relation = tuple(d for d in relation if not d.is_Stencil)
56+
relation = tuple(d for d in relation if d not in bound)
5257

5358
return relation
5459

@@ -61,7 +66,7 @@ def handle_indexed(indexed):
6166
relations.add(expr.implicit_dims)
6267

6368
# Add in leftover free dimensions (not an Indexed' index)
64-
extra = set(retrieve_dimensions(expr, deep=True))
69+
extra = set(retrieve_dimensions(expr, deep=True)) - bound
6570

6671
# Add in pure data dimensions (e.g., those accessed only via explicit values,
6772
# such as A[3])

‎devito/ir/equations/equation.py‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import numpy as np
55
import sympy
66

7-
from devito.finite_differences.differentiable import diff2sympy
7+
from devito.finite_differences.differentiable import LocalSum, diff2sympy
88
from devito.ir.equations.algorithms import dimension_sort, generate_conditionals
99
from devito.ir.support import (
10-
Interval, IntervalGroup, IterationSpace, Stencil, detect_accesses
10+
Interval, IntervalGroup, IterationSpace, Stencil, bounded, detect_accesses
1111
)
12-
from devito.symbolics import limits_mapper, retrieve_accesses
12+
from devito.symbolics import limits_mapper, retrieve_accesses, search
1313
from devito.tools import (
14-
Pickable, Tag, as_hashable, filter_sorted, frozendict, reuse_if_unchanged
14+
Pickable, Tag, as_hashable, filter_ordered, filter_sorted, frozendict,
15+
reuse_if_unchanged
1516
)
1617
from devito.types import Eq, Inc, ReduceMax, ReduceMin, ReduceMinMax
1718

@@ -50,6 +51,11 @@ def ispace(self):
5051
def dimensions(self):
5152
return set(self.ispace.dimensions)
5253

54+
@cached_property
55+
def local_sums(self):
56+
"""The local sums in dependency order, with nested sums first."""
57+
return tuple(filter_ordered(search(self, LocalSum, mode='all')))
58+
5359
@property
5460
def implicit_dims(self):
5561
return self._implicit_dims
@@ -273,11 +279,15 @@ def __new__(cls, *args, **kwargs):
273279
# Analyze the expression
274280
accesses = detect_accesses(expr)
275281
dimensions = Stencil.union(*accesses.values())
282+
bound = bounded(expr)
276283

277284
# Separate out the SubIterators from the main iteration Dimensions, that
278285
# is those which define an actual iteration space
279286
iterators = {}
280287
for d in dimensions:
288+
if d in bound:
289+
# Local sum dimensions belong to the sum's own iteration space
290+
continue
281291
if d.is_SubIterator:
282292
iterators.setdefault(d.root, set()).add(d)
283293
elif d.is_Conditional:

‎devito/ir/support/utils.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from contextlib import suppress
33
from itertools import product
44

5-
from devito.finite_differences import IndexDerivative
5+
from devito.finite_differences.differentiable import IndexDerivative, IndexSum
66
from devito.symbolics import retrieve_indexed, search
77
from devito.tools import DefaultOrderedDict, as_tuple, filter_sorted, split
88
from devito.types import (
@@ -13,6 +13,7 @@
1313
'AccessMode',
1414
'IMask',
1515
'Stencil',
16+
'bounded',
1617
'detect_accesses',
1718
'erange',
1819
'extrema',
@@ -231,7 +232,18 @@ def pull_dims(exprs, flag=True):
231232
return dims
232233

233234

234-
# *** Utility functions for expressions that potentially contain StencilDimensions
235+
# *** Utility functions for bound and unbound Dimensions
236+
237+
238+
def bounded(expr):
239+
"""
240+
Retrieve all Dimensions bound by symbolic sums in `expr`.
241+
"""
242+
sums = search(expr, IndexSum, mode='unique', deep=True)
243+
dims = set().union(*(i.bound_symbols for i in sums))
244+
245+
return dims - expr.free_symbols
246+
235247

236248
def unbounded(expr):
237249
"""

‎devito/operations/interpolators.py‎

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
except ImportError:
1212
from numpy import i0
1313

14-
from devito.finite_differences.differentiable import Mul
14+
from devito.finite_differences.differentiable import LocalSum, Mul
1515
from devito.finite_differences.elementary import floor
1616
from devito.logger import warning
1717
from devito.symbolics import INT, retrieve_function_carriers, retrieve_functions
1818
from devito.tools import (
1919
Pickable, as_fp64_decimal, as_list, as_tuple, filter_ordered, flatten, memoized_meth
2020
)
21-
from devito.types import CustomDimension, Eq, Evaluable, Inc, SubFunction, Symbol
21+
from devito.types import CustomDimension, Eq, Evaluable, Inc, SubFunction
2222
from devito.types.utils import DimensionTuple
2323

2424
__all__ = ['LinearInterpolator', 'NearestInterpolator',
@@ -453,19 +453,14 @@ def _interp_idx(self, variables, implicit_dims=None, subdomain=None,
453453

454454
return idx_subs, temps
455455

456-
def _local_accumulator(self, expr, idx_subs, implicit_dims=None, subdomain=None):
456+
def _local_accumulator(self, expr, idx_subs, subdomain=None):
457457
"""
458-
Generate a local accumulator for the interpolation/injection operation.
458+
Represent the local sum of weighted interpolation contributions.
459459
"""
460-
# Accumulate point-wise contributions into a temporary
461-
rhs = Symbol(name=f'sum{self.sfunction.name}', dtype=self.sfunction.dtype)
462-
summands = [Eq(rhs, 0., implicit_dims=implicit_dims)]
463-
# Substitute coordinate base symbols into the interpolation coefficients
464460
weights = self._weights(subdomain=subdomain)
465-
summands.extend([Inc(rhs, (weights * expr).xreplace(idx_subs),
466-
implicit_dims=implicit_dims)])
467-
468-
return summands, rhs
461+
rdims = self._rdim(subdomain=subdomain)
462+
summand = (weights * expr).xreplace(idx_subs)
463+
return LocalSum(summand, cdims=rdims, dtype=self.sfunction.dtype)
469464

470465
@check_radius
471466
@check_coords
@@ -539,16 +534,13 @@ def _interpolate(self, expr, increment=False, self_subs=None, implicit_dims=None
539534
idx_subs, temps = self._interp_idx(variables, implicit_dims=implicit_dims,
540535
subdomain=subdomain)
541536

542-
# Local scalar for accumulation over radius
543-
summands, rhs = self._local_accumulator(expr, idx_subs,
544-
implicit_dims=implicit_dims,
545-
subdomain=subdomain)
537+
rhs = self._local_accumulator(expr, idx_subs, subdomain=subdomain)
546538
# Write/Incr `self`
547539
lhs = self.sfunction.subs(self_subs)
548540
ecls = Inc if increment else Eq
549541
last = [ecls(lhs, rhs, implicit_dims=implicit_dims)]
550542

551-
return temps + summands + last
543+
return temps + last
552544

553545
def _inject(self, field, expr, increment=True, implicit_dims=None):
554546
"""
@@ -785,8 +777,8 @@ class NearestInterpolator(LinearInterpolator):
785777

786778
_name = 'nearest'
787779

788-
def _local_accumulator(self, expr, idx_subs, implicit_dims=None, subdomain=None):
789-
return [], expr.xreplace(idx_subs)
780+
def _local_accumulator(self, expr, idx_subs, subdomain=None):
781+
return expr.xreplace(idx_subs)
790782

791783
@memoized_meth
792784
def _rdim(self, subdomain=None, shifts=None):

‎devito/operator/operator.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
from devito.parameters import configuration
3434
from devito.passes import (
3535
Graph, error_mapper, finalize_args, generate_implicit, generate_macros, is_on_device,
36-
lower_dtypes, lower_index_derivatives, minimize_symbols, optimize_pows, unevaluate
36+
lower_dtypes, lower_index_derivatives, lower_local_sums, minimize_symbols,
37+
optimize_pows, unevaluate
3738
)
3839
from devito.symbolics import estimate_cost, subs_op_args
3940
from devito.tools import (
@@ -423,6 +424,7 @@ def _lower_clusters(cls, expressions, profiler=None, **kwargs):
423424
clusters = generate_implicit(clusters)
424425

425426
# Lower all remaining high order symbolic objects
427+
clusters = lower_local_sums(clusters, **kwargs)
426428
clusters = lower_index_derivatives(clusters, **kwargs)
427429

428430
# Turn pows into multiplications. This must happen as late as possible

‎devito/passes/clusters/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
from .implicit import * # noqa
1010
from .misc import * # noqa
1111
from .derivatives import * # noqa
12+
from .localsum import * # noqa
1213
from .unevaluate import * # noqa

‎devito/passes/clusters/cse.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Moved in 1.13
1212
from sympy.core.basic import ordering_of_classes
1313

14-
from devito.finite_differences.differentiable import IndexDerivative
14+
from devito.finite_differences.differentiable import IndexSum
1515
from devito.ir import Cluster, Scope, cluster_pass
1616
from devito.symbolics import (
1717
DefFunction, Reserved, estimate_cost, q_leaf, q_terminal, search
@@ -427,7 +427,7 @@ def _(expr):
427427
return {}
428428

429429

430-
@_catch.register(IndexDerivative)
430+
@_catch.register(IndexSum)
431431
def _(expr):
432432
"""
433433
Handler for symbol-binding objects. There can be many of them and therefore

0 commit comments

Comments
 (0)