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.
Files changed (31) hide show
  1. {mpengine-0.4.1/src/mpengine.egg-info → mpengine-0.5.0}/PKG-INFO +50 -4
  2. {mpengine-0.4.1 → mpengine-0.5.0}/README.md +49 -3
  3. {mpengine-0.4.1 → mpengine-0.5.0}/pyproject.toml +5 -1
  4. mpengine-0.5.0/src/mpengine/__init__.py +69 -0
  5. mpengine-0.5.0/src/mpengine/banner.py +22 -0
  6. mpengine-0.5.0/src/mpengine/engine.py +850 -0
  7. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/orchestrator.py +422 -307
  8. mpengine-0.5.0/src/mpengine/partition.py +93 -0
  9. {mpengine-0.4.1 → mpengine-0.5.0/src/mpengine.egg-info}/PKG-INFO +50 -4
  10. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/SOURCES.txt +3 -1
  11. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_perf.py +233 -255
  12. mpengine-0.5.0/tests/test_reliability.py +233 -0
  13. mpengine-0.5.0/tests/test_shared.py +153 -0
  14. mpengine-0.4.1/src/mpengine/__init__.py +0 -96
  15. mpengine-0.4.1/src/mpengine/banner.py +0 -34
  16. mpengine-0.4.1/src/mpengine/engine.py +0 -698
  17. mpengine-0.4.1/src/mpengine/partition.py +0 -122
  18. {mpengine-0.4.1 → mpengine-0.5.0}/LICENSE +0 -0
  19. {mpengine-0.4.1 → mpengine-0.5.0}/setup.cfg +0 -0
  20. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/py.typed +0 -0
  21. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/spider.txt +0 -0
  22. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine/worker_names.json +0 -0
  23. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/dependency_links.txt +0 -0
  24. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/requires.txt +0 -0
  25. {mpengine-0.4.1 → mpengine-0.5.0}/src/mpengine.egg-info/top_level.txt +0 -0
  26. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_broadcast.py +0 -0
  27. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_batching.py +0 -0
  28. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_engine_core.py +0 -0
  29. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_logging.py +0 -0
  30. {mpengine-0.4.1 → mpengine-0.5.0}/tests/test_orchestrator.py +0 -0
  31. {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.4.1
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
- | a manifest of exactly what was launched, and how it ended | `<base>/manifests/<run_id>.txt` |
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
- | a manifest of exactly what was launched, and how it ended | `<base>/manifests/<run_id>.txt` |
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.4.1"
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)