euler-train 2.6.0__tar.gz → 2.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -49,6 +49,57 @@ with euler_train.init(dir="runs/exp02", config=cfg) as run:
49
49
  ... # if an exception is raised, meta.json records status="crashed" + traceback
50
50
  ```
51
51
 
52
+ ## Euler View stream mode
53
+
54
+ `euler_train` can dual-write runs to Euler View while still keeping the normal file-based run directory as the primary source of truth. Streaming is best-effort: local files are always written first, and stream events are sent alongside them when a stream consumer is configured.
55
+
56
+ Pass the stream config via `stream=...` on `euler_train.init()`:
57
+
58
+ ```python
59
+ import os
60
+ import euler_train
61
+
62
+ run = euler_train.init(
63
+ dir="runs/experiment_01",
64
+ config=cfg,
65
+ stream={
66
+ "base_url": os.environ["EULER_VIEW_BASE_URL"],
67
+ "model_id": 42,
68
+ "api_token": os.environ["EULER_VIEW_API_TOKEN"],
69
+ "stream_attach_token": os.environ.get("EULER_VIEW_STREAM_ATTACH_TOKEN"),
70
+ },
71
+ )
72
+ ```
73
+
74
+ What each field is for:
75
+
76
+ - `base_url`: Euler View server base URL, for example `https://view.example.com`.
77
+ - `model_id`: Euler View model that should receive this run.
78
+ - `api_token` or `access_token`: credential used to open a stream session. This is the normal choice when the package should negotiate its own short-lived ingest token.
79
+ - `stream_attach_token`: optional opaque launch-attachment token. When present, the producer sends it during the session handshake and Euler View attaches the stream to that launch directly. This is the preferred path for interactive or otherwise non-SLURM launches. Internally Euler View currently maps this token to `model_launches.id`, but the package treats it as an opaque token.
80
+ - `stream_token`: optional pre-issued ingest token. If you pass this, the package skips the session handshake entirely and posts events directly to the ingest endpoint.
81
+ - `datasource_id`: optional Euler View datasource hint for the run record.
82
+ - `euler_train_dir` and `run_dir`: optional path overrides. These are auto-derived in normal usage.
83
+ - `session_expires_in_sec`: optional requested session lifetime for the short-lived ingest token.
84
+
85
+ How the handshake works:
86
+
87
+ 1. `euler_train` creates the local run directory and writes `meta.json`, `config.json`, and the other standard files as usual.
88
+ 2. When the first stream flush happens, the package requests a session from Euler View at `/api/models/{model_id}/runs/{run_id}/stream/session`.
89
+ 3. If `stream_attach_token` is configured, it is sent as `streamAttachToken` and used as the explicit launch attachment key.
90
+ 4. If no `stream_attach_token` is configured, the package falls back to the SLURM metadata already captured in `meta.json["slurm"]["job_id"]`.
91
+ 5. Euler View returns a short-lived ingest token plus the canonical `streamAttachToken`; the package caches that returned token and reuses it on later session refreshes.
92
+ 6. Subsequent events are sent to `/api/model-run-stream/ingest`.
93
+
94
+ What has to be configured where:
95
+
96
+ - In the training script or job wrapper: pass `stream={...}` to `euler_train.init(...)`.
97
+ - In Euler View / the launcher: if the run should attach to a specific launch regardless of SLURM availability, provide a `stream_attach_token` to the runtime and forward it into the `stream` config.
98
+ - Under plain SLURM launches: no extra attachment config is required as long as Euler View can uniquely match the run by `slurm.job_id`.
99
+ - If neither `stream_attach_token` nor a unique SLURM match is available, run streaming still works, but launch-to-run linking in Euler View may remain unresolved.
100
+
101
+ For new integrations, prefer `stream_attach_token`. The older `launch_id` / `launchId` config keys are still accepted as compatibility aliases, but new code should treat the attachment value as an opaque stream token rather than a launch ID.
102
+
52
103
  ## Directory structure
53
104
 
54
105
  Each `euler_train.init(dir=...)` call creates a timestamped subdirectory under `{dir}/runs/`:
@@ -67,6 +118,7 @@ Each `euler_train.init(dir=...)` call creates a timestamped subdirectory under `
67
118
  │ └── epoch_{N}.pt
68
119
  └── outputs/
69
120
  └── epoch_{N}_step_{M}/
121
+ ├── manifest.json
70
122
  └── {output_type}/
71
123
  ├── pred/
72
124
  ├── gt/
@@ -80,7 +132,7 @@ The run ID and directory are available as `run.run_id` and `run.dir`.
80
132
 
81
133
  ## API reference
82
134
 
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`
135
+ ### `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, stream=None, metric_naming=None) → Run`
84
136
 
85
137
  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
138
 
@@ -96,6 +148,8 @@ Creates the run directory and writes `meta.json`, `config.json`, `code_ref.json`
96
148
  | `run_name` | `str \| None` | Optional human-readable run label stored in `meta.json`. |
97
149
  | `evaluations` | `dict[str, dict] \| None` | Optional evaluation key → entry map. See [Evaluations](#evaluations). |
98
150
  | `mode` | `str \| None` | Optional process label such as `"train"`, `"val"`, or `"eval"`. When set, lifecycle and crash details are also written under `meta.json["modes"][mode]`. |
151
+ | `stream` | `dict \| EulerViewStreamConfig \| consumer \| sequence \| None` | Optional output stream config / consumer. A mapping with `base_url`, `model_id`, and `api_token` (or `access_token`) enables best-effort streaming to Euler View. Prefer `stream_attach_token` for explicit launch attachment; otherwise the package falls back to `meta.json["slurm"]["job_id"]` when available. |
152
+ | `metric_naming` | `dict[str, Any] \| None` | Optional structured metric naming declaration stored in `meta.json["metric_naming"]` and included in the stream init event. |
99
153
 
100
154
  ---
101
155
 
@@ -112,7 +166,7 @@ run.log({"rgb.psnr": 28.3, "depth.mae": 0.03}, step=100, epoch=1, mode="val")
112
166
 
113
167
  ---
114
168
 
115
- ### `run.save_outputs(*, epoch=None, step=None, **output_types)`
169
+ ### `run.save_outputs(*, epoch=None, step=None, metadata=None, **output_types)`
116
170
 
117
171
  Saves arrays/images to `outputs/epoch_{N}_step_{M}/{output_type}/{slot}/`.
118
172
 
@@ -125,10 +179,11 @@ Each output type is a dict with these slot keys:
125
179
  | `input` | Model input |
126
180
  | `aux` | Dict of named auxiliary outputs (each becomes a subdirectory) |
127
181
 
128
- Values can be:
182
+ Slot values can be:
129
183
  - A single numpy array, torch tensor, or PIL Image
130
184
  - A list of the above (saved as `0000.ext`, `0001.ext`, ...)
131
185
  - A 4D numpy/torch array (split along dim 0 as a batch)
186
+ - A dict mapping custom string IDs to items (saved as `{id}.ext`)
132
187
 
133
188
  ```python
134
189
  run.save_outputs(
@@ -142,6 +197,61 @@ run.save_outputs(
142
197
  )
143
198
  ```
144
199
 
200
+ #### Named sample IDs
201
+
202
+ When slot values are dicts with string keys, files are saved with the key as the basename instead of sequential indices. This is useful for matching outputs back to specific input samples:
203
+
204
+ ```python
205
+ run.save_outputs(
206
+ epoch=1, step=500,
207
+ rgb=dict(pred={"scene_042": img_a, "scene_117": img_b}),
208
+ )
209
+ # produces: rgb/pred/scene_042.png, rgb/pred/scene_117.png
210
+ ```
211
+
212
+ #### Source metadata
213
+
214
+ The optional `metadata` parameter attaches dataset provenance and other context to the output manifest. When provided, the `dataset` key is required and must be a non-empty string:
215
+
216
+ ```python
217
+ run.save_outputs(
218
+ epoch=1, step=500,
219
+ metadata={"dataset": "vkitti2", "split": "val", "scene": "0001"},
220
+ rgb=dict(pred={"frame_042": img_a}),
221
+ )
222
+ ```
223
+
224
+ #### Output manifest
225
+
226
+ Each `save_outputs` call writes a `manifest.json` alongside the saved files, recording what was written and how. The manifest is also streamed via the `output_snapshot` event when streaming is active.
227
+
228
+ ```json
229
+ {
230
+ "version": 1,
231
+ "epoch": 1,
232
+ "step": 500,
233
+ "metadata": {"dataset": "vkitti2", "split": "val"},
234
+ "output_types": {
235
+ "rgb": {
236
+ "pred": {
237
+ "id_mode": "named",
238
+ "files": [
239
+ {"sample_id": "scene_042", "filename": "scene_042.png", "format": "png"}
240
+ ]
241
+ },
242
+ "gt": {
243
+ "id_mode": "indexed",
244
+ "files": [
245
+ {"sample_id": 0, "filename": "0000.png", "format": "png"}
246
+ ]
247
+ }
248
+ }
249
+ },
250
+ "format_overrides": {},
251
+ "visualization_overrides": {}
252
+ }
253
+ ```
254
+
145
255
  Torch tensors in `(C,H,W)` or `(B,C,H,W)` layout are automatically transposed to channels-last before saving.
146
256
 
147
257
  Pass `None` for any slot or output type to skip it.
@@ -56,6 +56,8 @@ class EulerViewStreamConfig:
56
56
  api_token: str | None = None
57
57
  access_token: str | None = None
58
58
  stream_token: str | None = None
59
+ stream_attach_token: str | None = None
60
+ launch_id: str | None = None
59
61
  datasource_id: int | None = None
60
62
  euler_train_dir: str | None = None
61
63
  run_dir: str | None = None
@@ -84,6 +86,24 @@ class EulerViewStreamConfig:
84
86
  self.stream_token,
85
87
  field_name="stream.stream_token",
86
88
  )
89
+ self.stream_attach_token = _normalize_optional_text(
90
+ self.stream_attach_token,
91
+ field_name="stream.stream_attach_token",
92
+ )
93
+ self.launch_id = _normalize_optional_text(
94
+ self.launch_id,
95
+ field_name="stream.launch_id",
96
+ )
97
+ if (
98
+ self.stream_attach_token is not None
99
+ and self.launch_id is not None
100
+ and self.stream_attach_token != self.launch_id
101
+ ):
102
+ raise ValueError(
103
+ "stream.stream_attach_token and stream.launch_id must match when both are provided",
104
+ )
105
+ if self.stream_attach_token is None:
106
+ self.stream_attach_token = self.launch_id
87
107
  self.euler_train_dir = _normalize_optional_text(
88
108
  self.euler_train_dir,
89
109
  field_name="stream.euler_train_dir",
@@ -191,6 +211,7 @@ class EulerViewStreamConsumer:
191
211
  self._context: StreamContext | None = None
192
212
  self._pending: list[dict[str, Any]] = []
193
213
  self._stream_token: str | None = config.stream_token
214
+ self._resolved_stream_attach_token: str | None = config.stream_attach_token
194
215
  self._session_expires_at = float("inf") if config.stream_token else 0.0
195
216
  self._ingest_url = f"{self.config.base_url}/api/model-run-stream/ingest"
196
217
  self._last_flush_at = 0.0
@@ -299,6 +320,14 @@ class EulerViewStreamConsumer:
299
320
  "runDir": self.config.run_dir or str(self._context.run_dir),
300
321
  "eulerTrainDir": self.config.euler_train_dir or str(self._context.runs_dir),
301
322
  }
323
+ stream_attach_token = (
324
+ self.config.stream_attach_token or self._resolved_stream_attach_token
325
+ )
326
+ if stream_attach_token is not None:
327
+ session_body["streamAttachToken"] = stream_attach_token
328
+ slurm_job_id = _extract_slurm_job_id(self._context.meta)
329
+ if slurm_job_id is not None:
330
+ session_body["slurmJobId"] = slurm_job_id
302
331
  if self.config.datasource_id is not None:
303
332
  session_body["datasourceId"] = self.config.datasource_id
304
333
  if self.config.session_expires_in_sec is not None:
@@ -328,6 +357,10 @@ class EulerViewStreamConsumer:
328
357
  else:
329
358
  self._ingest_url = f"{self.config.base_url}/api/model-run-stream/ingest"
330
359
 
360
+ resolved_stream_attach_token = _extract_session_stream_attach_token(payload)
361
+ if resolved_stream_attach_token is not None:
362
+ self._resolved_stream_attach_token = resolved_stream_attach_token
363
+
331
364
  self._session_expires_at = _parse_timestamp(payload.get("expiresAt"))
332
365
  self._next_retry_at = 0.0
333
366
  return True
@@ -448,6 +481,12 @@ def _config_from_mapping(raw: Mapping[str, Any]) -> EulerViewStreamConfig:
448
481
  access_token=_mapping_get(raw, "access_token", "accessToken"),
449
482
  api_token=_mapping_get(raw, "api_token", "apiToken"),
450
483
  stream_token=_mapping_get(raw, "stream_token", "streamToken"),
484
+ stream_attach_token=_mapping_get(
485
+ raw,
486
+ "stream_attach_token",
487
+ "streamAttachToken",
488
+ ),
489
+ launch_id=_mapping_get(raw, "launch_id", "launchId"),
451
490
  datasource_id=_mapping_get(raw, "datasource_id", "datasourceId"),
452
491
  euler_train_dir=_mapping_get(raw, "euler_train_dir", "eulerTrainDir"),
453
492
  run_dir=_mapping_get(raw, "run_dir", "runDir"),
@@ -484,6 +523,45 @@ def _mapping_get(
484
523
  return default
485
524
 
486
525
 
526
+ def _extract_slurm_job_id(meta: Mapping[str, Any] | None) -> int | None:
527
+ if not isinstance(meta, Mapping):
528
+ return None
529
+ slurm = meta.get("slurm")
530
+ if not isinstance(slurm, Mapping):
531
+ return None
532
+ raw = slurm.get("job_id")
533
+ if isinstance(raw, bool):
534
+ return None
535
+ if isinstance(raw, int):
536
+ return raw if raw > 0 else None
537
+ if isinstance(raw, float):
538
+ return int(raw) if raw.is_integer() and raw > 0 else None
539
+ if isinstance(raw, str):
540
+ text = raw.strip()
541
+ if not text:
542
+ return None
543
+ try:
544
+ value = int(text, 10)
545
+ except ValueError:
546
+ return None
547
+ return value if value > 0 else None
548
+ return None
549
+
550
+
551
+ def _extract_session_stream_attach_token(payload: Mapping[str, Any]) -> str | None:
552
+ run = payload.get("run")
553
+ if not isinstance(run, Mapping):
554
+ return None
555
+ stream_attach_token = run.get("streamAttachToken")
556
+ if not isinstance(stream_attach_token, str):
557
+ legacy_launch_id = run.get("launchId")
558
+ if not isinstance(legacy_launch_id, str):
559
+ return None
560
+ stream_attach_token = legacy_launch_id
561
+ normalized = stream_attach_token.strip()
562
+ return normalized or None
563
+
564
+
487
565
  def _normalize_optional_text(
488
566
  value: Any,
489
567
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-train"
7
- version = "2.6.0"
7
+ version = "2.7.0"
8
8
  description = "Lightweight file-based experiment logger for PyTorch"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["numpy", "euler-dataset-contract"]
File without changes