mpengine 0.4.1__tar.gz → 0.5.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.
- {mpengine-0.4.1/src/mpengine.egg-info → mpengine-0.5.0}/PKG-INFO +50 -4
- {mpengine-0.4.1 → mpengine-0.5.0}/README.md +49 -3
- {mpengine-0.4.1 → mpengine-0.5.0}/pyproject.toml +5 -1
- mpengine-0.5.0/src/mpengine/__init__.py +69 -0
- mpengine-0.5.0/src/mpengine/banner.py +22 -0
- mpengine-0.5.0/src/mpengine/engine.py +850 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/orchestrator.py +422 -307
- mpengine-0.5.0/src/mpengine/partition.py +93 -0
- {mpengine-0.4.1 → mpengine-0.5.0/src/mpengine.egg-info}/PKG-INFO +50 -4
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/SOURCES.txt +3 -1
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_perf.py +233 -255
- mpengine-0.5.0/tests/test_reliability.py +233 -0
- mpengine-0.5.0/tests/test_shared.py +153 -0
- mpengine-0.4.1/src/mpengine/__init__.py +0 -96
- mpengine-0.4.1/src/mpengine/banner.py +0 -34
- mpengine-0.4.1/src/mpengine/engine.py +0 -698
- mpengine-0.4.1/src/mpengine/partition.py +0 -122
- {mpengine-0.4.1 → mpengine-0.5.0}/LICENSE +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/setup.cfg +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/py.typed +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/spider.txt +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/worker_names.json +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/dependency_links.txt +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/requires.txt +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/top_level.txt +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_broadcast.py +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_batching.py +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_core.py +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_logging.py +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_orchestrator.py +0 -0
- {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_partition.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpengine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A small, general-purpose multiprocessing engine: dispatch any callable across processes with run manifests, per-worker logs, on-disk outputs and failure isolation
|
|
5
5
|
Author: Amandeep Singh
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,7 +46,7 @@ Every run produces three things:
|
|
|
46
46
|
|
|
47
47
|
| what | where |
|
|
48
48
|
|---|---|
|
|
49
|
-
|
|
|
49
|
+
| human-readable and versioned JSON manifests | `<base>/manifests/<run_id>.txt` and `.json` |
|
|
50
50
|
| each job's result, saved to disk | `<base>/outputs/<run_id>/<label>` |
|
|
51
51
|
| one log file per **worker process** | `<base>/logs/<run_id>/worker_<pid>.log` |
|
|
52
52
|
| a log of the run as a whole | `<base>/logs/<run_id>/run.log` |
|
|
@@ -133,6 +133,10 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
133
133
|
| `n_workers` | worker *process* count (not threads — see below); defaults to `os.cpu_count()`, clamped down to the number of jobs if there are fewer jobs than that |
|
|
134
134
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
135
135
|
| `show_progress` | live terminal display: one overall bar for the whole run, plus a live rate number per worker process. Ignored when `debug=True` |
|
|
136
|
+
| `retries`, `retry_delay` | retry transient callback failures, optionally pausing between attempts |
|
|
137
|
+
| `failure_policy` | `continue` (default), `raise` after all jobs, or `fail_fast` |
|
|
138
|
+
| `timeout` | wall-clock limit for the complete parallel dispatch |
|
|
139
|
+
| `cancel_token` | a thread-safe `CancellationToken` another thread can use to abort the run |
|
|
136
140
|
|
|
137
141
|
Develop with `debug=True`, then flip it off. Chasing a bug through a process
|
|
138
142
|
pool means reading a traceback re-raised from a worker that has already exited,
|
|
@@ -152,7 +156,7 @@ rather than quoting a remembered number.
|
|
|
152
156
|
| `broadcast` | a `dict` of values shipped once per *worker* instead of once per job, delivered to your function as keyword arguments | 200 jobs + 80 MB panel **7.97s → 0.55s** |
|
|
153
157
|
| `reuse_pool` | keep the pool (and its broadcast payload) alive between calls instead of rebuilding it. Off by default | 10 dispatches **9.86s → 0.02s** |
|
|
154
158
|
| `initializer`, `initargs` | run once per worker before its first job — and unlike the stdlib's, may be a closure | — |
|
|
155
|
-
| `max_tasks_per_child` | recycle a worker every N jobs, so a slow leak can't end the run (Python 3.11+) | — |
|
|
159
|
+
| `max_tasks_per_child` | recycle a worker every N user jobs, so a slow leak can't end the run; disables batching to keep the count exact (Python 3.11+) | — |
|
|
156
160
|
|
|
157
161
|
**BLAS threads** is the one that catches people out. numpy hands matrix work to
|
|
158
162
|
a native library that is itself multi-threaded, and each worker process loads
|
|
@@ -176,6 +180,45 @@ it is a different order of magnitude — 0.55s on the 80 MB case, level with
|
|
|
176
180
|
joblib's memmapped 0.58s. Call `shutdown_pools()` when you're done with a
|
|
177
181
|
reused pool, or let `atexit` do it.
|
|
178
182
|
|
|
183
|
+
For a large NumPy array, wrap it in `Shared` to copy it once into OS shared
|
|
184
|
+
memory and let every worker read the same physical bytes:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from mpengine import Shared
|
|
188
|
+
|
|
189
|
+
summary = run(score, param_sets, base_dir="runs",
|
|
190
|
+
broadcast={"panel": Shared(panel)})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Shared arrays are read-only in workers and must be C-contiguous with a
|
|
194
|
+
non-object dtype. Keep the original array alive while a reused pool needs it.
|
|
195
|
+
|
|
196
|
+
### Reliability controls
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from mpengine import CancellationToken, RunFailedError, run
|
|
200
|
+
|
|
201
|
+
token = CancellationToken()
|
|
202
|
+
try:
|
|
203
|
+
summary = run(
|
|
204
|
+
score,
|
|
205
|
+
param_sets,
|
|
206
|
+
base_dir="runs",
|
|
207
|
+
retries=2,
|
|
208
|
+
retry_delay=0.25,
|
|
209
|
+
failure_policy="raise",
|
|
210
|
+
timeout=300,
|
|
211
|
+
cancel_token=token,
|
|
212
|
+
)
|
|
213
|
+
except RunFailedError as exc:
|
|
214
|
+
summary = exc.summary # outputs and both manifests are already durable
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`failure_policy="continue"` preserves the original behavior. `"raise"` runs
|
|
218
|
+
everything and then raises if any job failed. `"fail_fast"` terminates the
|
|
219
|
+
pool after the first failed result. Timeout and cancellation also terminate
|
|
220
|
+
the pool and raise exceptions carrying a partial `summary`.
|
|
221
|
+
|
|
179
222
|
### Logging
|
|
180
223
|
|
|
181
224
|
The library logs its whole lifecycle through the standard `logging` module and
|
|
@@ -227,10 +270,13 @@ That's a cost trade-off to be aware of, not a limitation on what's allowed.
|
|
|
227
270
|
you want to drive the pool yourself:
|
|
228
271
|
|
|
229
272
|
```python
|
|
230
|
-
from mpengine import expand_call, process_jobs, process_jobs_
|
|
273
|
+
from mpengine import expand_call, iter_jobs, process_jobs, process_jobs_
|
|
231
274
|
|
|
232
275
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
233
276
|
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
277
|
+
|
|
278
|
+
for result in iter_jobs(jobs): # consume each result as soon as it completes
|
|
279
|
+
store(result)
|
|
234
280
|
```
|
|
235
281
|
|
|
236
282
|
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
@@ -12,7 +12,7 @@ Every run produces three things:
|
|
|
12
12
|
|
|
13
13
|
| what | where |
|
|
14
14
|
|---|---|
|
|
15
|
-
|
|
|
15
|
+
| human-readable and versioned JSON manifests | `<base>/manifests/<run_id>.txt` and `.json` |
|
|
16
16
|
| each job's result, saved to disk | `<base>/outputs/<run_id>/<label>` |
|
|
17
17
|
| one log file per **worker process** | `<base>/logs/<run_id>/worker_<pid>.log` |
|
|
18
18
|
| a log of the run as a whole | `<base>/logs/<run_id>/run.log` |
|
|
@@ -99,6 +99,10 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
99
99
|
| `n_workers` | worker *process* count (not threads — see below); defaults to `os.cpu_count()`, clamped down to the number of jobs if there are fewer jobs than that |
|
|
100
100
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
101
101
|
| `show_progress` | live terminal display: one overall bar for the whole run, plus a live rate number per worker process. Ignored when `debug=True` |
|
|
102
|
+
| `retries`, `retry_delay` | retry transient callback failures, optionally pausing between attempts |
|
|
103
|
+
| `failure_policy` | `continue` (default), `raise` after all jobs, or `fail_fast` |
|
|
104
|
+
| `timeout` | wall-clock limit for the complete parallel dispatch |
|
|
105
|
+
| `cancel_token` | a thread-safe `CancellationToken` another thread can use to abort the run |
|
|
102
106
|
|
|
103
107
|
Develop with `debug=True`, then flip it off. Chasing a bug through a process
|
|
104
108
|
pool means reading a traceback re-raised from a worker that has already exited,
|
|
@@ -118,7 +122,7 @@ rather than quoting a remembered number.
|
|
|
118
122
|
| `broadcast` | a `dict` of values shipped once per *worker* instead of once per job, delivered to your function as keyword arguments | 200 jobs + 80 MB panel **7.97s → 0.55s** |
|
|
119
123
|
| `reuse_pool` | keep the pool (and its broadcast payload) alive between calls instead of rebuilding it. Off by default | 10 dispatches **9.86s → 0.02s** |
|
|
120
124
|
| `initializer`, `initargs` | run once per worker before its first job — and unlike the stdlib's, may be a closure | — |
|
|
121
|
-
| `max_tasks_per_child` | recycle a worker every N jobs, so a slow leak can't end the run (Python 3.11+) | — |
|
|
125
|
+
| `max_tasks_per_child` | recycle a worker every N user jobs, so a slow leak can't end the run; disables batching to keep the count exact (Python 3.11+) | — |
|
|
122
126
|
|
|
123
127
|
**BLAS threads** is the one that catches people out. numpy hands matrix work to
|
|
124
128
|
a native library that is itself multi-threaded, and each worker process loads
|
|
@@ -142,6 +146,45 @@ it is a different order of magnitude — 0.55s on the 80 MB case, level with
|
|
|
142
146
|
joblib's memmapped 0.58s. Call `shutdown_pools()` when you're done with a
|
|
143
147
|
reused pool, or let `atexit` do it.
|
|
144
148
|
|
|
149
|
+
For a large NumPy array, wrap it in `Shared` to copy it once into OS shared
|
|
150
|
+
memory and let every worker read the same physical bytes:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from mpengine import Shared
|
|
154
|
+
|
|
155
|
+
summary = run(score, param_sets, base_dir="runs",
|
|
156
|
+
broadcast={"panel": Shared(panel)})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Shared arrays are read-only in workers and must be C-contiguous with a
|
|
160
|
+
non-object dtype. Keep the original array alive while a reused pool needs it.
|
|
161
|
+
|
|
162
|
+
### Reliability controls
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from mpengine import CancellationToken, RunFailedError, run
|
|
166
|
+
|
|
167
|
+
token = CancellationToken()
|
|
168
|
+
try:
|
|
169
|
+
summary = run(
|
|
170
|
+
score,
|
|
171
|
+
param_sets,
|
|
172
|
+
base_dir="runs",
|
|
173
|
+
retries=2,
|
|
174
|
+
retry_delay=0.25,
|
|
175
|
+
failure_policy="raise",
|
|
176
|
+
timeout=300,
|
|
177
|
+
cancel_token=token,
|
|
178
|
+
)
|
|
179
|
+
except RunFailedError as exc:
|
|
180
|
+
summary = exc.summary # outputs and both manifests are already durable
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`failure_policy="continue"` preserves the original behavior. `"raise"` runs
|
|
184
|
+
everything and then raises if any job failed. `"fail_fast"` terminates the
|
|
185
|
+
pool after the first failed result. Timeout and cancellation also terminate
|
|
186
|
+
the pool and raise exceptions carrying a partial `summary`.
|
|
187
|
+
|
|
145
188
|
### Logging
|
|
146
189
|
|
|
147
190
|
The library logs its whole lifecycle through the standard `logging` module and
|
|
@@ -193,10 +236,13 @@ That's a cost trade-off to be aware of, not a limitation on what's allowed.
|
|
|
193
236
|
you want to drive the pool yourself:
|
|
194
237
|
|
|
195
238
|
```python
|
|
196
|
-
from mpengine import expand_call, process_jobs, process_jobs_
|
|
239
|
+
from mpengine import expand_call, iter_jobs, process_jobs, process_jobs_
|
|
197
240
|
|
|
198
241
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
199
242
|
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
243
|
+
|
|
244
|
+
for result in iter_jobs(jobs): # consume each result as soon as it completes
|
|
245
|
+
store(result)
|
|
200
246
|
```
|
|
201
247
|
|
|
202
248
|
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mpengine"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "A small, general-purpose multiprocessing engine: dispatch any callable across processes with run manifests, per-worker logs, on-disk outputs and failure isolation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -55,6 +55,10 @@ testpaths = ["tests"]
|
|
|
55
55
|
markers = ["slow: spawns real process pools; deselect with -m 'not slow'"]
|
|
56
56
|
filterwarnings = ["error::DeprecationWarning:mpengine.*"]
|
|
57
57
|
|
|
58
|
+
[[tool.mypy.overrides]]
|
|
59
|
+
module = ["cloudpickle", "threadpoolctl", "tqdm", "tqdm.contrib.logging"]
|
|
60
|
+
ignore_missing_imports = true
|
|
61
|
+
|
|
58
62
|
[project.urls]
|
|
59
63
|
Homepage = "https://github.com/singhamandeep-kgp/multiprocessor"
|
|
60
64
|
Repository = "https://github.com/singhamandeep-kgp/multiprocessor"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""A multiprocessing engine with durable run orchestration."""
|
|
2
|
+
|
|
3
|
+
import logging as _logging
|
|
4
|
+
|
|
5
|
+
# A library must never configure logging for its host application - it only
|
|
6
|
+
# names its loggers and attaches a no-op handler so that a caller who has set
|
|
7
|
+
# nothing up sees no "No handlers could be found" noise. To actually see any
|
|
8
|
+
# of it: logging.basicConfig(level=logging.INFO). Loggers are
|
|
9
|
+
# "mpengine.engine" and "mpengine.orchestrator", so either half can be tuned
|
|
10
|
+
# or silenced on its own.
|
|
11
|
+
_logging.getLogger("mpengine").addHandler(_logging.NullHandler())
|
|
12
|
+
|
|
13
|
+
from mpengine.engine import (
|
|
14
|
+
BroadcastRef,
|
|
15
|
+
CancellationToken,
|
|
16
|
+
Shared,
|
|
17
|
+
expand_call,
|
|
18
|
+
iter_jobs,
|
|
19
|
+
process_jobs,
|
|
20
|
+
process_jobs_,
|
|
21
|
+
report_progress,
|
|
22
|
+
shutdown_pools,
|
|
23
|
+
)
|
|
24
|
+
from mpengine.orchestrator import (
|
|
25
|
+
JobResult,
|
|
26
|
+
RunCancelledError,
|
|
27
|
+
RunFailedError,
|
|
28
|
+
RunSummary,
|
|
29
|
+
RunTimeoutError,
|
|
30
|
+
WorkerStats,
|
|
31
|
+
load_pickle,
|
|
32
|
+
load_run_outputs,
|
|
33
|
+
run,
|
|
34
|
+
save_pickle,
|
|
35
|
+
)
|
|
36
|
+
from mpengine.partition import equal_chunks, lin_parts, nested_parts, parts_to_molecules
|
|
37
|
+
|
|
38
|
+
__all__ = [ # noqa: RUF022 - grouped by public API role, not alphabetically
|
|
39
|
+
# the usual entry point
|
|
40
|
+
"run",
|
|
41
|
+
"RunSummary",
|
|
42
|
+
"RunFailedError",
|
|
43
|
+
"RunCancelledError",
|
|
44
|
+
"RunTimeoutError",
|
|
45
|
+
"JobResult",
|
|
46
|
+
"WorkerStats",
|
|
47
|
+
"save_pickle",
|
|
48
|
+
# reading a finished run's outputs back
|
|
49
|
+
"load_run_outputs",
|
|
50
|
+
"load_pickle",
|
|
51
|
+
# lower-level dispatch, if you want to drive the pool yourself
|
|
52
|
+
"expand_call",
|
|
53
|
+
"iter_jobs",
|
|
54
|
+
"process_jobs",
|
|
55
|
+
"process_jobs_",
|
|
56
|
+
"report_progress",
|
|
57
|
+
# sending one large shared payload per worker instead of per job
|
|
58
|
+
"BroadcastRef",
|
|
59
|
+
"CancellationToken",
|
|
60
|
+
"Shared",
|
|
61
|
+
"shutdown_pools",
|
|
62
|
+
# atom -> molecule partitioning
|
|
63
|
+
"equal_chunks",
|
|
64
|
+
"lin_parts",
|
|
65
|
+
"nested_parts",
|
|
66
|
+
"parts_to_molecules",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Terminal banner helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_spider() -> str:
|
|
10
|
+
"""Read the packaged banner art."""
|
|
11
|
+
return (
|
|
12
|
+
resources.files("mpengine").joinpath("spider.txt").read_text(encoding="utf-8")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_banner(task: str, n_workers: int, debug: bool, file=sys.stdout) -> None:
|
|
17
|
+
"""Print the banner and launch summary."""
|
|
18
|
+
print(_load_spider(), file=file)
|
|
19
|
+
if debug:
|
|
20
|
+
print(f" mpengine - '{task}' sequentially (debug mode)\n", file=file)
|
|
21
|
+
else:
|
|
22
|
+
print(f" mpengine - '{task}' on {n_workers} workers\n", file=file)
|