tensorless 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.
tensorless/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ Tensorless
3
+ ==========
4
+
5
+ ML with maximum automation and minimum setup.
6
+
7
+ import tensorless as tl
8
+
9
+ tl.train("./data")
10
+ tl.run("model.tl")
11
+
12
+ See https://github.com/tensorless/tensorless for full documentation.
13
+ """
14
+
15
+ from .api import train, run, load, inspect
16
+ from .config import TrainConfig
17
+ from .errors import (
18
+ TensorlessError,
19
+ DataError,
20
+ ConfigError,
21
+ ModelError,
22
+ CheckpointError,
23
+ SerializationError,
24
+ )
25
+ from ._version import __version__
26
+
27
+ __all__ = [
28
+ "train",
29
+ "run",
30
+ "load",
31
+ "inspect",
32
+ "TrainConfig",
33
+ "TensorlessError",
34
+ "DataError",
35
+ "ConfigError",
36
+ "ModelError",
37
+ "CheckpointError",
38
+ "SerializationError",
39
+ "__version__",
40
+ ]
tensorless/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ __version__ = "0.1.0"
2
+
3
+ # Bump this whenever the .tl serialization layout changes in a
4
+ # backward-incompatible way. Stored inside every .tl file so old
5
+ # files can be detected and (eventually) migrated.
6
+ TL_FORMAT_VERSION = 1
tensorless/api.py ADDED
@@ -0,0 +1,230 @@
1
+ """Public Tensorless API.
2
+
3
+ import tensorless as tl
4
+
5
+ tl.train("./data") # full auto
6
+ tl.train("./data", d_model=512, layers=6, batch_size=32) # override anything
7
+ tl.inspect("./data")
8
+ model = tl.load("model.tl")
9
+ tl.run("model.tl")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import dataclasses
15
+ import os
16
+ from typing import Any, Optional
17
+
18
+ from .config import TrainConfig
19
+ from .errors import ConfigError, DataError, ModelError
20
+ from .data.loader import load_dataset
21
+ from .data.fingerprint import fingerprint_path
22
+ from .data.inspector import inspect_path, InspectionReport
23
+ from .auto.config import resolve_config
24
+ from .checkpoint.manager import CheckpointManager
25
+ from .training.trainer import run_training
26
+ from .serialization.tl_format import save_tl, load_tl
27
+ from .runtime import LoadedModel, load_model
28
+ from ._version import __version__ as _tl_version, TL_FORMAT_VERSION as _tl_format_version
29
+
30
+ _KNOWN_FIELDS = {f.name for f in dataclasses.fields(TrainConfig)}
31
+
32
+
33
+ def _build_train_config(**kwargs: Any) -> TrainConfig:
34
+ unknown = set(kwargs) - _KNOWN_FIELDS
35
+ if unknown:
36
+ raise ConfigError(
37
+ f"Unknown train() argument(s): {sorted(unknown)}. "
38
+ f"Valid arguments: {sorted(_KNOWN_FIELDS)}"
39
+ )
40
+ return TrainConfig(**kwargs)
41
+
42
+
43
+ def inspect(path: str) -> InspectionReport:
44
+ """Inspect a dataset: detected task, size, problems, recommendations.
45
+
46
+ Does not train anything. Prints a human-readable report and also
47
+ returns a structured `InspectionReport` object.
48
+ """
49
+ report = inspect_path(path)
50
+ print(report)
51
+ return report
52
+
53
+
54
+ def train(path: str, **kwargs: Any) -> LoadedModel:
55
+ """Train a model on the dataset at `path`, fully automatically by
56
+ default. Any field of `TrainConfig` can be overridden via keyword
57
+ argument, e.g. `tl.train("./data", d_model=512, layers=6)`.
58
+
59
+ Implements the "Smart Auto Check":
60
+ - if an up-to-date trained model already exists for this exact
61
+ dataset -> return it without retraining
62
+ - if training was interrupted on this exact dataset -> resume
63
+ - if the dataset changed -> retrain (or raise, if
64
+ `ask_on_data_change=True`), unless `force=True` is passed
65
+ """
66
+ user_cfg = _build_train_config(**kwargs)
67
+ out = user_cfg.out or "model.tl"
68
+ checkpoint_dir = user_cfg.checkpoint_dir or (out + ".ckpt")
69
+ checkpoint_mgr = CheckpointManager(checkpoint_dir)
70
+
71
+ if not os.path.exists(path):
72
+ raise DataError(f"Path '{path}' does not exist.")
73
+
74
+ fingerprint = fingerprint_path(path)
75
+
76
+ resume_state = None
77
+
78
+ if not user_cfg.force:
79
+ # 1. Is there already a complete, up-to-date .tl file?
80
+ try:
81
+ existing = load_tl(out)
82
+ except Exception:
83
+ existing = None
84
+ if existing is not None:
85
+ if existing.get("dataset_fingerprint") == fingerprint and existing.get("training_complete"):
86
+ if user_cfg.verbose:
87
+ print(
88
+ f"[tensorless] '{out}' already exists and matches this "
89
+ f"dataset (fingerprint {fingerprint[:12]}...) -- using "
90
+ f"the existing model. Pass force=True to retrain."
91
+ )
92
+ return LoadedModel(existing)
93
+
94
+ # 2. Is there an interrupted / matching checkpoint to resume from?
95
+ if checkpoint_mgr.exists():
96
+ ckpt = checkpoint_mgr.load()
97
+ if ckpt.get("dataset_fingerprint") == fingerprint:
98
+ if ckpt.get("training_complete"):
99
+ # Training finished but the final .tl wasn't written
100
+ # (e.g. process died right after the last checkpoint).
101
+ # No need to retrain -- just package the .tl file.
102
+ return _finalize_from_checkpoint(ckpt, out, user_cfg.verbose)
103
+ resume_state = ckpt
104
+ if user_cfg.verbose:
105
+ print(
106
+ f"[tensorless] found an interrupted checkpoint for this "
107
+ f"exact dataset -- resuming training."
108
+ )
109
+ else:
110
+ if user_cfg.ask_on_data_change:
111
+ raise ConfigError(
112
+ "The dataset has changed since the existing checkpoint "
113
+ "was created. Pass force=True to retrain from scratch, "
114
+ "or ask_on_data_change=False to retrain automatically."
115
+ )
116
+ if user_cfg.verbose:
117
+ print(
118
+ "[tensorless] dataset has changed since the last "
119
+ "checkpoint -- retraining from scratch."
120
+ )
121
+ checkpoint_mgr.clear()
122
+ else:
123
+ checkpoint_mgr.clear()
124
+
125
+ ds = load_dataset(path)
126
+ resolved = resolve_config(ds, user_cfg)
127
+ cfg = resolved.to_dict()
128
+
129
+ if resume_state is not None:
130
+ # Resumed runs must keep the exact architecture/config used
131
+ # originally, regardless of any new overrides, so the checkpoint
132
+ # can actually be loaded.
133
+ cfg = resume_state["config"]
134
+
135
+ result = run_training(
136
+ ds=ds,
137
+ cfg=cfg,
138
+ checkpoint_mgr=checkpoint_mgr,
139
+ dataset_fingerprint=fingerprint,
140
+ resume_state=resume_state,
141
+ log_fn=print if cfg.get("verbose", True) else (lambda *a, **k: None),
142
+ )
143
+
144
+ payload = {
145
+ "tl_format_version": _tl_format_version,
146
+ "tensorless_version": _tl_version,
147
+ "task": cfg["task"],
148
+ "model_type": cfg["model_type"],
149
+ "config": cfg,
150
+ "meta": result["meta"],
151
+ "model_state_dict": result["model_state_dict"],
152
+ "tokenizer_state": result["tokenizer"].state_dict() if result["tokenizer"] else None,
153
+ "preprocessor_state": result["preprocessor"].state_dict() if result["preprocessor"] else None,
154
+ "dataset_fingerprint": fingerprint,
155
+ "training_complete": True,
156
+ "metrics": result["metrics"],
157
+ }
158
+ save_tl(out, payload)
159
+ if cfg.get("verbose", True):
160
+ print(f"[tensorless] saved trained model to '{out}'")
161
+
162
+ return LoadedModel(payload)
163
+
164
+
165
+ def _finalize_from_checkpoint(ckpt: dict, out: str, verbose: bool) -> LoadedModel:
166
+ payload = {
167
+ "tl_format_version": _tl_format_version,
168
+ "tensorless_version": _tl_version,
169
+ "task": ckpt["config"]["task"],
170
+ "model_type": ckpt["config"]["model_type"],
171
+ "config": ckpt["config"],
172
+ "meta": ckpt["meta"],
173
+ "model_state_dict": ckpt["model_state_dict"],
174
+ "tokenizer_state": ckpt.get("tokenizer_state"),
175
+ "preprocessor_state": ckpt.get("preprocessor_state"),
176
+ "dataset_fingerprint": ckpt["dataset_fingerprint"],
177
+ "training_complete": True,
178
+ "metrics": {},
179
+ }
180
+ save_tl(out, payload)
181
+ if verbose:
182
+ print(f"[tensorless] finalized already-complete checkpoint into '{out}'")
183
+ return LoadedModel(payload)
184
+
185
+
186
+ def load(path: str, device: Optional[str] = None) -> LoadedModel:
187
+ """Load a trained `.tl` model for inference."""
188
+ return load_model(path, device=device)
189
+
190
+
191
+ def run(path: str, prompt: Optional[str] = None) -> Any:
192
+ """Run a trained `.tl` model.
193
+
194
+ For text-generation models with no prompt given, starts an
195
+ interactive terminal chat. Otherwise runs one generation/prediction
196
+ and returns/prints the result.
197
+ """
198
+ model = load_model(path)
199
+ if model.task == "text-generation":
200
+ if prompt is None:
201
+ model.chat()
202
+ return None
203
+ result = model.generate(prompt)
204
+ print(result)
205
+ return result
206
+ elif model.task == "text-classification":
207
+ if prompt is None:
208
+ print(
209
+ f"Loaded a '{model.task}' model. Use tl.load('{path}').predict(text) "
210
+ f"to classify text, or pass prompt=... / --prompt on the CLI."
211
+ )
212
+ return None
213
+ result = model.predict(prompt)
214
+ print(result)
215
+ return result
216
+ else:
217
+ # tabular classification/regression: needs a structured record, not
218
+ # free text, so the CLI --prompt shortcut doesn't apply here.
219
+ if prompt is not None:
220
+ raise ModelError(
221
+ f"'{path}' is a tabular '{model.task}' model, which expects a "
222
+ f"structured record (e.g. {{'age': 30, 'income': 90000}}), not "
223
+ f"free text. Use tl.load('{path}').predict({{...}}) from Python "
224
+ f"instead of --prompt on the CLI."
225
+ )
226
+ print(
227
+ f"Loaded a tabular '{model.task}' model. Use "
228
+ f"tl.load('{path}').predict({{...}}) to run predictions."
229
+ )
230
+ return None
@@ -0,0 +1,4 @@
1
+ from .detector import detect_task
2
+ from .config import resolve_config
3
+
4
+ __all__ = ["detect_task", "resolve_config"]
@@ -0,0 +1,113 @@
1
+ """Automatic configuration.
2
+
3
+ Turns a `Dataset` + a user-supplied `TrainConfig` (with mostly-None fields)
4
+ into a fully-resolved `ResolvedConfig`. Every automatic choice is driven by
5
+ simple, explainable heuristics based on dataset size -- this is not meant
6
+ to be state-of-the-art NAS, it's meant to produce a *sane, working* default
7
+ so `tl.train("./data")` just works.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict
13
+
14
+ from ..config import TrainConfig, ResolvedConfig
15
+ from ..data.loader import Dataset
16
+ from ..devices.device import auto_select_device
17
+ from .detector import detect_task, target_column
18
+
19
+
20
+ def _auto_model_size(n_examples: int, kind: str):
21
+ """Return (d_model, layers, heads, ff_mult) scaled to dataset size.
22
+
23
+ Small datasets get small models (avoids absurd overfitting / long CPU
24
+ training times); larger datasets get bigger models. These are
25
+ deliberately modest sizes suitable for CPU-friendly experimentation --
26
+ users can always override with layers=, d_model=, etc.
27
+ """
28
+ if kind in ("text", "text_labeled"):
29
+ if n_examples < 200:
30
+ return 64, 2, 2, 2
31
+ elif n_examples < 5000:
32
+ return 128, 4, 4, 2
33
+ elif n_examples < 50000:
34
+ return 256, 6, 8, 4
35
+ else:
36
+ return 384, 8, 8, 4
37
+ else: # tabular
38
+ if n_examples < 500:
39
+ return 32, 2, 1, 2
40
+ elif n_examples < 20000:
41
+ return 64, 3, 1, 2
42
+ else:
43
+ return 128, 4, 1, 2
44
+
45
+
46
+ def _auto_batch_size(n_examples: int) -> int:
47
+ if n_examples < 200:
48
+ return 8
49
+ elif n_examples < 2000:
50
+ return 16
51
+ elif n_examples < 20000:
52
+ return 32
53
+ else:
54
+ return 64
55
+
56
+
57
+ def _auto_epochs(n_examples: int) -> int:
58
+ if n_examples < 200:
59
+ return 40
60
+ elif n_examples < 2000:
61
+ return 20
62
+ elif n_examples < 20000:
63
+ return 10
64
+ else:
65
+ return 5
66
+
67
+
68
+ def resolve_config(ds: Dataset, user: TrainConfig) -> ResolvedConfig:
69
+ n = len(ds)
70
+ task = user.task or detect_task(ds)
71
+ model_type = user.model_type or (
72
+ "transformer" if task in ("text-generation", "text-classification") else "mlp"
73
+ )
74
+
75
+ d_model, layers, heads, ff_mult = _auto_model_size(n, ds.kind)
76
+
77
+ device, precision = auto_select_device(user.device, user.precision)
78
+
79
+ out = user.out or "model.tl"
80
+ checkpoint_dir = user.checkpoint_dir or (out + ".ckpt")
81
+
82
+ resolved = ResolvedConfig(
83
+ out=out,
84
+ force=bool(user.force),
85
+ resume=user.resume,
86
+ ask_on_data_change=bool(user.ask_on_data_change),
87
+ task=task,
88
+ model_type=model_type,
89
+ d_model=user.d_model or d_model,
90
+ layers=user.layers or layers,
91
+ heads=user.heads or heads,
92
+ ff_mult=user.ff_mult or ff_mult,
93
+ dropout=user.dropout if user.dropout is not None else 0.1,
94
+ max_seq_len=user.max_seq_len or (256 if ds.kind in ("text", "text_labeled") else 1),
95
+ optimizer=user.optimizer or "adamw",
96
+ learning_rate=user.learning_rate or (3e-4 if model_type == "transformer" else 1e-3),
97
+ weight_decay=user.weight_decay if user.weight_decay is not None else 0.01,
98
+ batch_size=user.batch_size or _auto_batch_size(n),
99
+ epochs=user.epochs or _auto_epochs(n),
100
+ max_steps=user.max_steps,
101
+ grad_clip=user.grad_clip if user.grad_clip is not None else 1.0,
102
+ warmup_steps=user.warmup_steps if user.warmup_steps is not None else min(100, max(1, n // 10)),
103
+ val_split=user.val_split if user.val_split is not None else (0.1 if n >= 50 else 0.0),
104
+ patience=user.patience or 5,
105
+ min_delta=user.min_delta if user.min_delta is not None else 1e-4,
106
+ device=device,
107
+ precision=precision,
108
+ checkpoint_every=user.checkpoint_every or 50,
109
+ checkpoint_dir=checkpoint_dir,
110
+ seed=user.seed,
111
+ verbose=user.verbose,
112
+ )
113
+ return resolved
@@ -0,0 +1,95 @@
1
+ """Automatic task-type detection.
2
+
3
+ Given a loaded `Dataset`, decide what kind of ML task it represents:
4
+
5
+ - "text-generation" : raw text corpus -> train a language model
6
+ - "text-classification": (text, label) pairs -> classify text
7
+ - "classification" : tabular data with a categorical target
8
+ - "regression" : tabular data with a numeric target
9
+
10
+ The detector is intentionally simple and explainable: every decision can
11
+ be described in one sentence, which is important because `tl.inspect()`
12
+ reports *why* a task was chosen.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ from ..data.loader import Dataset
20
+ from ..errors import DataError
21
+
22
+ _TARGET_CANDIDATES = ("label", "target", "class", "category", "y", "output")
23
+
24
+
25
+ def _looks_numeric(values) -> bool:
26
+ n_ok = 0
27
+ n_total = 0
28
+ for v in values:
29
+ if v is None or v == "":
30
+ continue
31
+ n_total += 1
32
+ try:
33
+ float(v)
34
+ n_ok += 1
35
+ except (TypeError, ValueError):
36
+ pass
37
+ if n_total == 0:
38
+ return False
39
+ return (n_ok / n_total) > 0.95
40
+
41
+
42
+ def _find_target_column(ds: Dataset) -> Optional[str]:
43
+ for cand in _TARGET_CANDIDATES:
44
+ for col in ds.columns:
45
+ if col.lower() == cand:
46
+ return col
47
+ # Fall back to the last column, a common convention in tabular datasets.
48
+ return ds.columns[-1] if ds.columns else None
49
+
50
+
51
+ def detect_task(ds: Dataset) -> str:
52
+ """Return one of "text-generation", "text-classification",
53
+ "classification", "regression".
54
+ """
55
+ if ds.kind == "text":
56
+ return "text-generation"
57
+
58
+ if ds.kind == "text_labeled":
59
+ return "text-classification"
60
+
61
+ if ds.kind == "tabular":
62
+ if not ds.records:
63
+ raise DataError("Tabular dataset has no rows.")
64
+ target = _find_target_column(ds)
65
+ if target is None:
66
+ raise DataError(
67
+ "Could not find a target/label column in the tabular data. "
68
+ "Tensorless looks for a column named one of: "
69
+ f"{', '.join(_TARGET_CANDIDATES)} (or uses the last column)."
70
+ )
71
+ values = [r.get(target) for r in ds.records]
72
+ if _looks_numeric(values):
73
+ unique_vals = set(values)
74
+ # Small number of distinct numeric values that are all
75
+ # integer-like -> more likely classification (e.g. 0/1 labels)
76
+ # than regression.
77
+ if len(unique_vals) <= min(10, max(2, len(values) // 20)):
78
+ try:
79
+ all_int_like = all(float(v) == int(float(v)) for v in unique_vals if v not in (None, ""))
80
+ except (TypeError, ValueError):
81
+ all_int_like = False
82
+ if all_int_like:
83
+ return "classification"
84
+ return "regression"
85
+ return "classification"
86
+
87
+ raise DataError(f"Unknown dataset kind: {ds.kind}")
88
+
89
+
90
+ def target_column(ds: Dataset) -> Optional[str]:
91
+ """Public helper mirroring `_find_target_column`, used by the tabular
92
+ model pipeline so detection and training agree on which column is
93
+ the target.
94
+ """
95
+ return _find_target_column(ds)
@@ -0,0 +1,3 @@
1
+ from .manager import CheckpointManager
2
+
3
+ __all__ = ["CheckpointManager"]
@@ -0,0 +1,66 @@
1
+ """Checkpoint management.
2
+
3
+ Handles all the state needed to resume training safely and transparently:
4
+ model weights, optimizer state, scheduler state, epoch/step counters, the
5
+ resolved training config, tokenizer/preprocessor state, the dataset
6
+ fingerprint used for training, and the best-metric-so-far for early
7
+ stopping.
8
+
9
+ Users never touch this directly -- `tl.train()` decides automatically
10
+ whether to create, update, or resume from a checkpoint (see
11
+ `training/trainer.py` and the "Smart Auto Check" logic in `api.py`).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import shutil
18
+ import tempfile
19
+ from typing import Any, Dict, Optional
20
+
21
+ import torch
22
+
23
+ from ..errors import CheckpointError
24
+
25
+ CHECKPOINT_FILENAME = "checkpoint.pt"
26
+
27
+
28
+ class CheckpointManager:
29
+ def __init__(self, checkpoint_dir: str):
30
+ self.checkpoint_dir = checkpoint_dir
31
+ self.path = os.path.join(checkpoint_dir, CHECKPOINT_FILENAME)
32
+
33
+ def exists(self) -> bool:
34
+ return os.path.isfile(self.path)
35
+
36
+ def save(self, state: Dict[str, Any]) -> None:
37
+ """Atomically write `state` to the checkpoint file.
38
+
39
+ Writes to a temp file first and renames it into place, so a crash
40
+ or interruption mid-write never leaves a corrupt checkpoint that
41
+ would block resumption.
42
+ """
43
+ os.makedirs(self.checkpoint_dir, exist_ok=True)
44
+ fd, tmp_path = tempfile.mkstemp(dir=self.checkpoint_dir, suffix=".tmp")
45
+ os.close(fd)
46
+ try:
47
+ torch.save(state, tmp_path)
48
+ shutil.move(tmp_path, self.path)
49
+ except Exception as e:
50
+ if os.path.exists(tmp_path):
51
+ os.remove(tmp_path)
52
+ raise CheckpointError(f"Failed to save checkpoint to '{self.path}': {e}") from e
53
+
54
+ def load(self, map_location: Optional[str] = None) -> Dict[str, Any]:
55
+ if not self.exists():
56
+ raise CheckpointError(f"No checkpoint found at '{self.path}'.")
57
+ try:
58
+ return torch.load(self.path, map_location=map_location, weights_only=False)
59
+ except Exception as e:
60
+ raise CheckpointError(
61
+ f"Checkpoint at '{self.path}' is corrupt or incompatible: {e}"
62
+ ) from e
63
+
64
+ def clear(self) -> None:
65
+ if os.path.isdir(self.checkpoint_dir):
66
+ shutil.rmtree(self.checkpoint_dir, ignore_errors=True)
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]
tensorless/cli/main.py ADDED
@@ -0,0 +1,121 @@
1
+ """Command-line interface.
2
+
3
+ tensorless train ./data
4
+ tensorless run model.tl
5
+ tensorless inspect ./data
6
+ tensorless info model.tl
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+
15
+ from .. import api
16
+ from ..errors import TensorlessError
17
+ from ..serialization.tl_format import load_tl
18
+
19
+
20
+ def _add_train_parser(subparsers) -> None:
21
+ p = subparsers.add_parser("train", help="Train a model on a dataset (fully automatic by default)")
22
+ p.add_argument("path", help="Path to a dataset file or directory")
23
+ p.add_argument("--out", default=None, help="Output .tl file path (default: model.tl)")
24
+ p.add_argument("--force", action="store_true", help="Force retraining even if a matching model exists")
25
+ p.add_argument("--d-model", type=int, default=None, dest="d_model")
26
+ p.add_argument("--layers", type=int, default=None)
27
+ p.add_argument("--heads", type=int, default=None)
28
+ p.add_argument("--batch-size", type=int, default=None, dest="batch_size")
29
+ p.add_argument("--epochs", type=int, default=None)
30
+ p.add_argument("--learning-rate", type=float, default=None, dest="learning_rate")
31
+ p.add_argument("--device", default=None, choices=["cpu", "cuda", "tpu", "mps"])
32
+ p.add_argument("--quiet", action="store_true", help="Suppress training logs")
33
+
34
+
35
+ def _add_run_parser(subparsers) -> None:
36
+ p = subparsers.add_parser("run", help="Run a trained .tl model")
37
+ p.add_argument("path", help="Path to a .tl model file")
38
+ p.add_argument("--prompt", default=None, help="Input text/prompt (skips interactive chat)")
39
+
40
+
41
+ def _add_inspect_parser(subparsers) -> None:
42
+ p = subparsers.add_parser("inspect", help="Inspect a dataset without training")
43
+ p.add_argument("path", help="Path to a dataset file or directory")
44
+
45
+
46
+ def _add_info_parser(subparsers) -> None:
47
+ p = subparsers.add_parser("info", help="Show information about a trained .tl model")
48
+ p.add_argument("path", help="Path to a .tl model file")
49
+
50
+
51
+ def build_parser() -> argparse.ArgumentParser:
52
+ parser = argparse.ArgumentParser(prog="tensorless", description="Tensorless: ML with maximum automation.")
53
+ subparsers = parser.add_subparsers(dest="command", required=True)
54
+ _add_train_parser(subparsers)
55
+ _add_run_parser(subparsers)
56
+ _add_inspect_parser(subparsers)
57
+ _add_info_parser(subparsers)
58
+ return parser
59
+
60
+
61
+ def main(argv=None) -> int:
62
+ parser = build_parser()
63
+ args = parser.parse_args(argv)
64
+
65
+ try:
66
+ if args.command == "train":
67
+ overrides = {}
68
+ if args.out:
69
+ overrides["out"] = args.out
70
+ if args.force:
71
+ overrides["force"] = True
72
+ if args.d_model:
73
+ overrides["d_model"] = args.d_model
74
+ if args.layers:
75
+ overrides["layers"] = args.layers
76
+ if args.heads:
77
+ overrides["heads"] = args.heads
78
+ if args.batch_size:
79
+ overrides["batch_size"] = args.batch_size
80
+ if args.epochs:
81
+ overrides["epochs"] = args.epochs
82
+ if args.learning_rate:
83
+ overrides["learning_rate"] = args.learning_rate
84
+ if args.device:
85
+ overrides["device"] = args.device
86
+ if args.quiet:
87
+ overrides["verbose"] = False
88
+ api.train(args.path, **overrides)
89
+ return 0
90
+
91
+ elif args.command == "run":
92
+ api.run(args.path, prompt=args.prompt)
93
+ return 0
94
+
95
+ elif args.command == "inspect":
96
+ api.inspect(args.path)
97
+ return 0
98
+
99
+ elif args.command == "info":
100
+ payload = load_tl(args.path)
101
+ info = {
102
+ "task": payload["task"],
103
+ "model_type": payload["model_type"],
104
+ "tensorless_version": payload.get("tensorless_version"),
105
+ "tl_format_version": payload.get("tl_format_version"),
106
+ "training_complete": payload.get("training_complete"),
107
+ "metrics": payload.get("metrics"),
108
+ "dataset_fingerprint": (payload.get("dataset_fingerprint") or "")[:16],
109
+ }
110
+ print(json.dumps(info, indent=2, default=str))
111
+ return 0
112
+
113
+ except TensorlessError as e:
114
+ print(f"tensorless: error: {e}", file=sys.stderr)
115
+ return 1
116
+
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())