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.
- {euler_train-2.4.0 → euler_train-2.4.1}/PKG-INFO +3 -1
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/__init__.py +8 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/run.py +39 -2
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train.egg-info/PKG-INFO +3 -1
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train.egg-info/requires.txt +3 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/pyproject.toml +2 -1
- {euler_train-2.4.0 → euler_train-2.4.1}/tests/test_runlog.py +176 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/README.md +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/_dataset_contract.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/architecture.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/environment.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/git_info.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/outputs.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/serialization.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/slurm.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train/stream.py +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train.egg-info/SOURCES.txt +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train.egg-info/dependency_links.txt +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/euler_train.egg-info/top_level.txt +0 -0
- {euler_train-2.4.0 → euler_train-2.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-train
|
|
3
|
-
Version: 2.4.
|
|
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
|
-
|
|
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.
|
|
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"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euler-train"
|
|
7
|
-
version = "2.4.
|
|
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
|
|
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
|