Files
2026-07-13 13:22:34 +08:00

152 lines
5.5 KiB
Python

import io
import logging
import threading
from types import MappingProxyType
from typing import Any
import pandas as pd
from mlflow.diffusers import _detect_device
from mlflow.exceptions import MlflowException
_logger = logging.getLogger(__name__)
class _DiffusersAdapterWrapper:
def __init__(
self,
adapter_path: str,
flavor_conf: dict[str, Any],
model_config: dict[str, Any] | None = None,
):
self._adapter_path = adapter_path
self._flavor_conf = flavor_conf
self._model_config = MappingProxyType(model_config or {})
self._pipeline = None
self._load_lock = threading.Lock()
def _load_pipeline(self):
from diffusers import DiffusionPipeline
base_model = self._model_config.get("base_model") or self._flavor_conf["base_model"]
base_model_revision = self._flavor_conf.get("base_model_revision")
device = _detect_device(self._model_config.get("device"))
torch_dtype = self._model_config.get("torch_dtype", "auto")
load_kwargs = {"torch_dtype": torch_dtype}
if base_model_revision:
load_kwargs["revision"] = base_model_revision
weight_name = self._flavor_conf.get("weight_name")
lora_kwargs = {}
if weight_name:
lora_kwargs["weight_name"] = weight_name
_logger.info("Loading base pipeline: %s", base_model)
try:
pipe = DiffusionPipeline.from_pretrained(base_model, **load_kwargs)
except OSError as e:
raise MlflowException(
f"Failed to load base model '{base_model}'. If the model has moved, "
"pass the correct location via "
"model_config={{'base_model': '<new_path_or_hub_id>'}} "
"when loading with mlflow.pyfunc.load_model()."
) from e
_logger.info("Loading LoRA adapter from: %s", self._adapter_path)
pipe.load_lora_weights(self._adapter_path, **lora_kwargs)
self._pipeline = pipe.to(device)
def get_raw_model(self):
if self._pipeline is None:
with self._load_lock:
if self._pipeline is None:
self._load_pipeline()
return self._pipeline
def _flatten_prompts(self, prompts):
"""Flatten nested lists produced by schema enforcement."""
flat = []
for item in prompts:
if isinstance(item, list):
flat.extend(item)
else:
flat.append(item)
return flat
def predict(self, data, params: dict[str, Any] | None = None):
pipeline = self.get_raw_model()
if isinstance(data, pd.DataFrame):
if "prompt" in data.columns:
prompts = data["prompt"].tolist()
elif len(data.columns) == 1:
# Schema enforcement wraps scalar strings into a single-column DataFrame
prompts = data.iloc[:, 0].tolist()
else:
raise MlflowException(
f"Input DataFrame must contain a 'prompt' column. "
f"Got columns: {list(data.columns)}"
)
# Schema enforcement may wrap {"prompt": ["a","b"]} into a
# single-row DataFrame where the cell contains a list, producing
# [["a","b"]] after tolist(). Flatten to ["a","b"].
prompts = self._flatten_prompts(prompts)
elif isinstance(data, str):
prompts = [data]
elif isinstance(data, dict):
if "prompt" not in data:
raise MlflowException(
f"Input dict must contain a 'prompt' key. Got keys: {list(data.keys())}"
)
prompts = data["prompt"]
if isinstance(prompts, str):
prompts = [prompts]
elif isinstance(prompts, list):
prompts = self._flatten_prompts(prompts)
else:
raise MlflowException(
"'prompt' value must be a string or list of strings, "
f"got {type(prompts).__name__}."
)
elif isinstance(data, list):
prompts = self._flatten_prompts(data)
else:
raise MlflowException(f"Unsupported input type: {type(data)}")
if not prompts:
raise MlflowException(
"No prompts provided. Input must contain at least one prompt string."
)
if any(p is None for p in prompts):
raise MlflowException(
"Prompt values must be strings, not None. "
"Check your input for missing or null values."
)
params = params or {}
param_keys = ("num_inference_steps", "guidance_scale", "height", "width", "negative_prompt")
gen_kwargs = {k: params[k] for k in param_keys if k in params}
# Drop empty-string negative_prompt so the pipeline uses its own default
if gen_kwargs.get("negative_prompt") == "":
del gen_kwargs["negative_prompt"]
output = pipeline(prompt=prompts, **gen_kwargs)
if not hasattr(output, "images") or not output.images:
raise MlflowException(
"Pipeline returned no images. The output may have been filtered "
"by the safety checker, or the pipeline does not support image generation."
)
results = []
for image in output.images:
buf = io.BytesIO()
image.save(buf, format="PNG")
results.append(buf.getvalue())
buf.close()
return results