Skip to content

Commit 3e0e721

Browse files
committed
round 2
1 parent a8af203 commit 3e0e721

File tree

6 files changed

+340
-62
lines changed

6 files changed

+340
-62
lines changed

__init__.py

+118-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""YouTube Player plugin.
1+
"""MLflow Experiment Tracking plugin.
22
33
| Copyright 2017-2023, Voxel51, Inc.
44
| `voxel51.com <https://voxel51.com/>`_
@@ -8,20 +8,113 @@
88
import fiftyone.operators as foo
99
import fiftyone.operators.types as types
1010

11-
from mlflow import MlflowClient
11+
import mlflow
12+
13+
14+
def _format_run_name(run_name):
15+
return run_name.replace("-", "_")
16+
17+
18+
def _initialize_fiftyone_run_for_mlflow_experiment(
19+
dataset, experiment_name, tracking_uri=None
20+
):
21+
"""
22+
Initialize a new FiftyOne custom run given an MLflow experiment.
23+
24+
Args:
25+
- dataset: The FiftyOne `Dataset` used for the experiment
26+
- experiment_name: The name of the MLflow experiment to create the run for
27+
"""
28+
experiment = mlflow.get_experiment_by_name(experiment_name)
29+
tracking_uri = tracking_uri or "http://localhost:8080"
30+
31+
config = dataset.init_run()
32+
33+
config.method = "mlflow_experiment"
34+
config.artifact_location = experiment.artifact_location
35+
config.created_at = experiment.creation_time
36+
config.experiment_name = experiment_name
37+
config.experiment_id = experiment.experiment_id
38+
config.tracking_uri = tracking_uri
39+
config.tags = experiment.tags
40+
config.runs = []
41+
dataset.register_run(experiment_name, config)
42+
43+
44+
def _fiftyone_experiment_run_exists(dataset, experiment_name):
45+
return experiment_name in dataset.list_runs()
46+
47+
48+
def _add_fiftyone_run_for_mlflow_run(dataset, experiment_name, run_id):
49+
"""
50+
Add an MLflow run to a FiftyOne custom run.
51+
52+
Args:
53+
- dataset: The FiftyOne `Dataset` used for the experiment
54+
- run_id: The MLflow run_id to add
55+
"""
56+
run = mlflow.get_run(run_id)
57+
run_name = run.data.tags["mlflow.runName"]
58+
59+
config = dataset.init_run()
60+
config.method = "mlflow_run"
61+
config.run_name = run_name
62+
config.run_id = run_id
63+
config.run_uuid = run.info.run_uuid
64+
config.experiment_id = run.info.experiment_id
65+
config.artifact_uri = run.info.artifact_uri
66+
config.metrics = run.data.metrics
67+
config.tags = run.data.tags
68+
69+
dataset.register_run(_format_run_name(run_name), config)
70+
71+
## add run to experiment
72+
experiment_run_info = dataset.get_run_info(experiment_name)
73+
experiment_run_info.config.runs.append(run_name)
74+
dataset.update_run_config(experiment_name, experiment_run_info.config)
75+
76+
77+
def log_mlflow_run_to_fiftyone_dataset(
78+
sample_collection, experiment_name, run_id=None
79+
):
80+
"""
81+
Log an MLflow run to a FiftyOne custom run.
82+
83+
Args:
84+
- sample_collection: The FiftyOne `Dataset` or `DatasetView` used for the experiment
85+
- experiment_name: The name of the MLflow experiment to create the run for
86+
- run_id: The MLflow run_id to add
87+
"""
88+
dataset = sample_collection._dataset
89+
90+
if not _fiftyone_experiment_run_exists(dataset, experiment_name):
91+
_initialize_fiftyone_run_for_mlflow_experiment(
92+
dataset, experiment_name
93+
)
94+
if run_id:
95+
_add_fiftyone_run_for_mlflow_run(dataset, experiment_name, run_id)
1296

1397

1498
def get_candidate_experiments(dataset):
99+
urls = []
15100
name = dataset.name
16-
client = MlflowClient(tracking_uri="http://127.0.0.1:8080")
17-
all_experiments = client.search_experiments()
18-
experiment_names_and_ids = []
19-
for exp in all_experiments:
20-
if "dataset" not in exp.tags:
21-
continue
22-
if exp.tags["dataset"] == name:
23-
experiment_names_and_ids.append((exp.experiment_id, exp.name))
24-
return experiment_names_and_ids
101+
mlflow_experiment_runs = [
102+
dataset.get_run_info(r)
103+
for r in dataset.list_runs()
104+
if dataset.get_run_info(r).config.method == "mlflow_experiment"
105+
]
106+
107+
for mer in mlflow_experiment_runs:
108+
cfg = mer.config
109+
name = cfg.experiment_name
110+
try:
111+
uri = cfg.tracking_uri
112+
except:
113+
uri = "http://localhost:8080"
114+
id = cfg.experiment_id
115+
urls.append({"url": f"{uri}/#/experiments/{id}", "name": name})
116+
117+
return {"urls": urls}
25118

26119

27120
class OpenMLFlowPanel(foo.Operator):
@@ -54,5 +147,19 @@ def execute(self, ctx):
54147
)
55148

56149

150+
class GetExperimentURLs(foo.Operator):
151+
@property
152+
def config(self):
153+
return foo.OperatorConfig(
154+
name="get_mlflow_experiment_urls",
155+
label="MLFlow: Get experiment URLs",
156+
unlisted=True,
157+
)
158+
159+
def execute(self, ctx):
160+
return get_candidate_experiments(ctx.dataset)
161+
162+
57163
def register(p):
58164
p.register(OpenMLFlowPanel)
165+
p.register(GetExperimentURLs)

dist/index.umd.js

+27-27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fiftyone.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
name: "@jacobmarks/mlflow"
1+
name: "@jacobmarks/mlflow_tracking"
22
description: Play YouTube videos in the FiftyOne App!
3-
version: 0.1.0
3+
version: 0.1.1
44
fiftyone:
55
version: ">=0.21.0"
66
license: Apache 2.0
77
url: "https://github.com/jacobmarks/fiftyone_mlflow_plugin"
88
operators:
99
- open_mlflow_panel
10+
- get_mlflow_experiment_urls

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@jacobmarks/mlflow",
2+
"name": "@jacobmarks/mlflow_tracking",
33
"version": "1.0.0",
44
"main": "src/MLFlow.tsx",
55
"license": "MIT",

src/MLFlow.tsx

+105-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { registerComponent, PluginComponentType } from "@fiftyone/plugins";
2-
import React, { useState, useEffect } from "react";
3-
import { useRecoilValue } from "recoil";
4-
import * as fos from "@fiftyone/state";
5-
import { Box, TextField, Button } from "@mui/material";
2+
import { useOperatorExecutor } from "@fiftyone/operators";
3+
import React, { useState, useEffect, useMemo } from "react";
4+
import {
5+
Stack,
6+
Box,
7+
TextField,
8+
Button,
9+
CircularProgress,
10+
Select,
11+
MenuItem,
12+
Typography,
13+
} from "@mui/material";
614

715
export const MLFlowIcon = ({ size = "1rem", style = {} }) => {
816
return (
@@ -13,7 +21,7 @@ export const MLFlowIcon = ({ size = "1rem", style = {} }) => {
1321
style={style}
1422
viewBox="0 0 600 500"
1523
>
16-
<defs id="defsdoc">
24+
<defs id="defs">
1725
<pattern
1826
id="patternBool"
1927
x="0"
@@ -96,38 +104,114 @@ const URLInputForm = ({ onSubmit }) => {
96104

97105
export default function MLFlowPanel() {
98106
const defaultUrl = "http://127.0.0.1:8080";
107+
108+
const getExperimentURLs = useOperatorExecutor(
109+
"@jacobmarks/mlflow_tracking/get_mlflow_experiment_urls"
110+
);
111+
112+
const [experimentURLValue, setExperimentURLValue] = useState("");
113+
114+
const handleExperimentURLChange = (event) => {
115+
console.log(event.target.value);
116+
setExperimentURLValue(event.target.value);
117+
};
118+
99119
const { serverAvailable, setServerAvailable, url, setUrl } =
100120
useServerAvailability(defaultUrl);
101121

102122
const handleUpdateUrl = (newUrl) => {
103123
setUrl(newUrl);
104124
};
105125

106-
const datasetName = useRecoilValue(fos.datasetName);
107-
console.log(datasetName);
108-
// Use this dataset name to get candidate experiment urls...
126+
useEffect(() => {
127+
getExperimentURLs.execute();
128+
}, []);
129+
130+
const experimentURLs = useMemo(() => {
131+
return getExperimentURLs?.result?.urls;
132+
}, [getExperimentURLs]);
133+
134+
const loadingExperimentURLs = useMemo(() => {
135+
return getExperimentURLs?.isExecuting;
136+
}, [getExperimentURLs]);
137+
138+
if (loadingExperimentURLs || Array.isArray(experimentURLs) === false) {
139+
return (
140+
<Box
141+
sx={{
142+
width: "100%",
143+
height: "100%",
144+
alignItems: "center",
145+
display: "flex",
146+
justifyContent: "center",
147+
}}
148+
>
149+
<CircularProgress />
150+
</Box>
151+
);
152+
}
109153

110154
return (
111-
<Box
155+
<Stack
112156
sx={{
113157
width: "100%",
114158
height: "100%",
115-
overflow: "hidden",
116-
display: "flex",
117-
flexDirection: "column",
159+
alignItems: "center",
160+
justifyContent: "center",
118161
}}
162+
spacing={1}
119163
>
120164
{!serverAvailable && <URLInputForm onSubmit={handleUpdateUrl} />}
121-
<iframe
122-
style={{
123-
flexGrow: 1,
124-
border: "none",
165+
{experimentURLs.length == 0 && <Box>No experiments found</Box>}
166+
{experimentURLs.length > 1 && (
167+
<Stack
168+
direction="row"
169+
spacing={2}
170+
alignItems="center"
171+
justifyContent="center"
172+
>
173+
<Typography variant="h6" gutterBottom>
174+
Select an experiment on this dataset:
175+
</Typography>
176+
<Select
177+
labelId="experiment-select-label"
178+
id="experiment-select"
179+
value={experimentURLValue}
180+
onChange={handleExperimentURLChange}
181+
size="small"
182+
sx={{ minWidth: 300 }}
183+
>
184+
{experimentURLs.map((item) => (
185+
<MenuItem key={item.url} value={item.url}>
186+
{item.name}
187+
</MenuItem>
188+
))}
189+
</Select>
190+
</Stack>
191+
)}
192+
<Box
193+
sx={{
194+
width: "90%",
195+
height: "80%",
196+
overflow: "auto",
197+
display: "flex",
198+
flexDirection: "column",
199+
justifyContent: "center",
125200
}}
126-
src={url}
127-
title="MLFlow Embedded"
128-
allowFullScreen
129-
></iframe>
130-
</Box>
201+
>
202+
<iframe
203+
style={{
204+
flexGrow: 1,
205+
border: "none",
206+
width: "100%",
207+
height: "100%",
208+
}}
209+
src={experimentURLValue || defaultUrl}
210+
title="MLFlow Embedded"
211+
allowFullScreen
212+
></iframe>
213+
</Box>
214+
</Stack>
131215
);
132216
}
133217

0 commit comments

Comments
 (0)