Skip to content

MLflow

Experiment tracking, model registry, and serving for ML and LLM workflows.

Last reviewed · Download PDF

Prerequisites: Python for DE

Related: Databricks · Fine-Tuning · Evals · Glossary


Overview

Challenge: Machine learning work produces many experiments — training runs with different parameters, data versions, and results — spread across notebooks and machines. Without tracking, it becomes impossible to say which run produced the production model, what data it used, or how to reproduce it.

Solution: MLflow is an open-source system of record for ML and LLM work. It records every run (parameters, metrics, code version, artifacts), stores models in a standard format, maintains a registry of model versions with aliases such as @champion, and serves models as REST endpoints. Recent versions also trace LLM calls and evaluate GenAI applications.

train/eval runs ──log──→ Tracking server (params · metrics · artifacts · traces)
                                   │ pick best run
                                   ▼
                         Model Registry: orders-forecaster  v1  v2  v3 ← @champion
                                   │ load by alias
                                   ▼
                  batch scoring job · REST endpoint · Spark UDF · Databricks serving

Relevance to data engineering: data engineers typically operate the tracking server, integrate model scoring into pipelines (load @champion, score the latest partition), record data versions alongside models for lineage, and trigger retraining when data drifts.

flowchart LR
    TR["Training code"] -->|"log params, metrics"| TRK["Tracking server"]
    TRK --> ART[("Artifacts + models")]
    ART --> REG["Model registry<br/>versions + aliases"]
    REG --> SRV["Serving / batch scoring"]

On this page

Basic - What Is MLflow - Setup - Tracking Experiments - Logging Parameters, Metrics, Artifacts

Intermediate - MLflow Projects - Model Registry - MLflow with Scikit-learn & PySpark - MLflow with LLMs

Advanced - Model Serving - Custom Python Models - MLflow in Databricks - DE Integration Patterns

Reference - Common Pitfalls - Cheat Sheet - Interview Questions - Further Reading


What Is MLflow

MLflow is an open-source platform for managing the ML lifecycle:

Component What it does
Tracking Log parameters, metrics, code, and artifacts per experiment run
Projects Package ML code for reproducible execution
Models Standard format for packaging models for deployment
Registry Versioned model store; aliases (e.g. @champion) mark which version is live
Serving REST endpoint for serving models
Typical workflow:
  Train model (multiple runs, different hyperparameters)
  → Track each run in MLflow (params, metrics, artifacts)
  → Pick the best run
  → Register model in the Model Registry
  → Point the "champion" alias at the new version
  → Serve via MLflow serve or integrate into pipeline

Setup

pip install mlflow

# Start the MLflow UI (local)
mlflow ui --port 5000
# Open http://localhost:5000

# Or point to a remote tracking server
export MLFLOW_TRACKING_URI=http://my-mlflow-server:5000
import mlflow

# Configure tracking URI (default: ./mlruns in current directory)
mlflow.set_tracking_uri("http://localhost:5000")      # remote server
mlflow.set_tracking_uri("sqlite:///mlflow.db")        # local SQLite
mlflow.set_tracking_uri("file:///path/to/mlruns")     # local filesystem

# Set the experiment (creates it if it doesn't exist)
mlflow.set_experiment("orders-prediction")

Tracking Experiments

import mlflow
import numpy as np

mlflow.set_experiment("orders-forecasting")

# Context manager approach (recommended)
with mlflow.start_run(run_name="xgboost-v1") as run:
    print(f"Run ID: {run.info.run_id}")

    # ... train model, compute metrics ...
    mlflow.log_param("learning_rate", 0.01)
    mlflow.log_metric("rmse", 142.3)
    mlflow.log_artifact("feature_importance.png")

# Manual approach
run = mlflow.start_run(run_name="xgboost-v2")
mlflow.log_param("learning_rate", 0.05)
mlflow.end_run()

# Nested runs (hyperparameter search)
with mlflow.start_run(run_name="hyperparam-search") as parent:
    for lr in [0.001, 0.01, 0.1]:
        with mlflow.start_run(run_name=f"lr={lr}", nested=True):
            mlflow.log_param("learning_rate", lr)
            rmse = train_and_eval(lr)
            mlflow.log_metric("rmse", rmse)

Logging Parameters, Metrics, Artifacts

with mlflow.start_run():

    # ── Parameters (hyperparameters, config) ──────────────────────────────────
    mlflow.log_param("model_type",    "xgboost")
    mlflow.log_param("learning_rate", 0.01)
    mlflow.log_param("max_depth",     6)
    mlflow.log_param("n_estimators",  100)

    # Log dict of params at once
    mlflow.log_params({
        "model_type":    "xgboost",
        "learning_rate": 0.01,
        "max_depth":     6,
    })

    # ── Metrics (scalar values over time) ─────────────────────────────────────
    mlflow.log_metric("train_rmse", 120.4)
    mlflow.log_metric("val_rmse",   142.3)
    mlflow.log_metric("r2",         0.87)

    # Metrics over steps (for training curves)
    for epoch in range(100):
        train_loss = train_one_epoch()
        val_loss   = validate()
        mlflow.log_metric("train_loss", train_loss, step=epoch)
        mlflow.log_metric("val_loss",   val_loss,   step=epoch)

    # Log dict of metrics
    mlflow.log_metrics({"rmse": 142.3, "mae": 98.1, "r2": 0.87})

    # ── Artifacts (files) ─────────────────────────────────────────────────────
    mlflow.log_artifact("feature_importance.png")          # single file
    mlflow.log_artifacts("./output/")                      # entire directory
    mlflow.log_artifact("model_config.yaml", "configs")    # into a subfolder (no trailing slash)

    # Log in-memory objects
    import json
    with open("/tmp/metrics.json", "w") as f:
        json.dump({"rmse": 142.3}, f)
    mlflow.log_artifact("/tmp/metrics.json")

    # ── Tags ──────────────────────────────────────────────────────────────────
    mlflow.set_tag("team",         "data-science")
    mlflow.set_tag("data_version", "2024-03-15")
    mlflow.set_tag("git_commit",   "abc1234")
    mlflow.set_tags({"env": "dev", "validated": "false"})

    # ── Model ─────────────────────────────────────────────────────────────────
    # (see MLflow flavors below)
    mlflow.sklearn.log_model(model, name="model")

MLflow Projects

Package your training code for reproducibility.

# MLproject file
name: orders-forecasting

conda_env: conda.yaml   # or pip_env: requirements.txt

entry_points:
  main:
    parameters:
      learning_rate:  {type: float, default: 0.01}
      max_depth:      {type: int,   default: 6}
      data_path:      {type: str,   default: "s3://my-bucket/data/"}
    command: "python train.py --lr {learning_rate} --depth {max_depth} --data {data_path}"

  evaluate:
    parameters:
      model_uri: {type: str}
    command: "python evaluate.py --model {model_uri}"
# Run locally
mlflow run . -P learning_rate=0.05

# Run from GitHub
mlflow run https://github.com/org/project -P learning_rate=0.05

# Run on Databricks
mlflow run . --backend databricks --backend-config cluster.json

Model Registry

import mlflow
from mlflow.tracking import MlflowClient

client = MlflowClient()

# ── Register a model ───────────────────────────────────────────────────────────
# Option 1: register at log time
with mlflow.start_run():
    info = mlflow.sklearn.log_model(
        model,
        name="model",
        registered_model_name="orders-forecaster"
    )

# Option 2: register a model that is already logged
mlflow.register_model(model_uri=info.model_uri, name="orders-forecaster")

# ── List versions ──────────────────────────────────────────────────────────────
for v in client.search_model_versions("name='orders-forecaster'"):
    print(f"Version {v.version}: aliases={v.aliases}, run={v.run_id}")

# ── Promote with aliases ───────────────────────────────────────────────────────
# Stages (Staging/Production) are deprecated since MLflow 2.9 — use aliases instead.
# An alias is a movable pointer to one version; deployments load by alias.
client.set_registered_model_alias("orders-forecaster", "challenger", version=3)

# After validation, point "champion" at the new version (the old one is simply un-aliased)
client.set_registered_model_alias("orders-forecaster", "champion", version=3)

# Roll back = move the alias back
client.set_registered_model_alias("orders-forecaster", "champion", version=2)

# ── Load a registered model ────────────────────────────────────────────────────
# By alias
model = mlflow.pyfunc.load_model("models:/orders-forecaster@champion")

# By version
model = mlflow.pyfunc.load_model("models:/orders-forecaster/3")

# Predict
predictions = model.predict(X_test)

# ── Add descriptions and tags ─────────────────────────────────────────────────
client.update_registered_model(
    name="orders-forecaster",
    description="XGBoost model trained on 3 years of order history"
)
client.set_model_version_tag("orders-forecaster", "3", "validated_by", "alice")

MLflow with Scikit-learn & PySpark

# ── Scikit-learn autolog ───────────────────────────────────────────────────────
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import train_test_split

mlflow.sklearn.autolog()   # logs params, metrics, model automatically

with mlflow.start_run():
    model = GradientBoostingRegressor(n_estimators=100, learning_rate=0.05)
    model.fit(X_train, y_train)
    # params, metrics, and model logged automatically

# ── PySpark MLlib ──────────────────────────────────────────────────────────────
from pyspark.ml.regression import GBTRegressor
from pyspark.ml import Pipeline

mlflow.spark.autolog()

with mlflow.start_run():
    gbt = GBTRegressor(featuresCol="features", labelCol="label")
    pipeline = Pipeline(stages=[gbt])
    model = pipeline.fit(train_df)
    # Spark ML pipeline logged automatically

# Log Spark model explicitly
mlflow.spark.log_model(model, "spark-model")
loaded = mlflow.spark.load_model(f"runs:/{run_id}/spark-model")

MLflow with LLMs

Tracing LLM calls (MLflow 2.18+ / 3.x)

import mlflow
import anthropic

mlflow.set_experiment("de-rag-assistant")
mlflow.anthropic.autolog()     # every Anthropic SDK call is traced: prompt, response, tokens, latency

client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "What does the orders DAG load?"}],
)
# Traces appear in the MLflow UI under the experiment's "Traces" tab.
# Equivalent autologging exists for OpenAI (mlflow.openai), LangChain, and LlamaIndex.

Logging LLM experiment results

# Log LLM experiment results
with mlflow.start_run(run_name="rag-eval-v2"):
    mlflow.log_params({
        "llm_model":        "claude-sonnet-5-5",
        "embedding_model":  "text-embedding-3-small",
        "chunk_size":       512,
        "chunk_overlap":    50,
        "retrieval_k":      5,
        "reranking":        True,
    })

    mlflow.log_metrics({
        "faithfulness":       0.92,
        "relevance":          0.88,
        "context_precision":  0.85,
        "avg_latency_ms":     840,
        "avg_cost_usd":       0.0021,
    })

    # Log eval results as artifact
    import json
    with open("/tmp/eval_results.json", "w") as f:
        json.dump(eval_results, f, indent=2)
    mlflow.log_artifact("/tmp/eval_results.json", "evals")

    # Tag the experiment
    mlflow.set_tags({
        "rag_version": "v2",
        "knowledge_base_date": "2024-03-15",
    })

# Compare runs in UI — filter by parameters, sort by metrics

Model Serving

# Serve a registered model (--env-manager local reuses the current environment;
# the default builds a fresh virtualenv for the model, which takes longer)
mlflow models serve -m "models:/orders-forecaster@champion" -p 5001 --env-manager local

# Serve a model by URI from a run
mlflow models serve -m "models:/m-<model_id>" -p 5001 --env-manager local

# Test it
curl http://localhost:5001/invocations \
  -H "Content-Type: application/json" \
  -d '{"dataframe_records": [{"feature1": 1.0, "feature2": 2.0}]}'
# Serve programmatically and call from Python
import requests
import pandas as pd

def predict_via_mlflow(features: pd.DataFrame, endpoint: str = "http://localhost:5001") -> list:
    payload = {"dataframe_records": features.to_dict(orient="records")}
    response = requests.post(f"{endpoint}/invocations",
                             headers={"Content-Type": "application/json"},
                             json=payload)
    response.raise_for_status()
    return response.json()["predictions"]

Custom Python Models

Package any Python logic as an MLflow model.

import mlflow.pyfunc

class RAGModel(mlflow.pyfunc.PythonModel):
    """Wrap a RAG pipeline as an MLflow model for serving."""

    def load_context(self, context):
        """Called once when the model is loaded."""
        import pickle
        with open(context.artifacts["vector_index"], "rb") as f:
            self.index = pickle.load(f)
        import anthropic
        self.client = anthropic.Anthropic()

    def predict(self, context, model_input):
        """
        model_input: pd.DataFrame with a 'question' column
        returns: pd.Series of answers
        """
        import pandas as pd
        answers = []
        for question in model_input["question"]:
            chunks   = self.index.search(question, k=5)
            context_text = "\n".join(c["text"] for c in chunks)
            resp = self.client.messages.create(
                model="claude-haiku-4-5-20251001",
                max_tokens=512,
                messages=[{"role": "user", "content": f"Context: {context_text}\n\nQ: {question}"}]
            )
            answers.append(next(b.text for b in resp.content if b.type == "text"))
        return pd.Series(answers)

# Log and register
import pickle
with open("/tmp/vector_index.pkl", "wb") as f:
    pickle.dump(my_index, f)

with mlflow.start_run():
    mlflow.pyfunc.log_model(
        name="rag_model",
        python_model=RAGModel(),
        artifacts={"vector_index": "/tmp/vector_index.pkl"},
        registered_model_name="de-rag-assistant"
    )

# Load and use
model = mlflow.pyfunc.load_model("models:/de-rag-assistant@champion")
import pandas as pd
results = model.predict(pd.DataFrame({"question": ["What is the orders schema?"]}))

MLflow in Databricks

Databricks has MLflow built in — no setup required.

# In a Databricks notebook
import mlflow

# Tracking server is auto-configured
# Experiments live in /Users/<email>/my-experiment
mlflow.set_experiment("/Users/alice@example.com/orders-forecasting")

with mlflow.start_run():
    mlflow.log_param("model", "xgboost")
    mlflow.log_metric("rmse", 142.3)
    mlflow.sklearn.log_model(model, name="model",
                              registered_model_name="orders-forecaster")

# Unity Catalog model registry (Databricks Unity Catalog)
mlflow.set_registry_uri("databricks-uc")

mlflow.sklearn.log_model(
    model, name="model",
    registered_model_name="main.ml_models.orders_forecaster"   # catalog.schema.model
)

# Load from Unity Catalog
model = mlflow.pyfunc.load_model("models:/main.ml_models.orders_forecaster@champion")

DE Integration Patterns

Log pipeline metadata alongside model metrics

with mlflow.start_run(run_name="model-training-2024-03-15"):
    # Model metrics
    mlflow.log_metrics({"rmse": 142.3, "r2": 0.87})

    # Data pipeline metadata — ties the model to the data it was trained on
    mlflow.log_params({
        "training_data_path":    "s3://bucket/gold/orders/2024-03-15/",
        "training_data_rows":    1_523_400,
        "feature_pipeline_ver":  "v2.3",
        "data_cutoff_date":      "2024-03-14",
    })
    mlflow.set_tags({
        "airflow_run_id": "scheduled__2024-03-15T02:00:00",
        "dbt_job_id":     "1234",
        "data_team":      "platform",
    })

Trigger retraining when data drifts

# In Airflow or a monitoring pipeline:
def check_model_drift_and_retrain(**context):
    from scipy import stats
    import mlflow

    client = mlflow.MlflowClient()
    prod_model = client.get_model_version_by_alias("orders-forecaster", "champion")

    # Compare current data distribution to training data distribution
    current_data  = get_recent_features()
    training_dist = mlflow.artifacts.load_dict(f"runs:/{prod_model.run_id}/feature_stats.json")

    p_value = stats.ks_2samp(current_data["amount"], training_dist["amount_sample"]).pvalue
    if p_value < 0.05:
        print(f"Data drift detected (p={p_value:.4f}) — triggering retraining")
        # Trigger Airflow DAG or Databricks job
        trigger_retraining_pipeline()

Common Pitfalls

Pitfall Symptom Fix
Using the default local ./mlruns store in a team Runs scattered across laptops; nothing shared A tracking server with a database backend and object-storage artifacts (or managed MLflow)
Loading models by stage (models:/name/Production) Deprecation warnings; unclear promotion history Aliases (@champion, @challenger) with set_registered_model_alias
Not logging the data version Can't reproduce or explain a model Log the dataset path, snapshot/version (Delta/Iceberg), and row counts as params or datasets
No signature or input example on logged models Serving fails on schema mismatches infer_signature(X, y) and input_example when logging
Environment not captured The model loads locally but not in serving Let MLflow record requirements.txt/conda env; pin versions
Huge artifacts logged every run Storage costs balloon; the UI slows down Log only what you need; lifecycle rules on the artifact store
Sklearn tree models fail to log with an untrusted-types error MLflow 3 saves sklearn models with the skops format by default, which refuses types it has not been told to trust (for example sklearn.tree._tree.Tree) Review the listed types and pass them as skops_trusted_types=[...]; serialization_format="cloudpickle" works but can execute code when loaded
Pickled custom models relying on local code ModuleNotFoundError at load time Package code with code_paths, or use a models-from-code approach
Scoring with whatever model is newest Unvalidated models reach production Promote via alias only after automated evaluation passes

Cheat Sheet

Task Code
Point to a server mlflow.set_tracking_uri("http://mlflow:5000")
Choose an experiment mlflow.set_experiment("orders-forecasting")
Start a run with mlflow.start_run(run_name="xgb-v3"):
Log params / metrics mlflow.log_params({...}) · mlflow.log_metric("rmse", 12.3, step=epoch)
Log files mlflow.log_artifact("report.html") · mlflow.log_dict(d, "stats.json")
Autolog a framework mlflow.sklearn.autolog() · mlflow.xgboost.autolog() · mlflow.spark.autolog()
Trace LLM calls mlflow.anthropic.autolog() · mlflow.openai.autolog() · mlflow.langchain.autolog()
Log and register a model mlflow.sklearn.log_model(model, name="model", registered_model_name="orders-forecaster", signature=sig)
Promote MlflowClient().set_registered_model_alias("orders-forecaster", "champion", version=3)
Load mlflow.pyfunc.load_model("models:/orders-forecaster@champion")
Score in Spark mlflow.pyfunc.spark_udf(spark, "models:/orders-forecaster@champion")
Serve locally mlflow models serve -m "models:/orders-forecaster@champion" -p 5001 --env-manager local
Find the best run mlflow.search_runs(experiment_names=["x"], order_by=["metrics.rmse ASC"], max_results=1)
Start a server mlflow server --backend-store-uri postgresql://... --artifacts-destination s3://bucket/mlflow

Model URIs: models:/<model_id> (MLflow 3 logged model, returned as model_uri by log_model) · runs:/<run_id>/<artifact_path> (artifact logged to a run) · models:/name/3 (version) · models:/name@champion (alias) · on Databricks with Unity Catalog: models:/catalog.schema.name@champion


Interview Questions

Q: What are the main components of MLflow? A: Tracking (logging runs with parameters, metrics, artifacts, and — in recent versions — LLM traces), Models (a standard packaging format with "flavors" such as sklearn, PyTorch, and a generic pyfunc interface), the Model Registry (versioned models with aliases, tags, and descriptions), and serving and deployment tools. Projects package code for reproducible runs, and recent releases add GenAI evaluation and prompt management.

Q: How do you promote a model to production with MLflow? A: Register each candidate as a new version of a registered model, run automated validation (metrics above the current champion's, no regressions on key slices, signature checks), and then move an alias such as @champion to the new version. Consumers always load models:/name@champion, so promotion and rollback are just moving the alias — no code change. Record who approved it and why with tags. Older MLflow used stages (Staging/Production), which are now deprecated.

Q: How would you integrate MLflow into a data pipeline? A: The training pipeline logs parameters, metrics, the data snapshot version, and the model, then registers a new version. A validation step compares it with the champion and moves the alias if it's better. A scoring pipeline (Airflow or Databricks job) loads @champion, scores the new partition in batch (for example with spark_udf), and writes predictions along with the model version for lineage. Monitoring jobs compare incoming feature distributions with training statistics and trigger retraining on drift.

Q: What is the pyfunc flavor and why does it matter? A: pyfunc is MLflow's generic Python model interface: any model logged with MLflow can be loaded as a pyfunc and called with .predict() on a DataFrame, whatever library trained it. That gives serving, batch scoring, and Spark UDFs one consistent API, and custom PythonModel classes let you wrap anything — including a RAG chain calling an LLM — in the same format.


Further Reading


Previous: Evals · Next: Claude Code · Back to: Index