Skip to content

Commit 1bb8618

Browse files
authored
Default to loky executor on all platforms (#497)
Python 3.14 changed the default multiprocessing start method on Linux from "fork" to "forkserver" (python/cpython#84559), so workers of the stdlib ProcessPoolExecutor now re-import __main__ instead of inheriting it. Functions defined interactively (notebooks, executed doc pages) are pickled by reference and fail to unpickle in the workers with e.g.: AttributeError: module '__main__' has no attribute 'sphere' loky's reusable executor serializes functions by value via cloudpickle and was already the default on macOS and Windows (and loky is already a required dependency), so use it on Linux too. This also removes the now-dead lambda-pickling check in AsyncRunner that only applied when the default executor was ProcessPoolExecutor.
1 parent 7fcc606 commit 1bb8618

2 files changed

Lines changed: 15 additions & 35 deletions

File tree

adaptive/runner.py

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import functools
77
import inspect
88
import itertools
9-
import pickle
10-
import platform
119
import time
1210
import traceback
1311
import warnings
@@ -44,16 +42,16 @@
4442

4543

4644
# -- Runner definitions
47-
if platform.system() == "Linux":
48-
_default_executor = concurrent.ProcessPoolExecutor # type: ignore[misc]
49-
else:
50-
# On Windows and MacOS functions, the __main__ module must be
51-
# importable by worker subprocesses. This means that
52-
# ProcessPoolExecutor will not work in the interactive interpreter.
53-
# On Linux the whole process is forked, so the issue does not appear.
54-
# See https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor
55-
# and https://github.com/python-adaptive/adaptive/issues/301
56-
_default_executor = loky.get_reusable_executor # type: ignore[misc]
45+
# Functions submitted to a stdlib ProcessPoolExecutor must be importable from
46+
# `__main__` by the worker, which fails for functions defined interactively
47+
# (notebooks, doc pages). This used to work on Linux because workers were
48+
# forked, but Python 3.14 changed the default start method on Linux to
49+
# "forkserver", which re-imports `__main__` like Windows/macOS always did.
50+
# loky serializes functions by value with cloudpickle, so it works everywhere.
51+
# See https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor,
52+
# https://github.com/python-adaptive/adaptive/issues/301,
53+
# and https://github.com/python/cpython/issues/84559
54+
_default_executor = loky.get_reusable_executor
5755

5856

5957
class BaseRunner(metaclass=abc.ABCMeta):
@@ -86,8 +84,7 @@ class BaseRunner(metaclass=abc.ABCMeta):
8684
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
8785
`loky.get_reusable_executor`, optional
8886
The executor in which to evaluate the function to be learned.
89-
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
90-
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
87+
If not provided, a new `loky.get_reusable_executor` is used.
9188
ntasks : int, optional
9289
The number of concurrent function evaluations. Defaults to the number
9390
of cores available in `executor`.
@@ -373,8 +370,7 @@ class BlockingRunner(BaseRunner):
373370
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
374371
`loky.get_reusable_executor`, optional
375372
The executor in which to evaluate the function to be learned.
376-
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
377-
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
373+
If not provided, a new `loky.get_reusable_executor` is used.
378374
ntasks : int, optional
379375
The number of concurrent function evaluations. Defaults to the number
380376
of cores available in `executor`.
@@ -520,8 +516,7 @@ class AsyncRunner(BaseRunner):
520516
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
521517
`loky.get_reusable_executor`, optional
522518
The executor in which to evaluate the function to be learned.
523-
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
524-
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
519+
If not provided, a new `loky.get_reusable_executor` is used.
525520
ntasks : int, optional
526521
The number of concurrent function evaluations. Defaults to the number
527522
of cores available in `executor`.
@@ -595,22 +590,6 @@ def __init__(
595590
retries: int = 0,
596591
raise_if_retries_exceeded: bool = True,
597592
) -> None:
598-
if (
599-
executor is None
600-
and _default_executor is concurrent.ProcessPoolExecutor
601-
and not inspect.iscoroutinefunction(learner.function)
602-
):
603-
try:
604-
pickle.dumps(learner.function)
605-
except pickle.PicklingError as e:
606-
raise ValueError(
607-
"`learner.function` cannot be pickled (is it a lamdba function?)"
608-
" and therefore does not work with the default executor."
609-
" Either make sure the function is pickleble or use an executor"
610-
" that might work with 'hard to pickle'-functions"
611-
" , e.g. `ipyparallel` with `dill`."
612-
) from e
613-
614593
super().__init__(
615594
learner,
616595
goal=goal,

docs/source/tutorial/tutorial.parallelism.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Often you will want to evaluate the function on some remote computing resources.
1616

1717
## `concurrent.futures`
1818

19-
On Unix-like systems by default {class}`adaptive.Runner` creates a {class}`~concurrent.futures.ProcessPoolExecutor`, but you can also pass one explicitly e.g. to limit the number of workers:
19+
By default {class}`adaptive.Runner` creates a `loky.get_reusable_executor`, which serializes functions by value so it also works with functions defined interactively (e.g. in a notebook).
20+
You can also pass a {class}`~concurrent.futures.ProcessPoolExecutor` explicitly, e.g. to limit the number of workers, but then the function must be importable from `__main__` by the worker processes:
2021

2122
```python
2223
from concurrent.futures import ProcessPoolExecutor

0 commit comments

Comments
 (0)