bio-pype-runtime-models 0.1.0__py3-none-any.whl
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.
- bio_pype_runtime_models-0.1.0.dist-info/METADATA +289 -0
- bio_pype_runtime_models-0.1.0.dist-info/RECORD +20 -0
- bio_pype_runtime_models-0.1.0.dist-info/WHEEL +5 -0
- bio_pype_runtime_models-0.1.0.dist-info/entry_points.txt +2 -0
- bio_pype_runtime_models-0.1.0.dist-info/top_level.txt +1 -0
- pype_runtime_models/__init__.py +57 -0
- pype_runtime_models/__main__.py +4 -0
- pype_runtime_models/cli.py +214 -0
- pype_runtime_models/evaluate.py +217 -0
- pype_runtime_models/features.py +132 -0
- pype_runtime_models/gpu.py +105 -0
- pype_runtime_models/ingest.py +276 -0
- pype_runtime_models/model.py +521 -0
- pype_runtime_models/power.py +133 -0
- pype_runtime_models/predict.py +187 -0
- pype_runtime_models/provenance.py +111 -0
- pype_runtime_models/regression.py +267 -0
- pype_runtime_models/train.py +196 -0
- pype_runtime_models/units.py +92 -0
- pype_runtime_models/workflow.py +89 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bio-pype-runtime-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Learn per-workflow resource, timeline and power-curve models from bio-pype runtime dumps.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: numpy>=1.23
|
|
8
|
+
Provides-Extra: pype
|
|
9
|
+
Requires-Dist: bio-pype; extra == "pype"
|
|
10
|
+
|
|
11
|
+
# bio-pype-runtime-models
|
|
12
|
+
|
|
13
|
+
Learn per-workflow **resource**, **timeline** and **power-curve** models from
|
|
14
|
+
[Bio-Pype](https://bio-pype.readthedocs.io) runtime data, then use them to
|
|
15
|
+
predict a new run's cost from a single input size, to grade a finished run
|
|
16
|
+
against its own baseline, and to suggest better default requirements.
|
|
17
|
+
|
|
18
|
+
Built to be imported into other projects ([compute.bio](https://compute.bio)
|
|
19
|
+
trains and serves these models from its database). Pure `numpy`; models
|
|
20
|
+
serialise to plain JSON — no pickle.
|
|
21
|
+
|
|
22
|
+
> **Status: alpha.** The modelling choices and the JSON model format will
|
|
23
|
+
> change between releases, and estimates from few runs are rough. Retrain
|
|
24
|
+
> after upgrading rather than loading models written by an older version.
|
|
25
|
+
|
|
26
|
+
The distribution is `bio-pype-runtime-models`; the import name and the command
|
|
27
|
+
are `pype_runtime_models` / `pype-runtime-models`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install bio-pype-runtime-models
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python >= 3.9 and `numpy`. Bio-Pype itself is not needed: the package
|
|
36
|
+
only reads the runtime data Bio-Pype writes.
|
|
37
|
+
|
|
38
|
+
## Input data
|
|
39
|
+
|
|
40
|
+
One *run* is the content of a Bio-Pype `pipeline_runtime.yaml`: a mapping of
|
|
41
|
+
job id to job record (command, status, timings, `requirements`,
|
|
42
|
+
`resource_consumption`, `data_sizes`), plus run metadata and environment. It
|
|
43
|
+
can come from:
|
|
44
|
+
|
|
45
|
+
- a directory of `*.json` files, one per run ("dumps");
|
|
46
|
+
- parsed `Run` objects, built with `Run.from_dict(mapping)`, when the runs live
|
|
47
|
+
somewhere else, such as a database.
|
|
48
|
+
|
|
49
|
+
## What it does (mapped to the original goals)
|
|
50
|
+
|
|
51
|
+
| Goal | Where |
|
|
52
|
+
|------|-------|
|
|
53
|
+
| Detect which workflow a runtime object is | `workflow.detect_workflow` — `pipeline_name`, else a snippet-set signature match |
|
|
54
|
+
| One model per workflow, improving as more runs land | `train.train_models` rebuilds the whole registry each time; safe to schedule |
|
|
55
|
+
| Per-step resource consumption, spot deviations | `StepModel` per snippet; `evaluate_run` flags per-step anomalies |
|
|
56
|
+
| Predict timeline + resources of each step from input size | `predict.predict_run` |
|
|
57
|
+
| Flag steps whose memory/CPU is driven by input size | `StepModel.input_sensitive` / `sensitivity_report` |
|
|
58
|
+
| Recommend raising/lowering the default requirement | `StepModel.recommend` → increase / decrease / ok |
|
|
59
|
+
| Post-hoc performance eval, overall and per-step | `evaluate.evaluate_run` |
|
|
60
|
+
| Predict the power curve for a workflow | `predict.predict_power_curve` (experimental, see below) |
|
|
61
|
+
|
|
62
|
+
## Workflow identity & provenance
|
|
63
|
+
|
|
64
|
+
Snippets live in `$PYPE_MODULES/snippets` and are **shared by every pipeline in a
|
|
65
|
+
module**, so the natural owner of a snippet's resource model is the module, not
|
|
66
|
+
the individual pipeline. Identity is therefore pluggable (`--identity` on train,
|
|
67
|
+
`identity_mode=` in the API):
|
|
68
|
+
|
|
69
|
+
- **`module`** (default) — key models by the `PYPE_MODULES` basename, pooling
|
|
70
|
+
every pipeline in the module. Shared snippets like `mosdepth` train on all
|
|
71
|
+
their samples at once (e.g. n=116 across the 5 `ensemble_sv` pipelines instead
|
|
72
|
+
of split five ways). Resource models are pooled; each pipeline keeps its own
|
|
73
|
+
timeline view (snippet set + start offsets), so `predict <module> --pipeline X`
|
|
74
|
+
gives X's schedule against the pooled models.
|
|
75
|
+
- **`pipeline`** — the older one-model-per-`pipeline_name` behaviour.
|
|
76
|
+
- **`registry`** — key by a versioned `namespace/name:version` parsed from a
|
|
77
|
+
registry-style module path (e.g. `.../registry/sci-bioinformatics/somatic:20260701`).
|
|
78
|
+
This is the collision-proof, version-aware identity the module registry will
|
|
79
|
+
provide; it falls back to the module basename when no registry info is present.
|
|
80
|
+
|
|
81
|
+
Every model records its provenance (`module_paths`, `namespaces`, `versions`,
|
|
82
|
+
`pipeline_names`, `profiles`), so the day the registry lands, switching to
|
|
83
|
+
version-aware keying — or sharing/federating models at the app level — is a
|
|
84
|
+
config change, not a rewrite. `predict`/`evaluate` accept either an identity key
|
|
85
|
+
or a `pipeline_name` (auto-resolved to its module).
|
|
86
|
+
|
|
87
|
+
Provenance includes module paths, which often contain user or group names.
|
|
88
|
+
Strip it before showing a model to anyone other than the people whose runs
|
|
89
|
+
trained it.
|
|
90
|
+
|
|
91
|
+
Caveat for `module` mode: two *unrelated* modules that happen to share a basename
|
|
92
|
+
(your `somatic` vs a third party's) would be merged — the registry namespace is
|
|
93
|
+
what ultimately disambiguates them.
|
|
94
|
+
|
|
95
|
+
## Key modelling choices
|
|
96
|
+
|
|
97
|
+
- **Canonical step identity** is the *snippet name* parsed from the job command
|
|
98
|
+
(`ingest.snippet_name_from_command`), not the `name` field — the latter carries
|
|
99
|
+
per-sample suffixes (`mosdepth_EGAD2585`) that would fragment the models.
|
|
100
|
+
- **Driver size.** Each step is regressed against its own *driver size*:
|
|
101
|
+
`input_size_bytes`, or `output_size_bytes` when the input is 0 (generators /
|
|
102
|
+
aggregators). Because a caller supplies only one number at prediction time, we
|
|
103
|
+
also learn how each step's driver scales with the **run-level** driver
|
|
104
|
+
(`driver_scale`), so the whole chain is predictable from a single input size.
|
|
105
|
+
The run driver is the largest input among the earliest-submitted jobs — the
|
|
106
|
+
recorded `dependencies` are often partial in dumps, so we don't rely on a
|
|
107
|
+
strict topological root.
|
|
108
|
+
- **Per-step fit** picks the best of `constant` / `linear` / `power` (power fit
|
|
109
|
+
and scored in log space for numerical stability) by **leave-one-out R²**, and
|
|
110
|
+
only beats the constant baseline if it generalises — noisy/sparse steps stay
|
|
111
|
+
constant instead of overfitting.
|
|
112
|
+
- **Few samples means no size dependence.** A step needs at least 5 samples
|
|
113
|
+
with differing driver sizes before a size-dependent fit is even tried. Below
|
|
114
|
+
that the prediction is the median of what was seen, whatever the input size:
|
|
115
|
+
three runs of a workflow give the same expected duration for a small and a
|
|
116
|
+
large input.
|
|
117
|
+
- **Robust throughout**: medians, MAD, and Spearman rank correlation, because
|
|
118
|
+
runtime telemetry is heavy-tailed. Predictions are capped at 50× the largest
|
|
119
|
+
observed value so queries far outside the training range can't blow up.
|
|
120
|
+
|
|
121
|
+
## How a run is graded
|
|
122
|
+
|
|
123
|
+
`evaluate_run` compares each completed step with the prediction for that
|
|
124
|
+
step's own driver size:
|
|
125
|
+
|
|
126
|
+
- A step is **flagged** when duration or peak memory is at least 3 robust
|
|
127
|
+
standard deviations above the prediction, or at least 2x the prediction.
|
|
128
|
+
Average CPU is reported for context and never flagged.
|
|
129
|
+
- The spread used for the z-score has a floor of 5% of the predicted value, so
|
|
130
|
+
a step whose training runs happened to be almost identical does not flag
|
|
131
|
+
every normal variation.
|
|
132
|
+
- Steps with no model are reported as `unmodelled`, not dropped.
|
|
133
|
+
- The overall `performance_score` (0-100) and `verdict` come from the duration
|
|
134
|
+
z-scores. The verdict is `unknown` when no step's duration could be graded.
|
|
135
|
+
|
|
136
|
+
## Requirement recommendations
|
|
137
|
+
|
|
138
|
+
`StepModel.recommend` compares the default requirement recorded with the jobs
|
|
139
|
+
to what the step used: predicted peak memory with 1.3x headroom, predicted
|
|
140
|
+
duration with 1.5x headroom. It suggests lowering a requirement that is more
|
|
141
|
+
than 3x above the 95th percentile of use. Check a suggestion against the
|
|
142
|
+
underlying measurements before applying it; a bad measurement in the training
|
|
143
|
+
runs produces a bad suggestion.
|
|
144
|
+
|
|
145
|
+
## GPU steps
|
|
146
|
+
|
|
147
|
+
Steps are also grouped by a vendor-neutral GPU class. Once a class has at
|
|
148
|
+
least 3 samples the model learns a duration factor for it relative to the
|
|
149
|
+
size-based prediction; `--gpu-class` / `gpu_class=` applies it, and omitting it
|
|
150
|
+
predicts for the mix of cards seen in training.
|
|
151
|
+
|
|
152
|
+
## Data hygiene (split runs & mis-measured inputs)
|
|
153
|
+
|
|
154
|
+
These matter because of how runs are actually dumped, and are handled generally
|
|
155
|
+
— **no per-snippet logic is hardcoded anywhere**:
|
|
156
|
+
|
|
157
|
+
- **Split / partial runs are kept, not discarded.** A logical run is often
|
|
158
|
+
spread across several dump files: a failed run is dropped, a resumed run
|
|
159
|
+
completes the same file, and a *relaunched* run starts a fresh file missing
|
|
160
|
+
the steps that already succeeded. Every completed step in every file is a
|
|
161
|
+
valid per-step sample, so all of them train the per-step models. The
|
|
162
|
+
`quality` report shows how many runs per workflow are partial.
|
|
163
|
+
- **Run-level input size is only trusted when it's actually present.** Each
|
|
164
|
+
workflow's input-tier ("entry") snippets are learned from the data (the steps
|
|
165
|
+
whose driver size is largest by a wide margin). A partial file that lacks an
|
|
166
|
+
entry step contributes its per-step samples but no run-level driver, so it
|
|
167
|
+
can't feed the chain-scaling model a misleading input size.
|
|
168
|
+
- **Implausible driver sizes are dropped per step.** Some inputs are folders
|
|
169
|
+
historically mis-measured as a few bytes, or are overwritten in place with a
|
|
170
|
+
dummy value by convert/cleanup steps. A driver is dropped only when it is
|
|
171
|
+
*both* < 1 MB *and* > 10,000× smaller than that step's own largest driver —
|
|
172
|
+
a general statistical test that removes the bogus cluster while leaving
|
|
173
|
+
genuinely small-input steps (all a few KB) untouched. Steps left with no
|
|
174
|
+
size signal fall back to a constant (median) model, which is correct for
|
|
175
|
+
cleanup-style steps whose cost isn't driven by input size. Counts surface in
|
|
176
|
+
the `quality` report and per-step `n_driver_outliers`.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python -m pype_runtime_models quality models/ # partial runs, dropped outliers, entry snippets
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## CLI
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# train / retrain from a dumps directory
|
|
186
|
+
pype-runtime-models train dumps/ --out models/
|
|
187
|
+
|
|
188
|
+
# list what got trained
|
|
189
|
+
pype-runtime-models workflows models/
|
|
190
|
+
|
|
191
|
+
# predict per-step resources + timeline for a 50 GB input
|
|
192
|
+
pype-runtime-models predict models/ ensemble_sv_TO --input-size 50gb --full
|
|
193
|
+
|
|
194
|
+
# grade a finished run
|
|
195
|
+
pype-runtime-models evaluate models/ dumps/<hash>.json
|
|
196
|
+
|
|
197
|
+
# requirement changes worth making
|
|
198
|
+
pype-runtime-models recommend models/ ensemble_sv_TO --changes-only
|
|
199
|
+
|
|
200
|
+
# learned per-GPU-class duration factors
|
|
201
|
+
pype-runtime-models gpu models/ ensemble_sv_TO
|
|
202
|
+
|
|
203
|
+
# predict the power curve (30-min bins by default; experimental)
|
|
204
|
+
pype-runtime-models power models/ ensemble_sv_TO --input-size 50gb --bin-minutes 60
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`train` options: `--identity module|pipeline|registry` (default `module`),
|
|
208
|
+
`--min-workflow-runs N` (skip identities seen in fewer runs, default 1) and
|
|
209
|
+
`--min-step-samples N` (skip steps with fewer completed jobs, default 3).
|
|
210
|
+
`python -m pype_runtime_models ...` is equivalent to the `pype-runtime-models`
|
|
211
|
+
command.
|
|
212
|
+
|
|
213
|
+
## Library
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from pype_runtime_models import (
|
|
217
|
+
Run, train_models, load_models, predict_run, evaluate_run,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
reg = train_models("dumps/", "models/") # or load_models("models/")
|
|
221
|
+
pred = predict_run(reg, "ensemble_sv_TO", input_size_bytes=50 * 1024**3)
|
|
222
|
+
report = evaluate_run(reg, "dumps/somehash.json")
|
|
223
|
+
|
|
224
|
+
print(report["verdict"], report["performance_score"])
|
|
225
|
+
for step in report["flagged_steps"]:
|
|
226
|
+
print(step["name"], step["flags"])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Without files, for runs held in a database or loaded from
|
|
230
|
+
`pipeline_runtime.yaml`:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
runs = [Run.from_dict(mapping) for mapping in runtime_mappings]
|
|
234
|
+
reg = train_models(runs, min_workflow_runs=3) # nothing written to disk
|
|
235
|
+
report = evaluate_run(reg, runs[0])
|
|
236
|
+
|
|
237
|
+
for snippet, recs in reg.get("ensemble_sv").all_recommendations().items():
|
|
238
|
+
...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`train_models` only writes to disk when given a models directory. A workflow
|
|
242
|
+
model round-trips through `WorkflowModel.to_json()`.
|
|
243
|
+
|
|
244
|
+
## Power curve (experimental)
|
|
245
|
+
|
|
246
|
+
`predict_power_curve` turns a predicted run into a binned power curve with a
|
|
247
|
+
copy of Bio-Pype's power estimate that is bundled with this package.
|
|
248
|
+
|
|
249
|
+
**Known issue:** when Bio-Pype is importable in the same environment, the
|
|
250
|
+
package tries to use Bio-Pype's own `pipeline_power_curve` instead, and with
|
|
251
|
+
current Bio-Pype that call fails with a `TypeError`. Training, prediction of
|
|
252
|
+
resources and timeline, evaluation and recommendations are not affected.
|
|
253
|
+
|
|
254
|
+
## Recurrent retraining
|
|
255
|
+
|
|
256
|
+
`train_models` is idempotent — point it at the growing dumps directory and it
|
|
257
|
+
rebuilds every workflow model from the full corpus, so new runs simply sharpen
|
|
258
|
+
the estimates. Schedule it every few hours, e.g. cron:
|
|
259
|
+
|
|
260
|
+
```cron
|
|
261
|
+
0 */4 * * * pype-runtime-models train /path/to/dumps --out /path/to/models \
|
|
262
|
+
>> /var/log/pype_models.log 2>&1
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Package layout
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
pype_runtime_models/
|
|
269
|
+
ingest.py Run/Job parsing, snippet name parsing, driver sizes
|
|
270
|
+
units.py memory / time requirement parsing
|
|
271
|
+
provenance.py module/registry provenance + pluggable identity key
|
|
272
|
+
gpu.py vendor-neutral GPU-class key + display name guess
|
|
273
|
+
workflow.py identity detection (provenance + signature fallback)
|
|
274
|
+
features.py per-step training samples, metric list
|
|
275
|
+
regression.py numpy constant/linear/power fitting + driver cleaning
|
|
276
|
+
model.py StepModel / WorkflowModel / PipelineView / ModelRegistry
|
|
277
|
+
train.py train_models / load_models (module-level pooling)
|
|
278
|
+
predict.py predict_run / predict_power_curve (GPU band, pipeline view)
|
|
279
|
+
evaluate.py evaluate_run (per-step + overall, GPU-adjusted grading)
|
|
280
|
+
power.py power estimate (bundled copy of Bio-Pype's, see Power curve)
|
|
281
|
+
cli.py argparse entrypoint
|
|
282
|
+
tests/ self-contained smoke test (no pytest needed)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The smoke test is not part of the published package. From a source checkout,
|
|
286
|
+
with a directory of runtime dumps:
|
|
287
|
+
`python -m pype_runtime_models.tests.test_smoke dumps/`
|
|
288
|
+
|
|
289
|
+
Source and issues: <https://codeberg.org/bio-pype/bio-pype-runtime-models>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pype_runtime_models/__init__.py,sha256=vIeLLymWIdcqolbunVP1Zx5W_LD8PUQwZvM5QUVh28M,1895
|
|
2
|
+
pype_runtime_models/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
|
|
3
|
+
pype_runtime_models/cli.py,sha256=oKnzcFhKiIZEvi38974Lwj8hbWmoVJwe5Ybx6nykZ6s,7770
|
|
4
|
+
pype_runtime_models/evaluate.py,sha256=c3PZIKUXwtcRI9kGbDIrbow8bCBfSBmetraOKeBulJg,8009
|
|
5
|
+
pype_runtime_models/features.py,sha256=FYut4gHuo493zJkukloyR37t00gDZKaSn9w7oa1AFEs,4794
|
|
6
|
+
pype_runtime_models/gpu.py,sha256=HI-e1hZhulKes_G5eLUMLlcPCjfs3fYXDUEIdPE8Vaw,4131
|
|
7
|
+
pype_runtime_models/ingest.py,sha256=1k96l72LoGfcKYry7Lvp3MovVc54YrLLkaybArDJANE,9854
|
|
8
|
+
pype_runtime_models/model.py,sha256=W9Pt1ZSIVn5dJjvEpJNeselchFwtZYcRrTllu7TsGBw,22111
|
|
9
|
+
pype_runtime_models/power.py,sha256=1H0GpSuE3xENi-kHB7g6WJzRHn3KDfnFulNgGpcoJj4,5898
|
|
10
|
+
pype_runtime_models/predict.py,sha256=WKiX1-thl6pBGNbw6vbxu66ua5aus9hyugovaTH4iIY,7848
|
|
11
|
+
pype_runtime_models/provenance.py,sha256=nun0BMCsbqMtBNqgv0ja_VEjSyV21vAyGLWeK9Fimks,4109
|
|
12
|
+
pype_runtime_models/regression.py,sha256=ELxrGItU2Nlj9T6wdOK1yPJCDbh-86ELQ2LVzhJau1Y,9098
|
|
13
|
+
pype_runtime_models/train.py,sha256=qgF1Y56HdGBgH4FB24GRvgW4MA7n3T5f_wdks61wcno,7881
|
|
14
|
+
pype_runtime_models/units.py,sha256=zmuQA_NMVhOJg76v5qLgBJ1zKE1QbOzGdHdJ0eFWZwI,2362
|
|
15
|
+
pype_runtime_models/workflow.py,sha256=h4IXuE1jcZuRXSn91GV3qeba-8LzZ630LcgYWfkyilw,2816
|
|
16
|
+
bio_pype_runtime_models-0.1.0.dist-info/METADATA,sha256=5JTdC80k4DURU7qwt-_j25V1-FweGTcgqW3jRS7LUKA,13610
|
|
17
|
+
bio_pype_runtime_models-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
18
|
+
bio_pype_runtime_models-0.1.0.dist-info/entry_points.txt,sha256=Ky6vP0b1kbCCqDS3dHBQR6O8qh5beZxDQLfWJP9uDGA,69
|
|
19
|
+
bio_pype_runtime_models-0.1.0.dist-info/top_level.txt,sha256=2A3h6AnHAQ7RPwRv75qrlKKpRtoFOKSmDG3E1BMasx8,20
|
|
20
|
+
bio_pype_runtime_models-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pype_runtime_models
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""pype_runtime_models — learn resource / timeline / power models from bio-pype runtime dumps.
|
|
2
|
+
|
|
3
|
+
The package turns a directory of runtime-dump JSON files (one per pipeline run,
|
|
4
|
+
as produced by dumping the runtime database) into per-workflow predictive models
|
|
5
|
+
that can:
|
|
6
|
+
|
|
7
|
+
* detect which workflow a runtime object belongs to,
|
|
8
|
+
* predict per-step duration / memory / CPU and the run timeline from a single
|
|
9
|
+
input size,
|
|
10
|
+
* flag steps whose resource use is driven by input size and recommend whether the
|
|
11
|
+
default ``requirements`` value should go up or down,
|
|
12
|
+
* synthesise the pipeline power curve for a predicted run, and
|
|
13
|
+
* evaluate a finished run (overall and per-step) against the learned baseline.
|
|
14
|
+
|
|
15
|
+
Typical use::
|
|
16
|
+
|
|
17
|
+
from pype_runtime_models import train_models, load_models, predict_run, evaluate_run
|
|
18
|
+
|
|
19
|
+
reg = train_models("dumps/", "models/") # recurrent retraining entrypoint
|
|
20
|
+
reg = load_models("models/")
|
|
21
|
+
pred = predict_run(reg, "ensemble_sv_TO", input_size_bytes=50e9)
|
|
22
|
+
report = evaluate_run(reg, "dumps/somerun.json")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .ingest import Job, Run, iter_runs, load_run
|
|
28
|
+
from .provenance import Provenance, extract_provenance, identity_key
|
|
29
|
+
from .workflow import WorkflowSignature, detect_identity, detect_workflow
|
|
30
|
+
from .model import StepModel, WorkflowModel, PipelineView, ModelRegistry
|
|
31
|
+
from .train import train_models, load_models
|
|
32
|
+
from .predict import predict_run, predict_power_curve
|
|
33
|
+
from .evaluate import evaluate_run
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Job",
|
|
39
|
+
"Run",
|
|
40
|
+
"load_run",
|
|
41
|
+
"iter_runs",
|
|
42
|
+
"Provenance",
|
|
43
|
+
"extract_provenance",
|
|
44
|
+
"identity_key",
|
|
45
|
+
"WorkflowSignature",
|
|
46
|
+
"detect_identity",
|
|
47
|
+
"detect_workflow",
|
|
48
|
+
"StepModel",
|
|
49
|
+
"WorkflowModel",
|
|
50
|
+
"PipelineView",
|
|
51
|
+
"ModelRegistry",
|
|
52
|
+
"train_models",
|
|
53
|
+
"load_models",
|
|
54
|
+
"predict_run",
|
|
55
|
+
"predict_power_curve",
|
|
56
|
+
"evaluate_run",
|
|
57
|
+
]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Command-line interface.
|
|
2
|
+
|
|
3
|
+
python -m pype_runtime_models train <dumps_dir> --out models/
|
|
4
|
+
python -m pype_runtime_models predict models/ <workflow> --input-size 50gb
|
|
5
|
+
python -m pype_runtime_models power models/ <workflow> --input-size 50gb
|
|
6
|
+
python -m pype_runtime_models evaluate models/ <dump.json>
|
|
7
|
+
python -m pype_runtime_models recommend models/ [<workflow>]
|
|
8
|
+
python -m pype_runtime_models workflows models/
|
|
9
|
+
python -m pype_runtime_models quality models/
|
|
10
|
+
python -m pype_runtime_models gpu models/ [<workflow>]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .evaluate import evaluate_run
|
|
21
|
+
from .predict import predict_power_curve, predict_run
|
|
22
|
+
from .train import load_models, train_models
|
|
23
|
+
from .units import parse_mem_bytes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_json(obj) -> None:
|
|
27
|
+
json.dump(obj, sys.stdout, indent=1, default=str)
|
|
28
|
+
sys.stdout.write("\n")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_size(value: Optional[str]) -> Optional[float]:
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
return parse_mem_bytes(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv=None) -> int:
|
|
38
|
+
p = argparse.ArgumentParser(prog="pype_runtime_models", description=__doc__)
|
|
39
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
40
|
+
|
|
41
|
+
t = sub.add_parser("train", help="fit/retrain models from a dumps directory")
|
|
42
|
+
t.add_argument("dumps_dir")
|
|
43
|
+
t.add_argument("--out", help="directory to write models into")
|
|
44
|
+
t.add_argument("--min-step-samples", type=int, default=3)
|
|
45
|
+
t.add_argument("--min-workflow-runs", type=int, default=1)
|
|
46
|
+
t.add_argument(
|
|
47
|
+
"--identity", choices=["module", "pipeline", "registry"], default="module",
|
|
48
|
+
help="grouping identity: module (default, pools shared snippets), pipeline, or registry",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
pr = sub.add_parser("predict", help="predict per-step resources + timeline")
|
|
52
|
+
pr.add_argument("models_dir")
|
|
53
|
+
pr.add_argument("workflow", help="identity key (module) or a pipeline name")
|
|
54
|
+
pr.add_argument("--pipeline", help="pipeline timeline to use (module mode)")
|
|
55
|
+
pr.add_argument("--input-size", help="input size, e.g. 50gb or raw bytes")
|
|
56
|
+
pr.add_argument("--gpu-class", help="target GPU class (e.g. 700W); omit for a fleet band")
|
|
57
|
+
pr.add_argument("--full", action="store_true", help="include per-step detail")
|
|
58
|
+
|
|
59
|
+
pw = sub.add_parser("power", help="predict the pipeline power curve")
|
|
60
|
+
pw.add_argument("models_dir")
|
|
61
|
+
pw.add_argument("workflow")
|
|
62
|
+
pw.add_argument("--pipeline")
|
|
63
|
+
pw.add_argument("--input-size")
|
|
64
|
+
pw.add_argument("--gpu-class")
|
|
65
|
+
pw.add_argument("--bin-minutes", type=float, default=30.0)
|
|
66
|
+
|
|
67
|
+
gp = sub.add_parser("gpu", help="show learned per-GPU-class duration factors")
|
|
68
|
+
gp.add_argument("models_dir")
|
|
69
|
+
gp.add_argument("workflow", nargs="?")
|
|
70
|
+
|
|
71
|
+
ev = sub.add_parser("evaluate", help="grade a finished run against the baseline")
|
|
72
|
+
ev.add_argument("models_dir")
|
|
73
|
+
ev.add_argument("dump")
|
|
74
|
+
ev.add_argument("--full", action="store_true", help="include per-step detail")
|
|
75
|
+
|
|
76
|
+
rc = sub.add_parser("recommend", help="requirement recommendations per step")
|
|
77
|
+
rc.add_argument("models_dir")
|
|
78
|
+
rc.add_argument("workflow", nargs="?")
|
|
79
|
+
rc.add_argument("--changes-only", action="store_true")
|
|
80
|
+
|
|
81
|
+
wl = sub.add_parser("workflows", help="list trained workflows")
|
|
82
|
+
wl.add_argument("models_dir")
|
|
83
|
+
|
|
84
|
+
ql = sub.add_parser("quality", help="data-quality report (partial runs, dropped outliers)")
|
|
85
|
+
ql.add_argument("models_dir")
|
|
86
|
+
|
|
87
|
+
args = p.parse_args(argv)
|
|
88
|
+
|
|
89
|
+
if args.cmd == "train":
|
|
90
|
+
reg = train_models(
|
|
91
|
+
args.dumps_dir,
|
|
92
|
+
args.out,
|
|
93
|
+
min_step_samples=args.min_step_samples,
|
|
94
|
+
min_workflow_runs=args.min_workflow_runs,
|
|
95
|
+
identity_mode=args.identity,
|
|
96
|
+
verbose=True,
|
|
97
|
+
)
|
|
98
|
+
_print_json(
|
|
99
|
+
{
|
|
100
|
+
"trained_at": reg.trained_at,
|
|
101
|
+
"identity_mode": reg.identity_mode,
|
|
102
|
+
"workflows": {
|
|
103
|
+
n: {"n_runs": w.n_runs, "steps": len(w.steps), "pipelines": list(w.pipelines)}
|
|
104
|
+
for n, w in reg.workflows.items()
|
|
105
|
+
},
|
|
106
|
+
"saved_to": args.out,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
if args.cmd == "workflows":
|
|
112
|
+
reg = load_models(args.models_dir)
|
|
113
|
+
_print_json(
|
|
114
|
+
{
|
|
115
|
+
n: {
|
|
116
|
+
"n_runs": w.n_runs,
|
|
117
|
+
"steps": len(w.steps),
|
|
118
|
+
"identity_mode": w.identity_mode,
|
|
119
|
+
"pipelines": {p: v.n_runs for p, v in w.pipelines.items()},
|
|
120
|
+
"module_paths": w.provenance.get("module_paths", []),
|
|
121
|
+
"versions": w.provenance.get("versions", []),
|
|
122
|
+
}
|
|
123
|
+
for n, w in reg.workflows.items()
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
if args.cmd == "quality":
|
|
129
|
+
reg = load_models(args.models_dir)
|
|
130
|
+
_print_json(reg.quality)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
if args.cmd == "predict":
|
|
134
|
+
reg = load_models(args.models_dir)
|
|
135
|
+
out = predict_run(
|
|
136
|
+
reg, args.workflow, _parse_size(args.input_size), args.gpu_class, args.pipeline
|
|
137
|
+
)
|
|
138
|
+
if not args.full:
|
|
139
|
+
out["steps"] = [
|
|
140
|
+
{"snippet": s["snippet"], "start_offset_s": s["start_offset_s"],
|
|
141
|
+
"duration_s": round(s["predicted"].get("duration_seconds", 0.0), 1),
|
|
142
|
+
"duration_gpu_band_s": s.get("duration_gpu_band_s"),
|
|
143
|
+
"mem_max_mb": round(s["predicted"].get("memory_rss_mb_max", 0.0), 1)}
|
|
144
|
+
for s in out["steps"]
|
|
145
|
+
]
|
|
146
|
+
_print_json(out)
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
if args.cmd == "power":
|
|
150
|
+
reg = load_models(args.models_dir)
|
|
151
|
+
_print_json(
|
|
152
|
+
predict_power_curve(
|
|
153
|
+
reg, args.workflow, _parse_size(args.input_size),
|
|
154
|
+
args.bin_minutes, gpu_class=args.gpu_class, pipeline=args.pipeline,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
if args.cmd == "gpu":
|
|
160
|
+
from .gpu import guess_gpu_name
|
|
161
|
+
reg = load_models(args.models_dir)
|
|
162
|
+
names = [args.workflow] if args.workflow else list(reg.workflows)
|
|
163
|
+
result = {}
|
|
164
|
+
for name in names:
|
|
165
|
+
wf = reg.get(name)
|
|
166
|
+
if wf is None:
|
|
167
|
+
continue
|
|
168
|
+
steps = {}
|
|
169
|
+
for step, sm in wf.steps.items():
|
|
170
|
+
if not sm.gpu_classes:
|
|
171
|
+
continue
|
|
172
|
+
steps[step] = {
|
|
173
|
+
"classes_seen": sm.gpu_classes,
|
|
174
|
+
"duration_factors": {
|
|
175
|
+
f"{c} ({guess_gpu_name(c)})": round(v, 3)
|
|
176
|
+
for c, v in sm.gpu_duration_factor.items()
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
if steps:
|
|
180
|
+
result[name] = steps
|
|
181
|
+
_print_json(result)
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
if args.cmd == "evaluate":
|
|
185
|
+
reg = load_models(args.models_dir)
|
|
186
|
+
out = evaluate_run(reg, args.dump)
|
|
187
|
+
if not args.full and "steps" in out:
|
|
188
|
+
out["steps"] = [s for s in out["steps"] if s.get("flags")]
|
|
189
|
+
_print_json(out)
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
if args.cmd == "recommend":
|
|
193
|
+
reg = load_models(args.models_dir)
|
|
194
|
+
names = [args.workflow] if args.workflow else list(reg.workflows)
|
|
195
|
+
result = {}
|
|
196
|
+
for name in names:
|
|
197
|
+
wf = reg.get(name)
|
|
198
|
+
if wf is None:
|
|
199
|
+
continue
|
|
200
|
+
wf_out = {}
|
|
201
|
+
for step, recs in wf.all_recommendations().items():
|
|
202
|
+
items = [r.to_json() for r in recs
|
|
203
|
+
if not args.changes_only or r.action in ("increase", "decrease")]
|
|
204
|
+
if items:
|
|
205
|
+
wf_out[step] = items
|
|
206
|
+
result[name] = wf_out
|
|
207
|
+
_print_json(result)
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
raise SystemExit(main())
|