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.
- polymorph_ai-1.1.0/LICENSE +21 -0
- polymorph_ai-1.1.0/PKG-INFO +128 -0
- polymorph_ai-1.1.0/README.md +104 -0
- polymorph_ai-1.1.0/polymorph_ai/__init__.py +24 -0
- polymorph_ai-1.1.0/polymorph_ai/decorator.py +480 -0
- polymorph_ai-1.1.0/polymorph_ai/features.py +289 -0
- polymorph_ai-1.1.0/polymorph_ai/model.npz +0 -0
- polymorph_ai-1.1.0/polymorph_ai/model.py +48 -0
- polymorph_ai-1.1.0/polymorph_ai.egg-info/PKG-INFO +128 -0
- polymorph_ai-1.1.0/polymorph_ai.egg-info/SOURCES.txt +14 -0
- polymorph_ai-1.1.0/polymorph_ai.egg-info/dependency_links.txt +1 -0
- polymorph_ai-1.1.0/polymorph_ai.egg-info/requires.txt +2 -0
- polymorph_ai-1.1.0/polymorph_ai.egg-info/top_level.txt +1 -0
- polymorph_ai-1.1.0/pyproject.toml +34 -0
- polymorph_ai-1.1.0/setup.cfg +4 -0
- polymorph_ai-1.1.0/tests/test_adaptive_exec.py +259 -0
|
@@ -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"]
|