euler-train 2.8.0__tar.gz → 2.9.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.
Files changed (22) hide show
  1. {euler_train-2.8.0 → euler_train-2.9.0}/PKG-INFO +1 -1
  2. {euler_train-2.8.0 → euler_train-2.9.0}/README.md +9 -11
  3. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/stream.py +21 -18
  4. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/stream_check_cli.py +8 -1
  5. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/PKG-INFO +1 -1
  6. {euler_train-2.8.0 → euler_train-2.9.0}/pyproject.toml +1 -1
  7. {euler_train-2.8.0 → euler_train-2.9.0}/tests/test_runlog.py +77 -10
  8. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/__init__.py +0 -0
  9. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/_dataset_contract.py +0 -0
  10. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/architecture.py +0 -0
  11. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/environment.py +0 -0
  12. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/git_info.py +0 -0
  13. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/outputs.py +0 -0
  14. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/run.py +0 -0
  15. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/serialization.py +0 -0
  16. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train/slurm.py +0 -0
  17. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/SOURCES.txt +0 -0
  18. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/dependency_links.txt +0 -0
  19. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/entry_points.txt +0 -0
  20. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/requires.txt +0 -0
  21. {euler_train-2.8.0 → euler_train-2.9.0}/euler_train.egg-info/top_level.txt +0 -0
  22. {euler_train-2.8.0 → euler_train-2.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -64,9 +64,9 @@ run = euler_train.init(
64
64
  config=cfg,
65
65
  stream={
66
66
  "base_url": os.environ["EULER_VIEW_BASE_URL"],
67
- "model_id": 42,
68
67
  "api_token": os.environ["EULER_VIEW_API_TOKEN"],
69
68
  "stream_attach_token": os.environ.get("EULER_VIEW_STREAM_ATTACH_TOKEN"),
69
+ "model_id": 42, # only needed for the SLURM fallback path
70
70
  },
71
71
  )
72
72
  ```
@@ -74,7 +74,7 @@ run = euler_train.init(
74
74
  What each field is for:
75
75
 
76
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.
77
+ - `model_id`: Euler View model that should receive this run. This is optional when `stream_attach_token` is provided, and required for the SLURM fallback path.
78
78
  - `api_token` or `access_token`: credential used to open a stream session. This is required whenever the package needs to negotiate its own short-lived ingest token, even if `stream_attach_token` is also provided.
79
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
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.
@@ -85,9 +85,9 @@ What each field is for:
85
85
  How the handshake works:
86
86
 
87
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`.
88
+ 2. When the first stream flush happens, the package requests a session from Euler View at `POST /api/model-run-stream/session`.
89
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"]`.
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"]`. In that fallback mode, `model_id` must be configured so Euler View knows which model namespace to match against.
91
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
92
  6. Subsequent events are sent to `/api/model-run-stream/ingest`.
93
93
 
@@ -100,27 +100,25 @@ If you need to verify from a cluster node whether Euler View is reachable and wh
100
100
  ```bash
101
101
  euler-train.stream.check \
102
102
  --api-url https://view.example.com \
103
- --model-id 42 \
104
103
  --api-key "$EULER_VIEW_API_TOKEN" \
105
104
  --stream-attach-token "$EULER_VIEW_STREAM_ATTACH_TOKEN"
106
105
  ```
107
106
 
108
- This command calls:
109
-
110
- - `POST /api/models/{model_id}/runs/{run_id}/stream/check`
107
+ This command calls `POST /api/model-run-stream/check`.
111
108
 
112
109
  It validates:
113
110
 
114
111
  - network reachability to Euler View
115
112
  - API token validity and scope
116
- - model existence
117
113
  - stream attachment resolution via `stream_attach_token`, when provided
114
+ - model existence, when the request uses the `model_id` + `--slurm-job-id` fallback path
118
115
  - SLURM fallback matching via `--slurm-job-id`, when no attach token is provided
119
116
 
120
117
  Useful flags:
121
118
 
122
119
  - `--run-id`: override the dry-run run ID. If omitted, the CLI generates a temporary `stream-check-*` ID.
123
120
  - `--stream-attach-token`: test the explicit launch attachment path.
121
+ - `--model-id`: required for the fallback SLURM matching path, optional when `--stream-attach-token` is used.
124
122
  - `--slurm-job-id`: test the fallback SLURM matching path.
125
123
  - `--datasource-id`: include the datasource hint that a real stream session would send.
126
124
  - `--euler-train-dir` and `--run-dir`: include path hints in the dry-run payload.
@@ -131,8 +129,8 @@ The dry-run check does not start a run and does not mint an ingest token. It onl
131
129
  What has to be configured where:
132
130
 
133
131
  - In the training script or job wrapper: pass `stream={...}` to `euler_train.init(...)`.
134
- - 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.
135
- - Under plain SLURM launches: no extra attachment config is required as long as Euler View can uniquely match the run by `slurm.job_id`.
132
+ - 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. In that mode the producer does not need to know `model_id`.
133
+ - Under plain SLURM launches: no extra attachment config is required as long as Euler View can uniquely match the run by `slurm.job_id`, but `model_id` still has to be configured for the fallback lookup.
136
134
  - 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.
137
135
 
138
136
  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.
@@ -11,7 +11,6 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
  from typing import Any, Protocol
13
13
  from urllib.error import HTTPError, URLError
14
- from urllib.parse import quote
15
14
  from urllib.request import Request, urlopen
16
15
 
17
16
  from .serialization import _json_default
@@ -146,9 +145,9 @@ class EulerViewStreamConfig:
146
145
  )
147
146
 
148
147
  if self.stream_token is None:
149
- if self.model_id is None:
148
+ if self.model_id is None and self.stream_attach_token is None:
150
149
  raise ValueError(
151
- "stream.model_id is required when stream.stream_token is not provided",
150
+ "stream.model_id is required when neither stream.stream_token nor stream.stream_attach_token is provided",
152
151
  )
153
152
  if self.api_token is None and self.access_token is None:
154
153
  raise ValueError(
@@ -312,11 +311,10 @@ class EulerViewStreamConsumer:
312
311
  if not force and now < self._next_retry_at:
313
312
  return False
314
313
 
315
- session_url = (
316
- f"{self.config.base_url}/api/models/{self.config.model_id}"
317
- f"/runs/{quote(self._context.run_id, safe='')}/stream/session"
318
- )
314
+ session_url = f"{self.config.base_url}/api/model-run-stream/session"
319
315
  session_body = _build_stream_session_body(
316
+ run_id=self._context.run_id,
317
+ model_id=self.config.model_id,
320
318
  run_dir=self.config.run_dir or str(self._context.run_dir),
321
319
  euler_train_dir=self.config.euler_train_dir or str(self._context.runs_dir),
322
320
  stream_attach_token=(
@@ -447,9 +445,6 @@ def check_stream_handshake(
447
445
  "stream.stream_token bypasses the session handshake; a dry-run handshake requires api_token or access_token instead",
448
446
  )
449
447
 
450
- if resolved_config.model_id is None:
451
- raise ValueError("stream.model_id is required for handshake checks")
452
-
453
448
  normalized_run_id = (
454
449
  _normalize_optional_text(run_id, field_name="run_id")
455
450
  if run_id is not None
@@ -473,6 +468,13 @@ def check_stream_handshake(
473
468
  raise ValueError(
474
469
  "stream_attach_token argument does not match stream.stream_attach_token",
475
470
  )
471
+ resolved_stream_attach_token = (
472
+ normalized_stream_attach_token or resolved_config.stream_attach_token
473
+ )
474
+ if resolved_config.model_id is None and resolved_stream_attach_token is None:
475
+ raise ValueError(
476
+ "stream.model_id is required for handshake checks when stream_attach_token is not provided",
477
+ )
476
478
 
477
479
  resolved_datasource_id = (
478
480
  _require_positive_int(
@@ -506,16 +508,13 @@ def check_stream_handshake(
506
508
  else resolved_config.run_dir
507
509
  )
508
510
 
509
- url = (
510
- f"{resolved_config.base_url}/api/models/{resolved_config.model_id}"
511
- f"/runs/{quote(normalized_run_id, safe='')}/stream/check"
512
- )
511
+ url = f"{resolved_config.base_url}/api/model-run-stream/check"
513
512
  payload = _build_stream_session_body(
513
+ run_id=normalized_run_id,
514
+ model_id=resolved_config.model_id,
514
515
  run_dir=resolved_run_dir,
515
516
  euler_train_dir=resolved_euler_train_dir,
516
- stream_attach_token=(
517
- normalized_stream_attach_token or resolved_config.stream_attach_token
518
- ),
517
+ stream_attach_token=resolved_stream_attach_token,
519
518
  slurm_job_id=resolved_slurm_job_id,
520
519
  datasource_id=resolved_datasource_id,
521
520
  session_expires_in_sec=resolved_config.session_expires_in_sec,
@@ -633,6 +632,8 @@ def _extract_session_stream_attach_token(payload: Mapping[str, Any]) -> str | No
633
632
 
634
633
  def _build_stream_session_body(
635
634
  *,
635
+ run_id: str,
636
+ model_id: int | None,
636
637
  run_dir: str | None,
637
638
  euler_train_dir: str | None,
638
639
  stream_attach_token: str | None,
@@ -640,7 +641,9 @@ def _build_stream_session_body(
640
641
  datasource_id: int | None,
641
642
  session_expires_in_sec: int | None,
642
643
  ) -> dict[str, Any]:
643
- body: dict[str, Any] = {}
644
+ body: dict[str, Any] = {"runId": run_id}
645
+ if model_id is not None:
646
+ body["modelId"] = model_id
644
647
  if run_dir is not None:
645
648
  body["runDir"] = run_dir
646
649
  if euler_train_dir is not None:
@@ -14,7 +14,11 @@ def build_parser() -> argparse.ArgumentParser:
14
14
  description="Dry-run the Euler View stream handshake",
15
15
  )
16
16
  parser.add_argument("--api-url", required=True, help="Euler View base URL")
17
- parser.add_argument("--model-id", required=True, type=int, help="Euler View model ID")
17
+ parser.add_argument(
18
+ "--model-id",
19
+ type=int,
20
+ help="Euler View model ID. Required when using the SLURM fallback path instead of --stream-attach-token.",
21
+ )
18
22
  parser.add_argument(
19
23
  "--api-key",
20
24
  required=True,
@@ -64,6 +68,9 @@ def main(argv: Sequence[str] | None = None) -> int:
64
68
  parser = build_parser()
65
69
  args = parser.parse_args(argv)
66
70
 
71
+ if args.model_id is None and not args.stream_attach_token:
72
+ parser.error("--model-id is required unless --stream-attach-token is provided")
73
+
67
74
  try:
68
75
  result = check_stream_handshake(
69
76
  {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.8.0
3
+ Version: 2.9.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.8.0"
7
+ version = "2.9.0"
8
8
  description = "Lightweight file-based experiment logger for PyTorch"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["numpy", "euler-dataset-contract"]
@@ -1963,7 +1963,7 @@ class TestStreaming:
1963
1963
  "timeout": timeout,
1964
1964
  },
1965
1965
  )
1966
- if request.full_url.endswith("/stream/session"):
1966
+ if request.full_url == "https://sync.example/api/model-run-stream/session":
1967
1967
  return _FakeHttpResponse(
1968
1968
  {
1969
1969
  "token": "stream-token",
@@ -1997,12 +1997,12 @@ class TestStreaming:
1997
1997
  run.log({"loss": 0.25}, step=3, epoch=1)
1998
1998
  run.finish()
1999
1999
 
2000
- assert calls[0]["url"] == (
2001
- f"https://sync.example/api/models/42/runs/{run.run_id}/stream/session"
2002
- )
2000
+ assert calls[0]["url"] == "https://sync.example/api/model-run-stream/session"
2003
2001
  assert calls[0]["method"] == "POST"
2004
2002
  assert calls[0]["headers"]["authorization"] == "Bearer user-token"
2005
2003
  assert calls[0]["body"] == {
2004
+ "runId": run.run_id,
2005
+ "modelId": 42,
2006
2006
  "runDir": str(run.dir),
2007
2007
  "eulerTrainDir": str(run.dir.parent),
2008
2008
  "datasourceId": 7,
@@ -2028,6 +2028,73 @@ class TestStreaming:
2028
2028
  finish_event = ingest_calls[2]["body"]["events"][0]
2029
2029
  assert finish_event["status"] == "completed"
2030
2030
 
2031
+ def test_euler_view_http_consumer_can_handshake_without_model_id_when_attach_token_is_present(
2032
+ self,
2033
+ tmp_path,
2034
+ monkeypatch,
2035
+ ):
2036
+ calls: list[dict] = []
2037
+
2038
+ def fake_urlopen(request, timeout=0):
2039
+ headers = {k.lower(): v for k, v in request.header_items()}
2040
+ body = json.loads(request.data.decode("utf-8"))
2041
+ calls.append(
2042
+ {
2043
+ "url": request.full_url,
2044
+ "method": request.get_method(),
2045
+ "headers": headers,
2046
+ "body": body,
2047
+ "timeout": timeout,
2048
+ },
2049
+ )
2050
+ if request.full_url.endswith("/api/model-run-stream/session"):
2051
+ return _FakeHttpResponse(
2052
+ {
2053
+ "token": "stream-token",
2054
+ "expiresAt": "2099-01-01T00:00:00+00:00",
2055
+ "ingestUrl": "https://sync.example/api/model-run-stream/ingest",
2056
+ "run": {
2057
+ "modelId": 42,
2058
+ "runId": "placeholder",
2059
+ "streamAttachToken": "attach-123",
2060
+ },
2061
+ },
2062
+ )
2063
+ if request.full_url.endswith("/api/model-run-stream/ingest"):
2064
+ return _FakeHttpResponse(
2065
+ {
2066
+ "success": True,
2067
+ "events": [],
2068
+ "latestCursor": len(calls),
2069
+ },
2070
+ )
2071
+ raise AssertionError(f"Unexpected URL {request.full_url}")
2072
+
2073
+ monkeypatch.setattr(stream_mod, "urlopen", fake_urlopen)
2074
+
2075
+ run = euler_train.init(
2076
+ dir=str(tmp_path / "r"),
2077
+ config={"lr": 1e-3},
2078
+ stream={
2079
+ "base_url": "https://sync.example",
2080
+ "api_token": "user-token",
2081
+ "stream_attach_token": "attach-123",
2082
+ "batch_size": 1,
2083
+ },
2084
+ )
2085
+ run.log({"loss": 0.25}, step=3, epoch=1)
2086
+ run.finish()
2087
+
2088
+ assert calls[0]["url"] == "https://sync.example/api/model-run-stream/session"
2089
+ assert calls[0]["method"] == "POST"
2090
+ assert calls[0]["headers"]["authorization"] == "Bearer user-token"
2091
+ assert calls[0]["body"] == {
2092
+ "runId": run.run_id,
2093
+ "runDir": str(run.dir),
2094
+ "eulerTrainDir": str(run.dir.parent),
2095
+ "streamAttachToken": "attach-123",
2096
+ }
2097
+
2031
2098
  def test_stream_network_failures_do_not_break_training(
2032
2099
  self,
2033
2100
  tmp_path,
@@ -2072,7 +2139,7 @@ class TestStreaming:
2072
2139
  "timeout": timeout,
2073
2140
  },
2074
2141
  )
2075
- if request.full_url.endswith("/stream/check"):
2142
+ if request.full_url == "https://sync.example/api/model-run-stream/check":
2076
2143
  return _FakeHttpResponse(
2077
2144
  {
2078
2145
  "success": True,
@@ -2096,7 +2163,6 @@ class TestStreaming:
2096
2163
  payload = stream_mod.check_stream_handshake(
2097
2164
  {
2098
2165
  "base_url": "https://sync.example",
2099
- "model_id": 42,
2100
2166
  "api_token": "user-token",
2101
2167
  "stream_attach_token": "attach-123",
2102
2168
  "datasource_id": 7,
@@ -2107,7 +2173,7 @@ class TestStreaming:
2107
2173
  assert payload["success"] is True
2108
2174
  assert calls == [
2109
2175
  {
2110
- "url": "https://sync.example/api/models/42/runs/dry-run-1/stream/check",
2176
+ "url": "https://sync.example/api/model-run-stream/check",
2111
2177
  "method": "POST",
2112
2178
  "headers": {
2113
2179
  "accept": "application/json",
@@ -2115,6 +2181,7 @@ class TestStreaming:
2115
2181
  "content-type": "application/json",
2116
2182
  },
2117
2183
  "body": {
2184
+ "runId": "dry-run-1",
2118
2185
  "streamAttachToken": "attach-123",
2119
2186
  "datasourceId": 7,
2120
2187
  },
@@ -2128,7 +2195,7 @@ class TestStreaming:
2128
2195
  capsys,
2129
2196
  ):
2130
2197
  def fake_urlopen(request, timeout=0):
2131
- if not request.full_url.endswith("/stream/check"):
2198
+ if request.full_url != "https://sync.example/api/model-run-stream/check":
2132
2199
  raise AssertionError(f"Unexpected URL {request.full_url}")
2133
2200
  return _FakeHttpResponse(
2134
2201
  {
@@ -2153,12 +2220,12 @@ class TestStreaming:
2153
2220
  [
2154
2221
  "--api-url",
2155
2222
  "https://sync.example",
2156
- "--model-id",
2157
- "42",
2158
2223
  "--api-key",
2159
2224
  "user-token",
2160
2225
  "--run-id",
2161
2226
  "dry-run-2",
2227
+ "--stream-attach-token",
2228
+ "attach-123",
2162
2229
  "--json",
2163
2230
  ],
2164
2231
  )
File without changes