AQIT 0.1.0
Loading...
Searching...
No Matches
run.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6from __future__ import annotations
7
8import json
9import os
10import threading
11import time
12import uuid
13from pathlib import Path
14from typing import Any
15
16
17def init(
18 base_model: str | None = None,
19 run_name: str | None = None,
20 config: dict[str, Any] | None = None,
21) -> "Run":
22 """Start recording a training run. Call this before your training loop."""
23 return Run(base_model=base_model, run_name=run_name, config=config)
24
25
26class Run:
27 """
28 Records metrics and checkpoints locally during training.
29 Call aquin.init() to create one.
31 Writes under ``./aquin_run/`` (metrics, config, checkpoint).
32
33 Example
34 -------
35 run = aquin.init(
36 base_model="meta-llama/Llama-3.2-1B-Instruct",
37 run_name="my-run",
38 config={"lr": 2e-4, "epochs": 3, "rank": 16, "lora_alpha": 32,
39 "method": "qlora", "per_device_train_batch_size": 2,
40 "gradient_accumulation_steps": 8, "dataset": "data.jsonl"},
41 )
42 for step, batch in enumerate(dataloader):
43 loss = train_step(batch)
44 run.log(step, loss=loss.item(), learning_rate=scheduler.get_last_lr()[0],
45 grad_norm=grad_norm, epoch=epoch)
46 run.checkpoint(model, step=step)
47 run.finish()
48 """
49
50 def __init__(
51 self,
52 base_model: str | None = None,
53 run_name: str | None = None,
54 config: dict[str, Any] | None = None,
55 *,
56 quant: str = "none",
57 mode: str = "external",
58 run_dir: str | Path | None = None,
59 ) -> None:
60 _ = quant, mode # reserved for future recipe / runtime tags
61 self._run_id = str(uuid.uuid4())
62 self._run_name = run_name or f"run-{self._run_id[:8]}"
63 self._base_model = base_model or os.environ.get("AQUIN_BASE_MODEL") or ""
64 self._started_at = time.time()
66 self._run_dir = Path(run_dir) if run_dir is not None else Path.cwd() / "aquin_run"
67 self._run_dir.mkdir(parents=True, exist_ok=True)
68 (self._run_dir / "checkpoints").mkdir(exist_ok=True)
69
70 self._metrics: list[dict[str, Any]] = []
71 self._obs: list[dict[str, Any]] = []
72 self._signals: list[dict[str, Any]] = []
73 self._metrics_lock = threading.Lock()
76 if config:
77 self._write_config(config)
78
79 print(f"[aquin] Recording run '{self._run_name}' (id: {self._run_id})")
80 print(f"[aquin] Run data: {self._run_dir}")
81
82 def log(
83 self,
84 step: int,
85 *,
86 loss: float,
87 learning_rate: float | None = None,
88 grad_norm: float | None = None,
89 momentum_norm: float | None = None,
90 epoch: int | None = None,
91 batch: int | None = None,
92 total_batches: int | None = None,
93 step_ms: float | None = None,
94 **extra: float,
95 ) -> None:
96 """Record metrics for one training step. Call every step inside your loop."""
97 entry: dict[str, Any] = {"step": step, "metrics": {"loss": loss}}
98 if learning_rate is not None:
99 entry["metrics"]["learning_rate"] = learning_rate
100 if grad_norm is not None:
101 entry["metrics"]["grad_norm"] = grad_norm
102 if momentum_norm is not None:
103 entry["metrics"]["momentum_norm"] = momentum_norm
104 if step_ms is not None:
105 entry["metrics"]["step_ms"] = step_ms
106 if extra:
107 entry["metrics"].update(extra)
108 if epoch is not None:
109 entry["epoch"] = epoch
110 if batch is not None:
111 entry["batch"] = batch
112 if total_batches is not None:
113 entry["total_batches"] = total_batches
114
115 with self._metrics_lock:
116 self._metrics.append(entry)
117 self._write_metrics()
118
119 def observe(self, step: int, channel: str, value: float) -> None:
120 """Emit a custom scalar observation (eval metrics, etc.)."""
121 row = {"step": step, "channel": channel, "value": float(value), "ts": time.time()}
122 with self._metrics_lock:
123 self._obs.append(row)
124 self._write_obs()
125
126 def signal(self, step: int, message: str, *, severity: str = "info") -> None:
127 """Emit a non-scalar training signal (checkpoint saved, OOM, etc.)."""
128 row = {
129 "step": step,
130 "message": message,
131 "severity": severity,
132 "ts": time.time(),
133 }
134 with self._metrics_lock:
135 self._signals.append(row)
136 self._write_signals()
137
138 def checkpoint(self, model: Any, step: int) -> None:
139 """Save the final model checkpoint. One per run — replaces the previous save."""
140 try:
141 import torch
142 except ImportError:
143 raise ImportError("torch is required to save checkpoints.")
144
145 ckpt_path = self._run_dir / "checkpoints" / "checkpoint.pt"
146 print(f"[aquin] Saving checkpoint (step {step})...")
147 torch.save({"step": step, "state_dict": model.state_dict()}, ckpt_path)
148 self._write_metrics()
149 self.signal(step, f"checkpoint saved (step {step})", severity="info")
150 print(f"[aquin] Checkpoint saved (step {step}).")
151
152 def finish(self, config: dict[str, Any] | None = None) -> None:
153 """Finalise the run. Flushes metrics and optional config to disk."""
154 self._write_metrics()
155 if config:
156 self._write_config(config)
157 elapsed = round(time.time() - self._started_at)
158 print(f"[aquin] Run finished. {len(self._metrics)} steps recorded in {elapsed}s.")
159 print(f"[aquin] Data: {self._run_dir}")
160
161 @property
162 def run_id(self) -> str:
163 return self._run_id
164
165 def _write_meta(self) -> None:
166 meta = {
167 "run_id": self._run_id,
168 "run_name": self._run_name,
169 "base_model": self._base_model,
170 "started_at": self._started_at,
171 }
172 with open(self._run_dir / "meta.json", "w") as f:
173 json.dump(meta, f, indent=2)
174
175 def _write_metrics(self) -> None:
176 with self._metrics_lock:
177 data = list(self._metrics)
178 with open(self._run_dir / "metrics.json", "w") as f:
179 json.dump(data, f)
180
181 def _write_obs(self) -> None:
182 with self._metrics_lock:
183 data = list(self._obs)
184 with open(self._run_dir / "observations.json", "w") as f:
185 json.dump(data, f)
186
187 def _write_signals(self) -> None:
188 with self._metrics_lock:
189 data = list(self._signals)
190 with open(self._run_dir / "signals.json", "w") as f:
191 json.dump(data, f)
192
193 def _write_config(self, config: dict[str, Any]) -> None:
194 with open(self._run_dir / "config.json", "w") as f:
195 json.dump(config, f, indent=2)
str _run_name
Definition run.py:66
None observe(self, int step, str channel, float value)
Definition run.py:123
str run_id(self)
Definition run.py:166
None _write_signals(self)
Definition run.py:191
None checkpoint(self, Any model, int step)
Definition run.py:142
str _base_model
Definition run.py:67
None __init__(self, str|None base_model=None, str|None run_name=None, dict[str, Any]|None config=None, *, str quant="none", str mode="external", str|Path|None run_dir=None)
Definition run.py:63
list _obs
Definition run.py:75
list _signals
Definition run.py:76
list _metrics
Definition run.py:74
None _write_config(self, dict[str, Any] config)
Definition run.py:197
_metrics_lock
Definition run.py:77
None _write_obs(self)
Definition run.py:185
None _write_meta(self)
Definition run.py:169
None _write_metrics(self)
Definition run.py:179
None signal(self, int step, str message, *, str severity="info")
Definition run.py:130
str _run_dir
Definition run.py:70
None finish(self, dict[str, Any]|None config=None)
Definition run.py:156
None log(self, int step, *, float loss, float|None learning_rate=None, float|None grad_norm=None, float|None momentum_norm=None, int|None epoch=None, int|None batch=None, int|None total_batches=None, float|None step_ms=None, **float extra)
Definition run.py:99
"Run" init(str|None base_model=None, str|None run_name=None, dict[str, Any]|None config=None)
Definition run.py:25