euler-train 2.1.0__tar.gz → 2.3.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.3.0}/PKG-INFO +1 -1
- {euler_train-2.1.0 → euler_train-2.3.0}/README.md +20 -1
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/__init__.py +24 -1
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/run.py +367 -8
- euler_train-2.3.0/euler_train/stream.py +566 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train.egg-info/PKG-INFO +1 -1
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train.egg-info/SOURCES.txt +1 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/pyproject.toml +1 -1
- {euler_train-2.1.0 → euler_train-2.3.0}/tests/test_runlog.py +315 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/_dataset_contract.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/architecture.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/environment.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/git_info.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/outputs.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/serialization.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train/slurm.py +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train.egg-info/dependency_links.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train.egg-info/requires.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.3.0}/euler_train.egg-info/top_level.txt +0 -0
- {euler_train-2.1.0 → euler_train-2.3.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).
|
|
@@ -6,8 +6,24 @@ from typing import Any
|
|
|
6
6
|
from . import _dataset_contract # noqa: F401
|
|
7
7
|
from .run import Run
|
|
8
8
|
from .architecture import export_architecture
|
|
9
|
+
from .stream import (
|
|
10
|
+
EulerViewStreamConfig,
|
|
11
|
+
EulerViewStreamConsumer,
|
|
12
|
+
OutputStream,
|
|
13
|
+
OutputStreamConsumer,
|
|
14
|
+
StreamContext,
|
|
15
|
+
)
|
|
9
16
|
|
|
10
|
-
__all__ = [
|
|
17
|
+
__all__ = [
|
|
18
|
+
"EulerViewStreamConfig",
|
|
19
|
+
"EulerViewStreamConsumer",
|
|
20
|
+
"OutputStream",
|
|
21
|
+
"OutputStreamConsumer",
|
|
22
|
+
"StreamContext",
|
|
23
|
+
"init",
|
|
24
|
+
"Run",
|
|
25
|
+
"export_architecture",
|
|
26
|
+
]
|
|
11
27
|
__version__ = "0.1.0"
|
|
12
28
|
|
|
13
29
|
|
|
@@ -22,6 +38,7 @@ def init(
|
|
|
22
38
|
run_name: str | None = None,
|
|
23
39
|
evaluations: dict[str, dict] | None = None,
|
|
24
40
|
mode: str | None = None,
|
|
41
|
+
stream: Any = None,
|
|
25
42
|
) -> Run:
|
|
26
43
|
"""Create a new run — or resume an existing one — and return the handle.
|
|
27
44
|
|
|
@@ -89,6 +106,11 @@ def init(
|
|
|
89
106
|
``"train"``, ``"val"``, or ``"eval"``). When provided,
|
|
90
107
|
lifecycle fields and crash details are mirrored into
|
|
91
108
|
``meta.json`` under ``modes[mode]``.
|
|
109
|
+
stream:
|
|
110
|
+
Optional output-stream consumer or config. Passing a mapping
|
|
111
|
+
with ``base_url`` plus either ``stream_token`` or the pair
|
|
112
|
+
``model_id`` / ``access_token`` enables best-effort dual-write
|
|
113
|
+
streaming to the Euler View model-run ingest API.
|
|
92
114
|
"""
|
|
93
115
|
return Run(
|
|
94
116
|
dir=dir, config=config, meta=meta,
|
|
@@ -97,4 +119,5 @@ def init(
|
|
|
97
119
|
run_id=run_id,
|
|
98
120
|
datasets=datasets, run_name=run_name,
|
|
99
121
|
evaluations=evaluations, mode=mode,
|
|
122
|
+
stream=stream,
|
|
100
123
|
)
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import atexit
|
|
5
|
+
import copy
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import secrets
|
|
9
|
-
import warnings
|
|
10
10
|
import signal
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
import traceback
|
|
14
|
+
import warnings
|
|
14
15
|
from collections.abc import Mapping
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
@@ -20,6 +21,7 @@ from .outputs import save_output_tree
|
|
|
20
21
|
from .slurm import get_slurm_info
|
|
21
22
|
from .git_info import get_code_ref
|
|
22
23
|
from .environment import get_run_environment
|
|
24
|
+
from .stream import OutputStream, StreamContext, coerce_output_stream
|
|
23
25
|
|
|
24
26
|
log = logging.getLogger("euler_train")
|
|
25
27
|
|
|
@@ -45,6 +47,7 @@ class Run:
|
|
|
45
47
|
run_name: str | None = None,
|
|
46
48
|
evaluations: dict[str, dict[str, Any]] | None = None,
|
|
47
49
|
mode: str | None = None,
|
|
50
|
+
stream: Any = None,
|
|
48
51
|
) -> None:
|
|
49
52
|
# ── detect run-directory shorthand ────────────────────────
|
|
50
53
|
# When `dir` points to an existing run directory (contains
|
|
@@ -82,6 +85,9 @@ class Run:
|
|
|
82
85
|
self._gpu_stats_every: int = gpu_stats_every
|
|
83
86
|
self.run_name: str | None = run_name
|
|
84
87
|
self.mode: str | None = _normalize_mode(mode)
|
|
88
|
+
self._pending_updated_at: dict[str, dict[str, Any]] = {}
|
|
89
|
+
self._stream: OutputStream | None = coerce_output_stream(stream)
|
|
90
|
+
self._stream_event_counters: dict[str, int] = {}
|
|
85
91
|
|
|
86
92
|
# ── config ────────────────────────────────────────────────
|
|
87
93
|
if resuming:
|
|
@@ -92,12 +98,30 @@ class Run:
|
|
|
92
98
|
else:
|
|
93
99
|
self.config: dict = normalize_config(config)
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
config_path = self.dir / "config.json"
|
|
102
|
+
write_json(config_path, self.config)
|
|
103
|
+
self._record_artifact_update(config_path)
|
|
104
|
+
|
|
105
|
+
# ── code ref & run environment ───────────────────────────
|
|
106
|
+
code_ref_path = self.dir / "code_ref.json"
|
|
107
|
+
run_environment_path = self.dir / "run_environment.json"
|
|
108
|
+
if resuming:
|
|
109
|
+
self._code_ref_payload = (
|
|
110
|
+
read_json(code_ref_path) if code_ref_path.exists() else None
|
|
111
|
+
)
|
|
112
|
+
self._run_environment_payload = (
|
|
113
|
+
read_json(run_environment_path)
|
|
114
|
+
if run_environment_path.exists()
|
|
115
|
+
else None
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
self._code_ref_payload = get_code_ref()
|
|
119
|
+
write_json(code_ref_path, self._code_ref_payload)
|
|
120
|
+
self._record_artifact_update(code_ref_path)
|
|
96
121
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
write_json(self.dir / "run_environment.json", get_run_environment())
|
|
122
|
+
self._run_environment_payload = get_run_environment()
|
|
123
|
+
write_json(run_environment_path, self._run_environment_payload)
|
|
124
|
+
self._record_artifact_update(run_environment_path)
|
|
101
125
|
|
|
102
126
|
# ── meta ──────────────────────────────────────────────────
|
|
103
127
|
if resuming:
|
|
@@ -153,6 +177,8 @@ class Run:
|
|
|
153
177
|
)
|
|
154
178
|
self._mark_mode_running()
|
|
155
179
|
self._flush_meta()
|
|
180
|
+
self._bind_stream()
|
|
181
|
+
self._emit_stream_init()
|
|
156
182
|
self._setup_hooks()
|
|
157
183
|
|
|
158
184
|
verb = "Resumed" if resuming else "Started"
|
|
@@ -194,6 +220,19 @@ class Run:
|
|
|
194
220
|
existing=self._meta.get("evaluations"),
|
|
195
221
|
)
|
|
196
222
|
self._flush_meta()
|
|
223
|
+
evaluations = self._meta.get("evaluations", {})
|
|
224
|
+
if isinstance(evaluations, dict) and key in evaluations:
|
|
225
|
+
self._emit_stream_event(
|
|
226
|
+
{
|
|
227
|
+
"type": "meta",
|
|
228
|
+
"eventId": self._make_stream_event_id("evaluation", key),
|
|
229
|
+
"patch": {
|
|
230
|
+
"evaluations": {
|
|
231
|
+
key: copy.deepcopy(evaluations[key]),
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
)
|
|
197
236
|
|
|
198
237
|
def finish_evaluation(self, key: str, status: str = "completed") -> None:
|
|
199
238
|
"""Mark an existing evaluation as finished.
|
|
@@ -208,6 +247,23 @@ class Run:
|
|
|
208
247
|
)
|
|
209
248
|
evals[key]["status"] = status
|
|
210
249
|
self._flush_meta()
|
|
250
|
+
self._emit_stream_event(
|
|
251
|
+
{
|
|
252
|
+
"type": "meta",
|
|
253
|
+
"eventId": self._make_stream_event_id(
|
|
254
|
+
"evaluation_status",
|
|
255
|
+
key,
|
|
256
|
+
status,
|
|
257
|
+
),
|
|
258
|
+
"patch": {
|
|
259
|
+
"evaluations": {
|
|
260
|
+
key: {
|
|
261
|
+
"status": status,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
)
|
|
211
267
|
|
|
212
268
|
# ── logging ───────────────────────────────────────────────────
|
|
213
269
|
|
|
@@ -238,7 +294,19 @@ class Run:
|
|
|
238
294
|
record.update(self._get_gpu_stats())
|
|
239
295
|
|
|
240
296
|
filename = "train.jsonl" if mode == "train" else "val.jsonl"
|
|
241
|
-
|
|
297
|
+
log_path = self.dir / filename
|
|
298
|
+
append_jsonl(log_path, record)
|
|
299
|
+
self._record_artifact_update(log_path)
|
|
300
|
+
self._flush_meta()
|
|
301
|
+
split = "train" if mode == "train" else "val"
|
|
302
|
+
self._emit_stream_event(
|
|
303
|
+
{
|
|
304
|
+
"type": "metric",
|
|
305
|
+
"eventId": self._make_stream_event_id(split, epoch, step),
|
|
306
|
+
"split": split,
|
|
307
|
+
"records": [copy.deepcopy(record)],
|
|
308
|
+
},
|
|
309
|
+
)
|
|
242
310
|
|
|
243
311
|
# ── GPU stats ─────────────────────────────────────────────────
|
|
244
312
|
|
|
@@ -313,6 +381,22 @@ class Run:
|
|
|
313
381
|
self._output_visualization,
|
|
314
382
|
output_type,
|
|
315
383
|
)
|
|
384
|
+
if base.exists():
|
|
385
|
+
self._record_artifact_update(base)
|
|
386
|
+
self._flush_meta()
|
|
387
|
+
snapshot = self._build_output_snapshot(epoch, step, output_types)
|
|
388
|
+
if snapshot is not None:
|
|
389
|
+
self._emit_stream_event(
|
|
390
|
+
{
|
|
391
|
+
"type": "output_snapshot",
|
|
392
|
+
"eventId": self._make_stream_event_id(
|
|
393
|
+
"outputs",
|
|
394
|
+
epoch,
|
|
395
|
+
step,
|
|
396
|
+
),
|
|
397
|
+
"snapshot": snapshot,
|
|
398
|
+
},
|
|
399
|
+
)
|
|
316
400
|
return base
|
|
317
401
|
|
|
318
402
|
# ── checkpoints ───────────────────────────────────────────────
|
|
@@ -339,6 +423,7 @@ class Run:
|
|
|
339
423
|
if self.checkpoint_dir is not None:
|
|
340
424
|
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
341
425
|
self._meta["checkpoint_dir"] = str(self.checkpoint_dir)
|
|
426
|
+
self._record_artifact_update(self.checkpoint_dir)
|
|
342
427
|
self._flush_meta()
|
|
343
428
|
log.info("Checkpoint dir: %s", self.checkpoint_dir)
|
|
344
429
|
return self.checkpoint_dir
|
|
@@ -357,6 +442,7 @@ class Run:
|
|
|
357
442
|
)
|
|
358
443
|
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
359
444
|
self._meta["checkpoint_dir"] = str(self.checkpoint_dir)
|
|
445
|
+
self._record_artifact_update(self.checkpoint_dir)
|
|
360
446
|
self._flush_meta()
|
|
361
447
|
log.info("Checkpoint dir: %s", self.checkpoint_dir)
|
|
362
448
|
return self.checkpoint_dir
|
|
@@ -415,6 +501,7 @@ class Run:
|
|
|
415
501
|
:meth:`save_checkpoint` call it automatically. When *is_best*
|
|
416
502
|
is ``True``, any previous checkpoint marked as best is cleared.
|
|
417
503
|
"""
|
|
504
|
+
checkpoint_path = Path(path)
|
|
418
505
|
checkpoints: list[dict[str, Any]] = self._meta.get("checkpoints", [])
|
|
419
506
|
|
|
420
507
|
if is_best:
|
|
@@ -422,7 +509,7 @@ class Run:
|
|
|
422
509
|
entry.pop("is_best", None)
|
|
423
510
|
|
|
424
511
|
record: dict[str, Any] = {
|
|
425
|
-
"path": str(
|
|
512
|
+
"path": str(checkpoint_path),
|
|
426
513
|
"epoch": epoch,
|
|
427
514
|
"step": step,
|
|
428
515
|
}
|
|
@@ -431,7 +518,19 @@ class Run:
|
|
|
431
518
|
|
|
432
519
|
checkpoints.append(record)
|
|
433
520
|
self._meta["checkpoints"] = checkpoints
|
|
521
|
+
self._record_artifact_update(checkpoint_path)
|
|
434
522
|
self._flush_meta()
|
|
523
|
+
self._emit_stream_event(
|
|
524
|
+
{
|
|
525
|
+
"type": "checkpoint",
|
|
526
|
+
"eventId": self._make_stream_event_id(
|
|
527
|
+
"checkpoint",
|
|
528
|
+
epoch,
|
|
529
|
+
step,
|
|
530
|
+
),
|
|
531
|
+
"checkpoint": copy.deepcopy(record),
|
|
532
|
+
},
|
|
533
|
+
)
|
|
435
534
|
|
|
436
535
|
# ── architecture export ─────────────────────────────────────────
|
|
437
536
|
|
|
@@ -467,6 +566,7 @@ class Run:
|
|
|
467
566
|
model, dummy_input, self.dir / "architecture.onnx"
|
|
468
567
|
)
|
|
469
568
|
self._meta["architecture"] = "architecture.onnx"
|
|
569
|
+
self._record_artifact_update(output_path)
|
|
470
570
|
self._flush_meta()
|
|
471
571
|
return output_path
|
|
472
572
|
|
|
@@ -489,6 +589,16 @@ class Run:
|
|
|
489
589
|
self._clear_terminal_fields(self._meta, status=status)
|
|
490
590
|
self._finish_mode(status=status, end=end, duration=duration)
|
|
491
591
|
self._flush_meta()
|
|
592
|
+
self._emit_stream_event(
|
|
593
|
+
{
|
|
594
|
+
"type": "finish",
|
|
595
|
+
"eventId": self._make_stream_event_id("finish", status),
|
|
596
|
+
"status": status,
|
|
597
|
+
"patch": self._build_finish_patch(),
|
|
598
|
+
},
|
|
599
|
+
flush=True,
|
|
600
|
+
)
|
|
601
|
+
self._close_stream()
|
|
492
602
|
|
|
493
603
|
def detach(self) -> None:
|
|
494
604
|
"""Disconnect from the run without changing its status.
|
|
@@ -508,6 +618,7 @@ class Run:
|
|
|
508
618
|
self._finished = True
|
|
509
619
|
self._teardown_hooks()
|
|
510
620
|
self._flush_meta()
|
|
621
|
+
self._close_stream()
|
|
511
622
|
|
|
512
623
|
# ── context manager ───────────────────────────────────────────
|
|
513
624
|
|
|
@@ -652,9 +763,177 @@ class Run:
|
|
|
652
763
|
self._meta["modes"] = modes
|
|
653
764
|
return modes
|
|
654
765
|
|
|
766
|
+
def _updated_at_store(self) -> dict[str, Any]:
|
|
767
|
+
if not hasattr(self, "_meta"):
|
|
768
|
+
return self._pending_updated_at
|
|
769
|
+
|
|
770
|
+
updated_at = self._meta.get("updated_at")
|
|
771
|
+
if not isinstance(updated_at, dict):
|
|
772
|
+
updated_at = {}
|
|
773
|
+
self._meta["updated_at"] = updated_at
|
|
774
|
+
|
|
775
|
+
if self._pending_updated_at:
|
|
776
|
+
updated_at.update(self._pending_updated_at)
|
|
777
|
+
self._pending_updated_at.clear()
|
|
778
|
+
|
|
779
|
+
return updated_at
|
|
780
|
+
|
|
781
|
+
def _record_artifact_update(
|
|
782
|
+
self,
|
|
783
|
+
path: str | Path,
|
|
784
|
+
*,
|
|
785
|
+
when: float | None = None,
|
|
786
|
+
) -> None:
|
|
787
|
+
artifact_path = Path(path)
|
|
788
|
+
ts = when if when is not None else _artifact_mtime(artifact_path)
|
|
789
|
+
if ts is None:
|
|
790
|
+
ts = time.time()
|
|
791
|
+
self._updated_at_store()[self._artifact_key(artifact_path)] = (
|
|
792
|
+
_updated_at_entry(ts)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def _artifact_key(self, path: Path) -> str:
|
|
796
|
+
if path.is_absolute():
|
|
797
|
+
try:
|
|
798
|
+
return path.relative_to(self.dir).as_posix()
|
|
799
|
+
except ValueError:
|
|
800
|
+
return str(path)
|
|
801
|
+
return path.as_posix()
|
|
802
|
+
|
|
655
803
|
def _flush_meta(self) -> None:
|
|
804
|
+
self._record_artifact_update(self.dir / "meta.json", when=time.time())
|
|
656
805
|
write_json(self.dir / "meta.json", self._meta)
|
|
657
806
|
|
|
807
|
+
def _bind_stream(self) -> None:
|
|
808
|
+
if self._stream is None:
|
|
809
|
+
return
|
|
810
|
+
self._stream.bind(
|
|
811
|
+
StreamContext(
|
|
812
|
+
run_id=self.run_id,
|
|
813
|
+
project_dir=self.project_dir,
|
|
814
|
+
runs_dir=self.dir.parent,
|
|
815
|
+
run_dir=self.dir,
|
|
816
|
+
meta=copy.deepcopy(self._meta),
|
|
817
|
+
config=copy.deepcopy(self.config),
|
|
818
|
+
code_ref=copy.deepcopy(self._code_ref_payload),
|
|
819
|
+
run_environment=copy.deepcopy(self._run_environment_payload),
|
|
820
|
+
),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
def _emit_stream_init(self) -> None:
|
|
824
|
+
tags = self._extract_stream_tags()
|
|
825
|
+
self._emit_stream_event(
|
|
826
|
+
{
|
|
827
|
+
"type": "init",
|
|
828
|
+
"eventId": self._make_stream_event_id("init"),
|
|
829
|
+
"meta": copy.deepcopy(self._meta),
|
|
830
|
+
"config": copy.deepcopy(self.config),
|
|
831
|
+
"codeRef": copy.deepcopy(self._code_ref_payload),
|
|
832
|
+
"runEnvironment": copy.deepcopy(self._run_environment_payload),
|
|
833
|
+
"tags": tags,
|
|
834
|
+
},
|
|
835
|
+
flush=True,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
def _emit_stream_event(
|
|
839
|
+
self,
|
|
840
|
+
event: dict[str, Any],
|
|
841
|
+
*,
|
|
842
|
+
flush: bool = False,
|
|
843
|
+
) -> None:
|
|
844
|
+
if self._stream is None:
|
|
845
|
+
return
|
|
846
|
+
self._stream.emit(event)
|
|
847
|
+
if flush:
|
|
848
|
+
self._stream.flush()
|
|
849
|
+
|
|
850
|
+
def _close_stream(self) -> None:
|
|
851
|
+
if self._stream is None:
|
|
852
|
+
return
|
|
853
|
+
self._stream.close()
|
|
854
|
+
|
|
855
|
+
def _extract_stream_tags(self) -> list[str] | None:
|
|
856
|
+
raw_tags = self._meta.get("tags")
|
|
857
|
+
if not isinstance(raw_tags, list):
|
|
858
|
+
return None
|
|
859
|
+
tags = [
|
|
860
|
+
str(tag).strip()
|
|
861
|
+
for tag in raw_tags
|
|
862
|
+
if isinstance(tag, str) and str(tag).strip()
|
|
863
|
+
]
|
|
864
|
+
return tags or None
|
|
865
|
+
|
|
866
|
+
def _build_output_snapshot(
|
|
867
|
+
self,
|
|
868
|
+
epoch: int | None,
|
|
869
|
+
step: int | None,
|
|
870
|
+
output_types: dict[str, Any],
|
|
871
|
+
) -> dict[str, Any] | None:
|
|
872
|
+
if epoch is None or step is None:
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
snapshot_types: dict[str, list[str]] = {}
|
|
876
|
+
for output_type, slots in output_types.items():
|
|
877
|
+
if slots is None or not isinstance(slots, Mapping):
|
|
878
|
+
continue
|
|
879
|
+
|
|
880
|
+
categories: list[str] = []
|
|
881
|
+
for slot_name, data in slots.items():
|
|
882
|
+
if data is None:
|
|
883
|
+
continue
|
|
884
|
+
if slot_name == "aux" and isinstance(data, Mapping):
|
|
885
|
+
for aux_name, aux_value in data.items():
|
|
886
|
+
if aux_value is None:
|
|
887
|
+
continue
|
|
888
|
+
categories.append(f"aux/{aux_name}")
|
|
889
|
+
else:
|
|
890
|
+
categories.append(str(slot_name))
|
|
891
|
+
|
|
892
|
+
if categories:
|
|
893
|
+
snapshot_types[str(output_type)] = sorted(set(categories))
|
|
894
|
+
|
|
895
|
+
if not snapshot_types:
|
|
896
|
+
return None
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
"epoch": epoch,
|
|
900
|
+
"step": step,
|
|
901
|
+
"types": snapshot_types,
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
def _build_finish_patch(self) -> dict[str, Any]:
|
|
905
|
+
patch = {
|
|
906
|
+
"end_time": self._meta.get("end_time"),
|
|
907
|
+
"end_iso": self._meta.get("end_iso"),
|
|
908
|
+
"duration_sec": self._meta.get("duration_sec"),
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if "error" in self._meta:
|
|
912
|
+
patch["error"] = self._meta["error"]
|
|
913
|
+
if "traceback" in self._meta:
|
|
914
|
+
patch["traceback"] = self._meta["traceback"]
|
|
915
|
+
|
|
916
|
+
if self.mode is not None:
|
|
917
|
+
modes = self._meta.get("modes")
|
|
918
|
+
if isinstance(modes, dict) and self.mode in modes:
|
|
919
|
+
patch["modes"] = {
|
|
920
|
+
self.mode: copy.deepcopy(modes[self.mode]),
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return patch
|
|
924
|
+
|
|
925
|
+
def _make_stream_event_id(self, prefix: str, *parts: Any) -> str:
|
|
926
|
+
counter = self._stream_event_counters.get(prefix, 0)
|
|
927
|
+
self._stream_event_counters[prefix] = counter + 1
|
|
928
|
+
tokens = [prefix]
|
|
929
|
+
tokens.extend(
|
|
930
|
+
str(part)
|
|
931
|
+
for part in parts
|
|
932
|
+
if part is not None and str(part).strip()
|
|
933
|
+
)
|
|
934
|
+
tokens.append(str(counter))
|
|
935
|
+
return ":".join(tokens)
|
|
936
|
+
|
|
658
937
|
def __repr__(self) -> str:
|
|
659
938
|
return f"Run(id={self.run_id!r}, dir={str(self.dir)!r}, status={self._meta['status']!r})"
|
|
660
939
|
|
|
@@ -732,6 +1011,20 @@ def _isotime(ts: float) -> str:
|
|
|
732
1011
|
return time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(ts))
|
|
733
1012
|
|
|
734
1013
|
|
|
1014
|
+
def _updated_at_entry(ts: float) -> dict[str, Any]:
|
|
1015
|
+
return {
|
|
1016
|
+
"time": ts,
|
|
1017
|
+
"iso": _isotime(ts),
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _artifact_mtime(path: Path) -> float | None:
|
|
1022
|
+
try:
|
|
1023
|
+
return path.stat().st_mtime
|
|
1024
|
+
except OSError:
|
|
1025
|
+
return None
|
|
1026
|
+
|
|
1027
|
+
|
|
735
1028
|
def _normalize_mode(mode: str | None) -> str | None:
|
|
736
1029
|
if mode is None:
|
|
737
1030
|
return None
|
|
@@ -1050,6 +1343,9 @@ def _read_ds_crawler_descriptor(path: str) -> dict[str, Any]:
|
|
|
1050
1343
|
try:
|
|
1051
1344
|
cfg = load_dataset_config({"path": path})
|
|
1052
1345
|
except Exception:
|
|
1346
|
+
descriptor = _read_output_json_descriptor(path)
|
|
1347
|
+
if descriptor:
|
|
1348
|
+
return descriptor
|
|
1053
1349
|
try:
|
|
1054
1350
|
contract = get_dataset_contract(path)
|
|
1055
1351
|
except Exception:
|
|
@@ -1082,6 +1378,69 @@ def _read_ds_crawler_descriptor(path: str) -> dict[str, Any]:
|
|
|
1082
1378
|
return descriptor
|
|
1083
1379
|
|
|
1084
1380
|
|
|
1381
|
+
def _read_output_json_descriptor(path: str) -> dict[str, Any]:
|
|
1382
|
+
try:
|
|
1383
|
+
from ds_crawler import zip_utils
|
|
1384
|
+
except Exception:
|
|
1385
|
+
return {}
|
|
1386
|
+
|
|
1387
|
+
try:
|
|
1388
|
+
raw = zip_utils.read_metadata_json(Path(path), "output.json")
|
|
1389
|
+
except Exception:
|
|
1390
|
+
return {}
|
|
1391
|
+
|
|
1392
|
+
return _descriptor_from_output_json(raw, path)
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _descriptor_from_output_json(raw: Any, path: str) -> dict[str, Any]:
|
|
1396
|
+
if isinstance(raw, Mapping):
|
|
1397
|
+
candidates: list[Any]
|
|
1398
|
+
modalities = raw.get("modalities")
|
|
1399
|
+
if isinstance(modalities, list):
|
|
1400
|
+
candidates = modalities
|
|
1401
|
+
else:
|
|
1402
|
+
candidates = [raw]
|
|
1403
|
+
elif isinstance(raw, list):
|
|
1404
|
+
candidates = raw
|
|
1405
|
+
else:
|
|
1406
|
+
return {}
|
|
1407
|
+
|
|
1408
|
+
selected: Mapping[str, Any] | None = None
|
|
1409
|
+
path_name = Path(path).name
|
|
1410
|
+
for candidate in candidates:
|
|
1411
|
+
if not isinstance(candidate, Mapping):
|
|
1412
|
+
continue
|
|
1413
|
+
if _as_non_empty_str(candidate.get("name")) == path_name:
|
|
1414
|
+
selected = candidate
|
|
1415
|
+
break
|
|
1416
|
+
if selected is None:
|
|
1417
|
+
selected = candidate
|
|
1418
|
+
|
|
1419
|
+
if selected is None:
|
|
1420
|
+
return {}
|
|
1421
|
+
|
|
1422
|
+
descriptor: dict[str, Any] = {}
|
|
1423
|
+
properties = selected.get("properties")
|
|
1424
|
+
merged_properties = dict(properties) if isinstance(properties, Mapping) else {}
|
|
1425
|
+
|
|
1426
|
+
euler_train_props = selected.get("euler_train")
|
|
1427
|
+
if isinstance(euler_train_props, Mapping):
|
|
1428
|
+
merged_properties["euler_train"] = dict(euler_train_props)
|
|
1429
|
+
|
|
1430
|
+
if merged_properties:
|
|
1431
|
+
descriptor["properties"] = merged_properties
|
|
1432
|
+
|
|
1433
|
+
modality_key = _as_non_empty_str(selected.get("type"))
|
|
1434
|
+
if modality_key is not None:
|
|
1435
|
+
descriptor["modality_key"] = modality_key
|
|
1436
|
+
|
|
1437
|
+
hierarchy_regex = _as_non_empty_str(selected.get("hierarchy_regex"))
|
|
1438
|
+
if hierarchy_regex is not None:
|
|
1439
|
+
descriptor["hierarchy_regex"] = hierarchy_regex
|
|
1440
|
+
|
|
1441
|
+
return descriptor
|
|
1442
|
+
|
|
1443
|
+
|
|
1085
1444
|
def _build_modality_entry(
|
|
1086
1445
|
name: str,
|
|
1087
1446
|
path: str,
|