euler-train 2.4.0__tar.gz → 2.4.1__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.4.0
3
+ Version: 2.4.1
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -13,6 +13,8 @@ Provides-Extra: architecture
13
13
  Requires-Dist: onnx; extra == "architecture"
14
14
  Requires-Dist: onnxruntime; extra == "architecture"
15
15
  Requires-Dist: onnxsim; extra == "architecture"
16
+ Provides-Extra: naming
17
+ Requires-Dist: euler-metric-naming; extra == "naming"
16
18
  Provides-Extra: dev
17
19
  Requires-Dist: pytest; extra == "dev"
18
20
  Requires-Dist: Pillow; extra == "dev"
@@ -39,6 +39,7 @@ def init(
39
39
  evaluations: dict[str, dict] | None = None,
40
40
  mode: str | None = None,
41
41
  stream: Any = None,
42
+ metric_naming: dict[str, Any] | None = None,
42
43
  ) -> Run:
43
44
  """Create a new run — or resume an existing one — and return the handle.
44
45
 
@@ -112,6 +113,12 @@ def init(
112
113
  ``model_id`` / ``api_token`` (or legacy ``access_token``)
113
114
  enables best-effort dual-write streaming to the Euler View
114
115
  model-run ingest API.
116
+ metric_naming:
117
+ Optional structured metric naming declaration. When provided,
118
+ stored in ``meta.json`` under ``metric_naming`` and included
119
+ in the stream init event so euler-view can resolve per-metric
120
+ namespaces. Typically generated by
121
+ ``euler_metric_naming.MetricNamespace.training_naming_config()``.
115
122
  """
116
123
  return Run(
117
124
  dir=dir, config=config, meta=meta,
@@ -121,4 +128,5 @@ def init(
121
128
  datasets=datasets, run_name=run_name,
122
129
  evaluations=evaluations, mode=mode,
123
130
  stream=stream,
131
+ metric_naming=metric_naming,
124
132
  )
@@ -48,6 +48,7 @@ class Run:
48
48
  evaluations: dict[str, dict[str, Any]] | None = None,
49
49
  mode: str | None = None,
50
50
  stream: Any = None,
51
+ metric_naming: dict[str, Any] | None = None,
51
52
  ) -> None:
52
53
  # ── detect run-directory shorthand ────────────────────────
53
54
  # When `dir` points to an existing run directory (contains
@@ -175,6 +176,12 @@ class Run:
175
176
  evaluations=evaluations,
176
177
  existing=self._meta.get("evaluations"),
177
178
  )
179
+
180
+ # ── metric naming (optional structured naming declaration) ──
181
+ if metric_naming is not None:
182
+ _validate_metric_naming(metric_naming)
183
+ self._meta["metric_naming"] = metric_naming
184
+
178
185
  self._mark_mode_running()
179
186
  self._flush_meta()
180
187
  self._bind_stream()
@@ -311,7 +318,14 @@ class Run:
311
318
  # ── GPU stats ─────────────────────────────────────────────────
312
319
 
313
320
  def _get_gpu_stats(self) -> dict[str, Any]:
314
- """Return GPU utilization & memory stats, or {} if unavailable."""
321
+ """Return GPU utilization & memory stats, or {} if unavailable.
322
+
323
+ When the run has a ``metric_naming`` declaration, keys are
324
+ namespaced under ``sys.train.*`` so euler-view can resolve them
325
+ against the declared ``sys.train`` namespace. Legacy runs
326
+ (without ``metric_naming``) retain the flat key names for
327
+ backward compatibility.
328
+ """
315
329
  if self._gpu_available is None:
316
330
  try:
317
331
  import pynvml
@@ -328,7 +342,7 @@ class Run:
328
342
  try:
329
343
  util = pynvml.nvmlDeviceGetUtilizationRates(self._gpu_handle)
330
344
  mem = pynvml.nvmlDeviceGetMemoryInfo(self._gpu_handle)
331
- return {
345
+ raw = {
332
346
  "gpu_util_pct": util.gpu,
333
347
  "gpu_mem_util_pct": util.memory,
334
348
  "gpu_mem_used_gb": round(mem.used / 1e9, 3),
@@ -336,6 +350,9 @@ class Run:
336
350
  }
337
351
  except Exception:
338
352
  return {}
353
+ if self._meta.get("metric_naming") is not None:
354
+ return {f"sys.train.{k}": v for k, v in raw.items()}
355
+ return raw
339
356
 
340
357
  # ── visual / heavy outputs ────────────────────────────────────
341
358
 
@@ -1035,6 +1052,26 @@ def _normalize_mode(mode: str | None) -> str | None:
1035
1052
  return normalized
1036
1053
 
1037
1054
 
1055
+ def _validate_metric_naming(value: Any) -> None:
1056
+ """Lightweight structural validation of a metric_naming payload."""
1057
+ if not isinstance(value, dict):
1058
+ raise TypeError("metric_naming must be a dict")
1059
+ namespaces = value.get("namespaces")
1060
+ if not isinstance(namespaces, dict):
1061
+ raise ValueError(
1062
+ "metric_naming must contain a 'namespaces' dict"
1063
+ )
1064
+ for ns_key, ns_decl in namespaces.items():
1065
+ if not isinstance(ns_key, str):
1066
+ raise ValueError(
1067
+ f"metric_naming namespace key must be a string, got {type(ns_key).__name__}"
1068
+ )
1069
+ if not isinstance(ns_decl, dict):
1070
+ raise ValueError(
1071
+ f"metric_naming namespace {ns_key!r} must be a dict"
1072
+ )
1073
+
1074
+
1038
1075
  def _build_datasets_meta(
1039
1076
  datasets: Mapping[str, Any],
1040
1077
  existing: Any,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-train
3
- Version: 2.4.0
3
+ Version: 2.4.1
4
4
  Summary: Lightweight file-based experiment logger for PyTorch
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: numpy
@@ -13,6 +13,8 @@ Provides-Extra: architecture
13
13
  Requires-Dist: onnx; extra == "architecture"
14
14
  Requires-Dist: onnxruntime; extra == "architecture"
15
15
  Requires-Dist: onnxsim; extra == "architecture"
16
+ Provides-Extra: naming
17
+ Requires-Dist: euler-metric-naming; extra == "naming"
16
18
  Provides-Extra: dev
17
19
  Requires-Dist: pytest; extra == "dev"
18
20
  Requires-Dist: Pillow; extra == "dev"
@@ -20,3 +20,6 @@ nvidia-ml-py
20
20
 
21
21
  [images]
22
22
  Pillow
23
+
24
+ [naming]
25
+ euler-metric-naming
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-train"
7
- version = "2.4.0"
7
+ version = "2.4.1"
8
8
  description = "Lightweight file-based experiment logger for PyTorch"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = ["numpy", "euler-dataset-contract"]
@@ -13,6 +13,7 @@ dependencies = ["numpy", "euler-dataset-contract"]
13
13
  images = ["Pillow"]
14
14
  gpu = ["nvidia-ml-py"]
15
15
  architecture = ["onnx", "onnxruntime", "onnxsim"]
16
+ naming = ["euler-metric-naming"]
16
17
  dev = ["pytest", "Pillow", "torch", "nvidia-ml-py", "onnx", "onnxruntime", "onnxsim"]
17
18
 
18
19
  [tool.pytest.ini_options]
@@ -2042,3 +2042,179 @@ class TestInterruptHandling:
2042
2042
  meta = _read_json(run.dir / "meta.json")
2043
2043
  assert meta["status"] == "interrupted"
2044
2044
  assert "SIGINT" in meta["error"]
2045
+
2046
+
2047
+ # ═══════════════════════════════════════════════════════════════════════════
2048
+ # Metric naming
2049
+ # ═══════════════════════════════════════════════════════════════════════════
2050
+
2051
+ _SAMPLE_METRIC_NAMING = {
2052
+ "producer_key": "euler_train.weather_metric",
2053
+ "producer_version": "0.1.0",
2054
+ "namespaces": {
2055
+ "depth.train": {
2056
+ "axes": {
2057
+ "kind": {"position": 0, "optional": False, "values": ["loss", "diag", "stat"]},
2058
+ "stage": {"position": 1, "optional": True, "values": ["prior", "final"]},
2059
+ }
2060
+ },
2061
+ "sys.train": {
2062
+ "axes": {}
2063
+ },
2064
+ },
2065
+ }
2066
+
2067
+
2068
+ class TestMetricNaming:
2069
+ def test_metric_naming_stored_in_meta_json(self, tmp_path):
2070
+ run = euler_train.init(
2071
+ dir=str(tmp_path / "r"),
2072
+ config={"lr": 1e-3},
2073
+ metric_naming=_SAMPLE_METRIC_NAMING,
2074
+ )
2075
+ meta = _read_json(run.dir / "meta.json")
2076
+ assert meta["metric_naming"] == _SAMPLE_METRIC_NAMING
2077
+ run.finish()
2078
+
2079
+ def test_metric_naming_none_by_default(self, tmp_path):
2080
+ run = euler_train.init(
2081
+ dir=str(tmp_path / "r"),
2082
+ config={},
2083
+ )
2084
+ meta = _read_json(run.dir / "meta.json")
2085
+ assert "metric_naming" not in meta
2086
+ run.finish()
2087
+
2088
+ def test_metric_naming_in_stream_init_event(self, tmp_path):
2089
+ consumer = _RecordingStreamConsumer()
2090
+ run = euler_train.init(
2091
+ dir=str(tmp_path / "r"),
2092
+ config={},
2093
+ stream=consumer,
2094
+ metric_naming=_SAMPLE_METRIC_NAMING,
2095
+ )
2096
+ run.finish()
2097
+
2098
+ init_event = consumer.events[0]
2099
+ assert init_event["type"] == "init"
2100
+ assert init_event["meta"]["metric_naming"] == _SAMPLE_METRIC_NAMING
2101
+
2102
+ def test_metric_naming_not_in_stream_when_absent(self, tmp_path):
2103
+ consumer = _RecordingStreamConsumer()
2104
+ run = euler_train.init(
2105
+ dir=str(tmp_path / "r"),
2106
+ config={},
2107
+ stream=consumer,
2108
+ )
2109
+ run.finish()
2110
+
2111
+ init_event = consumer.events[0]
2112
+ assert "metric_naming" not in init_event["meta"]
2113
+
2114
+ def test_metric_naming_preserved_on_resume(self, tmp_path):
2115
+ run1 = euler_train.init(
2116
+ dir=str(tmp_path / "r"),
2117
+ config={},
2118
+ metric_naming=_SAMPLE_METRIC_NAMING,
2119
+ )
2120
+ run1.finish()
2121
+
2122
+ run2 = euler_train.init(
2123
+ dir=str(run1.dir),
2124
+ )
2125
+ meta = _read_json(run2.dir / "meta.json")
2126
+ assert meta["metric_naming"] == _SAMPLE_METRIC_NAMING
2127
+ run2.finish()
2128
+
2129
+ def test_metric_naming_validation_not_dict(self, tmp_path):
2130
+ with pytest.raises(TypeError, match="metric_naming must be a dict"):
2131
+ euler_train.init(
2132
+ dir=str(tmp_path / "r"),
2133
+ config={},
2134
+ metric_naming="invalid",
2135
+ )
2136
+
2137
+ def test_metric_naming_validation_missing_namespaces(self, tmp_path):
2138
+ with pytest.raises(ValueError, match="namespaces"):
2139
+ euler_train.init(
2140
+ dir=str(tmp_path / "r"),
2141
+ config={},
2142
+ metric_naming={"producer_key": "test"},
2143
+ )
2144
+
2145
+ def test_metric_naming_validation_namespaces_not_dict(self, tmp_path):
2146
+ with pytest.raises(ValueError, match="namespaces"):
2147
+ euler_train.init(
2148
+ dir=str(tmp_path / "r"),
2149
+ config={},
2150
+ metric_naming={"namespaces": "invalid"},
2151
+ )
2152
+
2153
+ def test_metric_naming_validation_namespace_entry_not_dict(self, tmp_path):
2154
+ with pytest.raises(ValueError, match="must be a dict"):
2155
+ euler_train.init(
2156
+ dir=str(tmp_path / "r"),
2157
+ config={},
2158
+ metric_naming={"namespaces": {"depth.train": "invalid"}},
2159
+ )
2160
+
2161
+ def test_gpu_stats_namespaced_when_metric_naming_present(self, tmp_path):
2162
+ """GPU stats should use sys.train.* keys when metric_naming is set."""
2163
+ run = euler_train.init(
2164
+ dir=str(tmp_path / "r"),
2165
+ config={},
2166
+ metric_naming=_SAMPLE_METRIC_NAMING,
2167
+ )
2168
+ # Force GPU stats to be "available" with a mock
2169
+ run._gpu_available = True
2170
+
2171
+ class _FakeUtil:
2172
+ gpu = 75
2173
+ memory = 50
2174
+
2175
+ class _FakeMem:
2176
+ used = 4_000_000_000
2177
+ total = 8_000_000_000
2178
+
2179
+ import unittest.mock as mock
2180
+ with mock.patch.dict("sys.modules", {"pynvml": mock.MagicMock()}) as _:
2181
+ import sys as _sys
2182
+ pynvml_mock = _sys.modules["pynvml"]
2183
+ pynvml_mock.nvmlDeviceGetUtilizationRates.return_value = _FakeUtil()
2184
+ pynvml_mock.nvmlDeviceGetMemoryInfo.return_value = _FakeMem()
2185
+
2186
+ stats = run._get_gpu_stats()
2187
+
2188
+ assert "sys.train.gpu_util_pct" in stats
2189
+ assert "sys.train.gpu_mem_used_gb" in stats
2190
+ assert "gpu_util_pct" not in stats
2191
+ run.finish()
2192
+
2193
+ def test_gpu_stats_flat_when_no_metric_naming(self, tmp_path):
2194
+ """GPU stats should use flat keys when metric_naming is absent."""
2195
+ run = euler_train.init(
2196
+ dir=str(tmp_path / "r"),
2197
+ config={},
2198
+ )
2199
+ run._gpu_available = True
2200
+
2201
+ class _FakeUtil:
2202
+ gpu = 75
2203
+ memory = 50
2204
+
2205
+ class _FakeMem:
2206
+ used = 4_000_000_000
2207
+ total = 8_000_000_000
2208
+
2209
+ import unittest.mock as mock
2210
+ with mock.patch.dict("sys.modules", {"pynvml": mock.MagicMock()}) as _:
2211
+ import sys as _sys
2212
+ pynvml_mock = _sys.modules["pynvml"]
2213
+ pynvml_mock.nvmlDeviceGetUtilizationRates.return_value = _FakeUtil()
2214
+ pynvml_mock.nvmlDeviceGetMemoryInfo.return_value = _FakeMem()
2215
+
2216
+ stats = run._get_gpu_stats()
2217
+
2218
+ assert "gpu_util_pct" in stats
2219
+ assert "sys.train.gpu_util_pct" not in stats
2220
+ run.finish()
File without changes
File without changes