Files
mlflow--mlflow/tests/data/test_huggingface_dataset_and_source.py
2026-07-13 13:22:34 +08:00

293 lines
11 KiB
Python

import json
import os
import datasets
import pandas as pd
import pytest
from huggingface_hub.errors import HfHubHTTPError
import mlflow.data
import mlflow.data.huggingface_dataset
from mlflow.data.code_dataset_source import CodeDatasetSource
from mlflow.data.dataset_source_registry import get_dataset_source_from_json
from mlflow.data.evaluation_dataset import EvaluationDataset
from mlflow.data.huggingface_dataset import HuggingFaceDataset
from mlflow.data.huggingface_dataset_source import HuggingFaceDatasetSource
from mlflow.exceptions import MlflowException
from mlflow.types.schema import Schema
from mlflow.types.utils import _infer_schema
from tests.helper_functions import skip_if_hf_hub_unhealthy
from tests.resources.data.dataset_source import SampleDatasetSource
pytestmark = skip_if_hf_hub_unhealthy()
@pytest.fixture(scope="module", autouse=True)
def prefetch_huggingface_datasets():
"""Pre-warm the HF cache so individual tests don't hit the Hub and risk HTTP 429s."""
try:
datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
datasets.load_dataset(
"cornell-movie-review-data/rotten_tomatoes",
split="train",
revision="aa13bc287fa6fcab6daf52f0dfb9994269ffea28",
trust_remote_code=True,
)
datasets.load_dataset(
"cornell-movie-review-data/rotten_tomatoes",
split="train",
revision="c33cbf965006dba64f134f7bef69c53d5d0d285d",
)
datasets.load_dataset(
"fka/awesome-chatgpt-prompts",
data_files={"train": "prompts.csv"},
split="train",
)
except HfHubHTTPError as e:
if e.response is not None and e.response.status_code == 429:
pytest.skip(f"HF Hub returned 429 while pre-warming the cache: {e}")
raise
def test_from_huggingface_dataset_constructs_expected_dataset():
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
mlflow_ds = mlflow.data.from_huggingface(ds, path="cornell-movie-review-data/rotten_tomatoes")
assert isinstance(mlflow_ds, HuggingFaceDataset)
assert mlflow_ds.ds == ds
assert mlflow_ds.schema == _infer_schema(ds.to_pandas())
assert mlflow_ds.profile == {
"num_rows": ds.num_rows,
"dataset_size": ds.dataset_size,
"size_in_bytes": ds.size_in_bytes,
}
assert isinstance(mlflow_ds.source, HuggingFaceDatasetSource)
with pytest.raises(KeyError, match="Found duplicated arguments*"):
# Test that we raise an error if the same key is specified in both
# `HuggingFaceDatasetSource` and `kwargs`.
mlflow_ds.source.load(path="dummy_path")
reloaded_ds = mlflow_ds.source.load()
assert reloaded_ds.builder_name == ds.builder_name
assert reloaded_ds.config_name == ds.config_name
assert reloaded_ds.split == ds.split == "train"
assert reloaded_ds.num_rows == ds.num_rows
reloaded_mlflow_ds = mlflow.data.from_huggingface(
reloaded_ds, path="cornell-movie-review-data/rotten_tomatoes"
)
assert reloaded_mlflow_ds.digest == mlflow_ds.digest
def test_from_huggingface_dataset_constructs_expected_dataset_with_revision():
# Load this revision:
# https://huggingface.co/datasets/cornell-movie-review-data/rotten_tomatoes/commit/aa13bc287fa6fcab6daf52f0dfb9994269ffea28
revision = "aa13bc287fa6fcab6daf52f0dfb9994269ffea28"
ds = datasets.load_dataset(
"cornell-movie-review-data/rotten_tomatoes",
split="train",
revision=revision,
trust_remote_code=True,
)
mlflow_ds_new = mlflow.data.from_huggingface(
ds,
path="cornell-movie-review-data/rotten_tomatoes",
revision=revision,
trust_remote_code=True,
)
mlflow_ds_new.source.load()
assert mlflow_ds_new.source.revision == revision
def test_from_huggingface_dataset_constructs_expected_dataset_with_data_files():
data_files = {"train": "prompts.csv"}
ds = datasets.load_dataset("fka/awesome-chatgpt-prompts", data_files=data_files, split="train")
mlflow_ds = mlflow.data.from_huggingface(
ds, path="fka/awesome-chatgpt-prompts", data_files=data_files
)
assert isinstance(mlflow_ds, HuggingFaceDataset)
assert mlflow_ds.ds == ds
assert mlflow_ds.schema == _infer_schema(ds.to_pandas())
assert mlflow_ds.profile == {
"num_rows": ds.num_rows,
"dataset_size": ds.dataset_size,
"size_in_bytes": ds.size_in_bytes,
}
assert isinstance(mlflow_ds.source, HuggingFaceDatasetSource)
reloaded_ds = mlflow_ds.source.load()
assert reloaded_ds.builder_name == ds.builder_name
assert reloaded_ds.config_name == ds.config_name
assert reloaded_ds.split == ds.split == "train"
assert reloaded_ds.num_rows == ds.num_rows
reloaded_mlflow_ds = mlflow.data.from_huggingface(
reloaded_ds, path="fka/awesome-chatgpt-prompts", data_files=data_files
)
assert reloaded_mlflow_ds.digest == mlflow_ds.digest
def test_from_huggingface_dataset_constructs_expected_dataset_with_data_dir(tmp_path):
df = pd.DataFrame.from_dict({"a": [1, 2, 3], "b": [4, 5, 6]})
data_dir = "data"
os.makedirs(tmp_path / data_dir)
df.to_csv(tmp_path / data_dir / "my_data.csv")
ds = datasets.load_dataset(str(tmp_path), data_dir=data_dir, name="default", split="train")
mlflow_ds = mlflow.data.from_huggingface(ds, path=str(tmp_path), data_dir=data_dir)
assert mlflow_ds.ds == ds
assert mlflow_ds.schema == _infer_schema(ds.to_pandas())
assert mlflow_ds.profile == {
"num_rows": ds.num_rows,
"dataset_size": ds.dataset_size,
"size_in_bytes": ds.size_in_bytes,
}
assert isinstance(mlflow_ds.source, HuggingFaceDatasetSource)
reloaded_ds = mlflow_ds.source.load()
assert reloaded_ds.builder_name == ds.builder_name
assert reloaded_ds.config_name == ds.config_name
assert reloaded_ds.split == ds.split == "train"
assert reloaded_ds.num_rows == ds.num_rows
reloaded_mlflow_ds = mlflow.data.from_huggingface(
reloaded_ds, path=str(tmp_path), data_dir=data_dir
)
assert reloaded_mlflow_ds.digest == mlflow_ds.digest
def test_from_huggingface_dataset_respects_user_specified_name_and_digest():
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
mlflow_ds = mlflow.data.from_huggingface(
ds, path="cornell-movie-review-data/rotten_tomatoes", name="myname", digest="mydigest"
)
assert mlflow_ds.name == "myname"
assert mlflow_ds.digest == "mydigest"
def test_from_huggingface_dataset_digest_is_consistent_for_large_ordered_datasets(tmp_path):
assert (
mlflow.data.huggingface_dataset._MAX_ROWS_FOR_DIGEST_COMPUTATION_AND_SCHEMA_INFERENCE
< 200000
)
df = pd.DataFrame.from_dict({
"a": list(range(200000)),
"b": list(range(200000)),
})
data_dir = "data"
os.makedirs(tmp_path / data_dir)
df.to_csv(tmp_path / data_dir / "my_data.csv")
ds = datasets.load_dataset(str(tmp_path), data_dir=data_dir, name="default", split="train")
mlflow_ds = mlflow.data.from_huggingface(ds, path=str(tmp_path), data_dir=data_dir)
assert mlflow_ds.digest == "1dda4ce8"
def test_from_huggingface_dataset_throws_for_dataset_dict():
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes")
assert isinstance(ds, datasets.DatasetDict)
with pytest.raises(
MlflowException, match="must be an instance of `datasets.Dataset`.*DatasetDict"
):
mlflow.data.from_huggingface(ds, path="cornell-movie-review-data/rotten_tomatoes")
def test_from_huggingface_dataset_no_source_specified():
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
mlflow_ds = mlflow.data.from_huggingface(ds)
assert isinstance(mlflow_ds, HuggingFaceDataset)
assert isinstance(mlflow_ds.source, CodeDatasetSource)
assert "mlflow.source.name" in mlflow_ds.source.to_json()
def test_dataset_conversion_to_json():
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
mlflow_ds = mlflow.data.from_huggingface(ds, path="cornell-movie-review-data/rotten_tomatoes")
dataset_json = mlflow_ds.to_json()
parsed_json = json.loads(dataset_json)
assert parsed_json.keys() <= {"name", "digest", "source", "source_type", "schema", "profile"}
assert parsed_json["name"] == mlflow_ds.name
assert parsed_json["digest"] == mlflow_ds.digest
assert parsed_json["source"] == mlflow_ds.source.to_json()
assert parsed_json["source_type"] == mlflow_ds.source._get_source_type()
assert parsed_json["profile"] == json.dumps(mlflow_ds.profile)
schema_json = json.dumps(json.loads(parsed_json["schema"])["mlflow_colspec"])
assert Schema.from_json(schema_json) == mlflow_ds.schema
def test_dataset_source_conversion_to_json():
ds = datasets.load_dataset(
"cornell-movie-review-data/rotten_tomatoes",
split="train",
revision="c33cbf965006dba64f134f7bef69c53d5d0d285d",
)
mlflow_ds = mlflow.data.from_huggingface(
ds,
path="cornell-movie-review-data/rotten_tomatoes",
revision="c33cbf965006dba64f134f7bef69c53d5d0d285d",
)
source = mlflow_ds.source
source_json = source.to_json()
parsed_source = json.loads(source_json)
assert parsed_source["revision"] == "c33cbf965006dba64f134f7bef69c53d5d0d285d"
assert parsed_source["split"] == "train"
assert parsed_source["config_name"] == "default"
assert parsed_source["path"] == "cornell-movie-review-data/rotten_tomatoes"
assert not parsed_source["data_dir"]
assert not parsed_source["data_files"]
reloaded_source = HuggingFaceDatasetSource.from_json(source_json)
assert json.loads(reloaded_source.to_json()) == parsed_source
reloaded_source = get_dataset_source_from_json(
source_json, source_type=source._get_source_type()
)
assert isinstance(reloaded_source, HuggingFaceDatasetSource)
assert type(source) == type(reloaded_source)
assert reloaded_source.to_json() == source.to_json()
def test_to_evaluation_dataset():
import numpy as np
ds = datasets.load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
dataset = mlflow.data.from_huggingface(
ds, path="cornell-movie-review-data/rotten_tomatoes", targets="label"
)
evaluation_dataset = dataset.to_evaluation_dataset()
assert isinstance(evaluation_dataset, EvaluationDataset)
assert evaluation_dataset.features_data.equals(dataset.ds.to_pandas().drop("label", axis=1))
assert np.array_equal(evaluation_dataset.labels_data, dataset.ds.to_pandas()["label"].values)
def test_from_huggingface_dataset_with_sample_source():
source_uri = "test:/my/test/uri"
source = SampleDatasetSource._resolve(source_uri)
data = {"text": ["This is a sample text.", "Another sample text."], "label": [0, 1]}
dataset = datasets.Dataset.from_dict(data)
train_dataset = mlflow.data.from_huggingface(
dataset,
name="sample-text-dataset",
source=source,
)
assert isinstance(train_dataset, HuggingFaceDataset)
assert train_dataset.source == source