|
| 1 | +""" |
| 2 | +Benchmark Devito's MPI-distributed data indexing against single-process NumPy. |
| 3 | +
|
| 4 | +Devito turns ``data[idx]`` get/set on an MPI-distributed array into the minimal |
| 5 | +point-to-point exchange (see ``devito/data/distributed``). This script measures |
| 6 | +that machinery against the equivalent operation on a single-process NumPy array |
| 7 | +holding the full domain, for three representative cases: |
| 8 | +
|
| 9 | +* a routed scatter assignment ``data[idx] = values`` (global-index labelled, |
| 10 | + the path that moves data across ranks); |
| 11 | +* the matching routed read ``data[idx]``; |
| 12 | +* a basic strided slice ``data[::2]`` (local/induced, no communication). |
| 13 | +
|
| 14 | +For the routed cases the distributed timing is reported both *cold* (first call, |
| 15 | +including plan construction) and *warm* (cached plan -- the steady state of a |
| 16 | +time loop, e.g. sparse injection every step). The NumPy column is the |
| 17 | +single-process reference for the same logical operation on the full domain. |
| 18 | +
|
| 19 | +Run, for example:: |
| 20 | +
|
| 21 | + DEVITO_MPI=1 mpirun -n 4 python examples/mpi/benchmark_data_indexing.py |
| 22 | + DEVITO_MPI=1 mpirun -n 4 python examples/mpi/benchmark_data_indexing.py \\ |
| 23 | + --shape 1024 1024 --reps 50 |
| 24 | +""" |
| 25 | + |
| 26 | +import argparse |
| 27 | +from time import perf_counter |
| 28 | + |
| 29 | +import numpy as np |
| 30 | + |
| 31 | +from devito import Function, Grid, configuration |
| 32 | +from devito.data.distributed import exchange as _exchange |
| 33 | +from devito.mpi import MPI |
| 34 | + |
| 35 | + |
| 36 | +def _measure(call, reps, comm, clear=None): |
| 37 | + """ |
| 38 | + Median wall time of ``call`` over ``reps`` repetitions. |
| 39 | +
|
| 40 | + The wall time of one repetition is the slowest rank (an ``MPI.MAX`` reduce |
| 41 | + of the per-rank time). With ``clear`` given it is invoked before every timed |
| 42 | + call to drop the plan cache, measuring the cold path; otherwise one warm-up |
| 43 | + call is made first and the cached path is measured. |
| 44 | + """ |
| 45 | + if clear is None: |
| 46 | + call() |
| 47 | + times = [] |
| 48 | + for _ in range(reps): |
| 49 | + if clear is not None: |
| 50 | + clear() |
| 51 | + comm.Barrier() |
| 52 | + start = perf_counter() |
| 53 | + call() |
| 54 | + elapsed = perf_counter() - start |
| 55 | + times.append(comm.allreduce(elapsed, op=MPI.MAX)) |
| 56 | + return float(np.median(times)) |
| 57 | + |
| 58 | + |
| 59 | +def main(): |
| 60 | + parser = argparse.ArgumentParser(description=__doc__) |
| 61 | + parser.add_argument('--shape', type=int, nargs='+', default=[512, 512], |
| 62 | + help='global grid shape (default: 512 512)') |
| 63 | + parser.add_argument('--reps', type=int, default=30, |
| 64 | + help='timed repetitions per operation (default: 30)') |
| 65 | + parser.add_argument('--npoint', type=int, default=None, |
| 66 | + help='routed points (default: an eighth of axis 0)') |
| 67 | + args = parser.parse_args() |
| 68 | + |
| 69 | + configuration['mpi'] = True |
| 70 | + |
| 71 | + shape = tuple(args.shape) |
| 72 | + # Creating the Grid initialises the MPI distributor (and MPI itself) |
| 73 | + grid = Grid(shape=shape) |
| 74 | + comm = grid.distributor.comm |
| 75 | + rank, nprocs = grid.distributor.myrank, grid.distributor.nprocs |
| 76 | + f = Function(name='f', grid=grid, dtype=np.float32) |
| 77 | + |
| 78 | + # Full replicated reference array; also the NumPy single-process baseline |
| 79 | + full = np.arange(int(np.prod(shape)), dtype=f.dtype).reshape(shape) |
| 80 | + f.data[:] = full |
| 81 | + |
| 82 | + # Global row indices to route (reversed, so they cross rank boundaries); |
| 83 | + # each rank contributes the slice it would hold in an external layout |
| 84 | + npoint = args.npoint or max(1, shape[0] // 8) |
| 85 | + glb_idx = np.arange(shape[0])[::-1][:npoint] |
| 86 | + loc_idx = np.array_split(glb_idx, nprocs)[rank] |
| 87 | + loc_vals = full[loc_idx] |
| 88 | + ref = full.copy() |
| 89 | + |
| 90 | + clear = _exchange._build.cache_clear |
| 91 | + |
| 92 | + rows = [] |
| 93 | + |
| 94 | + # 1. Routed scatter assignment: data[idx] = values |
| 95 | + rows.append(( |
| 96 | + 'scatter data[idx] = v', |
| 97 | + _measure(lambda: ref.__setitem__(glb_idx, full[glb_idx]), args.reps, comm), |
| 98 | + _measure(lambda: f.data.__setitem__(loc_idx, loc_vals), args.reps, comm, |
| 99 | + clear=clear), |
| 100 | + _measure(lambda: f.data.__setitem__(loc_idx, loc_vals), args.reps, comm), |
| 101 | + )) |
| 102 | + |
| 103 | + # 2. Routed gather read: data[idx] |
| 104 | + rows.append(( |
| 105 | + 'gather data[idx]', |
| 106 | + _measure(lambda: ref[glb_idx], args.reps, comm), |
| 107 | + _measure(lambda: f.data[loc_idx], args.reps, comm, clear=clear), |
| 108 | + _measure(lambda: f.data[loc_idx], args.reps, comm), |
| 109 | + )) |
| 110 | + |
| 111 | + # 3. Basic strided slice (local/induced, no communication) |
| 112 | + rows.append(( |
| 113 | + 'slice data[::2]', |
| 114 | + _measure(lambda: ref[::2, ::2], args.reps, comm), |
| 115 | + None, |
| 116 | + _measure(lambda: f.data[::2, ::2], args.reps, comm), |
| 117 | + )) |
| 118 | + |
| 119 | + if rank == 0: |
| 120 | + print(f"\nMPI distributed data indexing vs NumPy " |
| 121 | + f"(shape={shape}, ranks={nprocs}, " |
| 122 | + f"routed points={npoint}, reps={args.reps})\n") |
| 123 | + header = ('operation'.ljust(24) + 'numpy [us]'.rjust(14) |
| 124 | + + 'devito cold [us]'.rjust(18) + 'devito warm [us]'.rjust(18)) |
| 125 | + print(header) |
| 126 | + print('-' * len(header)) |
| 127 | + for name, np_t, cold, warm in rows: |
| 128 | + cold_s = '-' if cold is None else format(cold * 1e6, '.1f') |
| 129 | + print(name.ljust(24) |
| 130 | + + format(np_t * 1e6, '.1f').rjust(14) |
| 131 | + + cold_s.rjust(18) |
| 132 | + + format(warm * 1e6, '.1f').rjust(18)) |
| 133 | + print("\ncold = first call (plan construction); " |
| 134 | + "warm = cached plan (steady state of a time loop).") |
| 135 | + |
| 136 | + |
| 137 | +if __name__ == '__main__': |
| 138 | + main() |
0 commit comments