graphviper.graph_tools.process_with_mpi
MPI execution backend for GraphVIPER map/reduce graphs.
This is an optional alternative to the Dask backend
(graphviper.graph_tools.generate_dask_workflow.generate_dask_workflow()
followed by dask.compute). The GraphVIPER architecture keeps the graph a
plain, backend-agnostic description (map / optional reduce /optional
load stages produced by graphviper.graph_tools.map.map() and
graphviper.graph_tools.reduce.reduce()); dask.delayed only ever appears
inside generate_dask_workflow. Because of that modularity the same graph
can be executed by a different engine. This module provides an MPI engine so a
caller can swap:
viper_graph = map(...)
viper_graph = reduce(...)
dask_graph = generate_dask_workflow(viper_graph) # Dask backend
return_dict = dask.compute(dask_graph)[0]
for:
viper_graph = map(...)
viper_graph = reduce(...)
return_dict = processes_with_mpi(viper_graph, cluster_setup) # MPI backend
Execution model (manager / worker pool)
The backend uses mpi4py.futures.MPIPoolExecutor in the static
manager-worker model. The program must therefore be launched as:
mpiexec -n <N> python -m mpi4py.futures <script>.py ...
# or, on TACC:
ibrun python -m mpi4py.futures <script>.py ...
With -m mpi4py.futures exactly one rank (rank 0) runs the user script
(__main__) – it is the manager: it builds the graph and calls
processes_with_mpi. The remaining ranks never run the script body; they sit
in a worker server loop and execute map (and, optionally, reduce) node tasks
dispatched by the manager. This deliberately mirrors the “dedicated driver
node” pattern: the manager does the orchestration (and the small final reduce)
while every other rank does the heavy per-chunk compute – but unlike a Dask
in-process scheduler there is no large task graph to hold, and unlike a
dedicated Dask scheduler node the manager rank shares its node with worker
ranks (only one rank, not a whole node, is spent on orchestration).
Pickling
GraphVIPER’s map() adapts an explicit-signature node task into a
functools.wraps closure (see
graphviper.graph_tools.map.make_graph_node_task()), and plain pickle
cannot serialise closures. mpi4py is therefore reconfigured to pickle with
cloudpickle on the manager side; cloudpickle’s output is loadable by the
workers’ stdlib pickle.loads (cloudpickle only customises dumps), so the
manager->worker direction (the closure node task + its input_params) works
with only the manager reconfigured.
The worker->manager result direction is NOT symmetric: workers stay on stdlib
pickle (they are already in the server loop and cannot be reconfigured from
here), so node-task return values must be stdlib-pickle-serialisable. This
differs from the Dask backend (which serialises worker results with cloudpickle):
a result that is only cloudpickle-serialisable – a closure/lambda, or an instance
of a class defined in __main__ or a local scope – works under
dask.compute but raises PicklingError here. This is fine for the imaging
use case (results are small stdlib-picklable metadata). To make results fully
cloudpickle-capable, the launching program must call
MPI.pickle.__init__(cloudpickle.dumps, cloudpickle.loads) at import time on
every rank (before mpi4py.futures starts the worker loop), not just inside
this function.
Notes
Map node tasks write their result chunk straight to the shared (Lustre) output store and return only small metadata; the manager creates/initialises that store before calling this function, so all workers see it. Results are returned in input order.
A
loadstage (disk-chunk coalescing) is not specially handled here: each map task self-loads its data (input_data is None), exactly as in the no-load Dask path. Results are identical; only the cross-task I/O sharing optimisation is skipped. A warning is logged if aloadstage is present.
Functions
|
Arm (or re-arm) a daemon watchdog that hard-exits this process after |
|
Execute a GraphVIPER map/reduce graph with an MPI manager-worker pool. |
Module Contents
- force_exit_after(seconds, note='')[source]
Arm (or re-arm) a daemon watchdog that hard-exits this process after
seconds– a guard against the mpi4py.futures / MPI shutdown hanging after all work is done.Rationale. In the
python -m mpi4py.futuresmanager-worker model the global stop handshake andMPI_Finalizerun at interpreter exit. A single worker rank wedged during its own shutdown (typically a non-daemon thread stuck in an uninterruptible filesystem syscall) blocks the effectively-collective finalize on every rank, idling the whole allocation until walltime. Anatexithook CANNOT guard against this: CPython joins non-daemon threads (includingconcurrent.futurespools) before running atexit callbacks, which is exactly where the wedge occurs. The only robust guard is a daemon thread armed while the interpreter is still healthy – this function.Call it once the application’s results are safely persisted (or use the
teardown_force_exit_secondsoption ofprocesses_with_mpi()). Calling again re-arms the countdown; the previous timer is cancelled.secondsofNone/0just cancels any armed watchdog.The forced exit uses
os._exit(0)(no cleanup, noMPI_Finalize); the MPI launcher then tears down the remaining ranks. The launcher may log the exit as unclean – harmless next to an allocation idling for hours.Returns the armed
threading.Timer(orNoneif cancelling).
- processes_with_mpi(viper_graph, cluster_setup=None)[source]
Execute a GraphVIPER map/reduce graph with an MPI manager-worker pool.
Drop-in replacement for
generate_dask_workflow+dask.compute: returns exactly whatdask.compute(generate_dask_workflow(viper_graph))[0]would – the reduced result when areducestage is present, otherwise the list of per-map-task results (in input order).Must be launched in the static mpi4py.futures manager-worker model (
... python -m mpi4py.futures <script>); see the module docstring. Only the manager rank (rank 0) ever reaches this function.- Parameters:
viper_graph (dict) – Graph from
graphviper.graph_tools.map.map()and optionallygraphviper.graph_tools.reduce.reduce().cluster_setup (dict, optional) –
MPI execution options:
max_workers(int or None) – cap the pool size;None(default) uses every available worker rank.chunksize(int) –MPIPoolExecutor.mapchunk size;1(default) gives the best dynamic load balancing for long, uneven node tasks (per-task dispatch overhead is negligible next to ~100 s tasks).reduce_in_pool(bool) – ifTruerun the reduce tree on the worker pool; defaultFalsereduces on the manager (the right choice when map results are small metadata, as in imaging).use_cloudpickle(bool) – reconfigure mpi4py to pickle with cloudpickle so closure node tasks serialise; defaultTrue.progress_every(int or None) – if set, log a progress line every this many completed map tasks.teardown_force_exit_seconds(float or None) – if set, armforce_exit_after()with this grace period when the compute returns, guarding against the mpi4py.futures / MPI_Finalize shutdown hang at interpreter exit (one wedged worker rank can idle the whole allocation until walltime). DefaultNone(off): only enable in run-one-graph-then-exit programs (batch jobs); a long-lived application that keeps working after this call would be killed mid-flight unless it re-arms or cancels viaforce_exit_after(None). A subsequentprocesses_with_mpicall re-arms the countdown.
- Returns:
The reduced result, or (no reduce stage) the list of map-task results.
- Return type:
object