maatml 0.4.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.
- maatml/__init__.py +3 -0
- maatml/cli.py +565 -0
- maatml/config.py +222 -0
- maatml/data/__init__.py +5 -0
- maatml/data/datagen.py +142 -0
- maatml/data/formats.py +123 -0
- maatml/data/gated.py +60 -0
- maatml/data/ingest.py +158 -0
- maatml/data/pipeline.py +270 -0
- maatml/data/preference.py +109 -0
- maatml/data/sanitizer.py +93 -0
- maatml/data/schemas.py +14 -0
- maatml/data/teacher.py +101 -0
- maatml/device.py +209 -0
- maatml/evaluation/__init__.py +26 -0
- maatml/evaluation/harness.py +487 -0
- maatml/evaluation/predictors.py +434 -0
- maatml/evaluation/runner.py +107 -0
- maatml/export/__init__.py +12 -0
- maatml/export/bundle.py +283 -0
- maatml/export/gguf.py +109 -0
- maatml/export/manifest.py +104 -0
- maatml/export/mlx_export.py +59 -0
- maatml/overrides.py +230 -0
- maatml/registry.py +270 -0
- maatml/runs.py +380 -0
- maatml/scaffold.py +430 -0
- maatml/training/__init__.py +0 -0
- maatml/training/builtins.py +47 -0
- maatml/training/guards.py +179 -0
- maatml/training/load.py +109 -0
- maatml/training/multi_head.py +714 -0
- maatml/training/preference.py +320 -0
- maatml/training/seq2seq.py +423 -0
- maatml/training/sft_base.py +746 -0
- maatml/utils/__init__.py +0 -0
- maatml/utils/io.py +60 -0
- maatml/validation/__init__.py +17 -0
- maatml/validation/base.py +84 -0
- maatml-0.4.0.dist-info/METADATA +229 -0
- maatml-0.4.0.dist-info/RECORD +45 -0
- maatml-0.4.0.dist-info/WHEEL +5 -0
- maatml-0.4.0.dist-info/entry_points.txt +2 -0
- maatml-0.4.0.dist-info/licenses/LICENSE +202 -0
- maatml-0.4.0.dist-info/top_level.txt +1 -0
maatml/__init__.py
ADDED
maatml/cli.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""maatml CLI.
|
|
2
|
+
|
|
3
|
+
Each command takes a model folder (containing ``model.yml``) and dispatches via
|
|
4
|
+
the plugin registry (``architecture`` / ``dataset.format`` / ``evaluation``).
|
|
5
|
+
Outputs land under ``<model-dir>/output/`` (gitignored).
|
|
6
|
+
|
|
7
|
+
maatml prepare <model-dir>
|
|
8
|
+
maatml train <model-dir> [--smoke] [--resume auto|PATH] [--set K=V]
|
|
9
|
+
maatml sweep <model-dir> --param K=a,b [--metric NAME] [--smoke]
|
|
10
|
+
maatml evaluate <model-dir> [--checkpoint X] [--split test] [--gate]
|
|
11
|
+
maatml export <model-dir> [--checkpoint X] [--format gguf|mlx|safetensors]
|
|
12
|
+
maatml verify <export-dir-or-manifest>
|
|
13
|
+
maatml datagen <model-dir> [--target N] [--teacher]
|
|
14
|
+
maatml ingest <model-dir> --input PATH [--map field=col] [--sanitize tag]
|
|
15
|
+
maatml runs <model-dir>
|
|
16
|
+
maatml scaffold <dir> --architecture causal_sft [--name my-model]
|
|
17
|
+
maatml validate <model-dir>
|
|
18
|
+
maatml plan <model-dir>
|
|
19
|
+
maatml plugins
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
import typer
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
from .config import get_dataset_cfg, load_model_def
|
|
31
|
+
from .overrides import (
|
|
32
|
+
apply_overrides,
|
|
33
|
+
expand_param_grid,
|
|
34
|
+
overrides_from_mapping,
|
|
35
|
+
pick_metric,
|
|
36
|
+
)
|
|
37
|
+
from .registry import (
|
|
38
|
+
FORMATS,
|
|
39
|
+
PREDICTORS,
|
|
40
|
+
TRAINERS,
|
|
41
|
+
discover_plugins,
|
|
42
|
+
list_all_plugins,
|
|
43
|
+
load_model_plugins,
|
|
44
|
+
)
|
|
45
|
+
from .runs import list_runs, resolve_checkpoint
|
|
46
|
+
from .scaffold import normalize_architecture, scaffold_model, validate_model_dir
|
|
47
|
+
|
|
48
|
+
# MPS unsupported-op fallback to CPU; harmless on non-Mac.
|
|
49
|
+
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
app = typer.Typer(no_args_is_help=True, add_completion=False, help="maatml CLI")
|
|
53
|
+
console = Console()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Helpers
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _boot_plugins(md) -> None:
|
|
62
|
+
discover_plugins()
|
|
63
|
+
if md.plugins:
|
|
64
|
+
load_model_plugins(md.model_dir, md.plugins)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _default_eval_keys(md) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
|
68
|
+
"""Infer predictor/validator/metrics from evaluation: or architecture/task."""
|
|
69
|
+
ev = md.evaluation or {}
|
|
70
|
+
predictor = ev.get("predictor")
|
|
71
|
+
validator = ev.get("validator")
|
|
72
|
+
metrics = ev.get("metrics")
|
|
73
|
+
if isinstance(metrics, list):
|
|
74
|
+
metrics = metrics[0] if metrics else None
|
|
75
|
+
|
|
76
|
+
arch = normalize_architecture(md.architecture)
|
|
77
|
+
if predictor is None:
|
|
78
|
+
if arch in PREDICTORS.names() or PREDICTORS.get(md.architecture):
|
|
79
|
+
predictor = md.architecture if PREDICTORS.get(md.architecture) else arch
|
|
80
|
+
elif arch == "multi_head_classifier":
|
|
81
|
+
predictor = "multi_head_classifier"
|
|
82
|
+
elif arch == "seq2seq":
|
|
83
|
+
predictor = "seq2seq"
|
|
84
|
+
elif arch == "causal_sft":
|
|
85
|
+
predictor = "causal_sft"
|
|
86
|
+
|
|
87
|
+
# validator / metrics come from evaluation: (or model plugins); no
|
|
88
|
+
# hardcoded task-name fallbacks in core.
|
|
89
|
+
return predictor, validator, metrics
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _clone_model_def(md):
|
|
93
|
+
"""Deep-copy nested dict sections so sweep trials don't share state."""
|
|
94
|
+
clone = md.model_copy(deep=True)
|
|
95
|
+
object.__setattr__(clone, "model_dir", md.model_dir)
|
|
96
|
+
return clone
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_field_maps(maps: Optional[list[str]]) -> dict[str, str]:
|
|
100
|
+
"""Parse ``dest=src`` field maps for ingest."""
|
|
101
|
+
out: dict[str, str] = {}
|
|
102
|
+
for item in maps or []:
|
|
103
|
+
if "=" not in item:
|
|
104
|
+
raise typer.BadParameter(f"Expected field=col mapping, got {item!r}")
|
|
105
|
+
dest, src = item.split("=", 1)
|
|
106
|
+
dest, src = dest.strip(), src.strip()
|
|
107
|
+
if not dest or not src:
|
|
108
|
+
raise typer.BadParameter(f"Empty field map entry: {item!r}")
|
|
109
|
+
out[dest] = src
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Commands
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("prepare")
|
|
119
|
+
def cmd_prepare(
|
|
120
|
+
model_dir: Path = typer.Argument(
|
|
121
|
+
...,
|
|
122
|
+
exists=True,
|
|
123
|
+
file_okay=False,
|
|
124
|
+
help="Path to a model folder containing model.yml",
|
|
125
|
+
),
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Build train/val/test splits under <model-dir>/output/prepared/."""
|
|
128
|
+
md = load_model_def(model_dir)
|
|
129
|
+
_boot_plugins(md)
|
|
130
|
+
fmt = get_dataset_cfg(md).get("format", "jsonl_seed")
|
|
131
|
+
prepare_fn = FORMATS.require(str(fmt))
|
|
132
|
+
prepare_fn(md)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.command("train")
|
|
136
|
+
def cmd_train(
|
|
137
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
138
|
+
smoke: bool = typer.Option(False, "--smoke", help="Use the `smoke:` overrides in model.yml"),
|
|
139
|
+
limit: Optional[int] = typer.Option(None, "--limit", help="Cap number of train rows for ad-hoc smoke"),
|
|
140
|
+
device: str = typer.Option("auto", "--device", help="auto|mps|cpu|cuda"),
|
|
141
|
+
seed: Optional[int] = typer.Option(None, "--seed"),
|
|
142
|
+
resume: Optional[str] = typer.Option(
|
|
143
|
+
None,
|
|
144
|
+
"--resume",
|
|
145
|
+
help="Resume training: 'auto' (latest incomplete run) or checkpoint path/run_id",
|
|
146
|
+
),
|
|
147
|
+
set_overrides: Optional[list[str]] = typer.Option(
|
|
148
|
+
None,
|
|
149
|
+
"--set",
|
|
150
|
+
help="Override model.yml after load (repeatable), e.g. --set training.learning_rate=1e-4",
|
|
151
|
+
),
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Fine-tune the model declared by <model-dir>/model.yml."""
|
|
154
|
+
md = load_model_def(model_dir)
|
|
155
|
+
if set_overrides:
|
|
156
|
+
apply_overrides(md, set_overrides)
|
|
157
|
+
_boot_plugins(md)
|
|
158
|
+
arch = normalize_architecture(md.architecture)
|
|
159
|
+
trainer = TRAINERS.get(md.architecture) or TRAINERS.require(arch)
|
|
160
|
+
result = trainer(
|
|
161
|
+
md, smoke=smoke, limit=limit, device=device, seed=seed, resume=resume
|
|
162
|
+
)
|
|
163
|
+
console.print(f"[green]done[/] out_dir={result.out_dir} metrics={result.metrics}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command("sweep")
|
|
167
|
+
def cmd_sweep(
|
|
168
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
169
|
+
param: Optional[list[str]] = typer.Option(
|
|
170
|
+
None,
|
|
171
|
+
"--param",
|
|
172
|
+
help="Grid axis KEY=v1,v2 (repeatable); cartesian product of values",
|
|
173
|
+
),
|
|
174
|
+
set_overrides: Optional[list[str]] = typer.Option(
|
|
175
|
+
None,
|
|
176
|
+
"--set",
|
|
177
|
+
help="Base overrides applied to every trial (repeatable)",
|
|
178
|
+
),
|
|
179
|
+
metric: Optional[str] = typer.Option(
|
|
180
|
+
None,
|
|
181
|
+
"--metric",
|
|
182
|
+
help="Metric key to rank trials (default: first numeric metric)",
|
|
183
|
+
),
|
|
184
|
+
smoke: bool = typer.Option(False, "--smoke"),
|
|
185
|
+
limit: Optional[int] = typer.Option(None, "--limit"),
|
|
186
|
+
device: str = typer.Option("auto", "--device"),
|
|
187
|
+
seed: Optional[int] = typer.Option(None, "--seed"),
|
|
188
|
+
max_trials: Optional[int] = typer.Option(
|
|
189
|
+
None, "--max-trials", help="Cap number of grid combinations"
|
|
190
|
+
),
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Offline cartesian HPO over ``--param`` axes (no Optuna required)."""
|
|
193
|
+
base = load_model_def(model_dir)
|
|
194
|
+
_boot_plugins(base)
|
|
195
|
+
grid = expand_param_grid(param, max_trials=max_trials)
|
|
196
|
+
if not grid:
|
|
197
|
+
console.print("[yellow]empty grid — nothing to run[/]")
|
|
198
|
+
raise typer.Exit(code=0)
|
|
199
|
+
|
|
200
|
+
arch = normalize_architecture(base.architecture)
|
|
201
|
+
trainer = TRAINERS.get(base.architecture) or TRAINERS.require(arch)
|
|
202
|
+
ranking: list[tuple[Optional[float], dict, Any]] = []
|
|
203
|
+
|
|
204
|
+
for i, trial_map in enumerate(grid):
|
|
205
|
+
md = _clone_model_def(base)
|
|
206
|
+
if set_overrides:
|
|
207
|
+
apply_overrides(md, set_overrides)
|
|
208
|
+
apply_overrides(md, overrides_from_mapping(trial_map))
|
|
209
|
+
trial_meta = {"index": i, "params": trial_map}
|
|
210
|
+
console.print(
|
|
211
|
+
f"[cyan]sweep[/] trial {i + 1}/{len(grid)} params={trial_map}"
|
|
212
|
+
)
|
|
213
|
+
result = trainer(
|
|
214
|
+
md,
|
|
215
|
+
smoke=smoke,
|
|
216
|
+
limit=limit,
|
|
217
|
+
device=device,
|
|
218
|
+
seed=seed,
|
|
219
|
+
trial=trial_meta,
|
|
220
|
+
)
|
|
221
|
+
key, val = pick_metric(result.metrics, metric)
|
|
222
|
+
ranking.append((val, trial_map, result))
|
|
223
|
+
console.print(
|
|
224
|
+
f" -> run={Path(result.out_dir).name} "
|
|
225
|
+
f"{key or 'metric'}={val if val is not None else 'n/a'}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
scored: list[tuple[float, bool, dict, Any, Optional[str]]] = []
|
|
229
|
+
for val, trial_map, result in ranking:
|
|
230
|
+
key, _ = pick_metric(result.metrics, metric)
|
|
231
|
+
minimize = key is not None and "loss" in key.lower()
|
|
232
|
+
if val is None:
|
|
233
|
+
continue
|
|
234
|
+
scored.append((val, minimize, trial_map, result, key))
|
|
235
|
+
|
|
236
|
+
if scored:
|
|
237
|
+
minimize = scored[0][1]
|
|
238
|
+
scored.sort(key=lambda t: t[0], reverse=not minimize)
|
|
239
|
+
console.print("[bold]sweep ranking[/]")
|
|
240
|
+
for rank, (val, _, trial_map, result, key) in enumerate(scored, start=1):
|
|
241
|
+
console.print(
|
|
242
|
+
f" {rank}. {key}={val:.6g} params={trial_map} "
|
|
243
|
+
f"out={Path(result.out_dir).name}"
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
console.print("[yellow]sweep finished with no numeric metrics to rank[/]")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command("evaluate")
|
|
250
|
+
def cmd_evaluate(
|
|
251
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
252
|
+
checkpoint: Optional[str] = typer.Option(
|
|
253
|
+
None,
|
|
254
|
+
"--checkpoint",
|
|
255
|
+
help="run_id or checkpoint dir; defaults to latest completed run",
|
|
256
|
+
),
|
|
257
|
+
split: str = typer.Option("test", "--split"),
|
|
258
|
+
device: str = typer.Option("auto", "--device"),
|
|
259
|
+
baseline: Optional[Path] = typer.Option(None, "--baseline"),
|
|
260
|
+
max_input_tokens: int = typer.Option(2048, "--max-input-tokens"),
|
|
261
|
+
limit: Optional[int] = typer.Option(None, "--limit"),
|
|
262
|
+
gate: bool = typer.Option(
|
|
263
|
+
False,
|
|
264
|
+
"--gate",
|
|
265
|
+
help="Enforce evaluation.gates minima; exit non-zero on failure",
|
|
266
|
+
),
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Evaluate a checkpoint and write report.{json,md} under output/eval/."""
|
|
269
|
+
md = load_model_def(model_dir)
|
|
270
|
+
_boot_plugins(md)
|
|
271
|
+
# Ensure predictor registrations are loaded.
|
|
272
|
+
from .evaluation import predictors as _predictors # noqa: F401
|
|
273
|
+
from .evaluation.harness import run_evaluation
|
|
274
|
+
from .evaluation.runner import write_markdown_summary
|
|
275
|
+
from .runs import get_run, update_run_gates
|
|
276
|
+
|
|
277
|
+
ckpt = resolve_checkpoint(md, checkpoint)
|
|
278
|
+
md.eval_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
out_path = md.eval_dir / f"{ckpt.name}.json"
|
|
280
|
+
|
|
281
|
+
predictor, validator, metrics = _default_eval_keys(md)
|
|
282
|
+
if predictor is None:
|
|
283
|
+
raise typer.BadParameter(
|
|
284
|
+
f"No predictor for architecture={md.architecture!r}; "
|
|
285
|
+
"set evaluation.predictor in model.yml"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
report = run_evaluation(
|
|
289
|
+
checkpoint_dir=ckpt,
|
|
290
|
+
dataset_dir=md.prepared_dir,
|
|
291
|
+
out_path=out_path,
|
|
292
|
+
model_def=md,
|
|
293
|
+
predictor=predictor,
|
|
294
|
+
validator=validator,
|
|
295
|
+
metrics_fn=metrics,
|
|
296
|
+
device=device,
|
|
297
|
+
split=split,
|
|
298
|
+
max_input_tokens=max_input_tokens,
|
|
299
|
+
baseline_path=baseline,
|
|
300
|
+
limit=limit,
|
|
301
|
+
task=md.task,
|
|
302
|
+
enforce_gates=gate,
|
|
303
|
+
)
|
|
304
|
+
write_markdown_summary(report, out_path.with_suffix(".md"))
|
|
305
|
+
|
|
306
|
+
# Record gate results on the run registry when checkpoint is a known run_id.
|
|
307
|
+
run_rec = get_run(md, ckpt.name)
|
|
308
|
+
if run_rec is not None and report.gates is not None:
|
|
309
|
+
update_run_gates(
|
|
310
|
+
md,
|
|
311
|
+
run_rec.run_id,
|
|
312
|
+
report.gates,
|
|
313
|
+
metrics=report.metrics,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
console.print(f"[green]done[/] report={out_path}")
|
|
317
|
+
if gate and report.passed is False:
|
|
318
|
+
console.print("[red]eval gates failed[/]")
|
|
319
|
+
raise typer.Exit(code=1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command("export")
|
|
323
|
+
def cmd_export(
|
|
324
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
325
|
+
checkpoint: Optional[str] = typer.Option(
|
|
326
|
+
None,
|
|
327
|
+
"--checkpoint",
|
|
328
|
+
help="run_id or checkpoint dir; defaults to latest completed run",
|
|
329
|
+
),
|
|
330
|
+
format: Optional[str] = typer.Option(
|
|
331
|
+
None,
|
|
332
|
+
"--format",
|
|
333
|
+
help="safetensors (default) | gguf | mlx",
|
|
334
|
+
),
|
|
335
|
+
out: Optional[Path] = typer.Option(
|
|
336
|
+
None,
|
|
337
|
+
"--out",
|
|
338
|
+
help="Export directory (default: <model-dir>/output/export/<run_id>)",
|
|
339
|
+
),
|
|
340
|
+
parity: bool = typer.Option(
|
|
341
|
+
False,
|
|
342
|
+
"--parity",
|
|
343
|
+
help="Re-run evaluation.gates on dataset.benchmark_samples after export",
|
|
344
|
+
),
|
|
345
|
+
device: str = typer.Option("auto", "--device"),
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Export a checkpoint as a deployable bundle with manifest.json."""
|
|
348
|
+
md = load_model_def(model_dir)
|
|
349
|
+
_boot_plugins(md)
|
|
350
|
+
from .export.bundle import export_model, run_parity_check
|
|
351
|
+
from .runs import get_run
|
|
352
|
+
|
|
353
|
+
ckpt = resolve_checkpoint(md, checkpoint)
|
|
354
|
+
run_rec = get_run(md, ckpt.name)
|
|
355
|
+
run_id = run_rec.run_id if run_rec else ckpt.name
|
|
356
|
+
out_dir = out if out is not None else (md.output_dir / "export" / run_id)
|
|
357
|
+
try:
|
|
358
|
+
export_model(md, ckpt, out_dir, format=format, run_id=run_id)
|
|
359
|
+
except (ImportError, RuntimeError, ValueError, KeyError, FileNotFoundError) as exc:
|
|
360
|
+
console.print(f"[red]export failed[/] {exc}")
|
|
361
|
+
raise typer.Exit(code=1) from exc
|
|
362
|
+
console.print(f"[green]exported[/] {out_dir}")
|
|
363
|
+
|
|
364
|
+
if parity:
|
|
365
|
+
result = run_parity_check(md, out_dir, device=device)
|
|
366
|
+
if result.get("skipped"):
|
|
367
|
+
console.print(f"[yellow]parity skipped[/] {result.get('reason')}")
|
|
368
|
+
elif not result.get("passed"):
|
|
369
|
+
console.print(f"[red]parity gates failed[/] {result.get('gates')}")
|
|
370
|
+
raise typer.Exit(code=1)
|
|
371
|
+
else:
|
|
372
|
+
console.print("[green]parity ok[/]")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@app.command("verify")
|
|
376
|
+
def cmd_verify(
|
|
377
|
+
path: Path = typer.Argument(
|
|
378
|
+
...,
|
|
379
|
+
help="Export directory or path to manifest.json",
|
|
380
|
+
),
|
|
381
|
+
) -> None:
|
|
382
|
+
"""Recompute sha256 of files listed in an export manifest; exit 1 on mismatch."""
|
|
383
|
+
from .export.manifest import verify_manifest
|
|
384
|
+
|
|
385
|
+
errors = verify_manifest(path)
|
|
386
|
+
if errors:
|
|
387
|
+
console.print("[red]verify failed[/]")
|
|
388
|
+
for err in errors:
|
|
389
|
+
console.print(f" - {err}")
|
|
390
|
+
raise typer.Exit(code=1)
|
|
391
|
+
console.print(f"[green]OK[/] {path}")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@app.command("datagen")
|
|
395
|
+
def cmd_datagen(
|
|
396
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
397
|
+
target: int = typer.Option(100, "--target", help="Number of accepted samples"),
|
|
398
|
+
seed: int = typer.Option(0, "--seed"),
|
|
399
|
+
out: Optional[Path] = typer.Option(
|
|
400
|
+
None, "--out", help="Override seed JSONL path"
|
|
401
|
+
),
|
|
402
|
+
teacher: bool = typer.Option(
|
|
403
|
+
False,
|
|
404
|
+
"--teacher",
|
|
405
|
+
help="Use OpenAI-compatible teacher (MAATML_TEACHER_* env); requires [teacher]",
|
|
406
|
+
),
|
|
407
|
+
append: bool = typer.Option(True, "--append/--no-append"),
|
|
408
|
+
) -> None:
|
|
409
|
+
"""Generate validator-gated seed rows via a registered generator or teacher."""
|
|
410
|
+
md = load_model_def(model_dir)
|
|
411
|
+
_boot_plugins(md)
|
|
412
|
+
from .data.datagen import run_datagen
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
result = run_datagen(
|
|
416
|
+
md,
|
|
417
|
+
target=target,
|
|
418
|
+
seed=seed,
|
|
419
|
+
out_path=out,
|
|
420
|
+
use_teacher=teacher,
|
|
421
|
+
append=append,
|
|
422
|
+
)
|
|
423
|
+
except (KeyError, ImportError) as exc:
|
|
424
|
+
console.print(f"[red]datagen failed[/] {exc}")
|
|
425
|
+
raise typer.Exit(code=1) from exc
|
|
426
|
+
console.print(
|
|
427
|
+
f"[green]datagen[/] generator={result['generator']} "
|
|
428
|
+
f"accepted={result['accepted']} rejected={result['rejected']} "
|
|
429
|
+
f"out={result['out_path']}"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@app.command("ingest")
|
|
434
|
+
def cmd_ingest(
|
|
435
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
436
|
+
input_path: Path = typer.Option(
|
|
437
|
+
...,
|
|
438
|
+
"--input",
|
|
439
|
+
exists=True,
|
|
440
|
+
help="JSON or JSONL file to ingest",
|
|
441
|
+
),
|
|
442
|
+
field_map: Optional[list[str]] = typer.Option(
|
|
443
|
+
None,
|
|
444
|
+
"--map",
|
|
445
|
+
help="Map dest=src fields (repeatable), e.g. --map request=text",
|
|
446
|
+
),
|
|
447
|
+
sanitize: Optional[str] = typer.Option(
|
|
448
|
+
None, "--sanitize", help="Sanitizer registry tag (e.g. jcl, spool)"
|
|
449
|
+
),
|
|
450
|
+
append: bool = typer.Option(True, "--append/--no-append"),
|
|
451
|
+
out: Optional[Path] = typer.Option(None, "--out", help="Override seed JSONL path"),
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Ingest external samples into seed_samples with optional validation."""
|
|
454
|
+
md = load_model_def(model_dir)
|
|
455
|
+
_boot_plugins(md)
|
|
456
|
+
from .data.ingest import ingest_samples
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
result = ingest_samples(
|
|
460
|
+
md,
|
|
461
|
+
input_path,
|
|
462
|
+
field_map=_parse_field_maps(field_map),
|
|
463
|
+
sanitize_tag=sanitize,
|
|
464
|
+
append=append,
|
|
465
|
+
out_path=out,
|
|
466
|
+
)
|
|
467
|
+
except (KeyError, ValueError, FileNotFoundError) as exc:
|
|
468
|
+
console.print(f"[red]ingest failed[/] {exc}")
|
|
469
|
+
raise typer.Exit(code=1) from exc
|
|
470
|
+
console.print(
|
|
471
|
+
f"[green]ingest[/] accepted={result['accepted']} "
|
|
472
|
+
f"rejected={result['rejected']} seeds={result['seeds_path']}"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@app.command("runs")
|
|
477
|
+
def cmd_runs(
|
|
478
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
479
|
+
) -> None:
|
|
480
|
+
"""List training runs recorded in <model-dir>/output/runs.jsonl."""
|
|
481
|
+
md = load_model_def(model_dir)
|
|
482
|
+
records = list_runs(md)
|
|
483
|
+
if not records:
|
|
484
|
+
console.print(f"[dim]no runs yet under {md.output_dir}[/]")
|
|
485
|
+
return
|
|
486
|
+
for rec in records:
|
|
487
|
+
metrics = ""
|
|
488
|
+
if rec.metrics:
|
|
489
|
+
top = list(rec.metrics.items())[:3]
|
|
490
|
+
metrics = " " + " ".join(f"{k}={v:.4g}" for k, v in top)
|
|
491
|
+
console.print(
|
|
492
|
+
f"{rec.run_id} [{rec.status}] smoke={rec.smoke} "
|
|
493
|
+
f"device={rec.device or '-'} {rec.out_dir}{metrics}"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@app.command("scaffold")
|
|
498
|
+
def cmd_scaffold(
|
|
499
|
+
target_dir: Path = typer.Argument(..., help="Directory to create (model folder)"),
|
|
500
|
+
architecture: str = typer.Option(
|
|
501
|
+
...,
|
|
502
|
+
"--architecture",
|
|
503
|
+
"-a",
|
|
504
|
+
help="Registered trainer architecture (e.g. causal_sft, seq2seq, classifier, dpo)",
|
|
505
|
+
),
|
|
506
|
+
name: Optional[str] = typer.Option(None, "--name", help="Model name (default: folder name)"),
|
|
507
|
+
) -> None:
|
|
508
|
+
"""Create a new model folder with model.yml, datasets, and README."""
|
|
509
|
+
discover_plugins()
|
|
510
|
+
path = scaffold_model(target_dir, architecture=architecture, name=name)
|
|
511
|
+
console.print(f"[green]scaffolded[/] {path}")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@app.command("validate")
|
|
515
|
+
def cmd_validate(
|
|
516
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
517
|
+
) -> None:
|
|
518
|
+
"""Validate model.yml paths, architecture registration, and dataset.format."""
|
|
519
|
+
errors = validate_model_dir(model_dir)
|
|
520
|
+
if errors:
|
|
521
|
+
console.print("[red]validate failed[/]")
|
|
522
|
+
for err in errors:
|
|
523
|
+
console.print(f" - {err}")
|
|
524
|
+
raise typer.Exit(code=1)
|
|
525
|
+
md = load_model_def(model_dir)
|
|
526
|
+
console.print(f"[green]OK[/] {md.identity} ({md.architecture})")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@app.command("plan")
|
|
530
|
+
def cmd_plan(
|
|
531
|
+
model_dir: Path = typer.Argument(..., exists=True, file_okay=False),
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Print the lifecycle command plan for a model folder."""
|
|
534
|
+
md = load_model_def(model_dir)
|
|
535
|
+
console.print(f"[bold]{md.identity}[/] ({md.task} / {md.architecture})")
|
|
536
|
+
console.print(f"1. maatml prepare {md.model_dir}")
|
|
537
|
+
console.print(f"2. maatml train {md.model_dir} --smoke")
|
|
538
|
+
console.print(f"3. maatml train {md.model_dir}")
|
|
539
|
+
console.print(f"4. maatml evaluate {md.model_dir}")
|
|
540
|
+
console.print(f"5. maatml export {md.model_dir}")
|
|
541
|
+
console.print(f"6. maatml verify {md.model_dir}/output/export/<run_id>")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@app.command("plugins")
|
|
545
|
+
def cmd_plugins() -> None:
|
|
546
|
+
"""List registered trainers, validators, metrics, formats, and predictors."""
|
|
547
|
+
discover_plugins()
|
|
548
|
+
# Import submodule directly so listing plugins does not require torch.
|
|
549
|
+
from .evaluation import predictors as _predictors # noqa: F401
|
|
550
|
+
from .export import bundle as _bundle # noqa: F401
|
|
551
|
+
from .export import gguf as _gguf # noqa: F401
|
|
552
|
+
from .export import mlx_export as _mlx # noqa: F401
|
|
553
|
+
|
|
554
|
+
for kind, entries in list_all_plugins().items():
|
|
555
|
+
console.print(f"[bold]{kind}[/] ({len(entries)})")
|
|
556
|
+
for entry in entries:
|
|
557
|
+
console.print(f" {entry.name} [dim]{entry.source}[/]")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def main() -> None:
|
|
561
|
+
app()
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
if __name__ == "__main__":
|
|
565
|
+
main()
|