mpengine 0.1.2__tar.gz → 0.2.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.1.2/src/mpengine.egg-info → mpengine-0.2.0}/PKG-INFO +3 -3
- {mpengine-0.1.2 → mpengine-0.2.0}/README.md +2 -2
- {mpengine-0.1.2 → mpengine-0.2.0}/pyproject.toml +5 -5
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine/__init__.py +1 -1
- mpengine-0.2.0/src/mpengine/banner.py +41 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine/engine.py +14 -2
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine/orchestrator.py +8 -5
- mpengine-0.2.0/src/mpengine/spider.txt +31 -0
- {mpengine-0.1.2 → mpengine-0.2.0/src/mpengine.egg-info}/PKG-INFO +3 -3
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine.egg-info/SOURCES.txt +2 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/LICENSE +0 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/setup.cfg +0 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine/partition.py +0 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine/worker_names.json +0 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine.egg-info/dependency_links.txt +0 -0
- {mpengine-0.1.2 → mpengine-0.2.0}/src/mpengine.egg-info/requires.txt +0 -0
- {mpengine-0.1.2 → mpengine-0.2.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.2.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
|
|
@@ -122,7 +122,7 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
122
122
|
| `save_fn` | how each result is written. Default `save_pickle`; supply your own (e.g. parquet) |
|
|
123
123
|
| `labels` | names for each job; defaults to `job_0000`, `job_0001`, … |
|
|
124
124
|
| `task` | names the run; defaults to `func.__name__` |
|
|
125
|
-
| `
|
|
125
|
+
| `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 |
|
|
126
126
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
127
127
|
| `show_progress` | live terminal display: one overall bar for the whole run, plus a live rate number per worker process. Ignored when `debug=True` |
|
|
128
128
|
|
|
@@ -150,7 +150,7 @@ you want to drive the pool yourself:
|
|
|
150
150
|
from mpengine import expand_call, process_jobs, process_jobs_
|
|
151
151
|
|
|
152
152
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
153
|
-
results = process_jobs(jobs
|
|
153
|
+
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
@@ -93,7 +93,7 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
93
93
|
| `save_fn` | how each result is written. Default `save_pickle`; supply your own (e.g. parquet) |
|
|
94
94
|
| `labels` | names for each job; defaults to `job_0000`, `job_0001`, … |
|
|
95
95
|
| `task` | names the run; defaults to `func.__name__` |
|
|
96
|
-
| `
|
|
96
|
+
| `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 |
|
|
97
97
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
98
98
|
| `show_progress` | live terminal display: one overall bar for the whole run, plus a live rate number per worker process. Ignored when `debug=True` |
|
|
99
99
|
|
|
@@ -121,7 +121,7 @@ you want to drive the pool yourself:
|
|
|
121
121
|
from mpengine import expand_call, process_jobs, process_jobs_
|
|
122
122
|
|
|
123
123
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
124
|
-
results = process_jobs(jobs
|
|
124
|
+
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
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.2.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"
|
|
@@ -49,11 +49,11 @@ Issues = "https://github.com/singhamandeep-kgp/multiprocessor/issues"
|
|
|
49
49
|
where = ["src"]
|
|
50
50
|
include = ["mpengine*"]
|
|
51
51
|
|
|
52
|
-
# worker_names.json
|
|
53
|
-
# only picks up .py files by default, so
|
|
54
|
-
# actually ship inside a built wheel.
|
|
52
|
+
# worker_names.json and spider.txt are data, not code - setuptools' package
|
|
53
|
+
# discovery above only picks up .py files by default, so they need listing
|
|
54
|
+
# explicitly to actually ship inside a built wheel.
|
|
55
55
|
[tool.setuptools.package-data]
|
|
56
|
-
mpengine = ["worker_names.json"]
|
|
56
|
+
mpengine = ["worker_names.json", "spider.txt"]
|
|
57
57
|
|
|
58
58
|
# `statarb` (needed only by src/learning) is installed separately as an editable
|
|
59
59
|
# path install (pip install -e ../Equity_StatArb) rather than declared here,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""The run banner - purely cosmetic, deliberately kept out of everything else.
|
|
2
|
+
|
|
3
|
+
`orchestrator.py` imports exactly one name from here and calls it once. All the
|
|
4
|
+
ASCII art, formatting and printing lives in this module, so the engine and the
|
|
5
|
+
orchestration layer stay free of decoration.
|
|
6
|
+
|
|
7
|
+
The spider is the mascot: a multiprocessing engine, and a creature famous for
|
|
8
|
+
having eight legs. The art itself lives in `spider.txt` rather than as a string
|
|
9
|
+
literal here - it is data, not code, the same call made for `worker_names.json`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from importlib import resources
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_spider() -> str:
|
|
19
|
+
"""Read the art from packaged data.
|
|
20
|
+
|
|
21
|
+
`read_text` applies universal-newline translation, so the file's stored
|
|
22
|
+
CRLF endings come back as `\\n` and render correctly on any OS without the
|
|
23
|
+
stored bytes ever being modified - which matters, because the art is kept
|
|
24
|
+
byte-for-byte as supplied.
|
|
25
|
+
"""
|
|
26
|
+
return resources.files("mpengine").joinpath("spider.txt").read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def print_banner(task: str, n_workers: int, debug: bool, file=sys.stdout) -> None:
|
|
30
|
+
"""Print the spider, then one line saying what is being launched.
|
|
31
|
+
|
|
32
|
+
Goes to stdout by default, matching the other run-level reporting `run()`
|
|
33
|
+
does (the stored-here paths, the worker ranking). The stderr stream stays
|
|
34
|
+
reserved for the live-redraw progress display, which is a different kind of
|
|
35
|
+
output and would interleave badly with anything else written there.
|
|
36
|
+
"""
|
|
37
|
+
print(_load_spider(), file=file)
|
|
38
|
+
if debug:
|
|
39
|
+
print(f" mpengine - '{task}' sequentially (debug mode)\n", file=file)
|
|
40
|
+
else:
|
|
41
|
+
print(f" mpengine - '{task}' on {n_workers} workers\n", file=file)
|
|
@@ -87,11 +87,17 @@ def _run_from_blob(blob: bytes) -> tuple[int, float, Any]:
|
|
|
87
87
|
def process_jobs(
|
|
88
88
|
jobs: list[dict[str, Any]],
|
|
89
89
|
task: str | None = None,
|
|
90
|
-
|
|
90
|
+
n_workers: int = os.cpu_count() or 4,
|
|
91
91
|
on_progress: Callable[[int, float, Any], None] | None = None,
|
|
92
92
|
) -> list[Any]:
|
|
93
93
|
"""Ch.20 Snippet 20.9 (`processJobs`) - real `mp.Pool` + `imap_unordered`.
|
|
94
94
|
|
|
95
|
+
Named `n_workers`, not `n_threads`: this spawns OS *processes* via
|
|
96
|
+
`mp.Pool`, each with its own interpreter and memory space, not threads
|
|
97
|
+
sharing one. That distinction matters here specifically - ex02 measured
|
|
98
|
+
that CPython's GIL makes threads add nothing for CPU-bound work, which is
|
|
99
|
+
exactly what this engine dispatches.
|
|
100
|
+
|
|
95
101
|
Results arrive in *completion* order, not submission order, which is what
|
|
96
102
|
lets `report_progress` report honestly as each job lands - with uneven job
|
|
97
103
|
costs (see ex03), an early-submitted heavy job can finish long after
|
|
@@ -124,7 +130,13 @@ def process_jobs(
|
|
|
124
130
|
if task is None:
|
|
125
131
|
task = jobs[0]["func"].__name__
|
|
126
132
|
blobs = [cloudpickle.dumps(job) for job in jobs]
|
|
127
|
-
|
|
133
|
+
# `Pool(processes=n)` spawns exactly n OS processes immediately, whether
|
|
134
|
+
# or not there's n jobs' worth of work - never spin up more workers than
|
|
135
|
+
# there are jobs to hand them. This only ever clamps downward: when jobs
|
|
136
|
+
# outnumber n_workers, no clamp is needed at all - the same fixed pool
|
|
137
|
+
# keeps pulling jobs off the queue, one per worker at a time, until the
|
|
138
|
+
# whole list is done. Worker count is sized to hardware, not job count.
|
|
139
|
+
pool = mp.Pool(processes=min(n_workers, len(jobs)))
|
|
128
140
|
out: list[Any] = []
|
|
129
141
|
time0 = time.time()
|
|
130
142
|
try:
|
|
@@ -37,9 +37,10 @@ from typing import Any, Callable, Iterator
|
|
|
37
37
|
|
|
38
38
|
from tqdm import tqdm
|
|
39
39
|
|
|
40
|
+
from mpengine.banner import print_banner
|
|
40
41
|
from mpengine.engine import expand_call, process_jobs, process_jobs_
|
|
41
42
|
|
|
42
|
-
N_WORKERS =
|
|
43
|
+
N_WORKERS = os.cpu_count() or 4
|
|
43
44
|
|
|
44
45
|
# one FileHandler-backed logger per worker *process*, lazily created on that
|
|
45
46
|
# process's first job and reused for every job after - keyed by pid, which is
|
|
@@ -302,7 +303,7 @@ def run(
|
|
|
302
303
|
save_fn: Callable[[Any, Path], None] = save_pickle,
|
|
303
304
|
labels: list[str] | None = None,
|
|
304
305
|
task: str | None = None,
|
|
305
|
-
|
|
306
|
+
n_workers: int = N_WORKERS,
|
|
306
307
|
debug: bool = False,
|
|
307
308
|
show_progress: bool = False,
|
|
308
309
|
) -> RunSummary:
|
|
@@ -385,7 +386,7 @@ def run(
|
|
|
385
386
|
f.write(f"launched: {dt.datetime.now().isoformat(sep=' ', timespec='seconds')}\n")
|
|
386
387
|
f.write(f"func: {func.__name__}\n")
|
|
387
388
|
f.write(f"task: {task}\n")
|
|
388
|
-
f.write(f"
|
|
389
|
+
f.write(f"n_workers: {n_workers}\n")
|
|
389
390
|
f.write(f"debug: {debug}\n")
|
|
390
391
|
f.write(f"n_jobs: {len(jobs)}\n")
|
|
391
392
|
f.write(f"output_dir: {output_path}\n")
|
|
@@ -397,14 +398,16 @@ def run(
|
|
|
397
398
|
|
|
398
399
|
worker_stats: dict[int, WorkerStats] = {}
|
|
399
400
|
|
|
401
|
+
print_banner(task, n_workers, debug)
|
|
402
|
+
|
|
400
403
|
t0 = time.perf_counter()
|
|
401
404
|
if debug:
|
|
402
405
|
raw_results = process_jobs_(jobs)
|
|
403
406
|
elif show_progress:
|
|
404
407
|
with _progress_renderer(len(jobs), task, worker_stats) as on_progress:
|
|
405
|
-
raw_results = process_jobs(jobs, task=task,
|
|
408
|
+
raw_results = process_jobs(jobs, task=task, n_workers=n_workers, on_progress=on_progress)
|
|
406
409
|
else:
|
|
407
|
-
raw_results = process_jobs(jobs, task=task,
|
|
410
|
+
raw_results = process_jobs(jobs, task=task, n_workers=n_workers)
|
|
408
411
|
elapsed_s = time.perf_counter() - t0
|
|
409
412
|
|
|
410
413
|
if show_progress and not debug:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
; ,
|
|
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
|
+
"' '"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpengine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -122,7 +122,7 @@ run with a custom `save_fn`, pass the matching reader as
|
|
|
122
122
|
| `save_fn` | how each result is written. Default `save_pickle`; supply your own (e.g. parquet) |
|
|
123
123
|
| `labels` | names for each job; defaults to `job_0000`, `job_0001`, … |
|
|
124
124
|
| `task` | names the run; defaults to `func.__name__` |
|
|
125
|
-
| `
|
|
125
|
+
| `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 |
|
|
126
126
|
| `debug` | run sequentially in-process — real tracebacks you can attach a debugger to, no pool |
|
|
127
127
|
| `show_progress` | live terminal display: one overall bar for the whole run, plus a live rate number per worker process. Ignored when `debug=True` |
|
|
128
128
|
|
|
@@ -150,7 +150,7 @@ you want to drive the pool yourself:
|
|
|
150
150
|
from mpengine import expand_call, process_jobs, process_jobs_
|
|
151
151
|
|
|
152
152
|
jobs = [{"func": my_task, "x": 1, "y": 2}, {"func": other_task, "n": 5}]
|
|
153
|
-
results = process_jobs(jobs
|
|
153
|
+
results = process_jobs(jobs) # n_workers defaults to os.cpu_count(); or process_jobs_ to stay sequential
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
A job is just a dict carrying its own callback plus that callback's kwargs, so a
|
|
@@ -2,9 +2,11 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/mpengine/__init__.py
|
|
5
|
+
src/mpengine/banner.py
|
|
5
6
|
src/mpengine/engine.py
|
|
6
7
|
src/mpengine/orchestrator.py
|
|
7
8
|
src/mpengine/partition.py
|
|
9
|
+
src/mpengine/spider.txt
|
|
8
10
|
src/mpengine/worker_names.json
|
|
9
11
|
src/mpengine.egg-info/PKG-INFO
|
|
10
12
|
src/mpengine.egg-info/SOURCES.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|