mpengine 0.3.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.3.1/src/mpengine.egg-info → mpengine-0.5.0}/PKG-INFO +113 -3
- {mpengine-0.3.1 → mpengine-0.5.0}/README.md +291 -186
- {mpengine-0.3.1 → mpengine-0.5.0}/pyproject.toml +85 -61
- 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.5.0/src/mpengine/orchestrator.py +1048 -0
- mpengine-0.5.0/src/mpengine/partition.py +93 -0
- mpengine-0.5.0/src/mpengine/py.typed +3 -0
- {mpengine-0.3.1 → mpengine-0.5.0/src/mpengine.egg-info}/PKG-INFO +113 -3
- {mpengine-0.3.1 → mpengine-0.5.0}/src/mpengine.egg-info/SOURCES.txt +11 -1
- mpengine-0.5.0/src/mpengine.egg-info/requires.txt +8 -0
- mpengine-0.5.0/tests/test_broadcast.py +118 -0
- mpengine-0.5.0/tests/test_engine_batching.py +168 -0
- mpengine-0.5.0/tests/test_engine_core.py +138 -0
- mpengine-0.5.0/tests/test_engine_perf.py +233 -0
- mpengine-0.5.0/tests/test_logging.py +214 -0
- mpengine-0.5.0/tests/test_orchestrator.py +308 -0
- mpengine-0.5.0/tests/test_partition.py +155 -0
- mpengine-0.5.0/tests/test_reliability.py +233 -0
- mpengine-0.5.0/tests/test_shared.py +153 -0
- mpengine-0.3.1/src/mpengine/__init__.py +0 -86
- mpengine-0.3.1/src/mpengine/banner.py +0 -34
- mpengine-0.3.1/src/mpengine/engine.py +0 -276
- mpengine-0.3.1/src/mpengine/orchestrator.py +0 -788
- mpengine-0.3.1/src/mpengine/partition.py +0 -122
- mpengine-0.3.1/src/mpengine.egg-info/requires.txt +0 -3
- {mpengine-0.3.1 → mpengine-0.5.0}/LICENSE +0 -0
- {mpengine-0.3.1 → mpengine-0.5.0}/setup.cfg +0 -0
- {mpengine-0.3.1 → mpengine-0.5.0}/src/mpengine/spider.txt +0 -0
- {mpengine-0.3.1 → mpengine-0.5.0}/src/mpengine/worker_names.json +0 -0
- {mpengine-0.3.1 → mpengine-0.5.0}/src/mpengine.egg-info/dependency_links.txt +0 -0
- {mpengine-0.3.1 → mpengine-0.5.0}/src/mpengine.egg-info/top_level.txt +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
|
|
@@ -19,16 +19,23 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
22
23
|
Requires-Python: >=3.10
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
License-File: LICENSE
|
|
25
26
|
Requires-Dist: numpy>=1.26
|
|
26
27
|
Requires-Dist: cloudpickle>=3.0
|
|
27
28
|
Requires-Dist: tqdm>=4.60
|
|
29
|
+
Requires-Dist: threadpoolctl>=3.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
32
|
+
Requires-Dist: joblib>=1.3; extra == "test"
|
|
28
33
|
Dynamic: license-file
|
|
29
34
|
|
|
30
35
|
# mpengine
|
|
31
36
|
|
|
37
|
+
[](https://github.com/singhamandeep-kgp/multiprocessor/actions/workflows/ci.yml)
|
|
38
|
+
|
|
32
39
|
*(This repo is named `multiprocessor`; the package it ships is `mpengine` — see below.)*
|
|
33
40
|
|
|
34
41
|
A small, general-purpose multiprocessing engine. Give it any callable and a list
|
|
@@ -39,7 +46,7 @@ Every run produces three things:
|
|
|
39
46
|
|
|
40
47
|
| what | where |
|
|
41
48
|
|---|---|
|
|
42
|
-
|
|
|
49
|
+
| human-readable and versioned JSON manifests | `<base>/manifests/<run_id>.txt` and `.json` |
|
|
43
50
|
| each job's result, saved to disk | `<base>/outputs/<run_id>/<label>` |
|
|
44
51
|
| one log file per **worker process** | `<base>/logs/<run_id>/worker_<pid>.log` |
|
|
45
52
|
| a log of the run as a whole | `<base>/logs/<run_id>/run.log` |
|
|
@@ -126,11 +133,92 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
126
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 |
|
|
127
134
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
128
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 |
|
|
129
140
|
|
|
130
141
|
Develop with `debug=True`, then flip it off. Chasing a bug through a process
|
|
131
142
|
pool means reading a traceback re-raised from a worker that has already exited,
|
|
132
143
|
and it never tells you which job dict was at fault.
|
|
133
144
|
|
|
145
|
+
### Performance
|
|
146
|
+
|
|
147
|
+
The defaults are already tuned; these are the knobs for when they aren't
|
|
148
|
+
enough. Measured on 8 cores with OpenBLAS against joblib 1.5.3 — reproduce
|
|
149
|
+
with `python benchmarks/bench.py`, which keeps each "before" path runnable
|
|
150
|
+
rather than quoting a remembered number.
|
|
151
|
+
|
|
152
|
+
| argument | meaning | measured |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `blas_threads` | threads each worker's native BLAS may use for one numpy call. `'auto'` = `cpu_count // workers`; an int to set it; `None` to disable | 24 SVD jobs **8.26s → 1.20s** |
|
|
155
|
+
| `chunksize` | jobs per submission. `'auto'` batches large runs and collapses to 1 on small ones | 20,000 tiny jobs **5.55s → 1.01s** |
|
|
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** |
|
|
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** |
|
|
158
|
+
| `initializer`, `initargs` | run once per worker before its first job — and unlike the stdlib's, may be a closure | — |
|
|
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+) | — |
|
|
160
|
+
|
|
161
|
+
**BLAS threads** is the one that catches people out. numpy hands matrix work to
|
|
162
|
+
a native library that is itself multi-threaded, and each worker process loads
|
|
163
|
+
its own copy believing it owns the machine — so eight workers each spawn eight
|
|
164
|
+
BLAS threads and 64 threads fight over 8 cores. Capping them means parallelism
|
|
165
|
+
comes from mpengine, one job per core, instead. This is on by default.
|
|
166
|
+
|
|
167
|
+
**Broadcast** is for the panel or fitted model every job needs:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
summary = run(score, param_sets, base_dir="runs",
|
|
171
|
+
broadcast={"panel": panel}, reuse_pool=True)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`score` is then called as `score(**params, panel=panel)`. Worth being precise
|
|
175
|
+
about when it pays: batching alone already reduces a closure-captured payload
|
|
176
|
+
to one copy per batch, so on a single cold call with a payload under ~10 MB,
|
|
177
|
+
broadcast costs slightly more than it saves. Above that, or paired with
|
|
178
|
+
`reuse_pool=True` where the payload is delivered once for the life of the pool,
|
|
179
|
+
it is a different order of magnitude — 0.55s on the 80 MB case, level with
|
|
180
|
+
joblib's memmapped 0.58s. Call `shutdown_pools()` when you're done with a
|
|
181
|
+
reused pool, or let `atexit` do it.
|
|
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
|
+
|
|
134
222
|
### Logging
|
|
135
223
|
|
|
136
224
|
The library logs its whole lifecycle through the standard `logging` module and
|
|
@@ -182,10 +270,13 @@ That's a cost trade-off to be aware of, not a limitation on what's allowed.
|
|
|
182
270
|
you want to drive the pool yourself:
|
|
183
271
|
|
|
184
272
|
```python
|
|
185
|
-
from mpengine import expand_call, process_jobs, process_jobs_
|
|
273
|
+
from mpengine import expand_call, iter_jobs, process_jobs, process_jobs_
|
|
186
274
|
|
|
187
275
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
188
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)
|
|
189
280
|
```
|
|
190
281
|
|
|
191
282
|
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
@@ -203,6 +294,25 @@ for triangular workloads — where item `i` costs `O(i)`, such as an
|
|
|
203
294
|
expanding-window computation — which keeps workers from idling while one
|
|
204
295
|
overloaded worker finishes.
|
|
205
296
|
|
|
297
|
+
## Tests
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
pip install -e ".[test]"
|
|
301
|
+
pytest # everything, ~30s
|
|
302
|
+
pytest -m "not slow" # skip the process-pool tests, under a second
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
576 tests. The ones that spawn real pools are marked `slow` and dominate the
|
|
306
|
+
runtime, so the marker exists to keep a fast inner loop available - but they
|
|
307
|
+
run by default, because the behaviour they cover (a dead worker surfacing
|
|
308
|
+
instead of hanging, per-job failure attribution inside a batch, BLAS budgets
|
|
309
|
+
read back from inside a worker) is exactly the behaviour worth guarding.
|
|
310
|
+
|
|
311
|
+
CI runs the suite on Linux, Windows and macOS across Python 3.10, 3.12 and
|
|
312
|
+
3.14. The platform spread matters here: Windows and macOS start workers with
|
|
313
|
+
`spawn`, Linux does not, and mpengine caps BLAS threads by a different
|
|
314
|
+
mechanism in each case.
|
|
315
|
+
|
|
206
316
|
## Changelog
|
|
207
317
|
|
|
208
318
|
See [CHANGELOG.md](CHANGELOG.md). Versions below 1.0 may carry breaking changes
|