docs/source/tutorial/tutorial.parallelism.rst
6fca822a
 Parallelism - using multiple cores
 ----------------------------------
 
 Often you will want to evaluate the function on some remote computing
 resources. ``adaptive`` works out of the box with any framework that
 implements a `PEP 3148 <https://www.python.org/dev/peps/pep-3148/>`__
 compliant executor that returns `concurrent.futures.Future` objects.
 
 `concurrent.futures`
 ~~~~~~~~~~~~~~~~~~~~
 
 On Unix-like systems by default `adaptive.Runner` creates a
 `~concurrent.futures.ProcessPoolExecutor`, but you can also pass
 one explicitly e.g. to limit the number of workers:
 
 .. code:: python
 
     from concurrent.futures import ProcessPoolExecutor
 
     executor = ProcessPoolExecutor(max_workers=4)
 
     learner = adaptive.Learner1D(f, bounds=(-1, 1))
     runner = adaptive.Runner(learner, executor=executor, goal=lambda l: l.loss() < 0.05)
     runner.live_info()
     runner.live_plot(update_interval=0.1)
 
 `ipyparallel.Client`
 ~~~~~~~~~~~~~~~~~~~~
 
 .. code:: python
 
     import ipyparallel
 
     client = ipyparallel.Client()  # You will need to start an `ipcluster` to make this work
 
     learner = adaptive.Learner1D(f, bounds=(-1, 1))
     runner = adaptive.Runner(learner, executor=client, goal=lambda l: l.loss() < 0.01)
     runner.live_info()
     runner.live_plot()
 
 `distributed.Client`
 ~~~~~~~~~~~~~~~~~~~~
 
 On Windows by default `adaptive.Runner` uses a `distributed.Client`.
 
 .. code:: python
 
     import distributed
 
     client = distributed.Client()
 
     learner = adaptive.Learner1D(f, bounds=(-1, 1))
     runner = adaptive.Runner(learner, executor=client, goal=lambda l: l.loss() < 0.01)
     runner.live_info()
     runner.live_plot(update_interval=0.1)
bb319d98
 
 `mpi4py.futures.MPIPoolExecutor`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This makes sense if you want to run a ``Learner`` on a cluster non-interactively using a job script.
 
 For example, you create the following file called ``run_learner.py``:
 
 .. code:: python
 
3bac6c6b
     from mpi4py.futures import MPIPoolExecutor
bb319d98
 
d2985fe7
     # use the idiom below, see the warning at
     # https://mpi4py.readthedocs.io/en/stable/mpi4py.futures.html#mpipoolexecutor
     if __name__ == "__main__":
9d921fd0
 
         learner = adaptive.Learner1D(f, bounds=(-1, 1))
bb319d98
 
9d921fd0
         # load the data
         learner.load(fname)
bb319d98
 
9d921fd0
         # run until `goal` is reached with an `MPIPoolExecutor`
         runner = adaptive.Runner(
             learner,
             executor=MPIPoolExecutor(),
             shutdown_executor=True,
             goal=lambda l: l.loss() < 0.01,
         )
bb319d98
 
9d921fd0
         # periodically save the data (in case the job dies)
         runner.start_periodic_saving(dict(fname=fname), interval=600)
bb319d98
 
9d921fd0
         # block until runner goal reached
         runner.ioloop.run_until_complete(runner.task)
bb319d98
 
9d921fd0
         # save one final time before exiting
         learner.save(fname)
961b0a9a
 
bb319d98
 
 On your laptop/desktop you can run this script like:
 
45e28867
 .. code:: bash
bb319d98
 
     export MPI4PY_MAX_WORKERS=15
     mpiexec -n 1 python run_learner.py
 
033004c2
 Or you can pass ``max_workers=15`` programmatically when creating the `MPIPoolExecutor` instance.
bb319d98
 
 Inside the job script using a job queuing system use:
 
45e28867
 .. code:: bash
bb319d98
 
     mpiexec -n 16 python -m mpi4py.futures run_learner.py
 
 How you call MPI might depend on your specific queuing system, with SLURM for example it's:
 
45e28867
 .. code:: bash
bb319d98
 
     #!/bin/bash
     #SBATCH --job-name adaptive-example
     #SBATCH --ntasks 100
 
     srun -n $SLURM_NTASKS --mpi=pmi2 ~/miniconda3/envs/py37_min/bin/python -m mpi4py.futures run_learner.py
f404f16c
 
 `loky.get_reusable_executor`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
89e8649c
 This executor is basically a powered-up version of `~concurrent.futures.ProcessPoolExecutor`, check its `documentation <https://loky.readthedocs.io/>`_.
 Among other things, it allows to *reuse* the executor and uses ``cloudpickle`` for serialization.
 This means you can even learn closures, lambdas, or other functions that are not picklable with `pickle`.
f404f16c
 
 .. code:: python
 
     from loky import get_reusable_executor
     ex = get_reusable_executor()
 
     f = lambda x: x
     learner = adaptive.Learner1D(f, bounds=(-1, 1))
 
     runner = adaptive.Runner(learner, goal=lambda l: l.loss() < 0.01, executor=ex)
     runner.live_info()