Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 🗞️ Changelog

## [Unreleased]

**New features:**

- Use `InterpreterPoolExecutor` as default executor on Python 3.14+ [\#488](https://github.com/python-adaptive/adaptive/pull/488). See also [Python documentation](https://docs.python.org/3.14/whatsnew/3.14.html#pep-734-multiple-interpreters-in-the-standard-library).

## [v1.4.0](https://github.com/python-adaptive/adaptive/tree/v1.3.0) (2025-05-13)

[Full Changelog](https://github.com/python-adaptive/adaptive/compare/v1.3.2...v.1.4.0)
Expand Down
42 changes: 30 additions & 12 deletions adaptive/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Workaround described in https://github.com/agronholm/typeguard/issues/456

import concurrent.futures as concurrent
from typing import TypeAlias
import sys
from typing import TYPE_CHECKING, TypeAlias

import distributed
import ipyparallel
Expand All @@ -11,14 +12,31 @@

from adaptive.utils import SequentialExecutor

ExecutorTypes: TypeAlias = (
concurrent.ProcessPoolExecutor
| concurrent.ThreadPoolExecutor
| SequentialExecutor
| loky.reusable_executor._ReusablePoolExecutor
| distributed.Client
| distributed.cfexecutor.ClientExecutor
| mpi4py.futures.MPIPoolExecutor
| ipyparallel.Client
| ipyparallel.client.view.ViewExecutor
)
# For Python 3.14+, include InterpreterPoolExecutor in the type alias
if sys.version_info >= (3, 14):
if TYPE_CHECKING:
# Type checkers will see this when checking Python 3.14+ code
ExecutorTypes: TypeAlias = (
concurrent.ProcessPoolExecutor
| concurrent.ThreadPoolExecutor
| concurrent.InterpreterPoolExecutor # type: ignore[attr-defined]
| SequentialExecutor
| loky.reusable_executor._ReusablePoolExecutor
| distributed.Client
| distributed.cfexecutor.ClientExecutor
| mpi4py.futures.MPIPoolExecutor
| ipyparallel.Client
| ipyparallel.client.view.ViewExecutor
)
else:
ExecutorTypes: TypeAlias = (
concurrent.ProcessPoolExecutor
| concurrent.ThreadPoolExecutor
| SequentialExecutor
| loky.reusable_executor._ReusablePoolExecutor
| distributed.Client
| distributed.cfexecutor.ClientExecutor
| mpi4py.futures.MPIPoolExecutor
| ipyparallel.Client
| ipyparallel.client.view.ViewExecutor
)
29 changes: 22 additions & 7 deletions adaptive/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import itertools
import pickle
import platform
import sys
import time
import traceback
import warnings
Expand Down Expand Up @@ -44,7 +45,13 @@


# -- Runner definitions
if platform.system() == "Linux":
_has_interpreter_pool = sys.version_info >= (3, 14) and hasattr(
concurrent, "InterpreterPoolExecutor"
)

if _has_interpreter_pool:
_default_executor = concurrent.InterpreterPoolExecutor # type: ignore[misc,attr-defined]
elif platform.system() == "Linux":
_default_executor = concurrent.ProcessPoolExecutor # type: ignore[misc]
else:
# On Windows and MacOS functions, the __main__ module must be
Expand Down Expand Up @@ -86,8 +93,10 @@ class BaseRunner(metaclass=abc.ABCMeta):
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
`loky.get_reusable_executor`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
If not provided, a new `~concurrent.futures.InterpreterPoolExecutor`
on Python 3.14+, a `~concurrent.futures.ProcessPoolExecutor` on
Python < 3.14 on Linux, and a `loky.get_reusable_executor` on
Python < 3.14 on MacOS and Windows.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in `executor`.
Expand Down Expand Up @@ -373,8 +382,10 @@ class BlockingRunner(BaseRunner):
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
`loky.get_reusable_executor`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
If not provided, a new `~concurrent.futures.InterpreterPoolExecutor`
on Python 3.14+, a `~concurrent.futures.ProcessPoolExecutor` on
Python < 3.14 on Linux, and a `loky.get_reusable_executor` on
Python < 3.14 on MacOS and Windows.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in `executor`.
Expand Down Expand Up @@ -520,8 +531,10 @@ class AsyncRunner(BaseRunner):
`mpi4py.futures.MPIPoolExecutor`, `ipyparallel.Client` or\
`loky.get_reusable_executor`, optional
The executor in which to evaluate the function to be learned.
If not provided, a new `~concurrent.futures.ProcessPoolExecutor` on
Linux, and a `loky.get_reusable_executor` on MacOS and Windows.
If not provided, a new `~concurrent.futures.InterpreterPoolExecutor`
on Python 3.14+, a new `~concurrent.futures.ProcessPoolExecutor` on
Python < 3.14 on Linux, and a `loky.get_reusable_executor` on
Python < 3.14 on MacOS and Windows.
ntasks : int, optional
The number of concurrent function evaluations. Defaults to the number
of cores available in `executor`.
Expand Down Expand Up @@ -1023,6 +1036,8 @@ def _get_ncores(
import mpi4py.futures
if with_ipyparallel and isinstance(ex, ipyparallel.client.view.ViewExecutor):
return len(ex.view)
elif _has_interpreter_pool and isinstance(ex, concurrent.InterpreterPoolExecutor): # type: ignore[attr-defined]
return ex._max_workers # type: ignore[union-attr]
elif isinstance(ex, concurrent.ProcessPoolExecutor | concurrent.ThreadPoolExecutor):
return ex._max_workers # type: ignore[union-attr]
elif isinstance(ex, loky.reusable_executor._ReusablePoolExecutor):
Expand Down
23 changes: 23 additions & 0 deletions adaptive/tests/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,26 @@ def counting_ask(self, n, tell_pending=True):
finally:
# Restore original method
Learner1D.ask = original_ask


def test_interpreter_pool_executor_detection():
"""Test that InterpreterPoolExecutor is detected and used in Python 3.14+."""
import concurrent.futures as concurrent
import sys

from adaptive.runner import _default_executor, _has_interpreter_pool

if sys.version_info >= (3, 14) and hasattr(concurrent, "InterpreterPoolExecutor"):
# On Python 3.14+, InterpreterPoolExecutor should be available and used
assert _has_interpreter_pool is True
assert _default_executor == concurrent.InterpreterPoolExecutor
else:
# On older Python versions, it should not be available
assert _has_interpreter_pool is False
# Should fall back to ProcessPoolExecutor on Linux or loky on others
if OPERATING_SYSTEM == "Linux":
assert _default_executor == concurrent.ProcessPoolExecutor
else:
import loky

assert _default_executor == loky.get_reusable_executor
8 changes: 7 additions & 1 deletion docs/source/tutorial/tutorial.parallelism.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ Often you will want to evaluate the function on some remote computing resources.

## `concurrent.futures`

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:
By default, {class}`adaptive.Runner` automatically selects the best executor based on your Python version and platform:

- **Python 3.14+**: Uses {class}`~concurrent.futures.InterpreterPoolExecutor` (provides better isolation and performance)
- **Python < 3.14 on Unix-like systems**: Uses {class}`~concurrent.futures.ProcessPoolExecutor`
- **Python < 3.14 on Windows/macOS**: Uses `loky.get_reusable_executor` (better compatibility in interactive environments)

You can also pass an executor explicitly, e.g. to limit the number of workers:

```python
from concurrent.futures import ProcessPoolExecutor
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

nox.options.default_venv_backend = "uv"

python = ["3.11", "3.12", "3.13"]
python = ["3.11", "3.12", "3.13", "3.14"]
num_cpus = os.cpu_count() or 1
xdist = ("-n", "auto") if num_cpus > 2 else ()

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"scipy",
Expand Down
Loading