mpengine 0.3.0__tar.gz → 0.4.1__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.0/src/mpengine.egg-info → mpengine-0.4.1}/PKG-INFO +65 -1
- {mpengine-0.3.0 → mpengine-0.4.1}/README.md +245 -186
- {mpengine-0.3.0 → mpengine-0.4.1}/pyproject.toml +81 -61
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/__init__.py +96 -86
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/banner.py +2 -9
- mpengine-0.4.1/src/mpengine/engine.py +698 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/orchestrator.py +933 -665
- mpengine-0.4.1/src/mpengine/py.typed +3 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/spider.txt +2 -2
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/worker_names.json +1 -1
- {mpengine-0.3.0 → mpengine-0.4.1/src/mpengine.egg-info}/PKG-INFO +65 -1
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine.egg-info/SOURCES.txt +9 -1
- mpengine-0.4.1/src/mpengine.egg-info/requires.txt +8 -0
- mpengine-0.4.1/tests/test_broadcast.py +118 -0
- mpengine-0.4.1/tests/test_engine_batching.py +168 -0
- mpengine-0.4.1/tests/test_engine_core.py +138 -0
- mpengine-0.4.1/tests/test_engine_perf.py +255 -0
- mpengine-0.4.1/tests/test_logging.py +214 -0
- mpengine-0.4.1/tests/test_orchestrator.py +308 -0
- mpengine-0.4.1/tests/test_partition.py +155 -0
- mpengine-0.3.0/src/mpengine/engine.py +0 -258
- mpengine-0.3.0/src/mpengine.egg-info/requires.txt +0 -3
- {mpengine-0.3.0 → mpengine-0.4.1}/LICENSE +0 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/setup.cfg +0 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine/partition.py +0 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/src/mpengine.egg-info/dependency_links.txt +0 -0
- {mpengine-0.3.0 → mpengine-0.4.1}/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.4.1
|
|
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
|
|
@@ -131,6 +138,44 @@ Develop with `debug=True`, then flip it off. Chasing a bug through a process
|
|
|
131
138
|
pool means reading a traceback re-raised from a worker that has already exited,
|
|
132
139
|
and it never tells you which job dict was at fault.
|
|
133
140
|
|
|
141
|
+
### Performance
|
|
142
|
+
|
|
143
|
+
The defaults are already tuned; these are the knobs for when they aren't
|
|
144
|
+
enough. Measured on 8 cores with OpenBLAS against joblib 1.5.3 — reproduce
|
|
145
|
+
with `python benchmarks/bench.py`, which keeps each "before" path runnable
|
|
146
|
+
rather than quoting a remembered number.
|
|
147
|
+
|
|
148
|
+
| argument | meaning | measured |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `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** |
|
|
151
|
+
| `chunksize` | jobs per submission. `'auto'` batches large runs and collapses to 1 on small ones | 20,000 tiny jobs **5.55s → 1.01s** |
|
|
152
|
+
| `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
|
+
| `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
|
+
| `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+) | — |
|
|
156
|
+
|
|
157
|
+
**BLAS threads** is the one that catches people out. numpy hands matrix work to
|
|
158
|
+
a native library that is itself multi-threaded, and each worker process loads
|
|
159
|
+
its own copy believing it owns the machine — so eight workers each spawn eight
|
|
160
|
+
BLAS threads and 64 threads fight over 8 cores. Capping them means parallelism
|
|
161
|
+
comes from mpengine, one job per core, instead. This is on by default.
|
|
162
|
+
|
|
163
|
+
**Broadcast** is for the panel or fitted model every job needs:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
summary = run(score, param_sets, base_dir="runs",
|
|
167
|
+
broadcast={"panel": panel}, reuse_pool=True)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`score` is then called as `score(**params, panel=panel)`. Worth being precise
|
|
171
|
+
about when it pays: batching alone already reduces a closure-captured payload
|
|
172
|
+
to one copy per batch, so on a single cold call with a payload under ~10 MB,
|
|
173
|
+
broadcast costs slightly more than it saves. Above that, or paired with
|
|
174
|
+
`reuse_pool=True` where the payload is delivered once for the life of the pool,
|
|
175
|
+
it is a different order of magnitude — 0.55s on the 80 MB case, level with
|
|
176
|
+
joblib's memmapped 0.58s. Call `shutdown_pools()` when you're done with a
|
|
177
|
+
reused pool, or let `atexit` do it.
|
|
178
|
+
|
|
134
179
|
### Logging
|
|
135
180
|
|
|
136
181
|
The library logs its whole lifecycle through the standard `logging` module and
|
|
@@ -203,6 +248,25 @@ for triangular workloads — where item `i` costs `O(i)`, such as an
|
|
|
203
248
|
expanding-window computation — which keeps workers from idling while one
|
|
204
249
|
overloaded worker finishes.
|
|
205
250
|
|
|
251
|
+
## Tests
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pip install -e ".[test]"
|
|
255
|
+
pytest # everything, ~30s
|
|
256
|
+
pytest -m "not slow" # skip the process-pool tests, under a second
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
576 tests. The ones that spawn real pools are marked `slow` and dominate the
|
|
260
|
+
runtime, so the marker exists to keep a fast inner loop available - but they
|
|
261
|
+
run by default, because the behaviour they cover (a dead worker surfacing
|
|
262
|
+
instead of hanging, per-job failure attribution inside a batch, BLAS budgets
|
|
263
|
+
read back from inside a worker) is exactly the behaviour worth guarding.
|
|
264
|
+
|
|
265
|
+
CI runs the suite on Linux, Windows and macOS across Python 3.10, 3.12 and
|
|
266
|
+
3.14. The platform spread matters here: Windows and macOS start workers with
|
|
267
|
+
`spawn`, Linux does not, and mpengine caps BLAS threads by a different
|
|
268
|
+
mechanism in each case.
|
|
269
|
+
|
|
206
270
|
## Changelog
|
|
207
271
|
|
|
208
272
|
See [CHANGELOG.md](CHANGELOG.md). Versions below 1.0 may carry breaking changes
|
|
@@ -1,186 +1,245 @@
|
|
|
1
|
-
# mpengine
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
your
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
| `
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1
|
+
# mpengine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/singhamandeep-kgp/multiprocessor/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
*(This repo is named `multiprocessor`; the package it ships is `mpengine` — see below.)*
|
|
6
|
+
|
|
7
|
+
A small, general-purpose multiprocessing engine. Give it any callable and a list
|
|
8
|
+
of parameter sets; it runs them across processes and leaves behind a record of
|
|
9
|
+
what happened.
|
|
10
|
+
|
|
11
|
+
Every run produces three things:
|
|
12
|
+
|
|
13
|
+
| what | where |
|
|
14
|
+
|---|---|
|
|
15
|
+
| a manifest of exactly what was launched, and how it ended | `<base>/manifests/<run_id>.txt` |
|
|
16
|
+
| each job's result, saved to disk | `<base>/outputs/<run_id>/<label>` |
|
|
17
|
+
| one log file per **worker process** | `<base>/logs/<run_id>/worker_<pid>.log` |
|
|
18
|
+
| a log of the run as a whole | `<base>/logs/<run_id>/run.log` |
|
|
19
|
+
|
|
20
|
+
A single job failing does **not** abort the run — it is recorded as a failed
|
|
21
|
+
job and the rest continue.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install mpengine
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Python 3.10+. `numpy`, `cloudpickle` and `tqdm` come with it —
|
|
30
|
+
pip installs them for you.
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary>Installing from source instead</summary>
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install git+https://github.com/singhamandeep-kgp/multiprocessor.git # latest from GitHub
|
|
37
|
+
pip install -e path/to/multiprocessor # editable, for developing mpengine itself
|
|
38
|
+
```
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from mpengine import run
|
|
45
|
+
|
|
46
|
+
def my_task(x, y):
|
|
47
|
+
return x * y
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__": # required - see below
|
|
50
|
+
summary = run(
|
|
51
|
+
my_task,
|
|
52
|
+
[{"x": 2, "y": 3}, {"x": 4, "y": 5}],
|
|
53
|
+
base_dir="runs",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(summary.n_ok, summary.n_failed)
|
|
57
|
+
for r in summary.results:
|
|
58
|
+
print(r.label, r.status, r.output_path or r.error)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### The `if __name__ == "__main__":` guard
|
|
62
|
+
|
|
63
|
+
On Windows and macOS, Python starts worker processes with `spawn`, which
|
|
64
|
+
re-imports your module in every worker. Without the guard, that re-import runs
|
|
65
|
+
your `run(...)` call again in each worker, which spawns more workers, and so on
|
|
66
|
+
until the process dies. Put anything that *calls* `run()` inside the guard;
|
|
67
|
+
your task functions themselves stay at module level, as normal.
|
|
68
|
+
|
|
69
|
+
### Placing the outputs
|
|
70
|
+
|
|
71
|
+
`base_dir` derives all three locations. To place them independently, pass any of
|
|
72
|
+
`output_dir`, `log_dir`, `manifest_dir` — an explicit path always wins, so you
|
|
73
|
+
can give a `base_dir` and still redirect just the logs.
|
|
74
|
+
|
|
75
|
+
### Reading the results back
|
|
76
|
+
|
|
77
|
+
Results are written to disk, one file per job (pickled by default). To load a
|
|
78
|
+
finished run back into memory as `{label: result}`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mpengine import load_run_outputs
|
|
82
|
+
|
|
83
|
+
outputs = load_run_outputs(summary.output_dir) # or the path run() printed
|
|
84
|
+
print(outputs["job_0000"])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Only successful jobs appear — a failed job never wrote a file, so its label is
|
|
88
|
+
absent; check `summary.results` to see which failed and why. If you wrote the
|
|
89
|
+
run with a custom `save_fn`, pass the matching reader as
|
|
90
|
+
`load_run_outputs(..., load_fn=your_loader)`.
|
|
91
|
+
|
|
92
|
+
### Other options
|
|
93
|
+
|
|
94
|
+
| argument | meaning |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `save_fn` | how each result is written. Default `save_pickle`; supply your own (e.g. parquet) |
|
|
97
|
+
| `labels` | names for each job; defaults to `job_0000`, `job_0001`, … |
|
|
98
|
+
| `task` | names the run; defaults to `func.__name__` |
|
|
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
|
+
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
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
|
+
|
|
103
|
+
Develop with `debug=True`, then flip it off. Chasing a bug through a process
|
|
104
|
+
pool means reading a traceback re-raised from a worker that has already exited,
|
|
105
|
+
and it never tells you which job dict was at fault.
|
|
106
|
+
|
|
107
|
+
### Performance
|
|
108
|
+
|
|
109
|
+
The defaults are already tuned; these are the knobs for when they aren't
|
|
110
|
+
enough. Measured on 8 cores with OpenBLAS against joblib 1.5.3 — reproduce
|
|
111
|
+
with `python benchmarks/bench.py`, which keeps each "before" path runnable
|
|
112
|
+
rather than quoting a remembered number.
|
|
113
|
+
|
|
114
|
+
| argument | meaning | measured |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `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** |
|
|
117
|
+
| `chunksize` | jobs per submission. `'auto'` batches large runs and collapses to 1 on small ones | 20,000 tiny jobs **5.55s → 1.01s** |
|
|
118
|
+
| `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
|
+
| `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
|
+
| `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+) | — |
|
|
122
|
+
|
|
123
|
+
**BLAS threads** is the one that catches people out. numpy hands matrix work to
|
|
124
|
+
a native library that is itself multi-threaded, and each worker process loads
|
|
125
|
+
its own copy believing it owns the machine — so eight workers each spawn eight
|
|
126
|
+
BLAS threads and 64 threads fight over 8 cores. Capping them means parallelism
|
|
127
|
+
comes from mpengine, one job per core, instead. This is on by default.
|
|
128
|
+
|
|
129
|
+
**Broadcast** is for the panel or fitted model every job needs:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
summary = run(score, param_sets, base_dir="runs",
|
|
133
|
+
broadcast={"panel": panel}, reuse_pool=True)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`score` is then called as `score(**params, panel=panel)`. Worth being precise
|
|
137
|
+
about when it pays: batching alone already reduces a closure-captured payload
|
|
138
|
+
to one copy per batch, so on a single cold call with a payload under ~10 MB,
|
|
139
|
+
broadcast costs slightly more than it saves. Above that, or paired with
|
|
140
|
+
`reuse_pool=True` where the payload is delivered once for the life of the pool,
|
|
141
|
+
it is a different order of magnitude — 0.55s on the 80 MB case, level with
|
|
142
|
+
joblib's memmapped 0.58s. Call `shutdown_pools()` when you're done with a
|
|
143
|
+
reused pool, or let `atexit` do it.
|
|
144
|
+
|
|
145
|
+
### Logging
|
|
146
|
+
|
|
147
|
+
The library logs its whole lifecycle through the standard `logging` module and
|
|
148
|
+
writes nothing to stdout except the banner, so embedding it never pollutes a
|
|
149
|
+
host application's output. It installs only a `NullHandler` — to see anything,
|
|
150
|
+
configure logging yourself:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import logging
|
|
154
|
+
logging.basicConfig(level=logging.INFO)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Two loggers, so either half can be tuned or silenced independently:
|
|
158
|
+
|
|
159
|
+
| logger | emits |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `mpengine.orchestrator` | run start/done, resolved dirs, per-job outcomes, worker ranking |
|
|
162
|
+
| `mpengine.engine` | dispatch start/done, worker-count clamping, progress milestones, worker death |
|
|
163
|
+
|
|
164
|
+
Levels: **INFO** for run lifecycle, **DEBUG** for per-job success (a 10,000-job
|
|
165
|
+
sweep would otherwise be 10,000 INFO lines), **WARNING** for a failed job or a
|
|
166
|
+
clamped worker count, **ERROR** for a dead worker.
|
|
167
|
+
|
|
168
|
+
Every run also writes `<log_dir>/<run_id>/run.log` containing the parent's
|
|
169
|
+
whole view of that run — dispatch, milestones, every job outcome, the summary —
|
|
170
|
+
alongside the existing per-worker log files. That happens regardless of how you
|
|
171
|
+
configure logging, so a run is always a self-contained durable record.
|
|
172
|
+
|
|
173
|
+
**Progress display is terminal-aware.** Interactively you get the tqdm bars and
|
|
174
|
+
per-worker lines as before. When stderr is not a terminal — piped, redirected to
|
|
175
|
+
a log file, running under CI or a scheduler — those are suppressed (they render
|
|
176
|
+
as unreadable escape-sequence noise in a file) and periodic completion
|
|
177
|
+
milestones are logged at INFO instead.
|
|
178
|
+
|
|
179
|
+
### Closures and lambdas
|
|
180
|
+
|
|
181
|
+
`func`, and any custom `save_fn`, can be a closure or a lambda — not just a
|
|
182
|
+
module-level function. Jobs are serialized with `cloudpickle` before crossing
|
|
183
|
+
the process boundary, which (unlike stdlib `pickle`) can serialize a function
|
|
184
|
+
by *value* (its bytecode plus whatever it captured), not just by reference.
|
|
185
|
+
|
|
186
|
+
The one caveat: whatever a closure captures travels with **every job** that
|
|
187
|
+
uses it — a closure capturing a large array re-serializes that array per job.
|
|
188
|
+
That's a cost trade-off to be aware of, not a limitation on what's allowed.
|
|
189
|
+
|
|
190
|
+
## Going lower-level
|
|
191
|
+
|
|
192
|
+
`run()` is the convenient layer. The primitives underneath are exported too, if
|
|
193
|
+
you want to drive the pool yourself:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from mpengine import expand_call, process_jobs, process_jobs_
|
|
197
|
+
|
|
198
|
+
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
199
|
+
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
203
|
+
single call can dispatch entirely different functions with different signatures
|
|
204
|
+
and return types.
|
|
205
|
+
|
|
206
|
+
Partitioning helpers are available for splitting work into chunks:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from mpengine import lin_parts, nested_parts, parts_to_molecules
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`lin_parts` gives equal-count chunks. `nested_parts` gives equal-*work* chunks
|
|
213
|
+
for triangular workloads — where item `i` costs `O(i)`, such as an
|
|
214
|
+
expanding-window computation — which keeps workers from idling while one
|
|
215
|
+
overloaded worker finishes.
|
|
216
|
+
|
|
217
|
+
## Tests
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pip install -e ".[test]"
|
|
221
|
+
pytest # everything, ~30s
|
|
222
|
+
pytest -m "not slow" # skip the process-pool tests, under a second
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
576 tests. The ones that spawn real pools are marked `slow` and dominate the
|
|
226
|
+
runtime, so the marker exists to keep a fast inner loop available - but they
|
|
227
|
+
run by default, because the behaviour they cover (a dead worker surfacing
|
|
228
|
+
instead of hanging, per-job failure attribution inside a batch, BLAS budgets
|
|
229
|
+
read back from inside a worker) is exactly the behaviour worth guarding.
|
|
230
|
+
|
|
231
|
+
CI runs the suite on Linux, Windows and macOS across Python 3.10, 3.12 and
|
|
232
|
+
3.14. The platform spread matters here: Windows and macOS start workers with
|
|
233
|
+
`spawn`, Linux does not, and mpengine caps BLAS threads by a different
|
|
234
|
+
mechanism in each case.
|
|
235
|
+
|
|
236
|
+
## Changelog
|
|
237
|
+
|
|
238
|
+
See [CHANGELOG.md](CHANGELOG.md). Versions below 1.0 may carry breaking changes
|
|
239
|
+
in a minor release; each is listed there with the migration needed.
|
|
240
|
+
|
|
241
|
+
## What else is in the repo
|
|
242
|
+
|
|
243
|
+
The `src/learning/` package holds demo and exercise scripts used while
|
|
244
|
+
developing the engine. It depends on a separate private project and is
|
|
245
|
+
deliberately **not** packaged — installing `mpengine` never pulls it in.
|