Skip to content

Commit 2687cd0

Browse files
authored
Merge pull request #279 from Oxid15/develop
v0.16.0
2 parents 998de3f + 471cd21 commit 2687cd0

49 files changed

Lines changed: 476 additions & 159 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
# Cascade - Small-scale MLOps Library
2+
3+
Lightweight and modular MLOps library with the aim to make ML development more efficient targeted at small teams or individuals.
4+
15
![header](cascade/docs/imgs/header.png)
26

37
![ver](https://img.shields.io/github/v/release/oxid15/cascade?style=plastic)
48
![build](https://github.com/oxid15/cascade/actions/workflows/python-package.yml/badge.svg)
59
[![Downloads](https://pepy.tech/badge/cascade-ml)](https://pepy.tech/project/cascade-ml)
610
[![DOI](https://zenodo.org/badge/460920693.svg)](https://zenodo.org/badge/latestdoi/460920693)
711

8-
Lightweight and modular MLOps library with the aim to make ML development more efficient targeted at small teams or individuals.
9-
10-
Cascade was built especially for individuals or small teams that are in need of MLOps, but don't have time or resources to integrate with platforms.
12+
Cascade offers the solution that enables MLOps features for small projects while demanding little. There is usually no need for the full MLOps setups in most of the small-scale ML-projects.
1113

1214
**Included in [Model Lifecycle](https://github.com/kelvins/awesome-mlops#model-lifecycle) section of Awesome MLOps list**
1315

@@ -50,7 +52,7 @@ ds = cdd.RandomSampler(ds)
5052
train_ds, test_ds = cdd.split(ds)
5153
train_ds = cdd.ApplyModifier(
5254
train_ds,
53-
lambda pair: pair + np.random.random() * 0.1 - 0.05
55+
lambda pair: pair[0] + np.random.random() * 0.1 - 0.05, pair[1]
5456
)
5557

5658
pprint(train_ds.get_meta())
@@ -98,7 +100,6 @@ We see all the stages that we did in meta.
98100
See all datasets in [zoo](https://oxid15.github.io/cascade/en/latest/modules/dataset_zoo.html)
99101
See tutorial in [documentation](https://oxid15.github.io/cascade/en/latest/tutorials/tutorials.html)
100102

101-
102103
### Experiment tracking
103104

104105
Cascade provides a rich set of ML-experiment tracking tools.
@@ -122,7 +123,6 @@ line.save(model, only_meta=True)
122123
`Repo` is the collection of lines and `Line` can be a bunch of experiments on one model type.
123124
Lines can also store data pipelines.
124125

125-
126126
<details>
127127
<summary>Click to see full model metadata</summary>
128128

@@ -156,47 +156,25 @@ Lines can also store data pipelines.
156156

157157
</details>
158158

159-
160159
See tutorial in [documentation](https://oxid15.github.io/cascade/en/latest/tutorials/tutorials.html)
161160

161+
## Cascade UI
162162

163-
### Metadata analysis
164-
165-
During experiments Cascade produces many metadata which can be analyzed later.
166-
`MetricViewer` is the tool that allows to see the relationship between parameters and
167-
metrics of all models in repository.
168-
169-
```python
170-
from cascade.meta import MetricViewer
171-
from cascade.repos import Repo
163+
Cascade features web-based experiment dashboard. You can install it with:
172164

173-
repo = cdm.Repo("repo")
174-
175-
# This runs web-server that relies on optional dependency
176-
MetricViewer(repo).serve()
165+
```bash
166+
pip install cascade-ui
177167
```
178168

179-
![metric-viewer](cascade/docs/imgs/metric-viewer.gif)
180-
181-
`HistoryViewer` allows to see model's lineage, what parameters resulted in what metrics
182-
183-
```python
184-
from cascade import meta as cme
185-
from cascade.repos import Repo
169+
Then locate your Cascade Workspace and run:
186170

187-
188-
repo = cdm.Repo("repo")
189-
190-
# This returns plotly figure
191-
cme.HistoryViewer(repo).plot()
192-
193-
# This runs a dash server and allows to see changes in real time (for example while models are trained)
194-
cme.HistoryViewer(repo).serve()
171+
```bash
172+
cascade ui
195173
```
196174

197-
See tutorial in [documentation](https://oxid15.github.io/cascade/en/latest/tutorials/tutorials.html)
175+
[Cascade UI](https://github.com/Laiserk/cascade_ui) is a separate project, that provides visual interface for Cascade experiments. For more detailed explanation you can visit [UI docs](https://oxid15.github.io/cascade/en/latest/tutorials/ui.html).
198176

199-
![history-viewer](cascade/docs/imgs/history-viewer.gif)
177+
![Cascade UI Model Page](cascade/docs/source/_static/model-page.png)
200178

201179
## Who could find Cascade useful
202180

cascade/base/traceable.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import warnings
2121
from dataclasses import asdict, dataclass
2222
from datetime import datetime
23+
from functools import wraps
2324
from getpass import getuser
2425
from typing import Any, Callable, Dict, Iterable, Optional, Union
2526

@@ -54,6 +55,18 @@ def __post_init__(self) -> None:
5455
self.uri = os.path.abspath(self.uri)
5556

5657

58+
def apply_prefix(get_meta):
59+
@wraps(get_meta)
60+
def wrapper(self, *args, **kwargs):
61+
meta = get_meta(self, *args, **kwargs)
62+
if hasattr(self, "_meta_prefix"):
63+
meta[0].update(self._meta_prefix)
64+
else:
65+
self._warn_no_prefix()
66+
return meta
67+
return wrapper
68+
69+
5770
class Traceable:
5871
"""
5972
Base class for everything that has metadata in Cascade
@@ -89,6 +102,7 @@ def __init__(
89102
self.comments = list()
90103
self.links = list()
91104

105+
@apply_prefix
92106
def get_meta(self) -> Meta:
93107
"""
94108
Returns
@@ -104,10 +118,6 @@ def get_meta(self) -> Meta:
104118
"""
105119
meta = {}
106120
meta["name"] = repr(self)
107-
if hasattr(self, "_meta_prefix"):
108-
meta.update(self._meta_prefix)
109-
else:
110-
self._warn_no_prefix()
111121

112122
if hasattr(self, "description"):
113123
meta["description"] = self.description
@@ -197,7 +207,7 @@ def from_meta(self, meta: Union[Meta, MetaBlock]) -> None:
197207
self.links = [Link(**link) for link in meta[0]["links"]]
198208
del meta[0]["links"]
199209

200-
self.update_meta(meta)
210+
self.update_meta(meta[0])
201211

202212
def __repr__(self) -> str:
203213
"""

cascade/cli/artifact.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def remove_repo_artifacts(path) -> List[List[List[RemoveResult]]]:
7979
repo = create_container("repo", path)
8080
repo_results = []
8181
for name in repo.get_line_names():
82-
results = remove_line_artifacts(os.path.join(path, name))
82+
line_meta = repo[name].get_meta()
83+
line_type = line_meta[0].get("type", "model_line")
84+
results = remove_line_artifacts(os.path.join(path, name), line_type)
8385
repo_results.append(results)
8486
return repo_results
8587

cascade/cli/cli.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818

1919
import click
2020

21-
from ..base import MetaHandler, MetaIOError
2221
from .artifact import artifact
2322
from .comment import comment
24-
from .common import create_container
23+
from .common import create_container, init_ctx
2524
from .desc import desc
2625
from .query import query
2726
from .run import run
2827
from .tag import tag
28+
from .ui import ui
2929
from .view import view
3030

3131

@@ -40,14 +40,7 @@ def cli(ctx):
4040
current_dir_full = os.getcwd()
4141
ctx.obj["cwd"] = current_dir_full
4242

43-
try:
44-
meta = MetaHandler.read_dir(current_dir_full)
45-
ctx.obj["meta"] = meta
46-
ctx.obj["type"] = meta[0].get("type")
47-
ctx.obj["len"] = meta[0].get("len")
48-
ctx.obj["meta_fmt"] = MetaHandler.determine_meta_fmt(current_dir_full, "meta.*")
49-
except MetaIOError as e:
50-
click.echo(e)
43+
init_ctx(ctx, current_dir_full)
5144

5245

5346
@cli.command
@@ -112,6 +105,7 @@ def migrate(ctx):
112105
cli.add_command(run)
113106
cli.add_command(query)
114107
cli.add_command(tag)
108+
cli.add_command(ui)
115109
cli.add_command(view)
116110

117111

cascade/cli/common.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616

1717
from typing import Any
1818

19+
import click
20+
21+
from ..base import MetaHandler, MetaIOError
22+
23+
24+
def init_ctx(ctx, path):
25+
try:
26+
meta = MetaHandler.read_dir(path)
27+
ctx.obj["meta"] = meta
28+
ctx.obj["type"] = meta[0].get("type")
29+
ctx.obj["len"] = meta[0].get("len")
30+
ctx.obj["meta_fmt"] = MetaHandler.determine_meta_fmt(path, "meta.*")
31+
except MetaIOError as e:
32+
click.echo(e)
33+
1934

2035
def create_container(type: str, cwd: str) -> Any:
2136
# "line" fallback here is for compatibility

cascade/cli/run.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
import click
2828
import pendulum
29-
3029
from cascade.base import MetaHandler
3130

3231

@@ -60,7 +59,7 @@ def find_config(tree: ast.Module) -> Optional[ast.ClassDef]:
6059
if isinstance(node, ast.ClassDef):
6160
if len(node.bases):
6261
for base in node.bases:
63-
if base.id == "Config":
62+
if getattr(base, "id", None) == "Config":
6463
if cfg_node is not None:
6564
more_than_one = True
6665
cfg_node = node
@@ -151,9 +150,12 @@ def modify_assignments(tree: ast.Module, cfg_node: ast.ClassDef, kwargs: Dict[st
151150

152151
def parse_args(args):
153152
kwargs = {}
154-
for key, val in zip(args[::2], args[1::2]):
155-
key = re.sub(r"^-{1,2}\b", "", key)
156-
kwargs[key] = ast.literal_eval(val)
153+
for orig_key, val in zip(args[::2], args[1::2]):
154+
key = re.sub(r"^-{1,2}\b", "", orig_key)
155+
try:
156+
kwargs[key] = ast.literal_eval(val)
157+
except Exception as e:
158+
raise RuntimeError(f"Failed to parse the following argument: {orig_key} {val} See traceback above.") from e
157159
return kwargs
158160

159161

cascade/cli/ui.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
Copyright 2022-2025 Ilia Moiseev
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import os
18+
19+
import click
20+
21+
from .common import init_ctx
22+
23+
24+
@click.command("ui")
25+
@click.option("--path", default=".")
26+
@click.option("--host", default="127.0.0.1")
27+
@click.option("--port", default=8000)
28+
@click.pass_context
29+
def ui(ctx, path, host, port):
30+
if not os.path.exists(os.path.join(path, "meta.json")):
31+
click.echo("This folder is not a Cascade Workspace, would you like to create one?")
32+
response = input("y/n: ")
33+
if response.lower() == "y":
34+
from cascade.workspaces import Workspace
35+
36+
Workspace(path)
37+
init_ctx(ctx, path)
38+
else:
39+
click.echo("Exiting...")
40+
return
41+
42+
if ctx.obj["meta"][0]["type"] != "workspace":
43+
click.echo("UI is only available inside workspaces")
44+
return
45+
46+
try:
47+
from cascade_ui.app import run
48+
except ImportError as e:
49+
if "No module named 'cascade_ui'" in e.msg:
50+
raise ImportError(
51+
"cascade_ui package is required to run UI. Install it with 'pip install cascade-ui'"
52+
) from e
53+
else:
54+
raise e
55+
else:
56+
run(path, host, port)

cascade/data/modifier.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ def get_meta(self) -> Meta:
2828
self_meta += self._dataset.get_meta()
2929
return self_meta
3030

31+
def update_meta(self, meta):
32+
if isinstance(meta, list):
33+
super().update_meta(meta[0])
34+
if len(meta) > 1:
35+
self._dataset.update_meta(meta[1:])
36+
else:
37+
super().update_meta(meta)
38+
3139
def from_meta(self, meta: Meta) -> None:
3240
"""
3341
Calls the same method as base class but does
188 KB
Loading
183 KB
Loading

0 commit comments

Comments
 (0)