chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,723 @@
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
pytest.importorskip("diffusers")
|
||||
pytest.importorskip("safetensors")
|
||||
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from safetensors.numpy import save_file
|
||||
|
||||
import mlflow
|
||||
import mlflow.diffusers
|
||||
from mlflow.diffusers import FLAVOR_NAME, DiffusersAdapterModel
|
||||
from mlflow.exceptions import MlflowException
|
||||
from mlflow.models.model import MLMODEL_FILE_NAME
|
||||
|
||||
BASE_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
|
||||
|
||||
def _create_fake_adapter(tmp_path, filename="adapter.safetensors"):
|
||||
adapter_dir = tmp_path / "fake_adapter"
|
||||
adapter_dir.mkdir(exist_ok=True)
|
||||
tensors = {"lora_weight": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / filename))
|
||||
return adapter_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_dir(tmp_path):
|
||||
return _create_fake_adapter(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_file(tmp_path):
|
||||
adapter_dir = _create_fake_adapter(tmp_path)
|
||||
return adapter_dir / "adapter.safetensors"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_path(tmp_path):
|
||||
return tmp_path / "model_output"
|
||||
|
||||
|
||||
def test_save_model_from_directory(adapter_dir, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
mlmodel_path = model_path / MLMODEL_FILE_NAME
|
||||
assert mlmodel_path.exists()
|
||||
|
||||
with open(mlmodel_path) as f:
|
||||
mlmodel = yaml.safe_load(f)
|
||||
|
||||
assert FLAVOR_NAME in mlmodel["flavors"]
|
||||
assert "python_function" in mlmodel["flavors"]
|
||||
|
||||
flavor_conf = mlmodel["flavors"][FLAVOR_NAME]
|
||||
assert flavor_conf["base_model"] == BASE_MODEL_ID
|
||||
assert flavor_conf["adapter_type"] == "lora"
|
||||
assert flavor_conf["adapter_weights"] == "adapter_weights"
|
||||
|
||||
|
||||
def test_save_model_normalizes_single_file_input(adapter_file, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_file),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
weights_dir = model_path / "adapter_weights"
|
||||
assert weights_dir.exists()
|
||||
assert (weights_dir / "pytorch_lora_weights.safetensors").exists()
|
||||
|
||||
|
||||
def test_save_model_normalizes_single_file_directory(adapter_dir, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
weights_dir = model_path / "adapter_weights"
|
||||
assert weights_dir.exists()
|
||||
assert (weights_dir / "pytorch_lora_weights.safetensors").exists()
|
||||
|
||||
|
||||
def test_save_model_writes_environment_files(adapter_dir, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
assert (model_path / "conda.yaml").exists()
|
||||
assert (model_path / "requirements.txt").exists()
|
||||
assert (model_path / "python_env.yaml").exists()
|
||||
|
||||
|
||||
def test_save_model_default_signature(adapter_dir, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
with open(model_path / MLMODEL_FILE_NAME) as f:
|
||||
mlmodel = yaml.safe_load(f)
|
||||
|
||||
assert "signature" in mlmodel
|
||||
sig = mlmodel["signature"]
|
||||
inputs = json.loads(sig["inputs"])
|
||||
assert inputs[0]["name"] == "prompt"
|
||||
assert inputs[0]["type"] == "string"
|
||||
|
||||
|
||||
def test_save_model_custom_signature(adapter_dir, model_path):
|
||||
from mlflow.types import DataType, Schema
|
||||
from mlflow.types.schema import ColSpec
|
||||
|
||||
custom_sig = mlflow.models.ModelSignature(
|
||||
inputs=Schema([ColSpec(type=DataType.string, name="text")]),
|
||||
outputs=Schema([ColSpec(type=DataType.binary, name="img")]),
|
||||
)
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
signature=custom_sig,
|
||||
)
|
||||
|
||||
with open(model_path / MLMODEL_FILE_NAME) as f:
|
||||
mlmodel = yaml.safe_load(f)
|
||||
|
||||
inputs = json.loads(mlmodel["signature"]["inputs"])
|
||||
assert inputs[0]["name"] == "text"
|
||||
|
||||
|
||||
def test_save_model_invalid_adapter_type(adapter_dir, model_path):
|
||||
with pytest.raises(mlflow.exceptions.MlflowException, match="Unsupported adapter type"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
adapter_type="invalid",
|
||||
)
|
||||
|
||||
|
||||
def test_save_model_nonexistent_path(model_path):
|
||||
with pytest.raises(mlflow.exceptions.MlflowException, match="does not exist"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path="/nonexistent/path",
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
|
||||
def test_save_model_with_metadata(adapter_dir, model_path):
|
||||
test_metadata = {"training_dataset": "my-dataset", "lora_rank": 16}
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
metadata=test_metadata,
|
||||
)
|
||||
|
||||
with open(model_path / MLMODEL_FILE_NAME) as f:
|
||||
mlmodel = yaml.safe_load(f)
|
||||
|
||||
assert mlmodel["metadata"] == test_metadata
|
||||
|
||||
|
||||
def test_log_model(adapter_dir):
|
||||
with mlflow.start_run() as run:
|
||||
model_info = mlflow.diffusers.log_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
base_model=BASE_MODEL_ID,
|
||||
name="test_adapter",
|
||||
)
|
||||
|
||||
assert model_info is not None
|
||||
|
||||
client = mlflow.MlflowClient()
|
||||
artifacts = [a.path for a in client.list_artifacts(run.info.run_id, "test_adapter")]
|
||||
assert any("adapter_weights" in a for a in artifacts)
|
||||
assert any(MLMODEL_FILE_NAME in a for a in artifacts)
|
||||
|
||||
|
||||
def test_save_load_model_direct_roundtrip(adapter_dir, model_path):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
|
||||
assert isinstance(loaded, DiffusersAdapterModel)
|
||||
assert loaded.base_model == BASE_MODEL_ID
|
||||
assert loaded.adapter_type == "lora"
|
||||
assert Path(loaded.adapter_path).exists()
|
||||
|
||||
|
||||
def test_base_model_revision_roundtrip(adapter_dir, model_path):
|
||||
|
||||
fake_revision = "abc123def456"
|
||||
with patch("mlflow.diffusers._resolve_base_model_revision", return_value=fake_revision):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
# Verify revision stored in flavor config
|
||||
mlmodel_path = model_path / MLMODEL_FILE_NAME
|
||||
with open(mlmodel_path) as f:
|
||||
mlmodel = yaml.safe_load(f)
|
||||
assert mlmodel["flavors"][FLAVOR_NAME]["base_model_revision"] == fake_revision
|
||||
|
||||
# Verify revision survives load_model roundtrip
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
assert loaded.base_model_revision == fake_revision
|
||||
|
||||
|
||||
def test_load_model_via_tracking_roundtrip(adapter_dir):
|
||||
with mlflow.start_run() as run:
|
||||
mlflow.diffusers.log_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
base_model=BASE_MODEL_ID,
|
||||
name="test_adapter",
|
||||
)
|
||||
|
||||
model_uri = f"runs:/{run.info.run_id}/test_adapter"
|
||||
loaded = mlflow.diffusers.load_model(model_uri)
|
||||
|
||||
assert isinstance(loaded, DiffusersAdapterModel)
|
||||
assert loaded.base_model == BASE_MODEL_ID
|
||||
assert loaded.adapter_type == "lora"
|
||||
assert Path(loaded.adapter_path).exists()
|
||||
assert (Path(loaded.adapter_path) / "pytorch_lora_weights.safetensors").exists()
|
||||
|
||||
|
||||
def test_load_pyfunc_returns_wrapper(adapter_dir):
|
||||
from mlflow.diffusers.wrapper import _DiffusersAdapterWrapper
|
||||
|
||||
with mlflow.start_run() as run:
|
||||
mlflow.diffusers.log_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
base_model=BASE_MODEL_ID,
|
||||
name="test_adapter",
|
||||
)
|
||||
|
||||
model_uri = f"runs:/{run.info.run_id}/test_adapter"
|
||||
loaded_pyfunc = mlflow.pyfunc.load_model(model_uri)
|
||||
|
||||
assert hasattr(loaded_pyfunc, "predict")
|
||||
wrapper = loaded_pyfunc._model_impl
|
||||
assert isinstance(wrapper, _DiffusersAdapterWrapper)
|
||||
assert wrapper._flavor_conf["base_model"] == BASE_MODEL_ID
|
||||
|
||||
|
||||
def test_load_pipeline_base_model_override(adapter_dir, model_path):
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
override = "other-org/other-model"
|
||||
|
||||
with patch("diffusers.DiffusionPipeline.from_pretrained") as mock_fp:
|
||||
mock_pipe = MagicMock()
|
||||
mock_fp.return_value = mock_pipe
|
||||
loaded.load_pipeline(base_model=override)
|
||||
mock_fp.assert_called_once()
|
||||
assert mock_fp.call_args[0][0] == override
|
||||
|
||||
|
||||
def test_load_pipeline_wraps_oserror(adapter_dir, model_path):
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
|
||||
with patch(
|
||||
"diffusers.DiffusionPipeline.from_pretrained",
|
||||
side_effect=OSError("is not a local folder"),
|
||||
):
|
||||
with pytest.raises(MlflowException, match="Failed to load base model"):
|
||||
loaded.load_pipeline()
|
||||
|
||||
|
||||
def test_wrapper_model_config_base_model_override():
|
||||
from mlflow.diffusers.wrapper import _DiffusersAdapterWrapper
|
||||
|
||||
w = _DiffusersAdapterWrapper(
|
||||
adapter_path="/fake",
|
||||
flavor_conf={"base_model": "original/model", "adapter_type": "lora"},
|
||||
model_config={"base_model": "override/model"},
|
||||
)
|
||||
|
||||
with patch("diffusers.DiffusionPipeline.from_pretrained") as mock_fp:
|
||||
mock_pipe = MagicMock()
|
||||
mock_fp.return_value = mock_pipe
|
||||
w._load_pipeline()
|
||||
mock_fp.assert_called_once()
|
||||
assert mock_fp.call_args[0][0] == "override/model"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package", ["diffusers", "transformers", "torch", "peft", "safetensors"])
|
||||
def test_default_pip_requirements_contains_core(package):
|
||||
reqs = mlflow.diffusers.get_default_pip_requirements()
|
||||
req_names = [r.split("==")[0] for r in reqs]
|
||||
assert package in req_names
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package", ["accelerate"])
|
||||
def test_default_pip_requirements_contains_optional_if_installed(package):
|
||||
reqs = mlflow.diffusers.get_default_pip_requirements()
|
||||
req_names = [r.split("==")[0] for r in reqs]
|
||||
if importlib.util.find_spec(package):
|
||||
assert package in req_names
|
||||
else:
|
||||
assert package not in req_names
|
||||
|
||||
|
||||
def test_default_conda_env():
|
||||
env = mlflow.diffusers.get_default_conda_env()
|
||||
assert "dependencies" in env
|
||||
|
||||
|
||||
# -- predict() tests (mock-based, no GPU) --
|
||||
|
||||
|
||||
class _FakeImage:
|
||||
def save(self, buf, format=None):
|
||||
buf.write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
|
||||
|
||||
class _FakePipelineOutput:
|
||||
def __init__(self, n=1):
|
||||
self.images = [_FakeImage() for _ in range(n)]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wrapper():
|
||||
from mlflow.diffusers.wrapper import _DiffusersAdapterWrapper
|
||||
|
||||
w = _DiffusersAdapterWrapper(
|
||||
adapter_path="/fake",
|
||||
flavor_conf={"base_model": "test/model", "adapter_type": "lora"},
|
||||
)
|
||||
# Inject a mock pipeline so _load_pipeline is never called
|
||||
w._pipeline = lambda prompt, **kwargs: _FakePipelineOutput(len(prompt))
|
||||
return w
|
||||
|
||||
|
||||
def test_get_raw_model(wrapper):
|
||||
model = wrapper.get_raw_model()
|
||||
assert model is not None
|
||||
assert callable(model)
|
||||
|
||||
|
||||
def test_predict_dataframe(wrapper):
|
||||
import pandas as pd
|
||||
|
||||
result = wrapper.predict(pd.DataFrame({"prompt": ["a cat", "a dog"]}))
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(r, bytes) for r in result)
|
||||
|
||||
|
||||
def test_predict_string(wrapper):
|
||||
result = wrapper.predict("a cat")
|
||||
assert len(result) == 1
|
||||
assert result[0][:4] == b"\x89PNG"
|
||||
|
||||
|
||||
def test_predict_dict(wrapper):
|
||||
result = wrapper.predict({"prompt": "a cat"})
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
def test_predict_dict_list(wrapper):
|
||||
result = wrapper.predict({"prompt": ["a cat", "a dog"]})
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_predict_list(wrapper):
|
||||
result = wrapper.predict(["a cat", "a dog"])
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_predict_with_params(wrapper):
|
||||
|
||||
mock_pipeline = Mock(return_value=_FakePipelineOutput(1))
|
||||
wrapper._pipeline = mock_pipeline
|
||||
|
||||
result = wrapper.predict(
|
||||
"a cat",
|
||||
params={"num_inference_steps": 10, "height": 256, "negative_prompt": "blurry"},
|
||||
)
|
||||
assert len(result) == 1
|
||||
|
||||
mock_pipeline.assert_called_once()
|
||||
call_kwargs = mock_pipeline.call_args.kwargs
|
||||
assert call_kwargs["num_inference_steps"] == 10
|
||||
assert call_kwargs["height"] == 256
|
||||
assert call_kwargs["negative_prompt"] == "blurry"
|
||||
assert call_kwargs["prompt"] == ["a cat"]
|
||||
|
||||
|
||||
def test_predict_dataframe_missing_prompt(wrapper):
|
||||
import pandas as pd
|
||||
|
||||
with pytest.raises(MlflowException, match="prompt"):
|
||||
wrapper.predict(pd.DataFrame({"text": ["a cat"], "other": ["extra"]}))
|
||||
|
||||
|
||||
def test_predict_dict_missing_prompt(wrapper):
|
||||
with pytest.raises(MlflowException, match="prompt"):
|
||||
wrapper.predict({"text": "a cat"})
|
||||
|
||||
|
||||
def test_predict_empty_prompts(wrapper):
|
||||
with pytest.raises(MlflowException, match="No prompts"):
|
||||
wrapper.predict([])
|
||||
|
||||
|
||||
def test_predict_dict_invalid_prompt_type(wrapper):
|
||||
with pytest.raises(MlflowException, match="must be a string or list of strings"):
|
||||
wrapper.predict({"prompt": 42})
|
||||
|
||||
|
||||
def test_predict_unsupported_type(wrapper):
|
||||
with pytest.raises(MlflowException, match="Unsupported input type"):
|
||||
wrapper.predict(12345)
|
||||
|
||||
|
||||
def test_predict_no_images(wrapper):
|
||||
class _NoImagesOutput:
|
||||
images = None
|
||||
|
||||
wrapper._pipeline = lambda prompt, **kwargs: _NoImagesOutput()
|
||||
|
||||
with pytest.raises(MlflowException, match="Pipeline returned no images"):
|
||||
wrapper.predict("a cat")
|
||||
|
||||
|
||||
def test_predict_empty_images_list(wrapper):
|
||||
class _EmptyImagesOutput:
|
||||
images = []
|
||||
|
||||
wrapper._pipeline = lambda prompt, **kwargs: _EmptyImagesOutput()
|
||||
|
||||
with pytest.raises(MlflowException, match="Pipeline returned no images"):
|
||||
wrapper.predict("a cat")
|
||||
|
||||
|
||||
def test_predict_none_prompt_rejected(wrapper):
|
||||
with pytest.raises(MlflowException, match="must be strings, not None"):
|
||||
wrapper.predict([None])
|
||||
|
||||
|
||||
def test_predict_none_in_dataframe_rejected(wrapper):
|
||||
import pandas as pd
|
||||
|
||||
with pytest.raises(MlflowException, match="must be strings, not None"):
|
||||
wrapper.predict(pd.DataFrame({"prompt": [None]}))
|
||||
|
||||
|
||||
# -- _resolve_base_model_revision tests --
|
||||
|
||||
|
||||
def test_resolve_revision_absolute_path_returns_none():
|
||||
from mlflow.diffusers import _resolve_base_model_revision
|
||||
|
||||
assert _resolve_base_model_revision("/absolute/path/to/model") is None
|
||||
|
||||
|
||||
def test_resolve_revision_dot_relative_path_returns_none():
|
||||
from mlflow.diffusers import _resolve_base_model_revision
|
||||
|
||||
assert _resolve_base_model_revision("./local/model") is None
|
||||
assert _resolve_base_model_revision("../parent/model") is None
|
||||
|
||||
|
||||
def test_resolve_revision_hf_hub_success():
|
||||
|
||||
from mlflow.diffusers import _resolve_base_model_revision
|
||||
|
||||
fake_sha = "abc123"
|
||||
with patch(
|
||||
"mlflow.utils.huggingface_utils.get_latest_commit_for_repo",
|
||||
return_value=fake_sha,
|
||||
):
|
||||
result = _resolve_base_model_revision("org/model-name")
|
||||
assert result == fake_sha
|
||||
|
||||
|
||||
def test_resolve_revision_hf_hub_failure_returns_none():
|
||||
|
||||
from mlflow.diffusers import _resolve_base_model_revision
|
||||
|
||||
with patch(
|
||||
"mlflow.utils.huggingface_utils.get_latest_commit_for_repo",
|
||||
side_effect=Exception("network error"),
|
||||
):
|
||||
result = _resolve_base_model_revision("org/model-name")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_resolve_revision_cwd_collision_still_resolves(tmp_path, monkeypatch):
|
||||
|
||||
from mlflow.diffusers import _resolve_base_model_revision
|
||||
|
||||
(tmp_path / "org" / "model").mkdir(parents=True)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
fake_sha = "deadbeef"
|
||||
with patch(
|
||||
"mlflow.utils.huggingface_utils.get_latest_commit_for_repo",
|
||||
return_value=fake_sha,
|
||||
):
|
||||
result = _resolve_base_model_revision("org/model")
|
||||
assert result == fake_sha
|
||||
|
||||
|
||||
def test_save_model_multi_file_directory_preserved(tmp_path, model_path):
|
||||
adapter_dir = tmp_path / "multi_adapter"
|
||||
adapter_dir.mkdir()
|
||||
tensors = {"w": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / "pytorch_lora_weights.safetensors"))
|
||||
# Companion file (e.g., text encoder LoRA or adapter_config from PEFT)
|
||||
(adapter_dir / "adapter_config.json").write_text('{"type": "lora"}')
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
weights_dir = model_path / "adapter_weights"
|
||||
assert (weights_dir / "pytorch_lora_weights.safetensors").exists()
|
||||
assert (weights_dir / "adapter_config.json").exists()
|
||||
|
||||
|
||||
def test_save_model_records_weight_name_for_nonstandard_files(tmp_path, model_path):
|
||||
adapter_dir = tmp_path / "multi_nonstandard"
|
||||
adapter_dir.mkdir()
|
||||
tensors = {"w": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / "alpha_weights.safetensors"))
|
||||
save_file(tensors, str(adapter_dir / "beta_weights.safetensors"))
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
mlmodel = yaml.safe_load((model_path / MLMODEL_FILE_NAME).read_text())
|
||||
flavor_conf = mlmodel["flavors"]["diffusers"]
|
||||
assert flavor_conf["weight_name"] == "alpha_weights.safetensors"
|
||||
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
assert loaded.weight_name == "alpha_weights.safetensors"
|
||||
|
||||
|
||||
def test_save_model_records_weight_name_for_peft_adapter(tmp_path, model_path):
|
||||
adapter_dir = tmp_path / "peft_adapter"
|
||||
adapter_dir.mkdir()
|
||||
tensors = {"w": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / "adapter_model.safetensors"))
|
||||
(adapter_dir / "adapter_config.json").write_text('{"type": "lora"}')
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
mlmodel = yaml.safe_load((model_path / MLMODEL_FILE_NAME).read_text())
|
||||
flavor_conf = mlmodel["flavors"]["diffusers"]
|
||||
assert flavor_conf["weight_name"] == "adapter_model.safetensors"
|
||||
|
||||
loaded = mlflow.diffusers.load_model(str(model_path))
|
||||
assert loaded.weight_name == "adapter_model.safetensors"
|
||||
|
||||
|
||||
def test_save_model_rejects_non_safetensors_file(tmp_path, model_path):
|
||||
bad_file = tmp_path / "model.bin"
|
||||
bad_file.write_bytes(b"\x00" * 100)
|
||||
|
||||
with pytest.raises(MlflowException, match=".safetensors"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(bad_file),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
|
||||
def test_save_model_rejects_invalid_safetensors_content(tmp_path, model_path):
|
||||
fake_file = tmp_path / "bad.safetensors"
|
||||
fake_file.write_bytes(b"this is not safetensors format")
|
||||
|
||||
with pytest.raises(MlflowException, match="not a valid safetensors"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(fake_file),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("base_model", "match"),
|
||||
[
|
||||
("", "non-empty"),
|
||||
(" ", "non-empty"),
|
||||
(None, "must be a"),
|
||||
(123, "must be a"),
|
||||
],
|
||||
)
|
||||
def test_save_model_rejects_invalid_base_model(adapter_dir, model_path, base_model, match):
|
||||
with pytest.raises(MlflowException, match=match):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=base_model,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("adapter_type", [None, 123, []])
|
||||
def test_save_model_rejects_non_string_adapter_type(adapter_dir, model_path, adapter_type):
|
||||
with pytest.raises(MlflowException, match="adapter_type must be a string"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
adapter_type=adapter_type,
|
||||
)
|
||||
|
||||
|
||||
# -- _detect_device tests --
|
||||
|
||||
|
||||
def test_detect_device_explicit():
|
||||
from mlflow.diffusers import _detect_device
|
||||
|
||||
assert _detect_device("cpu") == "cpu"
|
||||
assert _detect_device("cuda:1") == "cuda:1"
|
||||
|
||||
|
||||
def test_detect_device_env_var(monkeypatch):
|
||||
from mlflow.diffusers import _detect_device
|
||||
|
||||
monkeypatch.setenv("MLFLOW_DEFAULT_PREDICTION_DEVICE", "cpu")
|
||||
assert _detect_device() == "cpu"
|
||||
|
||||
|
||||
def test_predict_single_column_dataframe(wrapper):
|
||||
import pandas as pd
|
||||
|
||||
result = wrapper.predict(pd.DataFrame(["a cat", "a dog"]))
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_save_model_rejects_empty_directory(tmp_path, model_path):
|
||||
empty_dir = tmp_path / "empty_adapter"
|
||||
empty_dir.mkdir()
|
||||
(empty_dir / "readme.txt").write_text("no weights here")
|
||||
|
||||
with pytest.raises(MlflowException, match="no .safetensors"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(empty_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
|
||||
def test_save_model_ignores_hidden_files(tmp_path, model_path):
|
||||
adapter_dir = tmp_path / "adapter_with_ds_store"
|
||||
adapter_dir.mkdir()
|
||||
tensors = {"w": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / "my_lora.safetensors"))
|
||||
(adapter_dir / ".DS_Store").write_bytes(b"\x00" * 10)
|
||||
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
|
||||
weights_dir = model_path / "adapter_weights"
|
||||
assert (weights_dir / "pytorch_lora_weights.safetensors").exists()
|
||||
|
||||
|
||||
def test_save_model_validates_all_safetensors_in_multi_file_dir(tmp_path, model_path):
|
||||
adapter_dir = tmp_path / "corrupt_multi"
|
||||
adapter_dir.mkdir()
|
||||
tensors = {"w": np.random.randn(4, 4).astype(np.float32)}
|
||||
save_file(tensors, str(adapter_dir / "pytorch_lora_weights.safetensors"))
|
||||
(adapter_dir / "corrupt.safetensors").write_bytes(b"not valid safetensors")
|
||||
|
||||
with pytest.raises(MlflowException, match="not a valid safetensors"):
|
||||
mlflow.diffusers.save_model(
|
||||
adapter_path=str(adapter_dir),
|
||||
path=str(model_path),
|
||||
base_model=BASE_MODEL_ID,
|
||||
)
|
||||
Reference in New Issue
Block a user