ml-dash 0.0.17__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- ml_dash/ARCHITECTURE.md +382 -0
- ml_dash/__init__.py +14 -1
- ml_dash/autolog.py +32 -0
- ml_dash/backends/__init__.py +11 -0
- ml_dash/backends/base.py +124 -0
- ml_dash/backends/dash_backend.py +571 -0
- ml_dash/backends/local_backend.py +90 -0
- ml_dash/components/__init__.py +13 -0
- ml_dash/components/files.py +246 -0
- ml_dash/components/logs.py +104 -0
- ml_dash/components/metrics.py +169 -0
- ml_dash/components/parameters.py +144 -0
- ml_dash/job_logger.py +42 -0
- ml_dash/ml_logger.py +234 -0
- ml_dash/run.py +331 -0
- ml_dash-0.4.0.dist-info/METADATA +1424 -0
- ml_dash-0.4.0.dist-info/RECORD +19 -0
- ml_dash-0.4.0.dist-info/WHEEL +4 -0
- ml_dash-0.4.0.dist-info/entry_points.txt +3 -0
- app-build/asset-manifest.json +0 -15
- app-build/favicon.ico +0 -0
- app-build/github-markdown.css +0 -957
- app-build/index.html +0 -1
- app-build/manifest.json +0 -15
- app-build/monaco-editor-worker-loader-proxy.js +0 -6
- app-build/precache-manifest.ffc09f8a591c529a1bd5c6f21f49815f.js +0 -26
- app-build/service-worker.js +0 -34
- ml_dash/app.py +0 -60
- ml_dash/config.py +0 -16
- ml_dash/example.py +0 -0
- ml_dash/file_events.py +0 -71
- ml_dash/file_handlers.py +0 -141
- ml_dash/file_utils.py +0 -5
- ml_dash/file_watcher.py +0 -30
- ml_dash/main.py +0 -60
- ml_dash/mime_types.py +0 -20
- ml_dash/schema/__init__.py +0 -110
- ml_dash/schema/archive.py +0 -165
- ml_dash/schema/directories.py +0 -59
- ml_dash/schema/experiments.py +0 -65
- ml_dash/schema/files/__init__.py +0 -204
- ml_dash/schema/files/file_helpers.py +0 -79
- ml_dash/schema/files/images.py +0 -27
- ml_dash/schema/files/metrics.py +0 -64
- ml_dash/schema/files/parameters.py +0 -50
- ml_dash/schema/files/series.py +0 -235
- ml_dash/schema/files/videos.py +0 -27
- ml_dash/schema/helpers.py +0 -66
- ml_dash/schema/projects.py +0 -65
- ml_dash/schema/schema_helpers.py +0 -19
- ml_dash/schema/users.py +0 -33
- ml_dash/sse.py +0 -18
- ml_dash-0.0.17.dist-info/METADATA +0 -67
- ml_dash-0.0.17.dist-info/RECORD +0 -38
- ml_dash-0.0.17.dist-info/WHEEL +0 -5
- ml_dash-0.0.17.dist-info/top_level.txt +0 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""File management component for ML-Logger."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pickle
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..backends.base import StorageBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileManager:
|
|
12
|
+
"""Manages file storage and retrieval.
|
|
13
|
+
|
|
14
|
+
Files are stored in the files/ subdirectory.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
backend: Storage backend
|
|
18
|
+
prefix: Experiment prefix path
|
|
19
|
+
namespace: Optional namespace for files (e.g., "checkpoints")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
backend: StorageBackend,
|
|
25
|
+
prefix: str,
|
|
26
|
+
namespace: str = ""
|
|
27
|
+
):
|
|
28
|
+
"""Initialize file manager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
backend: Storage backend
|
|
32
|
+
prefix: Experiment prefix path
|
|
33
|
+
namespace: Optional namespace subdirectory
|
|
34
|
+
"""
|
|
35
|
+
self.backend = backend
|
|
36
|
+
self.prefix = prefix
|
|
37
|
+
self.namespace = namespace
|
|
38
|
+
|
|
39
|
+
def _get_file_path(self, filename: str) -> str:
|
|
40
|
+
"""Get full file path with namespace.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
filename: File name
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Full path including prefix, files/, and namespace
|
|
47
|
+
"""
|
|
48
|
+
parts = [self.prefix, "files"]
|
|
49
|
+
if self.namespace:
|
|
50
|
+
parts.append(self.namespace)
|
|
51
|
+
parts.append(filename)
|
|
52
|
+
return "/".join(parts)
|
|
53
|
+
|
|
54
|
+
def save(self, data: Any, filename: str) -> None:
|
|
55
|
+
"""Save data to a file (auto-detects format).
|
|
56
|
+
|
|
57
|
+
Supports: JSON (.json), pickle (.pkl, .pickle), PyTorch (.pt, .pth),
|
|
58
|
+
NumPy (.npy, .npz), and raw bytes.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: Data to save
|
|
62
|
+
filename: File name
|
|
63
|
+
"""
|
|
64
|
+
file_path = self._get_file_path(filename)
|
|
65
|
+
suffix = Path(filename).suffix.lower()
|
|
66
|
+
|
|
67
|
+
if suffix == ".json":
|
|
68
|
+
# Save as JSON
|
|
69
|
+
json_str = json.dumps(data, indent=2)
|
|
70
|
+
self.backend.write_text(file_path, json_str)
|
|
71
|
+
|
|
72
|
+
elif suffix in [".pkl", ".pickle"]:
|
|
73
|
+
# Save as pickle
|
|
74
|
+
pickled = pickle.dumps(data)
|
|
75
|
+
self.backend.write_bytes(file_path, pickled)
|
|
76
|
+
|
|
77
|
+
elif suffix in [".pt", ".pth"]:
|
|
78
|
+
# Save PyTorch tensor/model
|
|
79
|
+
try:
|
|
80
|
+
import torch
|
|
81
|
+
import io
|
|
82
|
+
buffer = io.BytesIO()
|
|
83
|
+
torch.save(data, buffer)
|
|
84
|
+
self.backend.write_bytes(file_path, buffer.getvalue())
|
|
85
|
+
except ImportError:
|
|
86
|
+
raise ImportError("PyTorch is required to save .pt/.pth files")
|
|
87
|
+
|
|
88
|
+
elif suffix in [".npy", ".npz"]:
|
|
89
|
+
# Save NumPy array
|
|
90
|
+
try:
|
|
91
|
+
import numpy as np
|
|
92
|
+
import io
|
|
93
|
+
buffer = io.BytesIO()
|
|
94
|
+
if suffix == ".npy":
|
|
95
|
+
np.save(buffer, data)
|
|
96
|
+
else:
|
|
97
|
+
np.savez(buffer, data)
|
|
98
|
+
self.backend.write_bytes(file_path, buffer.getvalue())
|
|
99
|
+
except ImportError:
|
|
100
|
+
raise ImportError("NumPy is required to save .npy/.npz files")
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
# Save as raw bytes
|
|
104
|
+
if isinstance(data, bytes):
|
|
105
|
+
self.backend.write_bytes(file_path, data)
|
|
106
|
+
elif isinstance(data, str):
|
|
107
|
+
self.backend.write_text(file_path, data)
|
|
108
|
+
else:
|
|
109
|
+
# Fallback to pickle
|
|
110
|
+
pickled = pickle.dumps(data)
|
|
111
|
+
self.backend.write_bytes(file_path, pickled)
|
|
112
|
+
|
|
113
|
+
def save_pkl(self, data: Any, filename: str) -> None:
|
|
114
|
+
"""Save data as pickle file.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
data: Data to save
|
|
118
|
+
filename: File name (will add .pkl if missing)
|
|
119
|
+
"""
|
|
120
|
+
if not filename.endswith((".pkl", ".pickle")):
|
|
121
|
+
filename = f"{filename}.pkl"
|
|
122
|
+
self.save(data, filename)
|
|
123
|
+
|
|
124
|
+
def load(self, filename: str) -> Any:
|
|
125
|
+
"""Load data from a file (auto-detects format).
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
filename: File name
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Loaded data
|
|
132
|
+
"""
|
|
133
|
+
file_path = self._get_file_path(filename)
|
|
134
|
+
suffix = Path(filename).suffix.lower()
|
|
135
|
+
|
|
136
|
+
if suffix == ".json":
|
|
137
|
+
# Load JSON
|
|
138
|
+
json_str = self.backend.read_text(file_path)
|
|
139
|
+
return json.loads(json_str)
|
|
140
|
+
|
|
141
|
+
elif suffix in [".pkl", ".pickle"]:
|
|
142
|
+
# Load pickle
|
|
143
|
+
pickled = self.backend.read_bytes(file_path)
|
|
144
|
+
return pickle.loads(pickled)
|
|
145
|
+
|
|
146
|
+
elif suffix in [".pt", ".pth"]:
|
|
147
|
+
# Load PyTorch
|
|
148
|
+
try:
|
|
149
|
+
import torch
|
|
150
|
+
import io
|
|
151
|
+
data = self.backend.read_bytes(file_path)
|
|
152
|
+
buffer = io.BytesIO(data)
|
|
153
|
+
return torch.load(buffer)
|
|
154
|
+
except ImportError:
|
|
155
|
+
raise ImportError("PyTorch is required to load .pt/.pth files")
|
|
156
|
+
|
|
157
|
+
elif suffix in [".npy", ".npz"]:
|
|
158
|
+
# Load NumPy
|
|
159
|
+
try:
|
|
160
|
+
import numpy as np
|
|
161
|
+
import io
|
|
162
|
+
data = self.backend.read_bytes(file_path)
|
|
163
|
+
buffer = io.BytesIO(data)
|
|
164
|
+
if suffix == ".npy":
|
|
165
|
+
return np.load(buffer)
|
|
166
|
+
else:
|
|
167
|
+
return np.load(buffer, allow_pickle=True)
|
|
168
|
+
except ImportError:
|
|
169
|
+
raise ImportError("NumPy is required to load .npy/.npz files")
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
# For unknown extensions, try different strategies
|
|
173
|
+
data = self.backend.read_bytes(file_path)
|
|
174
|
+
|
|
175
|
+
# If it looks like a binary extension, return bytes directly
|
|
176
|
+
if suffix in [".bin", ".dat", ".raw"]:
|
|
177
|
+
return data
|
|
178
|
+
|
|
179
|
+
# Try to unpickle first (handles custom extensions from save())
|
|
180
|
+
try:
|
|
181
|
+
return pickle.loads(data)
|
|
182
|
+
except (pickle.UnpicklingError, EOFError, AttributeError):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Try to decode as text
|
|
186
|
+
try:
|
|
187
|
+
return data.decode('utf-8')
|
|
188
|
+
except UnicodeDecodeError:
|
|
189
|
+
# Return raw bytes as fallback
|
|
190
|
+
return data
|
|
191
|
+
|
|
192
|
+
def load_torch(self, filename: str) -> Any:
|
|
193
|
+
"""Load PyTorch checkpoint.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
filename: File name
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Loaded PyTorch data
|
|
200
|
+
"""
|
|
201
|
+
if not filename.endswith((".pt", ".pth")):
|
|
202
|
+
filename = f"{filename}.pt"
|
|
203
|
+
return self.load(filename)
|
|
204
|
+
|
|
205
|
+
def __call__(self, namespace: str) -> "FileManager":
|
|
206
|
+
"""Create a namespaced file manager.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
namespace: Namespace subdirectory (e.g., "checkpoints")
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
New FileManager with the namespace
|
|
213
|
+
"""
|
|
214
|
+
new_namespace = f"{self.namespace}/{namespace}" if self.namespace else namespace
|
|
215
|
+
return FileManager(
|
|
216
|
+
backend=self.backend,
|
|
217
|
+
prefix=self.prefix,
|
|
218
|
+
namespace=new_namespace
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def exists(self, filename: str) -> bool:
|
|
222
|
+
"""Check if a file exists.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
filename: File name
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if file exists
|
|
229
|
+
"""
|
|
230
|
+
file_path = self._get_file_path(filename)
|
|
231
|
+
return self.backend.exists(file_path)
|
|
232
|
+
|
|
233
|
+
def list(self) -> list:
|
|
234
|
+
"""List files in the current namespace.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of file names
|
|
238
|
+
"""
|
|
239
|
+
dir_path = f"{self.prefix}/files"
|
|
240
|
+
if self.namespace:
|
|
241
|
+
dir_path = f"{dir_path}/{self.namespace}"
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
return self.backend.list_dir(dir_path)
|
|
245
|
+
except Exception:
|
|
246
|
+
return []
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Text logging component for ML-Logger."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional, List
|
|
6
|
+
|
|
7
|
+
from ..backends.base import StorageBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogManager:
|
|
11
|
+
"""Manages structured text logging.
|
|
12
|
+
|
|
13
|
+
Logs are stored in a JSONL file (logs.jsonl).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
backend: Storage backend
|
|
17
|
+
prefix: Experiment prefix path
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, backend: StorageBackend, prefix: str):
|
|
21
|
+
"""Initialize log manager.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
backend: Storage backend
|
|
25
|
+
prefix: Experiment prefix path
|
|
26
|
+
"""
|
|
27
|
+
self.backend = backend
|
|
28
|
+
self.prefix = prefix
|
|
29
|
+
self.logs_file = f"{prefix}/logs.jsonl"
|
|
30
|
+
|
|
31
|
+
def log(self, message: str, level: str = "INFO", **context) -> None:
|
|
32
|
+
"""Log a message with context.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Log message
|
|
36
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR)
|
|
37
|
+
**context: Additional context fields
|
|
38
|
+
"""
|
|
39
|
+
entry = {
|
|
40
|
+
"timestamp": time.time(),
|
|
41
|
+
"level": level.upper(),
|
|
42
|
+
"message": message,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if context:
|
|
46
|
+
entry["context"] = context
|
|
47
|
+
|
|
48
|
+
line = json.dumps(entry) + "\n"
|
|
49
|
+
self.backend.append_text(self.logs_file, line)
|
|
50
|
+
|
|
51
|
+
def info(self, message: str, **context) -> None:
|
|
52
|
+
"""Log an info message.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
message: Log message
|
|
56
|
+
**context: Additional context fields
|
|
57
|
+
"""
|
|
58
|
+
self.log(message, level="INFO", **context)
|
|
59
|
+
|
|
60
|
+
def warning(self, message: str, **context) -> None:
|
|
61
|
+
"""Log a warning message.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
message: Log message
|
|
65
|
+
**context: Additional context fields
|
|
66
|
+
"""
|
|
67
|
+
self.log(message, level="WARNING", **context)
|
|
68
|
+
|
|
69
|
+
def error(self, message: str, **context) -> None:
|
|
70
|
+
"""Log an error message.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: Log message
|
|
74
|
+
**context: Additional context fields
|
|
75
|
+
"""
|
|
76
|
+
self.log(message, level="ERROR", **context)
|
|
77
|
+
|
|
78
|
+
def debug(self, message: str, **context) -> None:
|
|
79
|
+
"""Log a debug message.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message: Log message
|
|
83
|
+
**context: Additional context fields
|
|
84
|
+
"""
|
|
85
|
+
self.log(message, level="DEBUG", **context)
|
|
86
|
+
|
|
87
|
+
def read(self) -> List[Dict[str, Any]]:
|
|
88
|
+
"""Read all logs from file.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of log entries
|
|
92
|
+
"""
|
|
93
|
+
if not self.backend.exists(self.logs_file):
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
content = self.backend.read_text(self.logs_file)
|
|
97
|
+
logs = []
|
|
98
|
+
|
|
99
|
+
for line in content.strip().split("\n"):
|
|
100
|
+
if not line:
|
|
101
|
+
continue
|
|
102
|
+
logs.append(json.loads(line))
|
|
103
|
+
|
|
104
|
+
return logs
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Metrics logging component for ML-Logger."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional, List
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
from ..backends.base import StorageBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MetricsLogger:
|
|
12
|
+
"""Logs metrics with support for namespacing and aggregation.
|
|
13
|
+
|
|
14
|
+
Metrics are stored in a single JSONL file (metrics.jsonl).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
backend: Storage backend
|
|
18
|
+
prefix: Experiment prefix path
|
|
19
|
+
namespace: Optional namespace for metrics (e.g., "train", "val")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
backend: StorageBackend,
|
|
25
|
+
prefix: str,
|
|
26
|
+
namespace: str = ""
|
|
27
|
+
):
|
|
28
|
+
"""Initialize metrics logger.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
backend: Storage backend
|
|
32
|
+
prefix: Experiment prefix path
|
|
33
|
+
namespace: Optional namespace prefix
|
|
34
|
+
"""
|
|
35
|
+
self.backend = backend
|
|
36
|
+
self.prefix = prefix
|
|
37
|
+
self.namespace = namespace
|
|
38
|
+
self.metrics_file = f"{prefix}/metrics.jsonl"
|
|
39
|
+
self._collect_buffer: Dict[str, List[float]] = defaultdict(list)
|
|
40
|
+
|
|
41
|
+
def log(self, step: Optional[int] = None, **metrics) -> None:
|
|
42
|
+
"""Log metrics immediately.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
step: Step number (epoch, iteration, etc.)
|
|
46
|
+
**metrics: Metric name-value pairs
|
|
47
|
+
"""
|
|
48
|
+
# Apply namespace to metric names
|
|
49
|
+
namespaced_metrics = {}
|
|
50
|
+
for key, value in metrics.items():
|
|
51
|
+
if self.namespace:
|
|
52
|
+
key = f"{self.namespace}.{key}"
|
|
53
|
+
namespaced_metrics[key] = value
|
|
54
|
+
|
|
55
|
+
entry = {
|
|
56
|
+
"timestamp": time.time(),
|
|
57
|
+
"metrics": namespaced_metrics
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if step is not None:
|
|
61
|
+
entry["step"] = step
|
|
62
|
+
|
|
63
|
+
line = json.dumps(entry) + "\n"
|
|
64
|
+
self.backend.append_text(self.metrics_file, line)
|
|
65
|
+
|
|
66
|
+
def collect(self, step: Optional[int] = None, **metrics) -> None:
|
|
67
|
+
"""Collect metrics for later aggregation.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
step: Step number (optional, used by flush)
|
|
71
|
+
**metrics: Metric name-value pairs
|
|
72
|
+
"""
|
|
73
|
+
for key, value in metrics.items():
|
|
74
|
+
if self.namespace:
|
|
75
|
+
key = f"{self.namespace}.{key}"
|
|
76
|
+
self._collect_buffer[key].append(float(value))
|
|
77
|
+
|
|
78
|
+
def flush(
|
|
79
|
+
self,
|
|
80
|
+
_aggregation: str = "mean",
|
|
81
|
+
step: Optional[int] = None,
|
|
82
|
+
**additional_metrics
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Flush collected metrics with aggregation.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
_aggregation: Aggregation method ("mean", "sum", "min", "max", "last")
|
|
88
|
+
step: Step number for logged metrics
|
|
89
|
+
**additional_metrics: Additional metrics to log (not aggregated)
|
|
90
|
+
"""
|
|
91
|
+
if not self._collect_buffer and not additional_metrics:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
aggregated = {}
|
|
95
|
+
|
|
96
|
+
# Aggregate collected metrics
|
|
97
|
+
for key, values in self._collect_buffer.items():
|
|
98
|
+
if not values:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
if _aggregation == "mean":
|
|
102
|
+
aggregated[key] = sum(values) / len(values)
|
|
103
|
+
elif _aggregation == "sum":
|
|
104
|
+
aggregated[key] = sum(values)
|
|
105
|
+
elif _aggregation == "min":
|
|
106
|
+
aggregated[key] = min(values)
|
|
107
|
+
elif _aggregation == "max":
|
|
108
|
+
aggregated[key] = max(values)
|
|
109
|
+
elif _aggregation == "last":
|
|
110
|
+
aggregated[key] = values[-1]
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Unknown aggregation method: {_aggregation}")
|
|
113
|
+
|
|
114
|
+
# Add non-aggregated metrics
|
|
115
|
+
for key, value in additional_metrics.items():
|
|
116
|
+
if self.namespace:
|
|
117
|
+
key = f"{self.namespace}.{key}"
|
|
118
|
+
aggregated[key] = value
|
|
119
|
+
|
|
120
|
+
# Log aggregated metrics
|
|
121
|
+
if aggregated:
|
|
122
|
+
entry = {
|
|
123
|
+
"timestamp": time.time(),
|
|
124
|
+
"metrics": aggregated
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if step is not None:
|
|
128
|
+
entry["step"] = step
|
|
129
|
+
|
|
130
|
+
line = json.dumps(entry) + "\n"
|
|
131
|
+
self.backend.append_text(self.metrics_file, line)
|
|
132
|
+
|
|
133
|
+
# Clear buffer
|
|
134
|
+
self._collect_buffer.clear()
|
|
135
|
+
|
|
136
|
+
def __call__(self, namespace: str) -> "MetricsLogger":
|
|
137
|
+
"""Create a namespaced metrics logger.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
namespace: Namespace name (e.g., "train", "val")
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
New MetricsLogger with the namespace
|
|
144
|
+
"""
|
|
145
|
+
new_namespace = f"{self.namespace}.{namespace}" if self.namespace else namespace
|
|
146
|
+
return MetricsLogger(
|
|
147
|
+
backend=self.backend,
|
|
148
|
+
prefix=self.prefix,
|
|
149
|
+
namespace=new_namespace
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def read(self) -> List[Dict[str, Any]]:
|
|
153
|
+
"""Read all metrics from file.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of metric entries
|
|
157
|
+
"""
|
|
158
|
+
if not self.backend.exists(self.metrics_file):
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
content = self.backend.read_text(self.metrics_file)
|
|
162
|
+
metrics = []
|
|
163
|
+
|
|
164
|
+
for line in content.strip().split("\n"):
|
|
165
|
+
if not line:
|
|
166
|
+
continue
|
|
167
|
+
metrics.append(json.loads(line))
|
|
168
|
+
|
|
169
|
+
return metrics
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Parameter management component for ML-Logger."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ..backends.base import StorageBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def deep_merge(base: Dict, updates: Dict) -> Dict:
|
|
11
|
+
"""Deep merge two dictionaries.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
base: Base dictionary
|
|
15
|
+
updates: Updates to merge in
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Merged dictionary
|
|
19
|
+
"""
|
|
20
|
+
result = base.copy()
|
|
21
|
+
for key, value in updates.items():
|
|
22
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
23
|
+
result[key] = deep_merge(result[key], value)
|
|
24
|
+
else:
|
|
25
|
+
result[key] = value
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ParameterManager:
|
|
30
|
+
"""Manages experiment parameters.
|
|
31
|
+
|
|
32
|
+
Parameters are stored in an append-only JSONL file (parameters.jsonl)
|
|
33
|
+
with operations: set, extend, update.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
backend: Storage backend
|
|
37
|
+
prefix: Experiment prefix path
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, backend: StorageBackend, prefix: str):
|
|
41
|
+
"""Initialize parameter manager.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
backend: Storage backend
|
|
45
|
+
prefix: Experiment prefix path
|
|
46
|
+
"""
|
|
47
|
+
self.backend = backend
|
|
48
|
+
self.prefix = prefix
|
|
49
|
+
self.params_file = f"{prefix}/parameters.jsonl"
|
|
50
|
+
self._cache: Optional[Dict[str, Any]] = None
|
|
51
|
+
|
|
52
|
+
def set(self, **kwargs) -> None:
|
|
53
|
+
"""Set parameters (replaces existing).
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
**kwargs: Parameter key-value pairs
|
|
57
|
+
"""
|
|
58
|
+
self._append_operation("set", data=kwargs)
|
|
59
|
+
self._cache = None # Invalidate cache
|
|
60
|
+
|
|
61
|
+
def extend(self, **kwargs) -> None:
|
|
62
|
+
"""Extend parameters (deep merge with existing).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
**kwargs: Parameter key-value pairs to merge
|
|
66
|
+
"""
|
|
67
|
+
self._append_operation("extend", data=kwargs)
|
|
68
|
+
self._cache = None # Invalidate cache
|
|
69
|
+
|
|
70
|
+
def update(self, key: str, value: Any) -> None:
|
|
71
|
+
"""Update a single parameter.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
key: Parameter key (can be dot-separated like "model.layers")
|
|
75
|
+
value: New value
|
|
76
|
+
"""
|
|
77
|
+
self._append_operation("update", key=key, value=value)
|
|
78
|
+
self._cache = None # Invalidate cache
|
|
79
|
+
|
|
80
|
+
def read(self) -> Dict[str, Any]:
|
|
81
|
+
"""Read current parameters by replaying operations.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Current parameter dictionary
|
|
85
|
+
"""
|
|
86
|
+
if self._cache is not None:
|
|
87
|
+
return self._cache.copy()
|
|
88
|
+
|
|
89
|
+
params = {}
|
|
90
|
+
|
|
91
|
+
if not self.backend.exists(self.params_file):
|
|
92
|
+
return params
|
|
93
|
+
|
|
94
|
+
# Read and replay all operations
|
|
95
|
+
content = self.backend.read_text(self.params_file)
|
|
96
|
+
for line in content.strip().split("\n"):
|
|
97
|
+
if not line:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
operation = json.loads(line)
|
|
101
|
+
op_type = operation.get("operation")
|
|
102
|
+
|
|
103
|
+
if op_type == "set":
|
|
104
|
+
params = operation.get("data", {})
|
|
105
|
+
elif op_type == "extend":
|
|
106
|
+
params = deep_merge(params, operation.get("data", {}))
|
|
107
|
+
elif op_type == "update":
|
|
108
|
+
key = operation.get("key")
|
|
109
|
+
value = operation.get("value")
|
|
110
|
+
if key:
|
|
111
|
+
# Support dot-separated keys
|
|
112
|
+
keys = key.split(".")
|
|
113
|
+
current = params
|
|
114
|
+
for k in keys[:-1]:
|
|
115
|
+
if k not in current:
|
|
116
|
+
current[k] = {}
|
|
117
|
+
current = current[k]
|
|
118
|
+
current[keys[-1]] = value
|
|
119
|
+
|
|
120
|
+
self._cache = params
|
|
121
|
+
return params.copy()
|
|
122
|
+
|
|
123
|
+
def log(self, **kwargs) -> None:
|
|
124
|
+
"""Alias for set() to match API documentation.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
**kwargs: Parameter key-value pairs
|
|
128
|
+
"""
|
|
129
|
+
self.set(**kwargs)
|
|
130
|
+
|
|
131
|
+
def _append_operation(self, operation: str, **kwargs) -> None:
|
|
132
|
+
"""Append an operation to the parameters file.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
operation: Operation type (set, extend, update)
|
|
136
|
+
**kwargs: Operation-specific data
|
|
137
|
+
"""
|
|
138
|
+
entry = {
|
|
139
|
+
"timestamp": time.time(),
|
|
140
|
+
"operation": operation,
|
|
141
|
+
**kwargs
|
|
142
|
+
}
|
|
143
|
+
line = json.dumps(entry) + "\n"
|
|
144
|
+
self.backend.append_text(self.params_file, line)
|
ml_dash/job_logger.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""JobLogger - Job-based logging wrapper around ML_Logger.
|
|
2
|
+
|
|
3
|
+
This class provides a simple wrapper around ML_Logger for job-based logging.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .ml_logger import ML_Logger
|
|
9
|
+
from .backends.base import StorageBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JobLogger(ML_Logger):
|
|
13
|
+
"""Job-based logger wrapper.
|
|
14
|
+
|
|
15
|
+
This is a simple wrapper around ML_Logger that can be extended
|
|
16
|
+
with job-specific functionality in the future.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
prefix: Directory prefix for logging
|
|
20
|
+
backend: Storage backend (optional)
|
|
21
|
+
job_id: Optional job identifier
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
prefix: str,
|
|
27
|
+
backend: Optional[StorageBackend] = None,
|
|
28
|
+
job_id: Optional[str] = None,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize JobLogger.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
prefix: Directory prefix for logging
|
|
34
|
+
backend: Storage backend (optional)
|
|
35
|
+
job_id: Optional job identifier
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(prefix, backend)
|
|
38
|
+
self.job_id = job_id
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
"""String representation."""
|
|
42
|
+
return f"JobLogger(prefix='{self.prefix}', job_id='{self.job_id}', entries={len(self.buffer)})"
|