ml-dash 0.5.1__py3-none-any.whl → 0.5.3__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/__init__.py +2 -1
- ml_dash/auto_start.py +42 -0
- ml_dash/experiment.py +142 -61
- ml_dash/files.py +185 -3
- ml_dash/metric.py +93 -0
- ml_dash/params.py +2 -2
- {ml_dash-0.5.1.dist-info → ml_dash-0.5.3.dist-info}/METADATA +5 -5
- ml_dash-0.5.3.dist-info/RECORD +13 -0
- ml_dash-0.5.1.dist-info/RECORD +0 -12
- {ml_dash-0.5.1.dist-info → ml_dash-0.5.3.dist-info}/WHEEL +0 -0
ml_dash/__init__.py
CHANGED
|
@@ -38,7 +38,7 @@ Usage:
|
|
|
38
38
|
experiment.log("Training started")
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
|
-
from .experiment import Experiment, ml_dash_experiment, OperationMode
|
|
41
|
+
from .experiment import Experiment, ml_dash_experiment, OperationMode, RunManager
|
|
42
42
|
from .client import RemoteClient
|
|
43
43
|
from .storage import LocalStorage
|
|
44
44
|
from .log import LogLevel, LogBuilder
|
|
@@ -50,6 +50,7 @@ __all__ = [
|
|
|
50
50
|
"Experiment",
|
|
51
51
|
"ml_dash_experiment",
|
|
52
52
|
"OperationMode",
|
|
53
|
+
"RunManager",
|
|
53
54
|
"RemoteClient",
|
|
54
55
|
"LocalStorage",
|
|
55
56
|
"LogLevel",
|
ml_dash/auto_start.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-start module for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Provides a pre-configured, auto-started experiment singleton named 'dxp'.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from ml_dash.auto_start import dxp
|
|
8
|
+
|
|
9
|
+
# Ready to use immediately - no need to open/start
|
|
10
|
+
dxp.log("Hello from dxp!")
|
|
11
|
+
dxp.params.set(lr=0.001)
|
|
12
|
+
dxp.metrics("loss").append(step=0, value=0.5)
|
|
13
|
+
|
|
14
|
+
# Automatically closed on Python exit
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import atexit
|
|
18
|
+
from .experiment import Experiment
|
|
19
|
+
|
|
20
|
+
# Create pre-configured singleton experiment
|
|
21
|
+
dxp = Experiment(
|
|
22
|
+
name="dxp",
|
|
23
|
+
project="scratch",
|
|
24
|
+
local_path=".ml-dash"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Auto-start the experiment on import
|
|
28
|
+
dxp.run.start()
|
|
29
|
+
|
|
30
|
+
# Register cleanup handler to complete experiment on Python exit
|
|
31
|
+
def _cleanup():
|
|
32
|
+
"""Complete the dxp experiment on exit if still open."""
|
|
33
|
+
if dxp._is_open:
|
|
34
|
+
try:
|
|
35
|
+
dxp.run.complete()
|
|
36
|
+
except Exception:
|
|
37
|
+
# Silently ignore errors during cleanup
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
atexit.register(_cleanup)
|
|
41
|
+
|
|
42
|
+
__all__ = ["dxp"]
|
ml_dash/experiment.py
CHANGED
|
@@ -27,6 +27,74 @@ class OperationMode(Enum):
|
|
|
27
27
|
HYBRID = "hybrid" # Future: sync local to remote
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class RunManager:
|
|
31
|
+
"""
|
|
32
|
+
Lifecycle manager for experiments.
|
|
33
|
+
|
|
34
|
+
Supports three usage patterns:
|
|
35
|
+
1. Method calls: experiment.run.start(), experiment.run.complete()
|
|
36
|
+
2. Context manager: with Experiment(...).run as exp:
|
|
37
|
+
3. Decorator: @exp.run or @Experiment(...).run
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, experiment: "Experiment"):
|
|
41
|
+
"""
|
|
42
|
+
Initialize RunManager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
experiment: Parent Experiment instance
|
|
46
|
+
"""
|
|
47
|
+
self._experiment = experiment
|
|
48
|
+
|
|
49
|
+
def start(self) -> "Experiment":
|
|
50
|
+
"""
|
|
51
|
+
Start the experiment (sets status to RUNNING).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The experiment instance for chaining
|
|
55
|
+
"""
|
|
56
|
+
return self._experiment._open()
|
|
57
|
+
|
|
58
|
+
def complete(self) -> None:
|
|
59
|
+
"""Mark experiment as completed (status: COMPLETED)."""
|
|
60
|
+
self._experiment._close(status="COMPLETED")
|
|
61
|
+
|
|
62
|
+
def fail(self) -> None:
|
|
63
|
+
"""Mark experiment as failed (status: FAILED)."""
|
|
64
|
+
self._experiment._close(status="FAILED")
|
|
65
|
+
|
|
66
|
+
def cancel(self) -> None:
|
|
67
|
+
"""Mark experiment as cancelled (status: CANCELLED)."""
|
|
68
|
+
self._experiment._close(status="CANCELLED")
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> "Experiment":
|
|
71
|
+
"""Context manager entry - starts the experiment."""
|
|
72
|
+
return self.start()
|
|
73
|
+
|
|
74
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
75
|
+
"""Context manager exit - completes or fails the experiment."""
|
|
76
|
+
if exc_type is not None:
|
|
77
|
+
self.fail()
|
|
78
|
+
else:
|
|
79
|
+
self.complete()
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def __call__(self, func: Callable) -> Callable:
|
|
83
|
+
"""
|
|
84
|
+
Decorator support for wrapping functions with experiment lifecycle.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
@exp.run
|
|
88
|
+
def train(exp):
|
|
89
|
+
exp.log("Training...")
|
|
90
|
+
"""
|
|
91
|
+
@functools.wraps(func)
|
|
92
|
+
def wrapper(*args, **kwargs):
|
|
93
|
+
with self as exp:
|
|
94
|
+
return func(exp, *args, **kwargs)
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
|
|
30
98
|
class Experiment:
|
|
31
99
|
"""
|
|
32
100
|
ML-Dash experiment for metricing experiments.
|
|
@@ -67,13 +135,14 @@ class Experiment:
|
|
|
67
135
|
tags: Optional[List[str]] = None,
|
|
68
136
|
bindrs: Optional[List[str]] = None,
|
|
69
137
|
folder: Optional[str] = None,
|
|
70
|
-
write_protected: bool = False,
|
|
71
138
|
metadata: Optional[Dict[str, Any]] = None,
|
|
72
139
|
# Mode configuration
|
|
73
140
|
remote: Optional[str] = None,
|
|
74
141
|
api_key: Optional[str] = None,
|
|
75
142
|
user_name: Optional[str] = None,
|
|
76
143
|
local_path: Optional[str] = None,
|
|
144
|
+
# Internal parameters
|
|
145
|
+
_write_protected: bool = False,
|
|
77
146
|
):
|
|
78
147
|
"""
|
|
79
148
|
Initialize an ML-Dash experiment.
|
|
@@ -85,12 +154,12 @@ class Experiment:
|
|
|
85
154
|
tags: Optional list of tags
|
|
86
155
|
bindrs: Optional list of bindrs
|
|
87
156
|
folder: Optional folder path (e.g., "/experiments/baseline")
|
|
88
|
-
write_protected: If True, experiment becomes immutable after creation
|
|
89
157
|
metadata: Optional metadata dict
|
|
90
158
|
remote: Remote API URL (e.g., "http://localhost:3000")
|
|
91
159
|
api_key: JWT token for authentication (if not provided, will be generated from user_name)
|
|
92
160
|
user_name: Username for authentication (generates API key if api_key not provided)
|
|
93
161
|
local_path: Local storage root path (for local mode)
|
|
162
|
+
_write_protected: Internal parameter - if True, experiment becomes immutable after creation
|
|
94
163
|
"""
|
|
95
164
|
self.name = name
|
|
96
165
|
self.project = project
|
|
@@ -98,7 +167,7 @@ class Experiment:
|
|
|
98
167
|
self.tags = tags
|
|
99
168
|
self.bindrs = bindrs
|
|
100
169
|
self.folder = folder
|
|
101
|
-
self.
|
|
170
|
+
self._write_protected = _write_protected
|
|
102
171
|
self.metadata = metadata
|
|
103
172
|
|
|
104
173
|
# Generate API key from username if not provided
|
|
@@ -171,9 +240,9 @@ class Experiment:
|
|
|
171
240
|
|
|
172
241
|
return token
|
|
173
242
|
|
|
174
|
-
def
|
|
243
|
+
def _open(self) -> "Experiment":
|
|
175
244
|
"""
|
|
176
|
-
|
|
245
|
+
Internal method to open the experiment (create or update on server/filesystem).
|
|
177
246
|
|
|
178
247
|
Returns:
|
|
179
248
|
self for chaining
|
|
@@ -190,7 +259,7 @@ class Experiment:
|
|
|
190
259
|
tags=self.tags,
|
|
191
260
|
bindrs=self.bindrs,
|
|
192
261
|
folder=self.folder,
|
|
193
|
-
write_protected=self.
|
|
262
|
+
write_protected=self._write_protected,
|
|
194
263
|
metadata=self.metadata,
|
|
195
264
|
)
|
|
196
265
|
self._experiment_data = response
|
|
@@ -211,9 +280,9 @@ class Experiment:
|
|
|
211
280
|
self._is_open = True
|
|
212
281
|
return self
|
|
213
282
|
|
|
214
|
-
def
|
|
283
|
+
def _close(self, status: str = "COMPLETED"):
|
|
215
284
|
"""
|
|
216
|
-
|
|
285
|
+
Internal method to close the experiment and update status.
|
|
217
286
|
|
|
218
287
|
Args:
|
|
219
288
|
status: Status to set - "COMPLETED" (default), "FAILED", or "CANCELLED"
|
|
@@ -238,16 +307,52 @@ class Experiment:
|
|
|
238
307
|
|
|
239
308
|
self._is_open = False
|
|
240
309
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
310
|
+
@property
|
|
311
|
+
def run(self) -> RunManager:
|
|
312
|
+
"""
|
|
313
|
+
Get the RunManager for lifecycle operations.
|
|
244
314
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
315
|
+
Usage:
|
|
316
|
+
# Method calls
|
|
317
|
+
experiment.run.start()
|
|
318
|
+
experiment.run.complete()
|
|
319
|
+
|
|
320
|
+
# Context manager
|
|
321
|
+
with Experiment(...).run as exp:
|
|
322
|
+
exp.log("Training...")
|
|
323
|
+
|
|
324
|
+
# Decorator
|
|
325
|
+
@experiment.run
|
|
326
|
+
def train(exp):
|
|
327
|
+
exp.log("Training...")
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
RunManager instance
|
|
331
|
+
"""
|
|
332
|
+
return RunManager(self)
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def params(self) -> ParametersBuilder:
|
|
336
|
+
"""
|
|
337
|
+
Get a ParametersBuilder for parameter operations.
|
|
338
|
+
|
|
339
|
+
Usage:
|
|
340
|
+
# Set parameters
|
|
341
|
+
experiment.params.set(lr=0.001, batch_size=32)
|
|
342
|
+
|
|
343
|
+
# Get parameters
|
|
344
|
+
params = experiment.params.get()
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
ParametersBuilder instance
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
RuntimeError: If experiment is not open
|
|
351
|
+
"""
|
|
352
|
+
if not self._is_open:
|
|
353
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
354
|
+
|
|
355
|
+
return ParametersBuilder(self)
|
|
251
356
|
|
|
252
357
|
def log(
|
|
253
358
|
self,
|
|
@@ -591,31 +696,6 @@ class Experiment:
|
|
|
591
696
|
|
|
592
697
|
return result
|
|
593
698
|
|
|
594
|
-
def parameters(self) -> ParametersBuilder:
|
|
595
|
-
"""
|
|
596
|
-
Get a ParametersBuilder for fluent parameter operations.
|
|
597
|
-
|
|
598
|
-
Returns:
|
|
599
|
-
ParametersBuilder instance for chaining
|
|
600
|
-
|
|
601
|
-
Raises:
|
|
602
|
-
RuntimeError: If experiment is not open
|
|
603
|
-
|
|
604
|
-
Examples:
|
|
605
|
-
# Set parameters
|
|
606
|
-
experiment.parameters().set(
|
|
607
|
-
model={"lr": 0.001, "batch_size": 32},
|
|
608
|
-
optimizer="adam"
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
# Get parameters
|
|
612
|
-
params = experiment.parameters().get() # Flattened
|
|
613
|
-
params = experiment.parameters().get(flatten=False) # Nested
|
|
614
|
-
"""
|
|
615
|
-
if not self._is_open:
|
|
616
|
-
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
617
|
-
|
|
618
|
-
return ParametersBuilder(self)
|
|
619
699
|
|
|
620
700
|
def _write_params(self, flattened_params: Dict[str, Any]) -> None:
|
|
621
701
|
"""
|
|
@@ -665,48 +745,49 @@ class Experiment:
|
|
|
665
745
|
|
|
666
746
|
return params
|
|
667
747
|
|
|
668
|
-
|
|
669
|
-
|
|
748
|
+
@property
|
|
749
|
+
def metrics(self) -> 'MetricsManager':
|
|
670
750
|
"""
|
|
671
|
-
Get a
|
|
751
|
+
Get a MetricsManager for metric operations.
|
|
672
752
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
tags: Optional tags for categorization
|
|
677
|
-
metadata: Optional structured metadata
|
|
753
|
+
Supports two usage patterns:
|
|
754
|
+
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
755
|
+
2. Unnamed: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
678
756
|
|
|
679
757
|
Returns:
|
|
680
|
-
|
|
758
|
+
MetricsManager instance
|
|
681
759
|
|
|
682
760
|
Raises:
|
|
683
761
|
RuntimeError: If experiment is not open
|
|
684
762
|
|
|
685
763
|
Examples:
|
|
686
|
-
#
|
|
687
|
-
experiment.
|
|
764
|
+
# Named metric
|
|
765
|
+
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
766
|
+
|
|
767
|
+
# Unnamed (name in append call)
|
|
768
|
+
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
688
769
|
|
|
689
770
|
# Append batch
|
|
690
|
-
experiment.
|
|
771
|
+
experiment.metrics("metrics").append_batch([
|
|
691
772
|
{"loss": 0.5, "acc": 0.8, "step": 1},
|
|
692
773
|
{"loss": 0.4, "acc": 0.85, "step": 2}
|
|
693
774
|
])
|
|
694
775
|
|
|
695
776
|
# Read data
|
|
696
|
-
data = experiment.
|
|
777
|
+
data = experiment.metrics("train_loss").read(start_index=0, limit=100)
|
|
697
778
|
|
|
698
779
|
# Get statistics
|
|
699
|
-
stats = experiment.
|
|
780
|
+
stats = experiment.metrics("train_loss").stats()
|
|
700
781
|
"""
|
|
701
|
-
from .metric import
|
|
782
|
+
from .metric import MetricsManager
|
|
702
783
|
|
|
703
784
|
if not self._is_open:
|
|
704
785
|
raise RuntimeError(
|
|
705
|
-
"Cannot use
|
|
706
|
-
"Use 'with Experiment(...) as experiment:' or call experiment.
|
|
786
|
+
"Cannot use metrics on closed experiment. "
|
|
787
|
+
"Use 'with Experiment(...).run as experiment:' or call experiment.run.start() first."
|
|
707
788
|
)
|
|
708
789
|
|
|
709
|
-
return
|
|
790
|
+
return MetricsManager(self)
|
|
710
791
|
|
|
711
792
|
def _append_to_metric(
|
|
712
793
|
self,
|
|
@@ -931,7 +1012,7 @@ def ml_dash_experiment(
|
|
|
931
1012
|
def decorator(func: Callable) -> Callable:
|
|
932
1013
|
@functools.wraps(func)
|
|
933
1014
|
def wrapper(*args, **func_kwargs):
|
|
934
|
-
with Experiment(name=name, project=project, **kwargs) as experiment:
|
|
1015
|
+
with Experiment(name=name, project=project, **kwargs).run as experiment:
|
|
935
1016
|
# Inject experiment into function kwargs
|
|
936
1017
|
func_kwargs['experiment'] = experiment
|
|
937
1018
|
return func(*args, **func_kwargs)
|
ml_dash/files.py
CHANGED
|
@@ -44,6 +44,7 @@ class FileBuilder:
|
|
|
44
44
|
- prefix: Logical path prefix (default: "/")
|
|
45
45
|
- description: Optional description
|
|
46
46
|
- tags: Optional list of tags
|
|
47
|
+
- bindrs: Optional list of bindrs
|
|
47
48
|
- metadata: Optional metadata dict
|
|
48
49
|
- file_id: File ID for download/delete/update operations
|
|
49
50
|
- dest_path: Destination path for download
|
|
@@ -53,6 +54,7 @@ class FileBuilder:
|
|
|
53
54
|
self._prefix = kwargs.get('prefix', '/')
|
|
54
55
|
self._description = kwargs.get('description')
|
|
55
56
|
self._tags = kwargs.get('tags', [])
|
|
57
|
+
self._bindrs = kwargs.get('bindrs', [])
|
|
56
58
|
self._metadata = kwargs.get('metadata')
|
|
57
59
|
self._file_id = kwargs.get('file_id')
|
|
58
60
|
self._dest_path = kwargs.get('dest_path')
|
|
@@ -76,7 +78,7 @@ class FileBuilder:
|
|
|
76
78
|
if not self._experiment._is_open:
|
|
77
79
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
78
80
|
|
|
79
|
-
if self._experiment.
|
|
81
|
+
if self._experiment._write_protected:
|
|
80
82
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
81
83
|
|
|
82
84
|
if not self._file_path:
|
|
@@ -189,7 +191,7 @@ class FileBuilder:
|
|
|
189
191
|
if not self._experiment._is_open:
|
|
190
192
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
191
193
|
|
|
192
|
-
if self._experiment.
|
|
194
|
+
if self._experiment._write_protected:
|
|
193
195
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
194
196
|
|
|
195
197
|
if not self._file_id:
|
|
@@ -219,7 +221,7 @@ class FileBuilder:
|
|
|
219
221
|
if not self._experiment._is_open:
|
|
220
222
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
221
223
|
|
|
222
|
-
if self._experiment.
|
|
224
|
+
if self._experiment._write_protected:
|
|
223
225
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
224
226
|
|
|
225
227
|
if not self._file_id:
|
|
@@ -232,6 +234,186 @@ class FileBuilder:
|
|
|
232
234
|
metadata=self._metadata
|
|
233
235
|
)
|
|
234
236
|
|
|
237
|
+
def save_json(self, content: Any, file_name: str) -> Dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Save JSON content to a file.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
content: Content to save as JSON (dict, list, or any JSON-serializable object)
|
|
243
|
+
file_name: Name of the file to create
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
RuntimeError: If experiment is not open or write-protected
|
|
250
|
+
ValueError: If content is not JSON-serializable
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
config = {"model": "resnet50", "lr": 0.001}
|
|
254
|
+
result = experiment.file(prefix="/configs").save_json(config, "config.json")
|
|
255
|
+
"""
|
|
256
|
+
import json
|
|
257
|
+
import tempfile
|
|
258
|
+
import os
|
|
259
|
+
|
|
260
|
+
if not self._experiment._is_open:
|
|
261
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
262
|
+
|
|
263
|
+
if self._experiment._write_protected:
|
|
264
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
265
|
+
|
|
266
|
+
# Create temporary file with desired filename
|
|
267
|
+
temp_dir = tempfile.mkdtemp()
|
|
268
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
269
|
+
try:
|
|
270
|
+
# Write JSON content to temp file
|
|
271
|
+
with open(temp_path, 'w') as f:
|
|
272
|
+
json.dump(content, f, indent=2)
|
|
273
|
+
|
|
274
|
+
# Save using existing save() method
|
|
275
|
+
original_file_path = self._file_path
|
|
276
|
+
self._file_path = temp_path
|
|
277
|
+
|
|
278
|
+
# Upload and get result
|
|
279
|
+
result = self.save()
|
|
280
|
+
|
|
281
|
+
# Restore original file_path
|
|
282
|
+
self._file_path = original_file_path
|
|
283
|
+
|
|
284
|
+
return result
|
|
285
|
+
finally:
|
|
286
|
+
# Clean up temp file and directory
|
|
287
|
+
try:
|
|
288
|
+
os.unlink(temp_path)
|
|
289
|
+
os.rmdir(temp_dir)
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
def save_torch(self, model: Any, file_name: str) -> Dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Save PyTorch model to a file.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
model: PyTorch model or state dict to save
|
|
299
|
+
file_name: Name of the file to create (should end with .pt or .pth)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
RuntimeError: If experiment is not open or write-protected
|
|
306
|
+
ImportError: If torch is not installed
|
|
307
|
+
ValueError: If model cannot be saved
|
|
308
|
+
|
|
309
|
+
Examples:
|
|
310
|
+
import torch
|
|
311
|
+
model = torch.nn.Linear(10, 5)
|
|
312
|
+
result = experiment.file(prefix="/models").save_torch(model, "model.pt")
|
|
313
|
+
|
|
314
|
+
# Or save state dict
|
|
315
|
+
result = experiment.file(prefix="/models").save_torch(model.state_dict(), "model.pth")
|
|
316
|
+
"""
|
|
317
|
+
import tempfile
|
|
318
|
+
import os
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
import torch
|
|
322
|
+
except ImportError:
|
|
323
|
+
raise ImportError("PyTorch is not installed. Install it with: pip install torch")
|
|
324
|
+
|
|
325
|
+
if not self._experiment._is_open:
|
|
326
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
327
|
+
|
|
328
|
+
if self._experiment._write_protected:
|
|
329
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
330
|
+
|
|
331
|
+
# Create temporary file with desired filename
|
|
332
|
+
temp_dir = tempfile.mkdtemp()
|
|
333
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
# Save model to temp file
|
|
337
|
+
torch.save(model, temp_path)
|
|
338
|
+
|
|
339
|
+
# Save using existing save() method
|
|
340
|
+
original_file_path = self._file_path
|
|
341
|
+
self._file_path = temp_path
|
|
342
|
+
|
|
343
|
+
# Upload and get result
|
|
344
|
+
result = self.save()
|
|
345
|
+
|
|
346
|
+
# Restore original file_path
|
|
347
|
+
self._file_path = original_file_path
|
|
348
|
+
|
|
349
|
+
return result
|
|
350
|
+
finally:
|
|
351
|
+
# Clean up temp file and directory
|
|
352
|
+
try:
|
|
353
|
+
os.unlink(temp_path)
|
|
354
|
+
os.rmdir(temp_dir)
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
def save_pkl(self, content: Any, file_name: str) -> Dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Save Python object to a pickle file.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
content: Python object to pickle (must be pickle-serializable)
|
|
364
|
+
file_name: Name of the file to create (should end with .pkl or .pickle)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
RuntimeError: If experiment is not open or write-protected
|
|
371
|
+
ValueError: If content cannot be pickled
|
|
372
|
+
|
|
373
|
+
Examples:
|
|
374
|
+
data = {"model": "resnet50", "weights": np.array([1, 2, 3])}
|
|
375
|
+
result = experiment.file(prefix="/data").save_pkl(data, "data.pkl")
|
|
376
|
+
|
|
377
|
+
# Or save any Python object
|
|
378
|
+
result = experiment.file(prefix="/models").save_pkl(trained_model, "model.pickle")
|
|
379
|
+
"""
|
|
380
|
+
import pickle
|
|
381
|
+
import tempfile
|
|
382
|
+
import os
|
|
383
|
+
|
|
384
|
+
if not self._experiment._is_open:
|
|
385
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
386
|
+
|
|
387
|
+
if self._experiment._write_protected:
|
|
388
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
389
|
+
|
|
390
|
+
# Create temporary file with desired filename
|
|
391
|
+
temp_dir = tempfile.mkdtemp()
|
|
392
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
393
|
+
try:
|
|
394
|
+
# Write pickled content to temp file
|
|
395
|
+
with open(temp_path, 'wb') as f:
|
|
396
|
+
pickle.dump(content, f)
|
|
397
|
+
|
|
398
|
+
# Save using existing save() method
|
|
399
|
+
original_file_path = self._file_path
|
|
400
|
+
self._file_path = temp_path
|
|
401
|
+
|
|
402
|
+
# Upload and get result
|
|
403
|
+
result = self.save()
|
|
404
|
+
|
|
405
|
+
# Restore original file_path
|
|
406
|
+
self._file_path = original_file_path
|
|
407
|
+
|
|
408
|
+
return result
|
|
409
|
+
finally:
|
|
410
|
+
# Clean up temp file and directory
|
|
411
|
+
try:
|
|
412
|
+
os.unlink(temp_path)
|
|
413
|
+
os.rmdir(temp_dir)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
235
417
|
|
|
236
418
|
def compute_sha256(file_path: str) -> str:
|
|
237
419
|
"""
|
ml_dash/metric.py
CHANGED
|
@@ -11,6 +11,99 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from .experiment import Experiment
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class MetricsManager:
|
|
15
|
+
"""
|
|
16
|
+
Manager for metric operations that supports both named and unnamed usage.
|
|
17
|
+
|
|
18
|
+
Supports two usage patterns:
|
|
19
|
+
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
20
|
+
2. Unnamed: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
# With explicit metric name
|
|
24
|
+
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
25
|
+
|
|
26
|
+
# Without specifying name upfront (name in append call)
|
|
27
|
+
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, experiment: 'Experiment'):
|
|
31
|
+
"""
|
|
32
|
+
Initialize MetricsManager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
experiment: Parent Experiment instance
|
|
36
|
+
"""
|
|
37
|
+
self._experiment = experiment
|
|
38
|
+
|
|
39
|
+
def __call__(self, name: str, description: Optional[str] = None,
|
|
40
|
+
tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None) -> 'MetricBuilder':
|
|
41
|
+
"""
|
|
42
|
+
Get a MetricBuilder for a specific metric name.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name: Metric name (unique within experiment)
|
|
46
|
+
description: Optional metric description
|
|
47
|
+
tags: Optional tags for categorization
|
|
48
|
+
metadata: Optional structured metadata
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
MetricBuilder instance for the named metric
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
experiment.metrics("loss").append(value=0.5, step=1)
|
|
55
|
+
"""
|
|
56
|
+
return MetricBuilder(self._experiment, name, description, tags, metadata)
|
|
57
|
+
|
|
58
|
+
def append(self, name: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Append a data point to a metric (name specified in call).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
name: Metric name
|
|
64
|
+
data: Data dict (alternative to kwargs)
|
|
65
|
+
**kwargs: Data as keyword arguments
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Response dict with metric metadata
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
72
|
+
experiment.metrics.append(name="loss", data={"value": 0.5, "step": 1})
|
|
73
|
+
"""
|
|
74
|
+
if data is None:
|
|
75
|
+
data = kwargs
|
|
76
|
+
return self._experiment._append_to_metric(name, data, None, None, None)
|
|
77
|
+
|
|
78
|
+
def append_batch(self, name: str, data_points: List[Dict[str, Any]],
|
|
79
|
+
description: Optional[str] = None,
|
|
80
|
+
tags: Optional[List[str]] = None,
|
|
81
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Append multiple data points to a metric.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Metric name
|
|
87
|
+
data_points: List of data point dicts
|
|
88
|
+
description: Optional metric description
|
|
89
|
+
tags: Optional tags for categorization
|
|
90
|
+
metadata: Optional structured metadata
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Response dict with metric metadata
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
experiment.metrics.append_batch(
|
|
97
|
+
name="loss",
|
|
98
|
+
data_points=[
|
|
99
|
+
{"value": 0.5, "step": 1},
|
|
100
|
+
{"value": 0.4, "step": 2}
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
"""
|
|
104
|
+
return self._experiment._append_batch_to_metric(name, data_points, description, tags, metadata)
|
|
105
|
+
|
|
106
|
+
|
|
14
107
|
class MetricBuilder:
|
|
15
108
|
"""
|
|
16
109
|
Builder for metric operations.
|
ml_dash/params.py
CHANGED
|
@@ -62,9 +62,9 @@ class ParametersBuilder:
|
|
|
62
62
|
experiment.parameters().set(**{"model.lr": 0.001, "model.batch_size": 32})
|
|
63
63
|
"""
|
|
64
64
|
if not self._experiment._is_open:
|
|
65
|
-
raise RuntimeError("Experiment not open. Use experiment.
|
|
65
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
66
66
|
|
|
67
|
-
if self._experiment.
|
|
67
|
+
if self._experiment._write_protected:
|
|
68
68
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
69
69
|
|
|
70
70
|
# Flatten the kwargs
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ml-dash
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: ML experiment
|
|
5
|
-
Keywords: machine-learning,experiment-
|
|
3
|
+
Version: 0.5.3
|
|
4
|
+
Summary: ML experiment tracking and data storage
|
|
5
|
+
Keywords: machine-learning,experiment-tracking,mlops,data-storage
|
|
6
6
|
Author: Ge Yang, Tom Tao
|
|
7
7
|
License: MIT License
|
|
8
8
|
|
|
@@ -97,7 +97,7 @@ pip install ml-dash
|
|
|
97
97
|
</tr>
|
|
98
98
|
</table>
|
|
99
99
|
|
|
100
|
-
##
|
|
100
|
+
## Getting Started
|
|
101
101
|
|
|
102
102
|
### Remote Mode (with API Server)
|
|
103
103
|
|
|
@@ -107,7 +107,7 @@ from ml_dash import Experiment
|
|
|
107
107
|
with Experiment(
|
|
108
108
|
name="my-experiment",
|
|
109
109
|
project="my-project",
|
|
110
|
-
remote="https://
|
|
110
|
+
remote="https://api.dash.ml",
|
|
111
111
|
api_key="your-jwt-token"
|
|
112
112
|
) as experiment:
|
|
113
113
|
print(f"Experiment ID: {experiment.id}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ml_dash/__init__.py,sha256=o_LrWVJBY_VkUGhSBs5wdb_NqEsHD1AK9HGsjZGxHxQ,1414
|
|
2
|
+
ml_dash/auto_start.py,sha256=c3XcXFpZdvjtWauEoK5043Gw9k0L_5IDq4fdiB2ha88,959
|
|
3
|
+
ml_dash/client.py,sha256=vhWcS5o2n3o4apEjVeLmu7flCEzxBbBOoLSQNcAx_ew,17267
|
|
4
|
+
ml_dash/experiment.py,sha256=zdGB3oZsFNFyg9olRazWk7dTO7tfy-vTa4neFq5i2CY,30552
|
|
5
|
+
ml_dash/files.py,sha256=Nx7f0n9zoQ7XqPtiHu_SBEZK0cT2b3F6-EhJBNs3C6k,15824
|
|
6
|
+
ml_dash/log.py,sha256=0yXaNnFwYeBI3tRLHX3kkqWRpg0MbSGwmgjnOfsElCk,5350
|
|
7
|
+
ml_dash/metric.py,sha256=LMb6-T08VAl6UBAv6FlZee6LleVLjFaBNc19b17NlfI,9662
|
|
8
|
+
ml_dash/params.py,sha256=xaByDSVar4D1pZqxTANkMPeZTL5-V7ewJe5TXfPLhMQ,5980
|
|
9
|
+
ml_dash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
ml_dash/storage.py,sha256=UTuux2nfclLrrtlkC6TsOvDB_wIbSDvYGg8Gtbvk6mc,30471
|
|
11
|
+
ml_dash-0.5.3.dist-info/WHEEL,sha256=X16MKk8bp2DRsAuyteHJ-9qOjzmnY0x1aj0P1ftqqWA,78
|
|
12
|
+
ml_dash-0.5.3.dist-info/METADATA,sha256=AO80zIp9TG2-3S-zZEPQUOwFBowKCddCfQarQem096M,6043
|
|
13
|
+
ml_dash-0.5.3.dist-info/RECORD,,
|
ml_dash-0.5.1.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
ml_dash/__init__.py,sha256=5tT0Lmf0SS3J7BOwJGVai8FOjdpjKGBJCEYL5nXnkLA,1384
|
|
2
|
-
ml_dash/client.py,sha256=vhWcS5o2n3o4apEjVeLmu7flCEzxBbBOoLSQNcAx_ew,17267
|
|
3
|
-
ml_dash/experiment.py,sha256=x1jtQD1QroNjNULOxZiGtX5oFLi3ZXDaFbGHWt0yMJU,28652
|
|
4
|
-
ml_dash/files.py,sha256=WKWbcug6XADwZruYQio1EdSstmfTsty9-2-t2KPWz38,9719
|
|
5
|
-
ml_dash/log.py,sha256=0yXaNnFwYeBI3tRLHX3kkqWRpg0MbSGwmgjnOfsElCk,5350
|
|
6
|
-
ml_dash/metric.py,sha256=PcEd6_HTLDpf-kBIDeQq2LlTRAS7xDx6EvSBpin5iuY,6456
|
|
7
|
-
ml_dash/params.py,sha256=W-JkY1Mz7KdmvDjQ0HFV2QnpBov7Gf4dl70fuBnXTdo,5974
|
|
8
|
-
ml_dash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
ml_dash/storage.py,sha256=UTuux2nfclLrrtlkC6TsOvDB_wIbSDvYGg8Gtbvk6mc,30471
|
|
10
|
-
ml_dash-0.5.1.dist-info/WHEEL,sha256=X16MKk8bp2DRsAuyteHJ-9qOjzmnY0x1aj0P1ftqqWA,78
|
|
11
|
-
ml_dash-0.5.1.dist-info/METADATA,sha256=TBTqi4lJNoFO2eyztYJZptQypUSVTDVUdbGS0EnQZ2k,6067
|
|
12
|
-
ml_dash-0.5.1.dist-info/RECORD,,
|
|
File without changes
|