cluster_tools 4.2.5__tar.gz → 4.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/PKG-INFO +1 -1
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/__init__.py +7 -1
- cluster_tools-4.3.0/cluster_tools/_utils/cfut_options.py +108 -0
- cluster_tools-4.3.0/cluster_tools/_utils/output_wait_thread.py +127 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/string_.py +0 -4
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executor_protocol.py +1 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/batching.py +3 -2
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/dask.py +29 -25
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/multiprocessing_.py +29 -62
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/sequential.py +25 -26
- cluster_tools-4.3.0/cluster_tools/output_store.py +94 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/remote.py +20 -21
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/schedulers/cluster_executor.py +106 -89
- cluster_tools-4.3.0/cluster_tools/version.py +1 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools.egg-info/PKG-INFO +1 -1
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools.egg-info/SOURCES.txt +3 -1
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/pyproject.toml +1 -1
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_all.py +176 -16
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_slurm.py +34 -8
- cluster_tools-4.2.5/cluster_tools/_utils/file_wait_thread.py +0 -101
- cluster_tools-4.2.5/cluster_tools/version.py +0 -1
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/LICENSE +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/README.md +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/call.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/multiprocessing_logging_handler.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/pickling.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/reflection.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/tailf.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/_utils/warning.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/multiprocessing_pickle.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/executors/sequential_pickle.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/schedulers/__init__.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/schedulers/kube.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/schedulers/pbs.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools/schedulers/slurm.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools.egg-info/dependency_links.txt +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools.egg-info/requires.txt +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/cluster_tools.egg-info/top_level.txt +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/setup.cfg +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_batching.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_dask.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_deref_main.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_kubernetes.py +0 -0
- {cluster_tools-4.2.5 → cluster_tools-4.3.0}/tests/test_multiprocessing.py +0 -0
|
@@ -7,6 +7,10 @@ from cluster_tools.executors.multiprocessing_ import MultiprocessingExecutor
|
|
|
7
7
|
from cluster_tools.executors.multiprocessing_pickle import MultiprocessingPickleExecutor
|
|
8
8
|
from cluster_tools.executors.sequential import SequentialExecutor
|
|
9
9
|
from cluster_tools.executors.sequential_pickle import SequentialPickleExecutor
|
|
10
|
+
from cluster_tools.output_store import (
|
|
11
|
+
FileOutputStore, # noqa: F401 unused import
|
|
12
|
+
OutputStore, # noqa: F401 unused import
|
|
13
|
+
)
|
|
10
14
|
from cluster_tools.schedulers.cluster_executor import (
|
|
11
15
|
ClusterExecutor, # noqa: F401 `cluster_tools.schedulers.cluster_executor.ClusterExecutor` imported but unused;
|
|
12
16
|
RemoteOutOfMemoryException, # noqa: F401 `cluster_tools.schedulers.cluster_executor.ClusterExecutor` imported but unused;
|
|
@@ -108,7 +112,9 @@ def get_executor(environment: str, **kwargs: Any) -> "Executor":
|
|
|
108
112
|
return KubernetesExecutor(**kwargs)
|
|
109
113
|
elif environment == "dask":
|
|
110
114
|
if "client" in kwargs:
|
|
111
|
-
return DaskExecutor(
|
|
115
|
+
return DaskExecutor(
|
|
116
|
+
kwargs["client"], output_store=kwargs.get("output_store")
|
|
117
|
+
)
|
|
112
118
|
else:
|
|
113
119
|
return DaskExecutor.from_config(**kwargs)
|
|
114
120
|
elif environment == "multiprocessing":
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Handling of the `__cfut_options` kwarg and the `map_to_futures` getters, which
|
|
2
|
+
select where a job's output is persisted. Shared by all executors."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, TypedDict, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from typing_extensions import NotRequired, ParamSpec
|
|
11
|
+
|
|
12
|
+
from cluster_tools._utils import pickling
|
|
13
|
+
from cluster_tools.output_store import OutputStore
|
|
14
|
+
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
_P = ParamSpec("_P")
|
|
17
|
+
_S = TypeVar("_S")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# A job's output is persisted as the pickled tuple `(True, result)` in the executor's
|
|
21
|
+
# `OutputStore` (only in the success case, so that it can serve as a checkpoint).
|
|
22
|
+
# `output_key` selects where; for the default `FileOutputStore` it is a file path.
|
|
23
|
+
# `output_pickle_path` is a deprecated alias.
|
|
24
|
+
class CFutDict(TypedDict):
|
|
25
|
+
output_key: NotRequired[str]
|
|
26
|
+
output_pickle_path: NotRequired[str | os.PathLike]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_cfut_options(kwargs: dict[str, Any]) -> str | None:
|
|
30
|
+
"""Removes `__cfut_options` from kwargs and returns the output key, if any."""
|
|
31
|
+
if "__cfut_options" not in kwargs:
|
|
32
|
+
return None
|
|
33
|
+
options = cast(CFutDict, kwargs["__cfut_options"])
|
|
34
|
+
del kwargs["__cfut_options"]
|
|
35
|
+
output_key = options.get("output_key")
|
|
36
|
+
output_pickle_path = options.get("output_pickle_path")
|
|
37
|
+
if output_key is None and output_pickle_path is None:
|
|
38
|
+
raise ValueError("__cfut_options must contain output_key.")
|
|
39
|
+
if output_key is not None and output_pickle_path is not None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"__cfut_options must not contain both output_key and output_pickle_path."
|
|
42
|
+
)
|
|
43
|
+
if output_key is not None:
|
|
44
|
+
return output_key
|
|
45
|
+
warnings.warn(
|
|
46
|
+
"The output_pickle_path option of __cfut_options is deprecated, "
|
|
47
|
+
"use output_key instead.",
|
|
48
|
+
DeprecationWarning,
|
|
49
|
+
stacklevel=3,
|
|
50
|
+
)
|
|
51
|
+
return str(output_pickle_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_output_key_getter(
|
|
55
|
+
output_pickle_path_getter: Callable[[_S], os.PathLike] | None,
|
|
56
|
+
output_key_getter: Callable[[_S], str] | None,
|
|
57
|
+
) -> Callable[[_S], str] | None:
|
|
58
|
+
"""Merges the `map_to_futures` getters into a single key getter."""
|
|
59
|
+
if output_key_getter is not None and output_pickle_path_getter is not None:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"Specify either output_key_getter or output_pickle_path_getter, not both."
|
|
62
|
+
)
|
|
63
|
+
if output_key_getter is not None:
|
|
64
|
+
return output_key_getter
|
|
65
|
+
if output_pickle_path_getter is not None:
|
|
66
|
+
warnings.warn(
|
|
67
|
+
"The output_pickle_path_getter argument of map_to_futures is deprecated, "
|
|
68
|
+
"use output_key_getter instead.",
|
|
69
|
+
DeprecationWarning,
|
|
70
|
+
stacklevel=3,
|
|
71
|
+
)
|
|
72
|
+
getter = output_pickle_path_getter
|
|
73
|
+
return lambda arg: str(getter(arg))
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cfut_options_kwargs(
|
|
78
|
+
arg: _S,
|
|
79
|
+
output_pickle_path_getter: Callable[[_S], os.PathLike] | None,
|
|
80
|
+
output_key_getter: Callable[[_S], str] | None,
|
|
81
|
+
) -> dict[str, CFutDict]:
|
|
82
|
+
"""Builds the `__cfut_options` kwarg for `submit` from the `map_to_futures` getters."""
|
|
83
|
+
key_getter = resolve_output_key_getter(output_pickle_path_getter, output_key_getter)
|
|
84
|
+
if key_getter is None:
|
|
85
|
+
return {}
|
|
86
|
+
return {"__cfut_options": {"output_key": key_getter(arg)}}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def execute_and_persist(
|
|
90
|
+
output_store: OutputStore,
|
|
91
|
+
output_key: str,
|
|
92
|
+
fn: Callable[_P, _T],
|
|
93
|
+
*args: _P.args,
|
|
94
|
+
**kwargs: _P.kwargs,
|
|
95
|
+
) -> _T:
|
|
96
|
+
"""Runs `fn` in the job and stores its result. Used by the in-process executors;
|
|
97
|
+
the cluster executors do the same in `cluster_tools.remote`."""
|
|
98
|
+
try:
|
|
99
|
+
result = fn(*args, **kwargs)
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logging.warning(f"Job computation failed with:\n{exc.__repr__()}")
|
|
102
|
+
raise exc
|
|
103
|
+
else:
|
|
104
|
+
# Only store the result in the success case, since the output
|
|
105
|
+
# is used as a checkpoint. The cluster executor also stores failures,
|
|
106
|
+
# since it needs them to transport the exception back.
|
|
107
|
+
output_store.write(output_key, pickling.dumps((True, result)), success=True)
|
|
108
|
+
return result
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from cluster_tools.output_store import OutputStore
|
|
9
|
+
from cluster_tools.schedulers.cluster_executor import ClusterExecutor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OutputWaitThread(threading.Thread):
|
|
13
|
+
"""A thread that polls the executor's output store waiting for a set of
|
|
14
|
+
output keys to be written. When a key is written, it invokes a callback.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
MAX_RETRY = 30
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
callback: Callable[[str, bool], None],
|
|
22
|
+
executor: "ClusterExecutor",
|
|
23
|
+
interval: int = 2,
|
|
24
|
+
):
|
|
25
|
+
"""The callable ``callback`` will be invoked with the value
|
|
26
|
+
associated with each key that is written.
|
|
27
|
+
``interval`` specifies the polling rate.
|
|
28
|
+
"""
|
|
29
|
+
threading.Thread.__init__(self)
|
|
30
|
+
self.callback = callback
|
|
31
|
+
self.interval = interval
|
|
32
|
+
self.waiting: dict[str, str] = {}
|
|
33
|
+
self.retryMap: dict[str, int] = {}
|
|
34
|
+
self.lock = threading.Lock()
|
|
35
|
+
self.shutdown = False
|
|
36
|
+
self.executor = executor
|
|
37
|
+
|
|
38
|
+
def stop(self) -> None:
|
|
39
|
+
"""Stop the thread soon."""
|
|
40
|
+
with self.lock:
|
|
41
|
+
self.shutdown = True
|
|
42
|
+
|
|
43
|
+
def waitFor(self, key: str, value: str) -> None:
|
|
44
|
+
"""Adds a new output key (and its associated callback value) to
|
|
45
|
+
the set of keys being waited upon.
|
|
46
|
+
"""
|
|
47
|
+
with self.lock:
|
|
48
|
+
self.waiting[key] = value
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _poll_single(store: "OutputStore", key: str) -> set[str]:
|
|
52
|
+
try:
|
|
53
|
+
return store.poll([key])
|
|
54
|
+
except Exception:
|
|
55
|
+
logging.warning(
|
|
56
|
+
f"Polling the output store for {key} failed.", exc_info=True
|
|
57
|
+
)
|
|
58
|
+
return set()
|
|
59
|
+
|
|
60
|
+
def run(self) -> None:
|
|
61
|
+
def handle_completed_job(job_id: str, key: str, failed_early: bool) -> None:
|
|
62
|
+
# The callback must not raise, since that would stop this thread and
|
|
63
|
+
# leave all other jobs pending.
|
|
64
|
+
try:
|
|
65
|
+
self.callback(job_id, failed_early)
|
|
66
|
+
except Exception:
|
|
67
|
+
logging.exception(f"Handling the completion of job {job_id} failed.")
|
|
68
|
+
del self.waiting[key]
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
with self.lock:
|
|
72
|
+
if self.shutdown:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
pending_tasks = self.executor.get_pending_tasks()
|
|
76
|
+
store = self.executor.output_store
|
|
77
|
+
|
|
78
|
+
# Don't check status of pending tasks, since this
|
|
79
|
+
# can vastly slow down the polling.
|
|
80
|
+
keys_to_check = [
|
|
81
|
+
key
|
|
82
|
+
for key, job_id in self.waiting.items()
|
|
83
|
+
if job_id not in pending_tasks
|
|
84
|
+
]
|
|
85
|
+
# Check for outputs in one batch as a fast indicator for job completion.
|
|
86
|
+
# A store can be remote, so a failing poll must not kill this thread;
|
|
87
|
+
# it is retried on the next iteration.
|
|
88
|
+
try:
|
|
89
|
+
written_keys = store.poll(keys_to_check)
|
|
90
|
+
except Exception:
|
|
91
|
+
logging.warning(
|
|
92
|
+
"Polling the output store failed, retrying.", exc_info=True
|
|
93
|
+
)
|
|
94
|
+
written_keys = set()
|
|
95
|
+
|
|
96
|
+
for key in keys_to_check:
|
|
97
|
+
job_id = self.waiting[key]
|
|
98
|
+
|
|
99
|
+
if key in written_keys:
|
|
100
|
+
handle_completed_job(job_id, key, False)
|
|
101
|
+
elif self.executor is not None:
|
|
102
|
+
status = self.executor.check_job_state(job_id)
|
|
103
|
+
|
|
104
|
+
# We have to re-check for the output since this could be written in the mean time
|
|
105
|
+
if key in self._poll_single(store, key):
|
|
106
|
+
handle_completed_job(job_id, key, False)
|
|
107
|
+
else:
|
|
108
|
+
if status == "completed":
|
|
109
|
+
self.retryMap[key] = self.retryMap.get(key, 0)
|
|
110
|
+
self.retryMap[key] += 1
|
|
111
|
+
|
|
112
|
+
if self.retryMap[key] <= OutputWaitThread.MAX_RETRY:
|
|
113
|
+
# Retry by looping again
|
|
114
|
+
logging.warning(
|
|
115
|
+
f"Job state is completed, but output {key} couldn't be found. Retrying {self.retryMap[key]}/{OutputWaitThread.MAX_RETRY}"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
logging.error(
|
|
119
|
+
f"Job state is completed, but output {key} couldn't be found."
|
|
120
|
+
)
|
|
121
|
+
handle_completed_job(job_id, key, True)
|
|
122
|
+
|
|
123
|
+
elif status == "failed":
|
|
124
|
+
handle_completed_job(job_id, key, True)
|
|
125
|
+
elif status == "ignore":
|
|
126
|
+
pass
|
|
127
|
+
time.sleep(self.interval)
|
|
@@ -18,7 +18,3 @@ def random_string(
|
|
|
18
18
|
length: int = 32, chars: str = (string.ascii_letters + string.digits)
|
|
19
19
|
) -> str:
|
|
20
20
|
return "".join(random_generator.choice(chars) for i in range(length))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def with_preliminary_postfix(name: str) -> str:
|
|
24
|
-
return f"{name}.preliminary"
|
|
@@ -31,6 +31,7 @@ class Executor(Protocol, AbstractContextManager["Executor"]):
|
|
|
31
31
|
fn: Callable[[_S], _T],
|
|
32
32
|
args: Iterable[_S],
|
|
33
33
|
output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
|
|
34
|
+
output_key_getter: Callable[[_S], str] | None = None,
|
|
34
35
|
) -> list[Future[_T]]: ...
|
|
35
36
|
|
|
36
37
|
def map(
|
|
@@ -111,10 +111,11 @@ class BatchingExecutor:
|
|
|
111
111
|
fn: Callable[[_S], _T],
|
|
112
112
|
args: Iterable[_S],
|
|
113
113
|
output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
|
|
114
|
+
output_key_getter: Callable[[_S], str] | None = None,
|
|
114
115
|
) -> list[Future[_T]]:
|
|
115
|
-
if output_pickle_path_getter is not None:
|
|
116
|
+
if output_key_getter is not None or output_pickle_path_getter is not None:
|
|
116
117
|
raise NotImplementedError(
|
|
117
|
-
"BatchingExecutor does not support output_pickle_path_getter"
|
|
118
|
+
"BatchingExecutor does not support output_key_getter or output_pickle_path_getter"
|
|
118
119
|
)
|
|
119
120
|
|
|
120
121
|
items = list(args)
|
|
@@ -7,7 +7,6 @@ from concurrent import futures
|
|
|
7
7
|
from concurrent.futures import Future
|
|
8
8
|
from functools import partial
|
|
9
9
|
from multiprocessing import Queue, get_context
|
|
10
|
-
from pathlib import Path
|
|
11
10
|
from typing import (
|
|
12
11
|
TYPE_CHECKING,
|
|
13
12
|
Any,
|
|
@@ -19,8 +18,13 @@ from weakref import ReferenceType, ref
|
|
|
19
18
|
|
|
20
19
|
from typing_extensions import ParamSpec
|
|
21
20
|
|
|
21
|
+
from cluster_tools._utils.cfut_options import (
|
|
22
|
+
cfut_options_kwargs,
|
|
23
|
+
execute_and_persist,
|
|
24
|
+
parse_cfut_options,
|
|
25
|
+
)
|
|
22
26
|
from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
|
|
23
|
-
from cluster_tools.
|
|
27
|
+
from cluster_tools.output_store import FileOutputStore, OutputStore
|
|
24
28
|
|
|
25
29
|
if TYPE_CHECKING:
|
|
26
30
|
from distributed import Client
|
|
@@ -108,7 +112,10 @@ class DaskExecutor(futures.Executor):
|
|
|
108
112
|
is_shutting_down = False
|
|
109
113
|
|
|
110
114
|
def __init__(
|
|
111
|
-
self,
|
|
115
|
+
self,
|
|
116
|
+
client: "Client",
|
|
117
|
+
job_resources: dict[str, Any] | None = None,
|
|
118
|
+
output_store: OutputStore | None = None,
|
|
112
119
|
) -> None:
|
|
113
120
|
try:
|
|
114
121
|
import distributed # noqa: F401 unused import
|
|
@@ -127,6 +134,7 @@ class DaskExecutor(futures.Executor):
|
|
|
127
134
|
self.client = client
|
|
128
135
|
self.pending_futures = set()
|
|
129
136
|
self.job_resources = job_resources
|
|
137
|
+
self.output_store = FileOutputStore() if output_store is None else output_store
|
|
130
138
|
|
|
131
139
|
if self.job_resources is not None:
|
|
132
140
|
# `mem` needs to be a number for dask, so we need to parse it
|
|
@@ -151,6 +159,7 @@ class DaskExecutor(futures.Executor):
|
|
|
151
159
|
def from_config(
|
|
152
160
|
cls,
|
|
153
161
|
job_resources: dict[str, str],
|
|
162
|
+
output_store: OutputStore | None = None,
|
|
154
163
|
**_kwargs: Any,
|
|
155
164
|
) -> "DaskExecutor":
|
|
156
165
|
from distributed import Client
|
|
@@ -161,7 +170,7 @@ class DaskExecutor(futures.Executor):
|
|
|
161
170
|
address = os.environ.get("DASK_ADDRESS", None)
|
|
162
171
|
|
|
163
172
|
client = Client(address=address)
|
|
164
|
-
return cls(client, job_resources=job_resources)
|
|
173
|
+
return cls(client, job_resources=job_resources, output_store=output_store)
|
|
165
174
|
|
|
166
175
|
@classmethod
|
|
167
176
|
def as_completed(cls, futures: list[Future[_T]]) -> Iterator[Future[_T]]:
|
|
@@ -175,17 +184,14 @@ class DaskExecutor(futures.Executor):
|
|
|
175
184
|
*args: _P.args,
|
|
176
185
|
**kwargs: _P.kwargs,
|
|
177
186
|
) -> Future[_T]:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"output_pickle_path"
|
|
181
|
-
]
|
|
182
|
-
del kwargs["__cfut_options"]
|
|
183
|
-
|
|
187
|
+
output_key = parse_cfut_options(kwargs)
|
|
188
|
+
if output_key is not None:
|
|
184
189
|
__fn = cast(
|
|
185
190
|
Callable[_P, _T],
|
|
186
191
|
partial(
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
execute_and_persist,
|
|
193
|
+
self.output_store,
|
|
194
|
+
output_key,
|
|
189
195
|
__fn,
|
|
190
196
|
),
|
|
191
197
|
)
|
|
@@ -245,20 +251,18 @@ class DaskExecutor(futures.Executor):
|
|
|
245
251
|
_S
|
|
246
252
|
], # TODO change: allow more than one arg per call # noqa FIX002 Line contains TODO
|
|
247
253
|
output_pickle_path_getter: Callable[[_S], os.PathLike] | None = None,
|
|
254
|
+
output_key_getter: Callable[[_S], str] | None = None,
|
|
248
255
|
) -> list[Future[_T]]:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
]
|
|
260
|
-
else:
|
|
261
|
-
futs = [self.submit(fn, arg) for arg in args]
|
|
256
|
+
futs = [
|
|
257
|
+
self.submit( # type: ignore[call-arg]
|
|
258
|
+
fn,
|
|
259
|
+
arg,
|
|
260
|
+
**cfut_options_kwargs(
|
|
261
|
+
arg, output_pickle_path_getter, output_key_getter
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
for arg in args
|
|
265
|
+
]
|
|
262
266
|
|
|
263
267
|
return futs
|
|
264
268
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import multiprocessing
|
|
3
2
|
import os
|
|
4
3
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -6,39 +5,38 @@ from concurrent import futures
|
|
|
6
5
|
from concurrent.futures import Future, ProcessPoolExecutor
|
|
7
6
|
from functools import partial
|
|
8
7
|
from multiprocessing.context import BaseContext
|
|
9
|
-
from pathlib import Path
|
|
10
8
|
from typing import (
|
|
11
9
|
Any,
|
|
12
|
-
TypedDict,
|
|
13
10
|
TypeVar,
|
|
14
11
|
cast,
|
|
15
12
|
)
|
|
16
13
|
|
|
17
14
|
from typing_extensions import ParamSpec
|
|
18
15
|
|
|
19
|
-
from cluster_tools._utils import
|
|
16
|
+
from cluster_tools._utils.cfut_options import (
|
|
17
|
+
cfut_options_kwargs,
|
|
18
|
+
execute_and_persist,
|
|
19
|
+
parse_cfut_options,
|
|
20
|
+
)
|
|
20
21
|
from cluster_tools._utils.multiprocessing_logging_handler import (
|
|
21
22
|
_MultiprocessingLoggingHandlerPool,
|
|
22
23
|
)
|
|
23
24
|
from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
|
|
24
|
-
|
|
25
|
-
# The module name includes a _-suffix to avoid name clashes with the standard library multiprocessing module.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class CFutDict(TypedDict):
|
|
29
|
-
output_pickle_path: str
|
|
30
|
-
|
|
25
|
+
from cluster_tools.output_store import FileOutputStore, OutputStore
|
|
31
26
|
|
|
32
27
|
_T = TypeVar("_T")
|
|
33
28
|
_P = ParamSpec("_P")
|
|
34
29
|
_S = TypeVar("_S")
|
|
35
30
|
|
|
36
31
|
|
|
32
|
+
# The module name includes a _-suffix to avoid name clashes with the standard library multiprocessing module.
|
|
33
|
+
|
|
34
|
+
|
|
37
35
|
class MultiprocessingExecutor(ProcessPoolExecutor):
|
|
38
36
|
"""
|
|
39
37
|
Wraps the ProcessPoolExecutor to add various features:
|
|
40
38
|
- map_to_futures method
|
|
41
|
-
- pickling of job's output (see
|
|
39
|
+
- pickling of job's output (see output_key_getter, output_store and CFutDict)
|
|
42
40
|
"""
|
|
43
41
|
|
|
44
42
|
_mp_context: BaseContext
|
|
@@ -51,8 +49,10 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
|
|
|
51
49
|
mp_context: BaseContext | None = None,
|
|
52
50
|
initializer: Callable | None = None,
|
|
53
51
|
initargs: tuple = (),
|
|
52
|
+
output_store: OutputStore | None = None,
|
|
54
53
|
**__kwargs: Any,
|
|
55
54
|
) -> None:
|
|
55
|
+
self.output_store = FileOutputStore() if output_store is None else output_store
|
|
56
56
|
if mp_context is None:
|
|
57
57
|
if start_method is not None:
|
|
58
58
|
mp_context = multiprocessing.get_context(start_method)
|
|
@@ -88,26 +88,21 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
|
|
|
88
88
|
*args: _P.args,
|
|
89
89
|
**kwargs: _P.kwargs,
|
|
90
90
|
) -> Future[_T]:
|
|
91
|
-
|
|
92
|
-
output_pickle_path = cast(CFutDict, kwargs["__cfut_options"])[
|
|
93
|
-
"output_pickle_path"
|
|
94
|
-
]
|
|
95
|
-
del kwargs["__cfut_options"]
|
|
96
|
-
else:
|
|
97
|
-
output_pickle_path = None
|
|
91
|
+
output_key = parse_cfut_options(kwargs)
|
|
98
92
|
|
|
99
|
-
# Depending on the start_method and
|
|
93
|
+
# Depending on the start_method and output_key, setup functions may need to be
|
|
100
94
|
# executed in the new process context, before the actual code is ran.
|
|
101
95
|
# These wrapper functions consume their arguments from *args, **kwargs and assume
|
|
102
96
|
# that the next and last argument will be another function that is then called.
|
|
103
97
|
# Eventually, the actually submitted function will be called.
|
|
104
98
|
|
|
105
|
-
if
|
|
99
|
+
if output_key is not None:
|
|
106
100
|
__fn = cast(
|
|
107
101
|
Callable[_P, _T],
|
|
108
102
|
partial(
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
execute_and_persist,
|
|
104
|
+
self.output_store,
|
|
105
|
+
output_key,
|
|
111
106
|
__fn,
|
|
112
107
|
),
|
|
113
108
|
)
|
|
@@ -153,51 +148,23 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
|
|
|
153
148
|
multiprocessing_logging_setup_fn()
|
|
154
149
|
return fn(*args, **kwargs)
|
|
155
150
|
|
|
156
|
-
@staticmethod
|
|
157
|
-
def _execute_and_persist_function(
|
|
158
|
-
output_pickle_path: Path,
|
|
159
|
-
fn: Callable[_P, _T],
|
|
160
|
-
*args: _P.args,
|
|
161
|
-
**kwargs: _P.kwargs,
|
|
162
|
-
) -> _T:
|
|
163
|
-
try:
|
|
164
|
-
result = fn(*args, **kwargs)
|
|
165
|
-
except Exception as exc:
|
|
166
|
-
logging.warning(f"Job computation failed with:\n{exc.__repr__()}")
|
|
167
|
-
raise exc
|
|
168
|
-
else:
|
|
169
|
-
# Only pickle the result in the success case, since the output
|
|
170
|
-
# is used as a checkpoint.
|
|
171
|
-
# Note that this behavior differs a bit from the cluster executor
|
|
172
|
-
# which will always serialize the output (even exceptions) to
|
|
173
|
-
# disk. However, the output will have a .preliminary prefix at first
|
|
174
|
-
# which is only removed in the success case so that a checkpoint at
|
|
175
|
-
# the desired target only exists if the job was successful.
|
|
176
|
-
with output_pickle_path.open("wb") as file:
|
|
177
|
-
pickling.dump((True, result), file)
|
|
178
|
-
return result
|
|
179
|
-
|
|
180
151
|
def map_to_futures(
|
|
181
152
|
self,
|
|
182
153
|
fn: Callable[[_S], _T],
|
|
183
154
|
args: Iterable[_S],
|
|
184
155
|
output_pickle_path_getter: Callable[[_S], os.PathLike] | None = None,
|
|
156
|
+
output_key_getter: Callable[[_S], str] | None = None,
|
|
185
157
|
) -> list[Future[_T]]:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
]
|
|
197
|
-
else:
|
|
198
|
-
futs = [self.submit(fn, arg) for arg in args]
|
|
199
|
-
|
|
200
|
-
return futs
|
|
158
|
+
return [
|
|
159
|
+
self.submit( # type: ignore[call-arg]
|
|
160
|
+
fn,
|
|
161
|
+
arg,
|
|
162
|
+
**cfut_options_kwargs(
|
|
163
|
+
arg, output_pickle_path_getter, output_key_getter
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
for arg in args
|
|
167
|
+
]
|
|
201
168
|
|
|
202
169
|
def forward_log(self, fut: Future[_T]) -> _T:
|
|
203
170
|
"""
|
|
@@ -2,13 +2,17 @@ import warnings
|
|
|
2
2
|
from collections.abc import Callable, Iterable, Iterator
|
|
3
3
|
from concurrent.futures import Executor, Future, as_completed
|
|
4
4
|
from os import PathLike
|
|
5
|
-
from
|
|
6
|
-
from typing import Any, TypeVar, cast
|
|
5
|
+
from typing import Any, TypeVar
|
|
7
6
|
|
|
8
7
|
from typing_extensions import ParamSpec
|
|
9
8
|
|
|
9
|
+
from cluster_tools._utils.cfut_options import (
|
|
10
|
+
cfut_options_kwargs,
|
|
11
|
+
execute_and_persist,
|
|
12
|
+
parse_cfut_options,
|
|
13
|
+
)
|
|
10
14
|
from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
|
|
11
|
-
from cluster_tools.
|
|
15
|
+
from cluster_tools.output_store import FileOutputStore, OutputStore
|
|
12
16
|
|
|
13
17
|
_T = TypeVar("_T")
|
|
14
18
|
_S = TypeVar("_S")
|
|
@@ -22,9 +26,10 @@ class SequentialExecutor(Executor):
|
|
|
22
26
|
|
|
23
27
|
def __init__(
|
|
24
28
|
self,
|
|
29
|
+
output_store: OutputStore | None = None,
|
|
25
30
|
**__kwargs: Any,
|
|
26
31
|
) -> None:
|
|
27
|
-
|
|
32
|
+
self.output_store = FileOutputStore() if output_store is None else output_store
|
|
28
33
|
|
|
29
34
|
def submit( # type: ignore[override]
|
|
30
35
|
self,
|
|
@@ -33,13 +38,11 @@ class SequentialExecutor(Executor):
|
|
|
33
38
|
**kwargs: _P.kwargs,
|
|
34
39
|
) -> Future[_T]:
|
|
35
40
|
fut: Future[_T] = Future()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
result = MultiprocessingExecutor._execute_and_persist_function(
|
|
42
|
-
Path(output_pickle_path),
|
|
41
|
+
output_key = parse_cfut_options(kwargs)
|
|
42
|
+
if output_key is not None:
|
|
43
|
+
result = execute_and_persist(
|
|
44
|
+
self.output_store,
|
|
45
|
+
output_key,
|
|
43
46
|
__fn,
|
|
44
47
|
*args,
|
|
45
48
|
**kwargs,
|
|
@@ -60,22 +63,18 @@ class SequentialExecutor(Executor):
|
|
|
60
63
|
fn: Callable[[_S], _T],
|
|
61
64
|
args: Iterable[_S],
|
|
62
65
|
output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
|
|
66
|
+
output_key_getter: Callable[[_S], str] | None = None,
|
|
63
67
|
) -> list[Future[_T]]:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
]
|
|
75
|
-
else:
|
|
76
|
-
futs = [self.submit(fn, arg) for arg in args]
|
|
77
|
-
|
|
78
|
-
return futs
|
|
68
|
+
return [
|
|
69
|
+
self.submit( # type: ignore[call-arg]
|
|
70
|
+
fn,
|
|
71
|
+
arg,
|
|
72
|
+
**cfut_options_kwargs(
|
|
73
|
+
arg, output_pickle_path_getter, output_key_getter
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
for arg in args
|
|
77
|
+
]
|
|
79
78
|
|
|
80
79
|
def map( # type: ignore[override]
|
|
81
80
|
self,
|