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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpengine
3
- Version: 0.1.2
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
- | `n_threads` | worker count; defaults to `min(cpu_count, 8)` |
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, n_threads=8) # or process_jobs_ to stay sequential
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
- | `n_threads` | worker count; defaults to `min(cpu_count, 8)` |
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, n_threads=8) # or process_jobs_ to stay sequential
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.1.2"
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 is data, not code - setuptools' package discovery above
53
- # only picks up .py files by default, so it needs to be listed explicitly to
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,
@@ -73,4 +73,4 @@ __all__ = [
73
73
  "parts_to_molecules",
74
74
  ]
75
75
 
76
- __version__ = "0.1.2"
76
+ __version__ = "0.2.0"
@@ -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
- n_threads: int = 24,
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
- pool = mp.Pool(processes=n_threads)
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 = min(os.cpu_count() or 4, 8)
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
- n_threads: int = N_WORKERS,
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"n_threads: {n_threads}\n")
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, n_threads=n_threads, on_progress=on_progress)
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, n_threads=n_threads)
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.1.2
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
- | `n_threads` | worker count; defaults to `min(cpu_count, 8)` |
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, n_threads=8) # or process_jobs_ to stay sequential
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