Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/modelplane/runways/annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
is_debug_mode,
setup_annotator_credentials,
)
from modelplane.utils.input import build_input
from modelplane.utils.input import build_and_log_input

KNOWN_ENSEMBLES: Dict[str, AnnotatorSet] = {}
# try to load the private ensemble
Expand Down Expand Up @@ -95,14 +95,14 @@ def annotate(

with tempfile.TemporaryDirectory() as tmp:
# load/transform the prompt responses from the specified run
input_data = build_input(
input_data = build_and_log_input(
current_run_id=run.info.run_id,
path=response_file,
run_id=response_run_id,
artifact_path=PROMPT_RESPONSE_ARTIFACT_NAME,
dvc_repo=dvc_repo,
dest_dir=tmp,
)
input_data.log_input()
input_path = input_data.local_path() # type: ignore
pipeline_kwargs["input_path"] = pathlib.Path(input_path)
pipeline_kwargs["output_dir"] = pathlib.Path(tmp)
Expand Down
10 changes: 7 additions & 3 deletions src/modelplane/runways/responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
is_debug_mode,
setup_sut_credentials,
)
from modelplane.utils.input import build_input
from modelplane.utils.input import build_and_log_input


def respond(
Expand All @@ -41,8 +41,12 @@ def respond(
mlflow.log_params(params)
# Use temporary file as mlflow will log this into the artifact store
with tempfile.TemporaryDirectory() as tmp:
input_data = build_input(path=prompts, dvc_repo=dvc_repo, dest_dir=tmp)
input_data.log_input()
input_data = build_and_log_input(
current_run_id=run.info.run_id,
path=prompts,
dvc_repo=dvc_repo,
dest_dir=tmp,
)
pipeline_runner = build_runner(
num_workers=num_workers,
input_path=input_data.local_path(),
Expand Down
15 changes: 8 additions & 7 deletions src/modelplane/runways/scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import json
import math
import os
import tempfile
from pathlib import Path

Expand All @@ -19,7 +18,7 @@
RUN_TYPE_TAG_NAME,
get_experiment_id,
)
from modelplane.utils.input import build_input
from modelplane.utils.input import build_and_log_input


def score(
Expand Down Expand Up @@ -50,19 +49,21 @@ def score(

with tempfile.TemporaryDirectory() as tmp:
# Load annotations
annotation_input = build_input(
annotation_input = build_and_log_input(
current_run_id=run.info.run_id,
run_id=annotation_run_id,
artifact_path=ANNOTATION_RESPONSE_ARTIFACT_NAME,
dest_dir=tmp,
)
annotation_input.log_input()
annotation_data = AnnotationData(annotation_input.local_path(), is_json_annotation=True, sample_uid_col=sample_uid_col, annotator_uid_col=annotator_uid_col, annotation_col=annotation_col)

# Load ground truth
ground_truth_input = build_input(
path=ground_truth, dvc_repo=dvc_repo, dest_dir=tmp
ground_truth_input = build_and_log_input(
current_run_id=run.info.run_id,
path=ground_truth,
dvc_repo=dvc_repo,
dest_dir=tmp,
)
ground_truth_input.log_input()
ground_truth_data = AnnotationData(ground_truth_input.local_path(), is_json_annotation=False, annotation_col="is_safe", annotator_uid_col=None, sample_uid_col=sample_uid_col)
mlflow.log_metric("num_ground_truth_samples", len(ground_truth_data.df))

Expand Down
104 changes: 50 additions & 54 deletions src/modelplane/utils/input.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import os
import shutil
from abc import ABC, abstractmethod
Expand All @@ -7,17 +6,16 @@

import dvc.api
import mlflow

from modelplane.mlflow.datasets import LocalDatasetSource
import mlflow.artifacts
import pandas as pd


class BaseInput(ABC):
"""Base class for input datasets."""

@abstractmethod
def log_input(self):
"""Log the dataset to MLflow as input. This method should only be called inside an active MLflow run."""
pass
def log_artifact(self, current_run_id: str):
"""Log the dataset to MLflow as an artifact for the given `current_run_id`."""
mlflow.log_artifact(str(self.local_path()), run_id=current_run_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only log the local path? Can we also log the URL if it's a DVC input? Or the input is an artifact of a previous run?

@superdosh superdosh Aug 27, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bkorycki str(self.local_path()) is to provide it with the location of the file. The whole file is "logged" to mlflow, so it's visible/browse-able in that interface.

One thing we're losing is the details of the origin of the file, I could keep that as tags on the run maybe? Like an input tag with a nice string that represents the input?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it. Yes, I think it would be nice to keep track of the provenance of these data files. Maybe log_input makes most sense for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mlflow.log_input is just kind of annoying to deal with. (We had to have all that weird metadata stuff.) I feel like we could be much more human-friendly with tags?

I think we could log all the input params that we use to load the file. That might be simplest. Let me mock it up in a commit!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated; results in something like this:
image
Have a look @bkorycki

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks:)


@abstractmethod
def local_path(self) -> Path:
Expand All @@ -30,17 +28,35 @@ class LocalInput(BaseInput):
def __init__(self, path: str):
self.path = path

def log_input(self):
mlf_dataset = mlflow.data.meta_dataset.MetaDataset(
source=LocalDatasetSource(uri=self.path),
name=self.path,
)
mlflow.log_input(mlf_dataset)

def local_path(self) -> Path:
return Path(self.path)


class DataframeInput(BaseInput):
"""A dataset that is represented as a Pandas DataFrame."""

_INPUT_FILE_NAME = "input.csv"

def __init__(self, df: pd.DataFrame, dest_dir: str):
self._local_path = Path(dest_dir) / self._INPUT_FILE_NAME
self.df = df

@property
def df(self) -> pd.DataFrame:
return self._df

@df.setter
def df(self, df: pd.DataFrame):
self._df = df
self._update_local_file()

def _update_local_file(self):
self.df.to_csv(self._local_path, index=False)

def local_path(self) -> Path:
return self._local_path


class DVCInput(BaseInput):
"""A dataset from a DVC remote."""

Expand All @@ -50,8 +66,6 @@ def __init__(self, path: str, repo: str, dest_dir: str):
repo, self.rev = repo_path
else:
self.rev = "main"
self.path = path
self.url = dvc.api.get_url(path, repo=repo, rev=self.rev) # For logging.
self._local_path = self._download_dvc_file(path, repo, dest_dir)

def _download_dvc_file(self, path: str, repo: str, dest_dir: str) -> str:
Expand All @@ -64,22 +78,6 @@ def _download_dvc_file(self, path: str, repo: str, dest_dir: str) -> str:

return local_path

def digest(self) -> str:
"""Return the md5 hash of the dvc file."""
# TODO: Check if this works with other storage options (besides google cloud)
segments = self.url.split("/")
i = segments.index("md5")
digest = "".join(segments[i + 1 :])
return digest

def log_input(self):
dataset = mlflow.data.meta_dataset.MetaDataset(
source=mlflow.data.http_dataset_source.HTTPDatasetSource(self.url),
name=self.path,
digest=self.digest(),
)
mlflow.log_input(dataset)

def local_path(self) -> Path:
return Path(self._local_path)

Expand All @@ -101,48 +99,46 @@ def _download_artifacts(
)
return os.path.join(dest_dir, artifact_path)

def log_input(self):
run = mlflow.get_run(self.run_id)
for input in run.inputs.dataset_inputs:
ds = input.dataset
source_dict = json.loads(ds.source)
if ds.source_type == "http":
source = mlflow.data.http_dataset_source.HTTPDatasetSource(
source_dict["url"]
)
else:
source = LocalDatasetSource.from_dict(source_dict)
dataset = mlflow.data.dataset.Dataset(
source=source, name=ds.name, digest=ds.digest
)
mlflow.log_input(dataset)

def local_path(self) -> Path:
return Path(self._local_path)


def build_input(
def build_and_log_input(
current_run_id: str,
input_obj: Optional[BaseInput] = None,
path: Optional[str] = None,
run_id: Optional[str] = None,
artifact_path: Optional[str] = None,
dvc_repo: Optional[str] = None,
dest_dir: Optional[str] = None,
dest_dir: str = "",
df: Optional[pd.DataFrame] = None,
) -> BaseInput:
if dvc_repo is not None:
# Direct input
if input_obj is not None:
inp = input_obj
# DF case
elif df is not None:
inp = DataframeInput(df, dest_dir=dest_dir)
# DVC case
elif dvc_repo is not None:
if path is None:
raise ValueError("Path must be provided when dvc_repo is provided.")
if run_id is not None:
raise ValueError(
"Cannot provide both run_id and dvc_repo to build an input."
)
return DVCInput(path=path, repo=dvc_repo, dest_dir=dest_dir)
inp = DVCInput(path=path, repo=dvc_repo, dest_dir=dest_dir)
# Local case
elif path is not None:
if run_id is not None:
raise ValueError("Cannot provide both path and run_id.")
return LocalInput(path)
inp = LocalInput(path)
# MLFlow artifact case
elif run_id is not None:
if artifact_path is None:
raise ValueError("Artifact path must be provided when run_id is provided.")
return MLFlowArtifactInput(run_id, artifact_path, dest_dir)
inp = MLFlowArtifactInput(run_id, artifact_path, dest_dir)
else:
raise ValueError("Either path or run_id must be provided to build an input.")
inp.log_artifact(current_run_id=current_run_id)
return inp
Loading