Custom pyfunc models

MLflow · Advanced · 14 min

Real predictors include feature engineering, thresholds and multiple models; pyfunc ships them as one deployable unit.

When a model is more than one estimator's predict, subclass mlflow.pyfunc.PythonModel:

  • load_context(self, context) runs once at load time; context.artifacts maps names to local paths of files you attached, such as a tokenizer or a second model.
  • predict(self, context, model_input, params=None) implements inference. params receives per-request parameters declared in the signature.

Log it with mlflow.pyfunc.log_model(name=..., python_model=MyModel(), artifacts={...}, pip_requirements=[...]). Artifacts are copied into the model directory, so the result is self-contained.

There are two ways to provide the model:

  • An instance (python_model=MyModel()), which MLflow serializes with cloudpickle. Easy, but sensitive to Python and library versions.
  • Models from code: pass a path to a Python file that defines the model and calls mlflow.models.set_model(MyModel()). Nothing is pickled; the code itself is stored, which is more robust and readable. This is the recommended approach, especially for GenAI apps and agents.

Type hints on predict (e.g. model_input: list[str]) let MLflow infer the signature and validate inputs automatically.

Keep predict free of training logic and network calls you cannot control, and pin every dependency it needs in pip_requirements; the serving environment is built from them.

Example

import mlflow
import pandas as pd

class ThresholdModel(mlflow.pyfunc.PythonModel):
    def load_context(self, context):
        cfg = mlflow.artifacts.load_dict(context.artifacts["config"])
        self.threshold = cfg["threshold"]

    def predict(self, context, model_input: pd.DataFrame, params=None):
        thr = (params or {}).get("threshold", self.threshold)
        score = model_input["a"] * 0.6 + model_input["b"] * 0.4
        return (score > thr).astype(int)

with mlflow.start_run() as run:
    mlflow.log_dict({"threshold": 0.5}, "config.json")
    info = mlflow.pyfunc.log_model(
        name="thresh", python_model=ThresholdModel(),
        artifacts={"config": f"runs:/{run.info.run_id}/config.json"},
        input_example=pd.DataFrame({"a": [0.9, 0.1], "b": [0.8, 0.2]}),
    )

m = mlflow.pyfunc.load_model(info.model_uri)
print(list(m.predict(pd.DataFrame({"a": [0.9, 0.1], "b": [0.8, 0.2]}))))   # [1, 0]

Remember

  • Subclass PythonModel; load resources in load_context, infer in predict
  • Attach files via artifacts so the model is self-contained
  • Prefer models-from-code over pickled instances for robustness

Watch out for

  • Loading large resources inside predict on every call instead of once in load_context.

Check yourself

  1. Where should a custom pyfunc load a 500 MB embedding file it needs at inference?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: mlflow.org/docs/latest/ml/model/python_model/

Builds on

Unlocks

Nothing builds on this yet.