euler-train 2.1.0__tar.gz → 2.2.0__tar.gz
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.
- {euler_train-2.1.0 → euler_train-2.2.0}/PKG-INFO +1 -1
- {euler_train-2.1.0 → euler_train-2.2.0}/README.md +20 -1
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/run.py +142 -5
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train.egg-info/PKG-INFO +1 -1
- {euler_train-2.1.0 → euler_train-2.2.0}/pyproject.toml +1 -1
- {euler_train-2.1.0 → euler_train-2.2.0}/tests/test_runlog.py +57 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/__init__.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/_dataset_contract.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/architecture.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/environment.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/git_info.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/outputs.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/serialization.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train/slurm.py +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train.egg-info/SOURCES.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train.egg-info/dependency_links.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train.egg-info/requires.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/euler_train.egg-info/top_level.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.2.0}/setup.cfg +0 -0
|
@@ -82,7 +82,7 @@ The run ID and directory are available as `run.run_id` and `run.dir`.
|
|
|
82
82
|
|
|
83
83
|
### `euler_train.init(dir, config=None, meta=None, output_formats=None, output_visualization=None, run_id=None, datasets=None, run_name=None, evaluations=None, mode=None) → Run`
|
|
84
84
|
|
|
85
|
-
Creates the run directory and writes `meta.json`, `config.json`, `code_ref.json`, and `run_environment.json`. On resume (`run_id` provided), only `meta.json` and `config.json` are updated.
|
|
85
|
+
Creates the run directory and writes `meta.json`, `config.json`, `code_ref.json`, and `run_environment.json`. On resume (`run_id` provided), only `meta.json` and `config.json` are updated. `meta.json` also maintains an `updated_at` map with last-write timestamps for tracked artifacts.
|
|
86
86
|
|
|
87
87
|
| Parameter | Type | Description |
|
|
88
88
|
|---|---|---|
|
|
@@ -365,6 +365,24 @@ Auto-managed, not written to directly.
|
|
|
365
365
|
"traceback": "Traceback (most recent call last):\n ..."
|
|
366
366
|
}
|
|
367
367
|
},
|
|
368
|
+
"updated_at": {
|
|
369
|
+
"meta.json": {
|
|
370
|
+
"time": 1706403300.0,
|
|
371
|
+
"iso": "2025-01-28T16:25:00"
|
|
372
|
+
},
|
|
373
|
+
"config.json": {
|
|
374
|
+
"time": 1706400000.0,
|
|
375
|
+
"iso": "2025-01-28T15:30:42"
|
|
376
|
+
},
|
|
377
|
+
"train.jsonl": {
|
|
378
|
+
"time": 1706402999.0,
|
|
379
|
+
"iso": "2025-01-28T16:19:59"
|
|
380
|
+
},
|
|
381
|
+
"outputs/epoch_12_step_4800": {
|
|
382
|
+
"time": 1706403010.0,
|
|
383
|
+
"iso": "2025-01-28T16:20:10"
|
|
384
|
+
}
|
|
385
|
+
},
|
|
368
386
|
"error": "RuntimeError: CUDA OOM",
|
|
369
387
|
"traceback": "Traceback (most recent call last):\n ..."
|
|
370
388
|
}
|
|
@@ -375,6 +393,7 @@ Auto-managed, not written to directly.
|
|
|
375
393
|
- `datasets` is only present when `datasets=...` is passed to `euler_train.init`.
|
|
376
394
|
- `evaluations` is only present when evaluations are provided via `evaluations=...` on `init()` or added via `run.add_evaluation()`.
|
|
377
395
|
- `modes` is only present when `mode=...` is passed to `euler_train.init`; each key stores the latest lifecycle snapshot for that mode.
|
|
396
|
+
- `updated_at` stores last-write timestamps for tracked artifacts. Keys are run-relative paths when the artifact lives inside the run directory (for example `train.jsonl` or `outputs/epoch_2_step_500`) and absolute paths for external artifacts such as custom checkpoint locations.
|
|
378
397
|
- `error` is only present when `status` is `"crashed"` (context manager / excepthook) or `"interrupted"` (SIGTERM/SIGINT). `traceback` is only present when `status` is `"crashed"`. When `mode=...` is set, the same fields are mirrored under `modes[mode]`.
|
|
379
398
|
|
|
380
399
|
A formal JSON Schema for `meta.json` is available at [`meta-schema.json`](meta-schema.json).
|
|
@@ -82,6 +82,7 @@ class Run:
|
|
|
82
82
|
self._gpu_stats_every: int = gpu_stats_every
|
|
83
83
|
self.run_name: str | None = run_name
|
|
84
84
|
self.mode: str | None = _normalize_mode(mode)
|
|
85
|
+
self._pending_updated_at: dict[str, dict[str, Any]] = {}
|
|
85
86
|
|
|
86
87
|
# ── config ────────────────────────────────────────────────
|
|
87
88
|
if resuming:
|
|
@@ -92,12 +93,19 @@ class Run:
|
|
|
92
93
|
else:
|
|
93
94
|
self.config: dict = normalize_config(config)
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
config_path = self.dir / "config.json"
|
|
97
|
+
write_json(config_path, self.config)
|
|
98
|
+
self._record_artifact_update(config_path)
|
|
96
99
|
|
|
97
100
|
# ── code ref & run environment (fresh runs only) ─────────
|
|
98
101
|
if not resuming:
|
|
99
|
-
|
|
100
|
-
write_json(
|
|
102
|
+
code_ref_path = self.dir / "code_ref.json"
|
|
103
|
+
write_json(code_ref_path, get_code_ref())
|
|
104
|
+
self._record_artifact_update(code_ref_path)
|
|
105
|
+
|
|
106
|
+
run_environment_path = self.dir / "run_environment.json"
|
|
107
|
+
write_json(run_environment_path, get_run_environment())
|
|
108
|
+
self._record_artifact_update(run_environment_path)
|
|
101
109
|
|
|
102
110
|
# ── meta ──────────────────────────────────────────────────
|
|
103
111
|
if resuming:
|
|
@@ -238,7 +246,10 @@ class Run:
|
|
|
238
246
|
record.update(self._get_gpu_stats())
|
|
239
247
|
|
|
240
248
|
filename = "train.jsonl" if mode == "train" else "val.jsonl"
|
|
241
|
-
|
|
249
|
+
log_path = self.dir / filename
|
|
250
|
+
append_jsonl(log_path, record)
|
|
251
|
+
self._record_artifact_update(log_path)
|
|
252
|
+
self._flush_meta()
|
|
242
253
|
|
|
243
254
|
# ── GPU stats ─────────────────────────────────────────────────
|
|
244
255
|
|
|
@@ -313,6 +324,9 @@ class Run:
|
|
|
313
324
|
self._output_visualization,
|
|
314
325
|
output_type,
|
|
315
326
|
)
|
|
327
|
+
if base.exists():
|
|
328
|
+
self._record_artifact_update(base)
|
|
329
|
+
self._flush_meta()
|
|
316
330
|
return base
|
|
317
331
|
|
|
318
332
|
# ── checkpoints ───────────────────────────────────────────────
|
|
@@ -339,6 +353,7 @@ class Run:
|
|
|
339
353
|
if self.checkpoint_dir is not None:
|
|
340
354
|
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
341
355
|
self._meta["checkpoint_dir"] = str(self.checkpoint_dir)
|
|
356
|
+
self._record_artifact_update(self.checkpoint_dir)
|
|
342
357
|
self._flush_meta()
|
|
343
358
|
log.info("Checkpoint dir: %s", self.checkpoint_dir)
|
|
344
359
|
return self.checkpoint_dir
|
|
@@ -357,6 +372,7 @@ class Run:
|
|
|
357
372
|
)
|
|
358
373
|
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
359
374
|
self._meta["checkpoint_dir"] = str(self.checkpoint_dir)
|
|
375
|
+
self._record_artifact_update(self.checkpoint_dir)
|
|
360
376
|
self._flush_meta()
|
|
361
377
|
log.info("Checkpoint dir: %s", self.checkpoint_dir)
|
|
362
378
|
return self.checkpoint_dir
|
|
@@ -415,6 +431,7 @@ class Run:
|
|
|
415
431
|
:meth:`save_checkpoint` call it automatically. When *is_best*
|
|
416
432
|
is ``True``, any previous checkpoint marked as best is cleared.
|
|
417
433
|
"""
|
|
434
|
+
checkpoint_path = Path(path)
|
|
418
435
|
checkpoints: list[dict[str, Any]] = self._meta.get("checkpoints", [])
|
|
419
436
|
|
|
420
437
|
if is_best:
|
|
@@ -422,7 +439,7 @@ class Run:
|
|
|
422
439
|
entry.pop("is_best", None)
|
|
423
440
|
|
|
424
441
|
record: dict[str, Any] = {
|
|
425
|
-
"path": str(
|
|
442
|
+
"path": str(checkpoint_path),
|
|
426
443
|
"epoch": epoch,
|
|
427
444
|
"step": step,
|
|
428
445
|
}
|
|
@@ -431,6 +448,7 @@ class Run:
|
|
|
431
448
|
|
|
432
449
|
checkpoints.append(record)
|
|
433
450
|
self._meta["checkpoints"] = checkpoints
|
|
451
|
+
self._record_artifact_update(checkpoint_path)
|
|
434
452
|
self._flush_meta()
|
|
435
453
|
|
|
436
454
|
# ── architecture export ─────────────────────────────────────────
|
|
@@ -467,6 +485,7 @@ class Run:
|
|
|
467
485
|
model, dummy_input, self.dir / "architecture.onnx"
|
|
468
486
|
)
|
|
469
487
|
self._meta["architecture"] = "architecture.onnx"
|
|
488
|
+
self._record_artifact_update(output_path)
|
|
470
489
|
self._flush_meta()
|
|
471
490
|
return output_path
|
|
472
491
|
|
|
@@ -652,7 +671,45 @@ class Run:
|
|
|
652
671
|
self._meta["modes"] = modes
|
|
653
672
|
return modes
|
|
654
673
|
|
|
674
|
+
def _updated_at_store(self) -> dict[str, Any]:
|
|
675
|
+
if not hasattr(self, "_meta"):
|
|
676
|
+
return self._pending_updated_at
|
|
677
|
+
|
|
678
|
+
updated_at = self._meta.get("updated_at")
|
|
679
|
+
if not isinstance(updated_at, dict):
|
|
680
|
+
updated_at = {}
|
|
681
|
+
self._meta["updated_at"] = updated_at
|
|
682
|
+
|
|
683
|
+
if self._pending_updated_at:
|
|
684
|
+
updated_at.update(self._pending_updated_at)
|
|
685
|
+
self._pending_updated_at.clear()
|
|
686
|
+
|
|
687
|
+
return updated_at
|
|
688
|
+
|
|
689
|
+
def _record_artifact_update(
|
|
690
|
+
self,
|
|
691
|
+
path: str | Path,
|
|
692
|
+
*,
|
|
693
|
+
when: float | None = None,
|
|
694
|
+
) -> None:
|
|
695
|
+
artifact_path = Path(path)
|
|
696
|
+
ts = when if when is not None else _artifact_mtime(artifact_path)
|
|
697
|
+
if ts is None:
|
|
698
|
+
ts = time.time()
|
|
699
|
+
self._updated_at_store()[self._artifact_key(artifact_path)] = (
|
|
700
|
+
_updated_at_entry(ts)
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
def _artifact_key(self, path: Path) -> str:
|
|
704
|
+
if path.is_absolute():
|
|
705
|
+
try:
|
|
706
|
+
return path.relative_to(self.dir).as_posix()
|
|
707
|
+
except ValueError:
|
|
708
|
+
return str(path)
|
|
709
|
+
return path.as_posix()
|
|
710
|
+
|
|
655
711
|
def _flush_meta(self) -> None:
|
|
712
|
+
self._record_artifact_update(self.dir / "meta.json", when=time.time())
|
|
656
713
|
write_json(self.dir / "meta.json", self._meta)
|
|
657
714
|
|
|
658
715
|
def __repr__(self) -> str:
|
|
@@ -732,6 +789,20 @@ def _isotime(ts: float) -> str:
|
|
|
732
789
|
return time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(ts))
|
|
733
790
|
|
|
734
791
|
|
|
792
|
+
def _updated_at_entry(ts: float) -> dict[str, Any]:
|
|
793
|
+
return {
|
|
794
|
+
"time": ts,
|
|
795
|
+
"iso": _isotime(ts),
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _artifact_mtime(path: Path) -> float | None:
|
|
800
|
+
try:
|
|
801
|
+
return path.stat().st_mtime
|
|
802
|
+
except OSError:
|
|
803
|
+
return None
|
|
804
|
+
|
|
805
|
+
|
|
735
806
|
def _normalize_mode(mode: str | None) -> str | None:
|
|
736
807
|
if mode is None:
|
|
737
808
|
return None
|
|
@@ -1050,6 +1121,9 @@ def _read_ds_crawler_descriptor(path: str) -> dict[str, Any]:
|
|
|
1050
1121
|
try:
|
|
1051
1122
|
cfg = load_dataset_config({"path": path})
|
|
1052
1123
|
except Exception:
|
|
1124
|
+
descriptor = _read_output_json_descriptor(path)
|
|
1125
|
+
if descriptor:
|
|
1126
|
+
return descriptor
|
|
1053
1127
|
try:
|
|
1054
1128
|
contract = get_dataset_contract(path)
|
|
1055
1129
|
except Exception:
|
|
@@ -1082,6 +1156,69 @@ def _read_ds_crawler_descriptor(path: str) -> dict[str, Any]:
|
|
|
1082
1156
|
return descriptor
|
|
1083
1157
|
|
|
1084
1158
|
|
|
1159
|
+
def _read_output_json_descriptor(path: str) -> dict[str, Any]:
|
|
1160
|
+
try:
|
|
1161
|
+
from ds_crawler import zip_utils
|
|
1162
|
+
except Exception:
|
|
1163
|
+
return {}
|
|
1164
|
+
|
|
1165
|
+
try:
|
|
1166
|
+
raw = zip_utils.read_metadata_json(Path(path), "output.json")
|
|
1167
|
+
except Exception:
|
|
1168
|
+
return {}
|
|
1169
|
+
|
|
1170
|
+
return _descriptor_from_output_json(raw, path)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _descriptor_from_output_json(raw: Any, path: str) -> dict[str, Any]:
|
|
1174
|
+
if isinstance(raw, Mapping):
|
|
1175
|
+
candidates: list[Any]
|
|
1176
|
+
modalities = raw.get("modalities")
|
|
1177
|
+
if isinstance(modalities, list):
|
|
1178
|
+
candidates = modalities
|
|
1179
|
+
else:
|
|
1180
|
+
candidates = [raw]
|
|
1181
|
+
elif isinstance(raw, list):
|
|
1182
|
+
candidates = raw
|
|
1183
|
+
else:
|
|
1184
|
+
return {}
|
|
1185
|
+
|
|
1186
|
+
selected: Mapping[str, Any] | None = None
|
|
1187
|
+
path_name = Path(path).name
|
|
1188
|
+
for candidate in candidates:
|
|
1189
|
+
if not isinstance(candidate, Mapping):
|
|
1190
|
+
continue
|
|
1191
|
+
if _as_non_empty_str(candidate.get("name")) == path_name:
|
|
1192
|
+
selected = candidate
|
|
1193
|
+
break
|
|
1194
|
+
if selected is None:
|
|
1195
|
+
selected = candidate
|
|
1196
|
+
|
|
1197
|
+
if selected is None:
|
|
1198
|
+
return {}
|
|
1199
|
+
|
|
1200
|
+
descriptor: dict[str, Any] = {}
|
|
1201
|
+
properties = selected.get("properties")
|
|
1202
|
+
merged_properties = dict(properties) if isinstance(properties, Mapping) else {}
|
|
1203
|
+
|
|
1204
|
+
euler_train_props = selected.get("euler_train")
|
|
1205
|
+
if isinstance(euler_train_props, Mapping):
|
|
1206
|
+
merged_properties["euler_train"] = dict(euler_train_props)
|
|
1207
|
+
|
|
1208
|
+
if merged_properties:
|
|
1209
|
+
descriptor["properties"] = merged_properties
|
|
1210
|
+
|
|
1211
|
+
modality_key = _as_non_empty_str(selected.get("type"))
|
|
1212
|
+
if modality_key is not None:
|
|
1213
|
+
descriptor["modality_key"] = modality_key
|
|
1214
|
+
|
|
1215
|
+
hierarchy_regex = _as_non_empty_str(selected.get("hierarchy_regex"))
|
|
1216
|
+
if hierarchy_regex is not None:
|
|
1217
|
+
descriptor["hierarchy_regex"] = hierarchy_regex
|
|
1218
|
+
|
|
1219
|
+
return descriptor
|
|
1220
|
+
|
|
1221
|
+
|
|
1085
1222
|
def _build_modality_entry(
|
|
1086
1223
|
name: str,
|
|
1087
1224
|
path: str,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euler-train"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Lightweight file-based experiment logger for PyTorch"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = ["numpy", "euler-dataset-contract"]
|
|
@@ -27,6 +27,12 @@ def _read_jsonl(path: Path) -> list[dict]:
|
|
|
27
27
|
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _assert_updated_at_entry(entry: dict) -> None:
|
|
31
|
+
assert isinstance(entry["time"], (int, float))
|
|
32
|
+
assert isinstance(entry["iso"], str)
|
|
33
|
+
assert entry["iso"]
|
|
34
|
+
|
|
35
|
+
|
|
30
36
|
class _DummyModality:
|
|
31
37
|
def __init__(self, path: str) -> None:
|
|
32
38
|
self.path = path
|
|
@@ -228,6 +234,57 @@ class TestInit:
|
|
|
228
234
|
run.finish()
|
|
229
235
|
|
|
230
236
|
|
|
237
|
+
class TestUpdatedAt:
|
|
238
|
+
def test_init_tracks_bootstrap_artifacts(self, tmp_path):
|
|
239
|
+
run = euler_train.init(dir=str(tmp_path / "r"), config={})
|
|
240
|
+
|
|
241
|
+
meta = _read_json(run.dir / "meta.json")
|
|
242
|
+
updated_at = meta["updated_at"]
|
|
243
|
+
|
|
244
|
+
for key in (
|
|
245
|
+
"meta.json",
|
|
246
|
+
"config.json",
|
|
247
|
+
"code_ref.json",
|
|
248
|
+
"run_environment.json",
|
|
249
|
+
):
|
|
250
|
+
assert key in updated_at
|
|
251
|
+
_assert_updated_at_entry(updated_at[key])
|
|
252
|
+
run.finish()
|
|
253
|
+
|
|
254
|
+
def test_log_tracks_train_and_val_jsonl(self, tmp_path):
|
|
255
|
+
run = euler_train.init(dir=str(tmp_path / "r"), config={})
|
|
256
|
+
|
|
257
|
+
run.log({"loss": 1.0}, step=0, epoch=0)
|
|
258
|
+
run.log({"metric": 0.9}, step=1, epoch=0, mode="val")
|
|
259
|
+
|
|
260
|
+
meta = _read_json(run.dir / "meta.json")
|
|
261
|
+
updated_at = meta["updated_at"]
|
|
262
|
+
_assert_updated_at_entry(updated_at["train.jsonl"])
|
|
263
|
+
_assert_updated_at_entry(updated_at["val.jsonl"])
|
|
264
|
+
_assert_updated_at_entry(updated_at["meta.json"])
|
|
265
|
+
run.finish()
|
|
266
|
+
|
|
267
|
+
def test_save_outputs_tracks_output_directory(self, tmp_path):
|
|
268
|
+
run = euler_train.init(dir=str(tmp_path / "r"), config={})
|
|
269
|
+
arr = np.zeros((4, 4, 3), dtype=np.uint8)
|
|
270
|
+
|
|
271
|
+
run.save_outputs(epoch=0, step=0, rgb=dict(pred=arr))
|
|
272
|
+
|
|
273
|
+
meta = _read_json(run.dir / "meta.json")
|
|
274
|
+
_assert_updated_at_entry(meta["updated_at"]["outputs/epoch_0_step_0"])
|
|
275
|
+
run.finish()
|
|
276
|
+
|
|
277
|
+
def test_log_saved_checkpoint_tracks_checkpoint_path(self, tmp_path):
|
|
278
|
+
run = euler_train.init(dir=str(tmp_path / "r"), config={})
|
|
279
|
+
checkpoint_path = tmp_path / "external" / "epoch_1.pt"
|
|
280
|
+
|
|
281
|
+
run.log_saved_checkpoint(checkpoint_path, epoch=1, step=10)
|
|
282
|
+
|
|
283
|
+
meta = _read_json(run.dir / "meta.json")
|
|
284
|
+
_assert_updated_at_entry(meta["updated_at"][str(checkpoint_path)])
|
|
285
|
+
run.finish()
|
|
286
|
+
|
|
287
|
+
|
|
231
288
|
class TestDatasetMetadata:
|
|
232
289
|
def test_prefers_dataset_describe_for_runlog_contract(
|
|
233
290
|
self, tmp_path, monkeypatch
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|