Skip to content

Commit 3241a63

Browse files
authored
Merge pull request #3034 from devitocodes/fix-mpi-padinit
api: Fix initializers with mpi
2 parents 8bbe0d3 + 1c3196f commit 3241a63

8 files changed

Lines changed: 241 additions & 23 deletions

File tree

‎devito/builtins/initializers.py‎

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import numpy as np
22

33
import devito as dv
4-
from devito.builtins.utils import check_builtins_args, nbl_to_padsize, pad_outhalo
4+
from devito.builtins.utils import (
5+
axis_slice, check_builtins_args, nbl_to_padsize, pad_outhalo
6+
)
57
from devito.tools import as_list, as_tuple
68

79
__all__ = ['assign', 'gaussian_smooth', 'initialize_function', 'smooth']
@@ -217,15 +219,106 @@ def fset(f, g):
217219
return f
218220

219221

220-
def _initialize_function(function, data, nbl, mapper=None, mode='constant'):
222+
def _rank_without_interior(function, nbl):
221223
"""
222-
Construct the symbolic objects for `initialize_function`.
224+
Whether some rank owns no part of the interior along a padded Dimension.
225+
226+
The padding is extended by reading the boundary plane at a fixed global
227+
index, which only works while every rank owns interior to read from.
228+
229+
Collective: the answer is reduced over all ranks, since they must then all
230+
extend the padding the same way.
223231
"""
224-
nbl, slices = nbl_to_padsize(nbl, function.ndim)
232+
distributor = function.grid.distributor
233+
if not distributor.is_parallel:
234+
return False
235+
236+
owns = True
237+
for i, (d, (nl, nr)) in enumerate(
238+
zip(function.space_dimensions, as_tuple(nbl), strict=True)
239+
):
240+
if function.grid.is_distributed(d):
241+
# The interior spans `[nl, glb_max - nr]`
242+
span = distributor.glb_slices[d]
243+
owns &= not (span.stop <= nl or
244+
span.start > distributor.glb_shape[i] - 1 - nr)
245+
246+
return not distributor.comm.allreduce(owns, op=dv.mpi.MPI.LAND)
247+
248+
249+
def _global_plane(function, axis, index):
250+
"""
251+
The plane of `function` at global position `index` along `axis`, assembled
252+
identically on every rank and keeping `axis` as a length-1 dimension.
253+
254+
E.g for a 2D function f, the plane at `index` along `y` is f[:, index:index+1],
255+
which is a 2D array of shape (f.shape[0], 1).
256+
"""
257+
distributor = function.grid.distributor
258+
259+
local = np.asarray(function.data[axis_slice(function.ndim, axis, index,
260+
index + 1)])
261+
plane = np.zeros(tuple(distributor.glb_shape[i] if i != axis else 1
262+
for i in range(function.ndim)),
263+
dtype=function.dtype)
264+
265+
# `glb_slices` tile the domain, so each slot gets a single contribution
266+
if local.size:
267+
slot = tuple(distributor.glb_slices[dim] if i != axis else slice(None)
268+
for i, dim in enumerate(function.dimensions))
269+
plane[slot] = local.reshape(plane[slot].shape)
270+
distributor.comm.Allreduce(dv.mpi.MPI.IN_PLACE, plane, op=dv.mpi.MPI.SUM)
271+
272+
return plane
273+
274+
275+
def _extend_padding_mpi(function, nbl):
276+
"""
277+
Fill the padding of `function` by repeating its nearest boundary plane.
278+
279+
With MPI, an edge rank may own only padding, while the first interior
280+
plane belongs to another rank
281+
282+
edge rank | neighbouring rank
283+
padding | padding interior
284+
. . . . | . . . : a b c ...
285+
a a a a | a a a : a b c ...
286+
<-------------------------- a
287+
288+
The edge rank cannot read the interior boundary plane locally, so the
289+
symbolic extension cannot perform this copy. Global indexing through
290+
the distributed `Data` accesses that plane on the rank that owns it.
291+
"""
292+
distributor = function.grid.distributor
293+
294+
# `TimeFunction`s are rejected upstream, so `axis` indexes `glb_shape` too
295+
for axis, (nl, nr) in enumerate(as_tuple(nbl)):
296+
glb_max = distributor.glb_shape[axis] - 1
297+
298+
# One Dimension at a time, each reading the planes left by the previous
299+
# ones, so that corners come out filled
300+
for nb, src, lo in ((nl, nl, 0), (nr, glb_max - nr, glb_max - nr + 1)):
301+
if nb <= 0:
302+
continue
303+
# `plane` is size 1 along `axis`, so it broadcasts over the slot
304+
plane = _global_plane(function, axis, src)
305+
function.data[axis_slice(function.ndim, axis, lo, lo + nb)] = plane
306+
307+
308+
def _write_interior(function, data, slices):
309+
"""Fill the interior of `function`, that is everything but the padding."""
225310
if isinstance(data, dv.Function):
226311
function.data[slices] = data.data[:]
227312
else:
228313
function.data[slices] = data
314+
315+
316+
def _initialize_function(function, data, nbl, mapper=None, mode='constant'):
317+
"""
318+
Construct the symbolic objects for `initialize_function`.
319+
"""
320+
nbl, slices = nbl_to_padsize(nbl, function.ndim)
321+
_write_interior(function, data, slices)
229322
lhs = []
230323
rhs = []
231324
options = []
@@ -385,17 +478,24 @@ def initialize_function(function, data, nbl, mapper=None, mode='constant',
385478
else:
386479
lhss, rhss, optionss = [], [], []
387480
for f, data in zip(functions, datas, strict=True):
481+
padsizes, slices = nbl_to_padsize(nbl, f.ndim)
388482

389-
lhs, rhs, options = _initialize_function(f, data, nbl, mapper, mode)
483+
# `reflect` has its own, stricter check on the halo size
484+
if mode == 'constant' and _rank_without_interior(f, padsizes):
485+
_write_interior(f, data, slices)
486+
_extend_padding_mpi(f, padsizes)
487+
continue
390488

489+
lhs, rhs, options = _initialize_function(f, data, nbl, mapper, mode)
391490
lhss.extend(lhs)
392491
rhss.extend(rhs)
393492
optionss.extend(options)
394493

395494
assert len(lhss) == len(rhss) == len(optionss)
396495

397-
name = name or f'initialize_{"_".join(f.name for f in functions)}'
398-
assign(lhss, rhss, options=optionss, name=name, **kwargs)
496+
if lhss:
497+
name = name or f'initialize_{"_".join(f.name for f in functions)}'
498+
assign(lhss, rhss, options=optionss, name=name, **kwargs)
399499

400500
if pad_halo:
401501
for f in functions:

‎devito/builtins/utils.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
__all__ = [
1111
'abstract_args',
12+
'axis_slice',
1213
'check_builtins_args',
1314
'make_retval',
1415
'nbl_to_padsize',
@@ -90,6 +91,12 @@ def nbl_to_padsize(nbl, ndim):
9091
return tuple(nb_pad), tuple(slices)
9192

9293

94+
def axis_slice(ndim, axis, start, stop):
95+
"""An index tuple selecting `[start, stop)` along `axis`."""
96+
return tuple(slice(start, stop) if i == axis else slice(None)
97+
for i in range(ndim))
98+
99+
93100
def pad_outhalo(function):
94101
""" Pad outer halo with edge values."""
95102
h_shape = function._data_with_outhalo.shape

‎devito/data/data.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,11 @@ def __setitem__(self, glb_idx, val):
347347
processed.append(slice(j.start, j.stop, 1))
348348
else:
349349
processed.append(j)
350-
val_idx = as_tuple(processed)
350+
# An axis `val` broadcasts along carries the same value for every
351+
# rank, so it must be taken whole rather than cut down to this
352+
# rank's share of the destination
353+
val_idx = tuple(slice(None) if n == 1 else j
354+
for j, n in zip(processed, val.shape, strict=True))
351355
elif is_windowed(val):
352356
# Read all of it, there is no decomposition to restrict it to
353357
val_idx = tuple(slice(0, s) for s in val.shape)

‎devito/data/decomposition.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,15 +358,15 @@ def index_glb_to_loc(self, *args, rel=True):
358358
if abs_ofs >= base and abs_ofs <= top:
359359
return rel_ofs
360360
elif abs_ofs > top:
361-
return top + 1
361+
return top - base
362362
else:
363363
return None
364364
else:
365365
rel_ofs = abs_ofs - (glb_max - top)
366366
if abs_ofs >= glb_max - top and abs_ofs <= glb_max - base:
367367
return rel_ofs
368368
elif abs_ofs > glb_max - base:
369-
return glb_max - base + 1
369+
return top - base
370370
else:
371371
return None
372372
else:

‎devito/types/dimension.py‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -607,19 +607,10 @@ def _arg_values(self, grid=None, **kwargs):
607607
root = next((d for d in grid.dimensions if d.name == root.name), root)
608608

609609
rtkn = self.value
610-
if grid is not None and grid.is_distributed(root):
611-
# Get local thickness
612-
if self.local:
613-
# Dimension is of type `left`/`right` - compute the offset
614-
# and then add 1 to get the appropriate thickness
615-
if self.value is not None:
616-
tkn = grid.distributor.glb_to_loc(root, rtkn-1, self.side)
617-
tkn = tkn+1 if tkn is not None else 0
618-
else:
619-
tkn = 0
620-
else:
621-
# Dimension is of type `middle`
622-
tkn = grid.distributor.glb_to_loc(root, rtkn, self.side) or 0
610+
if rtkn and grid is not None and grid.is_distributed(root):
611+
# Convert the last boundary index to a local count
612+
tkn = grid.distributor.glb_to_loc(root, rtkn-1, self.side)
613+
tkn = tkn+1 if tkn is not None else 0
623614
else:
624615
tkn = rtkn or 0
625616

‎tests/test_builtins.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,32 @@ def test_batching(self):
344344
assert np.all(a[::-1, :] - np.array(i.data[0:4, 4:8]) == 0)
345345
assert np.all(a[::-1, :] - np.array(i.data[8:12, 4:8]) == 0)
346346

347+
@pytest.mark.parallel(mode=[(3, 'basic'), (4, 'basic')])
348+
def test_initialize_function_pad_over_rank(self, mode):
349+
"""
350+
`initialize_function` must pad correctly even when a rank owns no part
351+
of the interior, since the boundary plane it reads is then on a
352+
different rank.
353+
"""
354+
nbl = 6
355+
inner = (7, 5, 6)
356+
shape = tuple(s + 2*nbl for s in inner)
357+
358+
# Decompose along `y` only, so some rank holds nothing but padding
359+
grid = Grid(shape=shape, topology=(1, '*', 1))
360+
361+
f = Function(name='f', grid=grid, space_order=(8, 4, 4), dtype=np.float32)
362+
363+
# Asymmetric, so that a misplaced plane cannot go unnoticed
364+
data = np.arange(np.prod(inner), dtype=np.float32).reshape(inner) + 1.
365+
366+
initialize_function(f, data, [(nbl, nbl)]*3)
367+
368+
glb = f.data_gather(rank=0)
369+
if grid.distributor.myrank == 0:
370+
expected = np.pad(data, nbl, mode='edge')
371+
assert np.array_equal(np.asarray(glb), expected)
372+
347373

348374
class TestBuiltinsResult:
349375

‎tests/test_data.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,10 @@ def test_glb_to_loc_w_side(self):
503503
assert d.index_glb_to_loc(2, LEFT) is None
504504
assert d.index_glb_to_loc(3, RIGHT) is None
505505

506+
# An offset covering the whole subdomain gives its last index
507+
assert d.index_glb_to_loc(8, LEFT) == 2
508+
assert d.index_glb_to_loc(7, RIGHT) == 2
509+
506510
def test_loc_to_glb_index_conversions(self):
507511
d = Decomposition([[0, 1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]], 2)
508512

@@ -1217,6 +1221,26 @@ def test_big_steps(self, mode):
12171221
(slice(6, None, -3), slice(6, None, -3))]:
12181222
self._assert_induced(f.data[gslice], a[gslice])
12191223

1224+
@pytest.mark.parallel(mode=4)
1225+
def test_setitem_broadcast(self, mode):
1226+
"""
1227+
An RHS that is size 1 along an axis broadcasts over that axis, rather
1228+
than being cut down to each rank's share of the destination.
1229+
"""
1230+
grid = Grid(shape=(4, 8), topology=(1, 4))
1231+
f = Function(name='f', grid=grid, space_order=0, dtype=np.int32)
1232+
f.data[:] = 0
1233+
1234+
col = np.array([10, 20, 30, 40], dtype=np.int32)[:, None]
1235+
f.data[:, 0:3] = col
1236+
1237+
expected = np.zeros((4, 8), dtype=np.int32)
1238+
expected[:, 0:3] = col
1239+
1240+
glb = f.data_gather(rank=0)
1241+
if grid.distributor.myrank == 0:
1242+
assert np.array_equal(np.asarray(glb), expected)
1243+
12201244
@pytest.mark.parallel(mode=4)
12211245
def test_setitem(self, mode):
12221246
# __setitem__ mpi slicing tests

‎tests/test_mpi.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3320,6 +3320,37 @@ def test_misc_subdims_3D(self, mode):
33203320
assert np.all(u.data[1, :, :, 0:2] == 1.)
33213321
assert np.all(u.data[1, :, :, -2:] == 1.)
33223322

3323+
@pytest.mark.parallel(mode=[(4, 'basic')])
3324+
def test_subdim_thickness_spanning_rank(self, mode):
3325+
"""
3326+
A `left`/`right` SubDimension whose thickness covers a whole rank must
3327+
not produce a local thickness larger than that rank's extent, or the
3328+
generated loop runs off the ends of its temporaries.
3329+
"""
3330+
# 4 ranks along `y`, so each holds 2 points against 3-point layers
3331+
grid = Grid(shape=(4, 8), topology=(1, 4))
3332+
y = grid.dimensions[1]
3333+
3334+
f = Function(name='f', grid=grid)
3335+
3336+
yl = SubDimension.left(name='yl', parent=y, thickness=3)
3337+
yr = SubDimension.right(name='yr', parent=y, thickness=3)
3338+
3339+
op = Operator([Eq(f.subs(y, yl), 1), Eq(f.subs(y, yr), 2)])
3340+
3341+
args = op.arguments(f=f)
3342+
loc_size = grid.distributor.shape[1]
3343+
assert args['y_ltkn0'] <= loc_size
3344+
assert args['y_rtkn1'] <= loc_size
3345+
3346+
op.apply(f=f)
3347+
3348+
glb = f.data_gather(rank=0)
3349+
if grid.distributor.myrank == 0:
3350+
assert np.all(glb[:, :3] == 1.)
3351+
assert np.all(glb[:, 3:-3] == 0.)
3352+
assert np.all(glb[:, -3:] == 2.)
3353+
33233354
@pytest.mark.parallel(mode=[(4, 'full')])
33243355
def test_custom_subdomain(self, mode):
33253356
"""
@@ -3448,6 +3479,41 @@ def test_interpolation_at_uforward(self, mode):
34483479
assert t2.name == 't2'
34493480
assert t2.origin == t + 1
34503481

3482+
@pytest.mark.parallel(mode=4)
3483+
@pytest.mark.parametrize('side', ['left', 'right', 'middle'])
3484+
def test_thickness_covering_ranks(self, side, mode):
3485+
"""
3486+
A thickness of 20 points over four ranks of 16 points each covers a rank
3487+
entirely, so the local thickness of the latter is its whole extent.
3488+
"""
3489+
class Band(SubDomain):
3490+
name = 'band'
3491+
3492+
def define(self, dimensions):
3493+
x, y = dimensions
3494+
return {x: (side, 20, 20) if side == 'middle' else (side, 20), y: y}
3495+
3496+
grid = Grid(shape=(64, 4), topology=(4, 1))
3497+
band = Band(grid=grid)
3498+
f = Function(name='f', grid=grid, space_order=0)
3499+
g = Function(name='g', grid=grid, space_order=0)
3500+
3501+
op = Operator(Eq(f, 1, subdomain=band), name='Thickness')
3502+
3503+
args = op.arguments()
3504+
rank = grid.distributor.myrank
3505+
if side in ['left', 'middle']:
3506+
assert args['x_ltkn0'] == [16, 4, 0, 0][rank]
3507+
if side in ['right', 'middle']:
3508+
assert args['x_rtkn0'] == [0, 0, 4, 16][rank]
3509+
3510+
op.apply()
3511+
3512+
inside = {'left': slice(0, 20), 'right': slice(44, 64),
3513+
'middle': slice(20, 44)}[side]
3514+
g.data[inside] = 1
3515+
assert np.all(f.data == g.data)
3516+
34513517

34523518
def gen_serial_norms(shape, so):
34533519
"""

0 commit comments

Comments
 (0)