loopkit 0.0.1a1__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.
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 Kim Kunhee
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: loopkit
3
+ Version: 0.0.1a1
4
+ Author-email: Kunhee Kim <kunhee.kim@kaist.ac.kr>
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/kunheek/loopkit
7
+ Keywords: machine-learning,deep-learning,experiment-tracking,pytorch,configuration,logging,reproducibility,research,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=1.21.0
24
+ Requires-Dist: matplotlib>=3.5.0
25
+ Requires-Dist: polars>=0.20.0
26
+ Requires-Dist: psutil>=5.9.0
27
+ Requires-Dist: pyyaml>=6.0
28
+ Provides-Extra: wandb
29
+ Requires-Dist: wandb>=0.15.0; extra == "wandb"
30
+ Provides-Extra: tensorboard
31
+ Requires-Dist: tensorboard>=2.13.0; extra == "tensorboard"
32
+ Provides-Extra: tracking
33
+ Requires-Dist: wandb>=0.15.0; extra == "tracking"
34
+ Requires-Dist: tensorboard>=2.13.0; extra == "tracking"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
37
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
38
+ Requires-Dist: black>=23.0.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
40
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
41
+ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # LoopKit
@@ -0,0 +1 @@
1
+ # LoopKit
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "loopkit"
7
+ version = "0.0.1a1"
8
+ description = ""
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Kunhee Kim", email = "kunhee.kim@kaist.ac.kr"}
13
+ ]
14
+ keywords = [
15
+ "machine-learning",
16
+ "deep-learning",
17
+ "experiment-tracking",
18
+ "pytorch",
19
+ "configuration",
20
+ "logging",
21
+ "reproducibility",
22
+ "research",
23
+ "cli"
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Science/Research",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.8",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ ]
39
+ requires-python = ">=3.8"
40
+ dependencies = [
41
+ "numpy>=1.21.0",
42
+ "matplotlib>=3.5.0",
43
+ "polars>=0.20.0",
44
+ "psutil>=5.9.0",
45
+ "pyyaml>=6.0",
46
+ ]
47
+
48
+ [project.urls]
49
+ Repository = "https://github.com/kunheek/loopkit"
50
+
51
+ [project.scripts]
52
+ loopkit = "loopkit.cli:main"
53
+
54
+ [project.optional-dependencies]
55
+ wandb = ["wandb>=0.15.0"]
56
+ tensorboard = ["tensorboard>=2.13.0"]
57
+ tracking = ["wandb>=0.15.0", "tensorboard>=2.13.0"]
58
+ dev = [
59
+ "pytest>=7.0.0",
60
+ "pytest-cov>=4.0.0",
61
+ "black>=23.0.0",
62
+ "ruff>=0.1.0",
63
+ "mypy>=1.0.0",
64
+ "pre-commit>=3.0.0"
65
+ ]
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ import os
2
+
3
+ __version__ = "0.0.1a1"
4
+
5
+ # Import submodules to make them available
6
+ from . import config, git, logger, monitor, tracking, utils
7
+
8
+ # Import optional modules
9
+ try:
10
+ from . import torch
11
+ except ImportError:
12
+ torch = None # type: ignore
13
+
14
+ # Import commonly used classes and functions
15
+ from .config import Config, override_config
16
+ from .logger import ExperimentLogger
17
+ from .utils import get_gpu_devices, set_seed, str2bool
18
+
19
+ # Export public API
20
+ __all__ = [
21
+ "__version__",
22
+ "Config",
23
+ "ExperimentLogger",
24
+ "config",
25
+ "debug",
26
+ "force_single_process",
27
+ "get_gpu_devices",
28
+ "git",
29
+ "log_level",
30
+ "logger",
31
+ "monitor",
32
+ "override_config",
33
+ "save_checkpoints",
34
+ "set_seed",
35
+ "str2bool",
36
+ "timer",
37
+ "torch",
38
+ "track_git",
39
+ "track_metrics",
40
+ "tracking",
41
+ "utils",
42
+ "verbose",
43
+ ]
44
+
45
+ # Global configuration flags
46
+ # These can be modified at runtime or set via environment variables
47
+
48
+ # Debug mode - enables debug logging and additional checks
49
+ # Set via: loopkit.debug = True or LK_DEBUG=1
50
+ debug = str2bool(os.environ.get("LK_DEBUG", "0"))
51
+
52
+ # Logging level - default log level for ExperimentLogger instances
53
+ # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
54
+ # Set via: loopkit.log_level = "DEBUG" or LK_LOG_LEVEL=DEBUG
55
+ log_level = os.environ.get("LK_LOG_LEVEL", "INFO").upper()
56
+
57
+ # Profiling - controls whether logger.timer() performs timing measurements
58
+ # Disable for production to eliminate profiling overhead
59
+ # Set via: loopkit.timer = False or LK_TIMER=0
60
+ timer = str2bool(os.environ.get("LK_TIMER", "1"))
61
+
62
+ # Verbose mode - controls console output verbosity
63
+ # Disable for cleaner logs when running many experiments
64
+ # Set via: loopkit.verbose = False or LK_VERBOSE=0
65
+ verbose = str2bool(os.environ.get("LK_VERBOSE", "1"))
66
+
67
+ # Checkpoint saving - controls whether to save model checkpoints
68
+ # Disable for quick experiments or testing to skip I/O
69
+ # Set via: loopkit.save_checkpoints = False or LK_SAVE_CHECKPOINTS=0
70
+ save_checkpoints = str2bool(os.environ.get("LK_SAVE_CHECKPOINTS", "1"))
71
+
72
+ # Metrics tracking - controls whether to log metrics to CSV
73
+ # Disable for faster iteration when metrics aren't needed
74
+ # Set via: loopkit.track_metrics = False or LK_TRACK_METRICS=0
75
+ track_metrics = str2bool(os.environ.get("LK_TRACK_METRICS", "1"))
76
+
77
+ # Git tracking - controls whether to collect git repository information
78
+ # Disable to skip git operations (useful in CI/CD or when git is slow)
79
+ # Set via: loopkit.track_git = False or LK_TRACK_GIT=0
80
+ track_git = str2bool(os.environ.get("LK_TRACK_GIT", "1"))
81
+
82
+ # Force single process - force single-process mode in distributed environments
83
+ # Useful for debugging distributed code without multiple processes
84
+ # Set via: loopkit.force_single_process = True or LK_FORCE_SINGLE=1
85
+ force_single_process = str2bool(os.environ.get("LK_FORCE_SINGLE", "0"))
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,274 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional, Union
5
+
6
+ import polars as pl
7
+ import yaml
8
+
9
+
10
+ def load_run_info(run_dir: Union[str, Path]) -> Dict:
11
+ """Load information about a single run.
12
+
13
+ Args:
14
+ run_dir: Path to run directory
15
+
16
+ Returns:
17
+ Dict with run information including config, metrics, git info
18
+ """
19
+ run_dir = Path(run_dir)
20
+ info = {"run_dir": str(run_dir), "run_id": run_dir.name}
21
+
22
+ # Load config
23
+ config_file = run_dir / "run.yaml"
24
+ if config_file.exists():
25
+ with open(config_file) as f:
26
+ info["config"] = yaml.safe_load(f)
27
+
28
+ # Load best metrics
29
+ best_file = run_dir / "best.json"
30
+ if best_file.exists():
31
+ with open(best_file) as f:
32
+ info["best_metrics"] = json.load(f)
33
+
34
+ # Load final metrics from CSV
35
+ metrics_file = run_dir / "metrics.csv"
36
+ if metrics_file.exists():
37
+ df = pl.read_csv(metrics_file)
38
+ if not df.is_empty():
39
+ # Get last values for each metric
40
+ final_metrics = {}
41
+ for name in df["name"].unique():
42
+ metric_df = df.filter(pl.col("name") == name)
43
+ if not metric_df.is_empty():
44
+ final_metrics[name] = metric_df.tail(1)["value"][0]
45
+ info["final_metrics"] = final_metrics
46
+
47
+ # Load git info
48
+ git_file = run_dir / "git_info.json"
49
+ if git_file.exists():
50
+ with open(git_file) as f:
51
+ info["git"] = json.load(f)
52
+
53
+ return info
54
+
55
+
56
+ def compare_runs(
57
+ run_dirs: List[Union[str, Path]],
58
+ metrics: Optional[List[str]] = None,
59
+ show_config: bool = True,
60
+ show_git: bool = False,
61
+ ) -> pl.DataFrame:
62
+ """Compare multiple experiment runs.
63
+
64
+ Args:
65
+ run_dirs: List of run directories to compare
66
+ metrics: Specific metrics to compare (None = all)
67
+ show_config: Whether to include config values
68
+ show_git: Whether to include git info
69
+
70
+ Returns:
71
+ DataFrame with comparison
72
+ """
73
+ runs_data = []
74
+
75
+ for run_dir in run_dirs:
76
+ info = load_run_info(run_dir)
77
+ row = {"run_id": info["run_id"], "run_dir": info["run_dir"]}
78
+
79
+ # Add metrics
80
+ if "best_metrics" in info:
81
+ for metric_name, metric_info in info["best_metrics"].items():
82
+ if metrics is None or metric_name in metrics:
83
+ row[f"best_{metric_name}"] = metric_info.get("value")
84
+ row[f"best_{metric_name}_step"] = metric_info.get("step")
85
+
86
+ if "final_metrics" in info:
87
+ for metric_name, value in info["final_metrics"].items():
88
+ if metrics is None or metric_name in metrics:
89
+ row[f"final_{metric_name}"] = value
90
+
91
+ # Add config
92
+ if show_config and "config" in info:
93
+ config = info["config"]
94
+ # Flatten config
95
+ for key, value in _flatten_dict(config).items():
96
+ row[f"config.{key}"] = value
97
+
98
+ # Add git info
99
+ if show_git and "git" in info:
100
+ row["git_sha"] = info["git"]["sha_short"]
101
+ row["git_dirty"] = info["git"]["dirty"]
102
+
103
+ runs_data.append(row)
104
+
105
+ return pl.DataFrame(runs_data)
106
+
107
+
108
+ def _flatten_dict(d: Dict, parent_key: str = "", sep: str = ".") -> Dict:
109
+ """Flatten nested dictionary."""
110
+ items = []
111
+ for k, v in d.items():
112
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
113
+ if isinstance(v, dict):
114
+ items.extend(_flatten_dict(v, new_key, sep=sep).items())
115
+ else:
116
+ items.append((new_key, v))
117
+ return dict(items)
118
+
119
+
120
+ def find_config_diff(run_dirs: List[Union[str, Path]]) -> Dict[str, List]:
121
+ """Find configuration differences between runs.
122
+
123
+ Args:
124
+ run_dirs: List of run directories
125
+
126
+ Returns:
127
+ Dict mapping config keys to their values across runs
128
+ """
129
+ all_configs = []
130
+ for run_dir in run_dirs:
131
+ info = load_run_info(run_dir)
132
+ if "config" in info:
133
+ all_configs.append(_flatten_dict(info["config"]))
134
+
135
+ if not all_configs:
136
+ return {}
137
+
138
+ # Find keys that differ
139
+ all_keys = set()
140
+ for config in all_configs:
141
+ all_keys.update(config.keys())
142
+
143
+ diffs = {}
144
+ for key in sorted(all_keys):
145
+ values = [config.get(key, None) for config in all_configs]
146
+ # Only include if values differ
147
+ if len(set(str(v) for v in values)) > 1:
148
+ diffs[key] = values
149
+
150
+ return diffs
151
+
152
+
153
+ def print_comparison(
154
+ run_dirs: List[Union[str, Path]],
155
+ metrics: Optional[List[str]] = None,
156
+ show_all_config: bool = False,
157
+ ):
158
+ """Print a formatted comparison of runs.
159
+
160
+ Args:
161
+ run_dirs: List of run directories to compare
162
+ metrics: Specific metrics to compare (None = all)
163
+ show_all_config: If False, only show differing config values
164
+ """
165
+ print(f"\n{'=' * 80}")
166
+ print(f"Comparing {len(run_dirs)} runs")
167
+ print(f"{'=' * 80}\n")
168
+
169
+ # Load run info
170
+ runs_info = [load_run_info(rd) for rd in run_dirs]
171
+
172
+ # Print run IDs
173
+ print("Run IDs:")
174
+ for i, info in enumerate(runs_info):
175
+ print(f" [{i}] {info['run_id']}")
176
+ print()
177
+
178
+ # Print config differences
179
+ if show_all_config:
180
+ print("Configuration:")
181
+ df = compare_runs(run_dirs, show_config=True, show_git=False)
182
+ config_cols = [c for c in df.columns if c.startswith("config.")]
183
+ if config_cols:
184
+ for col in sorted(config_cols):
185
+ print(f" {col}:")
186
+ for i, val in enumerate(df[col]):
187
+ print(f" [{i}] {val}")
188
+ print()
189
+ else:
190
+ diffs = find_config_diff(run_dirs)
191
+ if diffs:
192
+ print("Configuration Differences:")
193
+ for key, values in diffs.items():
194
+ print(f" {key}:")
195
+ for i, val in enumerate(values):
196
+ print(f" [{i}] {val}")
197
+ print()
198
+ else:
199
+ print("No configuration differences found.\n")
200
+
201
+ # Print metrics comparison
202
+ print("Metrics Comparison:")
203
+ df = compare_runs(run_dirs, metrics=metrics, show_config=False, show_git=False)
204
+
205
+ # Show best metrics
206
+ best_cols = [
207
+ c for c in df.columns if c.startswith("best_") and not c.endswith("_step")
208
+ ]
209
+ if best_cols:
210
+ print("\n Best Metrics:")
211
+ for col in sorted(best_cols):
212
+ metric_name = col.replace("best_", "")
213
+ print(f" {metric_name}:")
214
+ for i, val in enumerate(df[col]):
215
+ step_col = f"best_{metric_name}_step"
216
+ step = df[step_col].iloc[i] if step_col in df.columns else "?"
217
+ print(f" [{i}] {val:.4f} (step {step})")
218
+
219
+ # Show final metrics
220
+ final_cols = [c for c in df.columns if c.startswith("final_")]
221
+ if final_cols:
222
+ print("\n Final Metrics:")
223
+ for col in sorted(final_cols):
224
+ metric_name = col.replace("final_", "")
225
+ print(f" {metric_name}:")
226
+ for i, val in enumerate(df[col]):
227
+ print(f" [{i}] {val:.4f}")
228
+
229
+ print(f"\n{'=' * 80}\n")
230
+
231
+
232
+ def setup_compare_parser(subparsers):
233
+ """Setup the compare subcommand parser."""
234
+ compare_parser = subparsers.add_parser(
235
+ "compare",
236
+ help="Compare multiple experiment runs",
237
+ formatter_class=argparse.RawDescriptionHelpFormatter,
238
+ epilog="""
239
+ Examples:
240
+ # Compare runs
241
+ loopkit compare runs/exp1 runs/exp2 runs/exp3
242
+
243
+ # Compare specific metrics
244
+ loopkit compare runs/exp_* --metrics loss accuracy
245
+
246
+ # Show all config (not just diffs)
247
+ loopkit compare runs/exp1 runs/exp2 --all-config
248
+
249
+ # Export to CSV
250
+ loopkit compare runs/exp_* --export comparison.csv
251
+ """,
252
+ )
253
+ compare_parser.add_argument(
254
+ "run_dirs", nargs="+", help="Run directories to compare"
255
+ )
256
+ compare_parser.add_argument(
257
+ "--metrics", "-m", nargs="+", help="Specific metrics to compare"
258
+ )
259
+ compare_parser.add_argument(
260
+ "--all-config", action="store_true", help="Show all config, not just diffs"
261
+ )
262
+ compare_parser.add_argument("--export", "-e", help="Export comparison to CSV file")
263
+
264
+
265
+ def run_compare_command(args):
266
+ """Run the compare command."""
267
+ if args.export:
268
+ df = compare_runs(args.run_dirs, metrics=args.metrics)
269
+ df.write_csv(args.export)
270
+ print(f"✅ Comparison exported to {args.export}")
271
+ else:
272
+ print_comparison(
273
+ args.run_dirs, metrics=args.metrics, show_all_config=args.all_config
274
+ )
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import sys
4
+
5
+
6
+ def main():
7
+ """Main CLI entrypoint for LoopKit."""
8
+ parser = argparse.ArgumentParser(
9
+ prog="loopkit",
10
+ description="LoopKit CLI",
11
+ formatter_class=argparse.RawDescriptionHelpFormatter,
12
+ epilog="""
13
+ Commands:
14
+ sweep Run hyperparameter sweeps
15
+ viz Visualize experiment metrics
16
+ compare Compare multiple experiment runs
17
+
18
+ Examples:
19
+ loopkit sweep "python train.py {config}" --config config.yaml --sweep "lr=[0.001,0.01]"
20
+ loopkit viz runs/exp_* --metrics loss accuracy
21
+ loopkit compare runs/exp1 runs/exp2 runs/exp3
22
+ """,
23
+ )
24
+
25
+ parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
26
+
27
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
28
+
29
+ # Import command setup functions
30
+ from .sweep import setup_sweep_parser
31
+ from .visualize import setup_visualize_parser
32
+ from .compare import setup_compare_parser
33
+
34
+ # Setup subcommands
35
+ setup_sweep_parser(subparsers)
36
+ setup_visualize_parser(subparsers)
37
+ setup_compare_parser(subparsers)
38
+
39
+ # Parse arguments
40
+ args = parser.parse_args()
41
+
42
+ if not args.command:
43
+ parser.print_help()
44
+ sys.exit(1)
45
+
46
+ # Dispatch to appropriate command
47
+ if args.command == "sweep":
48
+ from .sweep import run_sweep_command
49
+
50
+ run_sweep_command(args)
51
+ elif args.command in ("viz", "visualize"):
52
+ from .visualize import run_visualize_command
53
+
54
+ run_visualize_command(args)
55
+ elif args.command == "compare":
56
+ from .compare import run_compare_command
57
+
58
+ run_compare_command(args)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()