diff --git a/devito/finite_differences/interpolation.py b/devito/finite_differences/interpolation.py index 5cc92ab381..7fb397b080 100644 --- a/devito/finite_differences/interpolation.py +++ b/devito/finite_differences/interpolation.py @@ -14,6 +14,8 @@ def interp_mapper(source, target, dims): """ mapper = {} for d in dims: + if d.is_Time: + continue try: s = source[d] t = target[d] diff --git a/devito/mpi/routines.py b/devito/mpi/routines.py index f861bf9e75..3c446b24ba 100644 --- a/devito/mpi/routines.py +++ b/devito/mpi/routines.py @@ -367,8 +367,7 @@ def _make_copy(self, f, hse, key, swap=False): eqns.extend([Eq(d.symbolic_max, d.symbolic_size - 1) for d in bdims]) vd = CustomDimension(name='vd', symbolic_size=f.ncomp) - buf = Array(name='buf', dimensions=[vd] + bdims, dtype=f.c0.dtype, - padding=0) + buf = Array(name='buf', dimensions=[vd] + bdims, dtype=f.c0.dtype) mapper = dict(zip(dims, bdims, strict=True)) findices = [ @@ -410,9 +409,9 @@ def _make_sendrecv(self, f, hse, key, **kwargs): bdims = [CustomDimension(name='vd', symbolic_size=f.ncomp)] + dims bufg = Array(name='bufg', dimensions=bdims, dtype=f.c0.dtype, - padding=0, liveness='eager') + liveness='eager') bufs = Array(name='bufs', dimensions=bdims, dtype=f.c0.dtype, - padding=0, liveness='eager') + liveness='eager') ofsg = [Symbol(name=f'og{d.root}') for d in f.dimensions] ofss = [Symbol(name=f'os{d.root}') for d in f.dimensions] diff --git a/devito/passes/clusters/buffering.py b/devito/passes/clusters/buffering.py index 2a8c78e7fc..3f2da55789 100644 --- a/devito/passes/clusters/buffering.py +++ b/devito/passes/clusters/buffering.py @@ -376,6 +376,9 @@ def generate_buffers(clusters, key, sregistry, options, **kwargs): assert len(buffers) == 1, "Unexpected form of multi-level buffering" buffer, = buffers xd = buffer.indices[dim] + # The new buffer is fed by `buffer`, so it inherits its padding + # policy regardless of `f`'s + extra_kwargs = {'is_autopaddable': buffer.is_autopaddable} else: size = infer_buffer_size(f, dim, clusters) @@ -395,6 +398,7 @@ def generate_buffers(clusters, key, sregistry, options, **kwargs): except KeyError: name = sregistry.make_name(prefix='db') xd = xds[(dim, size)] = BufferDimension(name, 0, size-1, size, dim) + extra_kwargs = {} # The buffer dimensions dimensions = list(f.dimensions) @@ -404,13 +408,9 @@ def generate_buffers(clusters, key, sregistry, options, **kwargs): # Finally create the actual buffer cls = callback or Array name = sregistry.make_name(prefix=f'{f.name}b') - # We specify the padding to match the input Function's one, so that - # the array can be used in place of the Function with valid strides - # Plain Array do not track mapped so we default to no padding - padding = 0 if cls is Array else f.padding mapper[f] = cls(name=name, dimensions=dimensions, dtype=f.dtype, - padding=padding, grid=f.grid, halo=f.halo, - space='mapped', mapped=f, f=f) + grid=f.grid, halo=f.halo, + space='mapped', mapped=f, f=f, **extra_kwargs) return mapper diff --git a/devito/passes/iet/linearization.py b/devito/passes/iet/linearization.py index aca2485444..ecca4d5325 100644 --- a/devito/passes/iet/linearization.py +++ b/devito/passes/iet/linearization.py @@ -70,19 +70,7 @@ def key1(f, d): if f.is_regular: # For paddable objects the following holds: # `same dim + same halo + same padding_dtype => same (auto-)padding` - if d is f.dimensions[-1]: - # Only the last dimension is padded - try: - if f.padding == f.mapped.padding: - # Padding set from the mapped Function - # e.g. from buffering or fft temp array - pad_key = f.mapped.__padding_dtype__ - else: - pad_key = f.__padding_dtype__ - except AttributeError: - pad_key = f.__padding_dtype__ - else: - pad_key = None + pad_key = f.__padding_dtype__ if d is f.dimensions[-1] else None return (d, f._size_halo[d], pad_key) else: diff --git a/devito/types/array.py b/devito/types/array.py index 13d3b105a2..ea80bed19b 100644 --- a/devito/types/array.py +++ b/devito/types/array.py @@ -94,8 +94,6 @@ class Array(ArrayBasic): to ``np.float32``. halo : iterable of 2-tuples, optional The halo region of the object. - padding : iterable of 2-tuples, optional - The padding region of the object. liveness : str, optional The liveness of the object. Allowed values: 'eager', 'lazy'. Defaults to 'lazy'. Used to override `_mem_internal_eager` and `_mem_internal_lazy`. @@ -163,17 +161,12 @@ def __dtype_setup__(cls, **kwargs): return kwargs.get('dtype', np.float32) def __padding_setup__(self, **kwargs): - padding = kwargs.get('padding') - if padding is None: - padding = ((0, 0),)*self.ndim - elif isinstance(padding, DimensionTuple): - padding = tuple(padding[d] for d in self.dimensions) - elif is_integer(padding): - padding = tuple((0, padding) for _ in range(self.ndim)) - elif isinstance(padding, tuple) and len(padding) == self.ndim: - padding = tuple((0, i) if is_integer(i) else i for i in padding) + # `padding=` is never honored: derived from policy to avoid stride + # inconsistencies between Functions sharing dimensions/halo + if self.is_autopaddable: + padding = self.__padding_setup_smart__(**kwargs) else: - raise TypeError(f'`padding` must be int or {self.ndim}-tuple of ints') + padding = ((0, 0),)*self.ndim return DimensionTuple(*padding, getters=self.dimensions) @property @@ -244,7 +237,42 @@ class MappedArrayMixin: class ArrayMapped(MappedArrayMixin, Array): - pass + + __rkwargs__ = Array.__rkwargs__ + ('mapped', 'is_autopaddable') + + def __init_finalize__(self, *args, **kwargs): + self._mapped = kwargs.get('mapped') + self._is_autopaddable = kwargs.get('is_autopaddable') + super().__init_finalize__(*args, **kwargs) + + @property + def mapped(self): + return self._mapped + + @property + def is_autopaddable(self): + if self._is_autopaddable is not None: + return self._is_autopaddable + if self.mapped is None: + return True + return self.mapped.is_autopaddable + + @property + def __padding_dtype__(self): + if not self.is_autopaddable: + return None + if self.mapped is not None: + return self.mapped.__padding_dtype__ + return super().__padding_dtype__ + + @cached_property + def _signature(self): + # Exclude `mapped` so buf-reuse can dedup buffers across distinct + # mapped Functions + ret = [type(self), self.indices] + attrs = set(self.__rkwargs__) - {'name', 'function', 'mapped'} + ret.extend(getattr(self, i) for i in attrs) + return frozenset(ret) class ArrayObject(ArrayBasic): diff --git a/devito/types/basic.py b/devito/types/basic.py index 22a87e848f..558c7bb2e7 100644 --- a/devito/types/basic.py +++ b/devito/types/basic.py @@ -711,7 +711,7 @@ class AbstractFunction(sympy.Function, Basic, Pickable, Evaluable): effect if autopadding is disabled, which is the default behavior. """ - __rkwargs__ = ('name', 'dtype', 'grid', 'halo', 'padding', 'ghost', + __rkwargs__ = ('name', 'dtype', 'grid', 'halo', 'ghost', 'alias', 'space', 'function', 'is_transient', 'avg_mode') __properties__ = ('is_const', 'is_transient') @@ -743,17 +743,16 @@ def __new__(cls, *args, **kwargs): return function # If dimensions have been replaced, then it is necessary to set `function` - # to None. It may also be necessary to remove halo and padding so that - # they are rebuilt with the new dimensions + # to None. It may also be necessary to remove halo so that it is rebuilt + # with the new dimensions if function is not None and function.dimensions != dimensions: function = kwargs['function'] = None - for i in ('halo', 'padding'): - if len(kwargs[i]) != len(dimensions): - kwargs.pop(i) - else: - # Downcast from DimensionTuple so that the new `dimensions` - # are used down the line - kwargs[i] = tuple(kwargs[i]) + if len(kwargs['halo']) != len(dimensions): + kwargs.pop('halo') + else: + # Downcast from DimensionTuple so that the new `dimensions` + # are used down the line + kwargs['halo'] = tuple(kwargs['halo']) with sympy_mutex: # Go straight through Basic, thus bypassing caching and machinery @@ -890,8 +889,10 @@ def __halo_setup__(self, **kwargs): halo = tuple(kwargs.get('halo', ((0, 0),)*self.ndim)) return DimensionTuple(*halo, getters=self.dimensions) - def __padding_setup__(self, padding=None, **kwargs): - padding = tuple(padding or ((0, 0),)*self.ndim) + def __padding_setup__(self, **kwargs): + # `padding=` is never honored: derived from policy to avoid stride + # inconsistencies between Functions sharing dimensions/halo + padding = ((0, 0),)*self.ndim return DimensionTuple(*padding, getters=self.dimensions) @cached_property diff --git a/devito/types/dense.py b/devito/types/dense.py index f62c452058..6e260895f0 100644 --- a/devito/types/dense.py +++ b/devito/types/dense.py @@ -1013,9 +1013,6 @@ class Function(DiscreteFunction): Controller for memory allocation. To be used, for example, when one wants to take advantage of the memory hierarchy in a NUMA architecture. Refer to `default_allocator.__doc__` for more information. - padding : int or tuple of ints, optional - Allocate extra grid points to maximize data access alignment. When a tuple - of ints, one int per Dimension should be provided. Examples -------- @@ -1271,25 +1268,12 @@ def __halo_setup__(self, **kwargs): return DimensionTuple(*halo, getters=self.dimensions) def __padding_setup__(self, **kwargs): - padding = kwargs.get('padding') - if padding is None: - if self.is_autopaddable: - padding = self.__padding_setup_smart__(**kwargs) - else: - padding = super().__padding_setup__(**kwargs) - - elif isinstance(padding, DimensionTuple): - padding = tuple(padding[d] for d in self.dimensions) - - elif is_integer(padding): - padding = tuple((0, padding) if d.is_Space else (0, 0) - for d in self.dimensions) - - elif isinstance(padding, tuple) and len(padding) == self.ndim: - padding = tuple((0, i) if is_integer(i) else i for i in padding) - + # `padding=` is never honored: derived from policy to avoid stride + # inconsistencies between Functions sharing dimensions/halo + if self.is_autopaddable: + padding = self.__padding_setup_smart__(**kwargs) else: - raise TypeError(f"`padding` must be int or {self.ndim}-tuple of ints") + return super().__padding_setup__(**kwargs) return DimensionTuple(*padding, getters=self.dimensions) @property @@ -1410,9 +1394,6 @@ class TimeFunction(Function): Controller for memory allocation. To be used, for example, when one wants to take advantage of the memory hierarchy in a NUMA architecture. Refer to `default_allocator.__doc__` for more information. - padding : int or tuple of ints, optional - Allocate extra grid points to maximize data access alignment. When a tuple - of ints, one int per Dimension should be provided. Examples -------- diff --git a/devito/types/misc.py b/devito/types/misc.py index 7b979465c6..621e04e665 100644 --- a/devito/types/misc.py +++ b/devito/types/misc.py @@ -267,12 +267,6 @@ def __init_finalize__(self, *args, shift=None, **kwargs): # for homogeneity reasons self._shift = as_tuple(shift) - def __padding_setup__(self, **kwargs): - padding = kwargs.pop('padding', None) - if padding is None: - padding = self.__padding_setup_smart__(**kwargs) - return super().__padding_setup__(padding=padding, **kwargs) - @property def shift(self): return self._shift diff --git a/examples/userapi/01_dsl.ipynb b/examples/userapi/01_dsl.ipynb index 268ad8e9c6..f932218311 100644 --- a/examples/userapi/01_dsl.ipynb +++ b/examples/userapi/01_dsl.ipynb @@ -132,9 +132,6 @@ " Controller for memory allocation. To be used, for example, when one wants\n", " to take advantage of the memory hierarchy in a NUMA architecture. Refer to\n", " `default_allocator.__doc__` for more information.\n", - " padding : int or tuple of ints, optional\n", - " Allocate extra grid points to maximize data access alignment. When a tuple\n", - " of ints, one int per Dimension should be provided.\n", "\n", " Examples\n", " --------\n", @@ -700,7 +697,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Operator `Kernel` ran in 0.07 s\n" + "Operator `Kernel` ran in 0.06 s\n" ] }, { diff --git a/tests/test_data.py b/tests/test_data.py index fb09d4ef47..f1177421e1 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -325,7 +325,7 @@ class TestMetaData: def test_wo_halo_wo_padding(self): grid = Grid(shape=(4, 4, 4)) - u = Function(name='u', grid=grid, space_order=0, padding=0) + u = Function(name='u', grid=grid, space_order=0) assert u.shape == u._shape_with_inhalo == u.shape_allocated assert u.shape_with_halo == u._shape_with_inhalo # W/o MPI, these two coincide @@ -336,7 +336,7 @@ def test_wo_halo_wo_padding(self): def test_w_halo_wo_padding(self): grid = Grid(shape=(4, 4, 4)) - u = Function(name='u', grid=grid, space_order=2, padding=0) + u = Function(name='u', grid=grid, space_order=2) assert len(u.shape) == len(u._size_halo.left) assert u._size_halo == u._size_owned == ((2, 2), (2, 2), (2, 2)) @@ -349,7 +349,7 @@ def test_w_halo_wo_padding(self): # Try with different grid shape and space_order grid2 = Grid(shape=(3, 3, 3)) - u2 = Function(name='u2', grid=grid2, space_order=4, padding=0) + u2 = Function(name='u2', grid=grid2, space_order=4) assert u2.shape == (3, 3, 3) assert u2._offset_domain == (4, 4, 4) assert u2._offset_halo == ((0, 7), (0, 7), (0, 7)) @@ -358,57 +358,25 @@ def test_w_halo_wo_padding(self): ) == u2.shape_with_halo assert u2.shape_with_halo == (11, 11, 11) - def test_wo_halo_w_padding(self): - grid = Grid(shape=(4, 4, 4)) - u = Function(name='u', grid=grid, space_order=2, padding=((1, 1), (3, 3), (4, 4))) - - assert tuple( - i + j + k for i, (j, k) in zip(u.shape_with_halo, u._padding, strict=True) - ) == u.shape_allocated - assert u._halo == ((2, 2), (2, 2), (2, 2)) - assert u._size_padding == ((1, 1), (3, 3), (4, 4)) - assert u._size_padding.left == u._size_padding.right == (1, 3, 4) - assert u._size_nodomain == ((3, 3), (5, 5), (6, 6)) - assert u._size_nodomain.left == u._size_nodomain.right == (3, 5, 6) - assert u._size_nopad == (8, 8, 8) - assert u._offset_domain == (3, 5, 6) - assert u._offset_halo == ((1, 7), (3, 9), (4, 10)) - assert u._offset_halo.left == (1, 3, 4) - assert u._offset_halo.right == (7, 9, 10) - assert u._offset_owned == ((3, 5), (5, 7), (6, 8)) - - def test_w_halo_w_padding(self): - grid = Grid(shape=(4, 4, 4)) - u = Function(name='u', grid=grid, space_order=(2, 1, 4), - padding=((1, 1), (2, 2), (3, 3))) - - assert u._size_halo == ((1, 4), (1, 4), (1, 4)) - assert u._size_owned == ((4, 1), (4, 1), (4, 1)) - assert u._size_nodomain == ((2, 5), (3, 6), (4, 7)) - assert u._size_nodomain.left == (2, 3, 4) - assert u._size_nodomain.right == (5, 6, 7) - assert u._size_nopad == (9, 9, 9) - assert u._offset_domain == (2, 3, 4) - assert u._offset_halo == ((1, 6), (2, 7), (3, 8)) - assert u._offset_owned == ((2, 5), (3, 6), (4, 7)) - - @switchconfig(autopadding=True, platform='bdw') # Platform is to fix pad value + @switchconfig(autopadding=True, platform='bdw') def test_w_halo_w_autopadding(self): grid = Grid(shape=(4, 4, 4)) u0 = Function(name='u0', grid=grid, space_order=0) u1 = Function(name='u1', grid=grid, space_order=3) - assert configuration['platform'].simd_items_per_reg(u1.dtype) == 8 + mmts = configuration['platform'].max_mem_trans_size(u1.dtype) + u0_pad = mmts - 4 # RoundUp(4, mmts) - 4 assert u0._size_halo == ((0, 0), (0, 0), (0, 0)) - assert u0._size_padding == ((0, 0), (0, 0), (0, 12)) + assert u0._size_padding == ((0, 0), (0, 0), (0, u0_pad)) assert u0._size_nodomain == u0._size_padding - assert u0.shape_allocated == (4, 4, 16) + assert u0.shape_allocated == (4, 4, 4 + u0_pad) + u1_pad = mmts - 10 # RoundUp(3+4+3, mmts) - (3+4+3) assert u1._size_halo == ((3, 3), (3, 3), (3, 3)) - assert u1._size_padding == ((0, 0), (0, 0), (0, 6)) # 6 stems from 16-(3+4+3) - assert u1._size_nodomain == ((3, 3), (3, 3), (3, 9)) - assert u1.shape_allocated == (10, 10, 16) + assert u1._size_padding == ((0, 0), (0, 0), (0, u1_pad)) + assert u1._size_nodomain == ((3, 3), (3, 3), (3, 3 + u1_pad)) + assert u1.shape_allocated == (10, 10, 10 + u1_pad) @switchconfig(autopadding=True, platform='bdw') def test_temp_array_smart_padding_no_overshoot(self): diff --git a/tests/test_linearize.py b/tests/test_linearize.py index f63fa50dfa..e3c8580b10 100644 --- a/tests/test_linearize.py +++ b/tests/test_linearize.py @@ -663,6 +663,21 @@ def test_int64_array(order): assert f'({2*order} + {long}y_size)*({2*order} + {long}x_size))' in str(op) +@switchconfig(autopadding=np.float32) +def test_mapped_array_symbolic_autopad(): + grid = Grid(shape=(30, 30)) + f = Function(name='f', grid=grid, space_order=4) + + a = Array(name='ab', dimensions=grid.dimensions, dtype=f.dtype, + grid=grid, halo=f.halo, space='mapped') + + assert type(a).__name__ == 'ArrayMapped' + assert a.is_autopaddable + pad = a.padding[-1][1] + assert not pad.is_Integer + assert pad.free_symbols == {grid.dimensions[-1].symbolic_size} + + def test_cire_n_strides(): grid = Grid(shape=(4, 4, 4)) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 7329cc31e3..bdb9ac73ca 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -438,14 +438,8 @@ def test_halo_exchange_bilateral(self, mode): assert np.all(f._data_ro_with_inhalo[:, 0] == 0.) assert np.all(f._data_ro_with_inhalo[:, -1] == 0.) - @pytest.mark.parametrize("paddings", [ - (None, None), - ((0, 0), (0, 0)), - ((0, 0), (0, 1)), - ((1, 0), (0, 1)), - ]) @pytest.mark.parallel(mode=2) - def test_halo_exchange_bilateral_asymmetric(self, paddings, mode): + def test_halo_exchange_bilateral_asymmetric(self, mode): """ Test halo exchange between two processes organised in a 2x1 cartesian grid. @@ -474,9 +468,8 @@ def test_halo_exchange_bilateral_asymmetric(self, paddings, mode): """ grid = Grid(shape=(12, 12)) x, y = grid.dimensions - padding = paddings[grid.distributor.comm.rank] - f = Function(name='f', grid=grid, space_order=(1, 1, 2), padding=padding) + f = Function(name='f', grid=grid, space_order=(1, 1, 2)) f.data[:] = grid.distributor.myrank + 1 # Now trigger a halo exchange... diff --git a/tests/test_operator.py b/tests/test_operator.py index eec0399f29..806b091e24 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -1188,23 +1188,21 @@ def test_argument_unknown(self): finally: configuration['ignore-unknowns'] = configuration._defaults['ignore-unknowns'] - @pytest.mark.parametrize('so,to,pad,expected', [ - (0, 1, 0, (2, 4, 4, 4)), - (2, 1, 0, (2, 8, 8, 8)), - (4, 1, 0, (2, 12, 12, 12)), - (4, 3, 0, (4, 12, 12, 12)), - (4, 1, 3, (2, 15, 15, 15)), - ((2, 5, 2), 1, 0, (2, 11, 11, 11)), - ((2, 5, 4), 1, 3, (2, 16, 16, 16)), + @pytest.mark.parametrize('so,to,expected', [ + (0, 1, (2, 4, 4, 4)), + (2, 1, (2, 8, 8, 8)), + (4, 1, (2, 12, 12, 12)), + (4, 3, (4, 12, 12, 12)), + ((2, 5, 2), 1, (2, 11, 11, 11)), ]) - def test_function_dataobj(self, so, to, pad, expected): + def test_function_dataobj(self, so, to, expected): """ Tests that the C-level structs from DiscreteFunctions are properly populated upon application of an Operator. """ grid = Grid(shape=(4, 4, 4)) - u = TimeFunction(name='u', grid=grid, space_order=so, time_order=to, padding=pad) + u = TimeFunction(name='u', grid=grid, space_order=so, time_order=to) op = Operator(Eq(u, 1), opt='noop') diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 407e0433ff..f29a96649b 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -308,7 +308,7 @@ def test_array(self, pickle): d = Dimension(name='d') a = Array(name='a', dimensions=grid.dimensions, dtype=np.int32, - halo=((1, 1), (2, 2)), padding=((2, 2), (2, 2)), + halo=((1, 1), (2, 2)), space='host', scope='stack') pkl_a = pickle.dumps(a) @@ -318,7 +318,7 @@ def test_array(self, pickle): assert new_a.dimensions[0].name == 'x' assert new_a.dimensions[1].name == 'y' assert new_a.halo == ((1, 1), (2, 2)) - assert new_a.padding == ((2, 2), (2, 2)) + assert new_a.padding == ((0, 0), (0, 0)) assert new_a.space == 'host' assert new_a.scope == 'stack'