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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pype-runtime-models = pype_runtime_models.cli:main
@@ -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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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())