polymorph-ai 1.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the polymorph-ai authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: polymorph-ai
3
+ Version: 1.1.0
4
+ Summary: A decorator that uses a trained neural network to run your function sequentially, with threads, or with processes - whichever is fastest.
5
+ Author: Worachat Songmuangnu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Worachat-Songmuangnu/polymorph-ai
8
+ Project-URL: Source, https://github.com/Worachat-Songmuangnu/polymorph-ai
9
+ Project-URL: Issues, https://github.com/Worachat-Songmuangnu/polymorph-ai/issues
10
+ Keywords: parallel,multiprocessing,threading,decorator,machine-learning,performance
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: System :: Distributed Computing
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy
22
+ Requires-Dist: cloudpickle
23
+ Dynamic: license-file
24
+
25
+ # polymorph-ai
26
+
27
+ `@adaptive_exec` runs a function over a list of items **sequentially, with threads, or with processes**, and a small trained neural network picks whichever mode should be fastest for that job on that machine.
28
+
29
+ ```python
30
+ from polymorph_ai import adaptive_exec
31
+
32
+ @adaptive_exec
33
+ def process(item):
34
+ ...
35
+
36
+ if __name__ == "__main__": # required on Windows/macOS for processes
37
+ results = process.map(items) # same as [process(x) for x in items], in order
38
+ print(process.last_run)
39
+ # [polymorph_ai] process: 5,000 items -> multiprocessing p=0.97 (model) | overhead 0.7 ms | total 1.84 s
40
+ ```
41
+
42
+ `process(x)` still works as a normal function call. Only `.map()` adds the automatic part.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install polymorph-ai
48
+ ```
49
+
50
+ Dependencies: numpy and cloudpickle. Tested on Linux with Python 3.10, 3.12 and 3.14, and on Windows with Python 3.14.
51
+
52
+ Works in Jupyter too: a function written in a notebook cell is sent to the worker processes by value (with cloudpickle), so it can still use multiprocessing on Windows and macOS.
53
+
54
+ ## How it decides
55
+
56
+ Every `.map(items)` call goes through these steps:
57
+
58
+ 1. **Safety checks.** Empty or very short lists, one worker, or a call nested inside another polymorph_ai worker all just run as a plain loop. If the function or the items cannot be sent to another process (a lambda, a function defined inside another function, unpicklable items), multiprocessing is ruled out.
59
+ 2. **First item.** The first item runs and is timed. If `time × number of items` is under 10 ms, the job is too small for any pool to pay off, so the rest runs as a plain loop.
60
+ 3. **Cache.** A call that looks like an earlier one (similar number of items, first-item time, and item size) reuses that decision and skips the probe.
61
+ 4. **Probe.** The next items run for about 50 ms, first one by one and then on 2 threads, to measure the 13 features: call site, static code analysis (AST), runtime probe, and thread probe. The probe uses the same code (`polymorph_ai/features.py`) that collected the training data. **Probed items are real work: their results are kept and no item ever runs twice.**
62
+ 5. **Model.** An MLP (13 → 225 ReLU → 3 softmax, 3,828 weights) turns the features into probabilities. It is trained in Keras and runs here in numpy, at about 30 µs per decision.
63
+ 6. **Run.** The remaining items run in the chosen mode on a pool that stays alive for the next call.
64
+
65
+ ## Options
66
+
67
+ ```python
68
+ @adaptive_exec(n_workers=4, verbose=True, cache=True) # verbose prints every decision
69
+ def process(item): ...
70
+
71
+ process.map(items, mode="threading") # force one mode for this call
72
+ # POLYMORPH_MODE=sequential python app.py # force one mode for the whole program (A/B testing, debugging)
73
+
74
+ from polymorph_ai import run, warm_up
75
+ results, decision = run(some_function, items) # for functions you cannot decorate
76
+ warm_up() # start the pools early (e.g. at server start-up)
77
+ ```
78
+
79
+ `process.last_run` (a `Decision`) records the chosen mode, why it was chosen, the model's probabilities, the 13 features, how many items the probe computed, and the overhead.
80
+
81
+ ## Results
82
+
83
+ **Model** (13,603 jobs collected on 16 machine settings, see [`dataset/`](https://github.com/Worachat-Songmuangnu/polymorph-ai/tree/main/dataset), GroupKFold by workload template, so every test job comes from code the model never trained on). Time lost compared with always picking the fastest mode:
84
+
85
+ - Always multiprocessing: +22.1%
86
+ - if-else rules: +13.0%
87
+ - Random Forest: +5.8%
88
+ - **MLP: +4.3%**
89
+
90
+ **Library** ([`examples/demo.py`](https://github.com/Worachat-Songmuangnu/polymorph-ai/blob/main/examples/demo.py): 8 everyday jobs that are not in the training data, 8 CPUs). Total time compared with perfect picks:
91
+
92
+ | | Windows | Linux (WSL) |
93
+ |---|---|---|
94
+ | always sequential | +349% | +426% |
95
+ | always threading | +117% | +138% |
96
+ | always multiprocessing | +19% | +30% |
97
+ | polymorph_ai, first call | +52% | +66% |
98
+ | polymorph_ai, repeated call | **+13%** | **+24%** |
99
+
100
+ **When does it pay off?** The first call on a job pays about 0.2 s for the probe (`python examples/demo.py --overhead`).
101
+
102
+ - Compared with a plain loop, polymorph_ai is already faster on jobs from about 0.3–0.6 s.
103
+ - Compared with a perfect choice of mode, the first call is within 15% once the job takes 5 s or more.
104
+ - Repeated calls skip the probe, so they cost almost nothing.
105
+
106
+ ## Limitations
107
+
108
+ - The decorated function takes **one item** and is applied to every item (`map`). polymorph_ai cannot parallelise code that does not have this shape.
109
+ - On Windows and macOS, the script that uses processes needs the `if __name__ == "__main__":` guard (a Python rule for processes, not specific to polymorph_ai).
110
+ - The first item and the probe items run in the calling thread, so a job with a few very long items loses one item's worth of parallel time.
111
+ - The model was trained on synthetic workloads from 13 families. Code that behaves unlike all of them can still be mispredicted. In the demo, sorting 20k-number lists stays sequential when processes would be twice as fast.
112
+ - asyncio is not supported: a decorator cannot turn ordinary code into `async def` code.
113
+ - In Jupyter on Windows, call `shutdown()` before you close or restart the kernel. If the kernel is stopped straight after the cell that started the process pool, the worker processes can be left running.
114
+
115
+ ## Tests
116
+
117
+ ```bash
118
+ python -m pytest tests -v # 28 tests, pass on Windows and Linux
119
+ ```
120
+
121
+ ## Project layout
122
+
123
+ ```
124
+ polymorph_ai/ the library: decorator.py, features.py, model.py + model.npz (the trained network)
125
+ tests/ pytest suite
126
+ examples/ demo.py (benchmark vs fixed modes) and its results/
127
+ dataset/ the training dataset (13,603 jobs) and the real-code results (92 jobs)
128
+ ```
@@ -0,0 +1,104 @@
1
+ # polymorph-ai
2
+
3
+ `@adaptive_exec` runs a function over a list of items **sequentially, with threads, or with processes**, and a small trained neural network picks whichever mode should be fastest for that job on that machine.
4
+
5
+ ```python
6
+ from polymorph_ai import adaptive_exec
7
+
8
+ @adaptive_exec
9
+ def process(item):
10
+ ...
11
+
12
+ if __name__ == "__main__": # required on Windows/macOS for processes
13
+ results = process.map(items) # same as [process(x) for x in items], in order
14
+ print(process.last_run)
15
+ # [polymorph_ai] process: 5,000 items -> multiprocessing p=0.97 (model) | overhead 0.7 ms | total 1.84 s
16
+ ```
17
+
18
+ `process(x)` still works as a normal function call. Only `.map()` adds the automatic part.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install polymorph-ai
24
+ ```
25
+
26
+ Dependencies: numpy and cloudpickle. Tested on Linux with Python 3.10, 3.12 and 3.14, and on Windows with Python 3.14.
27
+
28
+ Works in Jupyter too: a function written in a notebook cell is sent to the worker processes by value (with cloudpickle), so it can still use multiprocessing on Windows and macOS.
29
+
30
+ ## How it decides
31
+
32
+ Every `.map(items)` call goes through these steps:
33
+
34
+ 1. **Safety checks.** Empty or very short lists, one worker, or a call nested inside another polymorph_ai worker all just run as a plain loop. If the function or the items cannot be sent to another process (a lambda, a function defined inside another function, unpicklable items), multiprocessing is ruled out.
35
+ 2. **First item.** The first item runs and is timed. If `time × number of items` is under 10 ms, the job is too small for any pool to pay off, so the rest runs as a plain loop.
36
+ 3. **Cache.** A call that looks like an earlier one (similar number of items, first-item time, and item size) reuses that decision and skips the probe.
37
+ 4. **Probe.** The next items run for about 50 ms, first one by one and then on 2 threads, to measure the 13 features: call site, static code analysis (AST), runtime probe, and thread probe. The probe uses the same code (`polymorph_ai/features.py`) that collected the training data. **Probed items are real work: their results are kept and no item ever runs twice.**
38
+ 5. **Model.** An MLP (13 → 225 ReLU → 3 softmax, 3,828 weights) turns the features into probabilities. It is trained in Keras and runs here in numpy, at about 30 µs per decision.
39
+ 6. **Run.** The remaining items run in the chosen mode on a pool that stays alive for the next call.
40
+
41
+ ## Options
42
+
43
+ ```python
44
+ @adaptive_exec(n_workers=4, verbose=True, cache=True) # verbose prints every decision
45
+ def process(item): ...
46
+
47
+ process.map(items, mode="threading") # force one mode for this call
48
+ # POLYMORPH_MODE=sequential python app.py # force one mode for the whole program (A/B testing, debugging)
49
+
50
+ from polymorph_ai import run, warm_up
51
+ results, decision = run(some_function, items) # for functions you cannot decorate
52
+ warm_up() # start the pools early (e.g. at server start-up)
53
+ ```
54
+
55
+ `process.last_run` (a `Decision`) records the chosen mode, why it was chosen, the model's probabilities, the 13 features, how many items the probe computed, and the overhead.
56
+
57
+ ## Results
58
+
59
+ **Model** (13,603 jobs collected on 16 machine settings, see [`dataset/`](https://github.com/Worachat-Songmuangnu/polymorph-ai/tree/main/dataset), GroupKFold by workload template, so every test job comes from code the model never trained on). Time lost compared with always picking the fastest mode:
60
+
61
+ - Always multiprocessing: +22.1%
62
+ - if-else rules: +13.0%
63
+ - Random Forest: +5.8%
64
+ - **MLP: +4.3%**
65
+
66
+ **Library** ([`examples/demo.py`](https://github.com/Worachat-Songmuangnu/polymorph-ai/blob/main/examples/demo.py): 8 everyday jobs that are not in the training data, 8 CPUs). Total time compared with perfect picks:
67
+
68
+ | | Windows | Linux (WSL) |
69
+ |---|---|---|
70
+ | always sequential | +349% | +426% |
71
+ | always threading | +117% | +138% |
72
+ | always multiprocessing | +19% | +30% |
73
+ | polymorph_ai, first call | +52% | +66% |
74
+ | polymorph_ai, repeated call | **+13%** | **+24%** |
75
+
76
+ **When does it pay off?** The first call on a job pays about 0.2 s for the probe (`python examples/demo.py --overhead`).
77
+
78
+ - Compared with a plain loop, polymorph_ai is already faster on jobs from about 0.3–0.6 s.
79
+ - Compared with a perfect choice of mode, the first call is within 15% once the job takes 5 s or more.
80
+ - Repeated calls skip the probe, so they cost almost nothing.
81
+
82
+ ## Limitations
83
+
84
+ - The decorated function takes **one item** and is applied to every item (`map`). polymorph_ai cannot parallelise code that does not have this shape.
85
+ - On Windows and macOS, the script that uses processes needs the `if __name__ == "__main__":` guard (a Python rule for processes, not specific to polymorph_ai).
86
+ - The first item and the probe items run in the calling thread, so a job with a few very long items loses one item's worth of parallel time.
87
+ - The model was trained on synthetic workloads from 13 families. Code that behaves unlike all of them can still be mispredicted. In the demo, sorting 20k-number lists stays sequential when processes would be twice as fast.
88
+ - asyncio is not supported: a decorator cannot turn ordinary code into `async def` code.
89
+ - In Jupyter on Windows, call `shutdown()` before you close or restart the kernel. If the kernel is stopped straight after the cell that started the process pool, the worker processes can be left running.
90
+
91
+ ## Tests
92
+
93
+ ```bash
94
+ python -m pytest tests -v # 28 tests, pass on Windows and Linux
95
+ ```
96
+
97
+ ## Project layout
98
+
99
+ ```
100
+ polymorph_ai/ the library: decorator.py, features.py, model.py + model.npz (the trained network)
101
+ tests/ pytest suite
102
+ examples/ demo.py (benchmark vs fixed modes) and its results/
103
+ dataset/ the training dataset (13,603 jobs) and the real-code results (92 jobs)
104
+ ```
@@ -0,0 +1,24 @@
1
+ """polymorph_ai: pick sequential / threading / multiprocessing automatically.
2
+
3
+ from polymorph_ai import adaptive_exec
4
+
5
+ @adaptive_exec
6
+ def process(item):
7
+ ...
8
+
9
+ results = process.map(items)
10
+ print(process.last_run)
11
+ """
12
+
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
15
+ from .decorator import Decision, adaptive_exec, run, shutdown, warm_up
16
+ from .features import FEATURE_NAMES, extract_features
17
+
18
+ try:
19
+ __version__ = version("polymorph-ai")
20
+ except PackageNotFoundError: # running from a source checkout that is not installed
21
+ __version__ = "unknown"
22
+
23
+ __all__ = ["adaptive_exec", "run", "warm_up", "shutdown", "Decision",
24
+ "FEATURE_NAMES", "extract_features"]