Skip to content

MLflow model packaging

Library layout and upgrades

MLflow implementation lives under skyulf.integrations.mlflow: models owns pyfunc adapters, spark owns distributed prediction and worker environments, lifecycle owns validation and champion/challenger decisions, registration owns registry resolution, and runs owns tracking. Shared client, signature and nullable transport helpers live in shared.

Use these grouped paths directly. The former local_model and local_feature_model modules and their renamed APIs have been removed. Saved packages that reference removed paths may require rebuilding and logging with the current imports; see upgrading integration code. The root exports TrackingConfig, TrackingRun and track_run without loading the optional MLflow dependency. Worker wheels and source certificates cover the entire skyulf package.

Fitted pipelines (pandas or Polars)

Use a fitted pipeline artifact when fitted preprocessing contains nodes that the portable Spark bundle does not yet support, such as categorical encoding or binning. This path saves the whole fitted pipeline, its recorded training engine, raw and model-feature column order and dtypes, dependency versions, and a payload checksum. It never fits the pipeline again. It is separate from the portable Spark-capable InferenceBundle described below.

import mlflow
import pandas as pd

from skyulf.inference.fitted_pipeline import (
    load_pipeline,
    predict_pipeline,
    save_pipeline,
)
from skyulf.integrations.mlflow import TrackingConfig, track_run
from skyulf.integrations.mlflow.models.pipeline_model import log_pipeline_model

# fitted_pipeline was already trained on pandas or Polars data.
save_pipeline(fitted_pipeline, "artifacts/fitted-pipeline")
config = TrackingConfig(
    enabled=True,
    tracking_uri="sqlite:///mlflow.db",
    experiment_name="customer-risk",
)
with track_run(config, run_name="fit-2026-09") as run:
    model_uri = log_pipeline_model(
        "artifacts/fitted-pipeline",
        run_id=run.run_id,
        artifact_path="model",
        tracking_uri=config.tracking_uri,
    )

request_frame = pd.DataFrame({"age": [42.0], "income": [72000.0]})
artifact = load_pipeline("artifacts/fitted-pipeline")
direct_predictions = predict_pipeline(request_frame, artifact)
mlflow.set_tracking_uri(config.tracking_uri)
pyfunc_predictions = mlflow.pyfunc.load_model(model_uri).predict(request_frame)

The example columns must match the fitted pipeline's actual raw schema. Direct local prediction accepts a pandas or Polars frame, validates column names, order, and dtypes after converting to the recorded fit engine, and returns a pandas DataFrame. A pandas input retains its index; a Polars input receives a range index. The MLflow pyfunc boundary takes named pandas input; a pipeline trained on Polars is explicitly converted back to Polars before applying its learned preprocessing and model. MLflow may align named columns and cast values according to its signature before that conversion. Check parity for each input dtype you intend to serve, especially nullable or categorical dtypes.

Nullable integers and Boolean inputs

MLflow validates its input signature before calling the saved pipeline. A nullable integer or Boolean column can therefore be rejected before its fitted imputer runs. Newly logged local pipelines and model sets declare these columns as strings at the MLflow boundary and restore their recorded dtype before applying feature engineering. Nulls remain nulls until the saved preprocessing handles them; the adapter does not fill them or retrain an imputer. ColSpec(required=False) makes a column optional; it does not make nullable integer or Boolean values pass their primitive signature. Floating-point columns keep numeric signatures and do not need this string transport.

Use the public helper with a loaded model:

from skyulf.integrations.mlflow.models.pipeline_model import prepare_pyfunc_input

loaded = mlflow.pyfunc.load_model(model_uri)
# raw_frame contains the original, typed values expected by this artifact.
request = prepare_pyfunc_input(raw_frame, loaded)
predictions = loaded.predict(request)

For example, an Int64 column [25, None, 40] travels as ["25", None, "40"], becomes Int64 again inside the adapter, and then enters the fitted cleaning steps. A pipeline with no suitable missing-value handler can still fail in its model. The helper preserves input order and index and does not mutate the caller's frame. It is needed only at the MLflow boundary; direct predict_pipeline calls continue to take native values. Call the helper once: it accepts native values and deliberately rejects already encoded strings. REST/JSON clients can send the declared strings and nulls directly; they do not call this Python helper.

The transport covers pandas extension Int32, Int64, and boolean, and Polars Int32, Int64, and Boolean. Polars does not record column nullability separately, so newly logged models use this transport for all columns of those three Polars types. Ordinary pandas NumPy int32, int64, and bool signatures stay native. Other unsupported dtypes still fail at packaging time; portable InferenceBundle signatures are unchanged.

The package records skyulf_input_transport metadata with codec nullable_primitives_v1 and its column-to-dtype mapping. The helper checks this mapping against the loaded artifact. Integer strings must be canonical decimal values within the fitted signed integer range; Boolean strings must be "true" or "false". Invalid strings and floating-point integer inputs are rejected instead of rounded. This preserves integers above 2**53. Existing logged packages keep their previous contract; log a new version to use the nullable transport.

For a separately verified batch-safe pipeline, Spark callers must cast the listed integer and Boolean columns to strings in Spark before passing them to mlflow.pyfunc.spark_udf. Casting after conversion to pandas can be too late: Arrow may already have represented nullable integers as floats. Other feature columns keep their declared types. This transport does not make arbitrary preprocessing safe across Spark batches or change nullable prediction-output limitations. See MLflow signature validation.

For classifiers, the output is prediction followed by probability_0, probability_1, and so on in the saved model class order. Pass use_tuned_thresholds=True to save_pipeline only when the fitted pipeline has stored tuned thresholds and those decisions are intended for this artifact; otherwise predictions use the estimator's default decision rule. Validate replay for the actual recipe, input dtypes and estimator you package. Use preprocessing diagnostics to inspect saved-state and request-context behavior; compare direct and pyfunc predictions with the same typed features. Diagnostic success does not grant worker eligibility.

This package retains its whole_frame_local scope. It supports bounded local batch inference, including inside a Databricks Python task with the recorded dependencies installed. It does not certify independent HTTP row requests, Spark worker partitions, or Spark-native feature engineering by itself. Newly logged, inspected pandas artifacts may additionally carry a partition-safety certificate for the Spark pyfunc route. That adapter revalidates the loaded artifact, package signature, source hash and certificate before creating a named-input UDF with all output columns. Workers revalidate the same evidence on load. This package does not write Delta tables. The model URI points to a concrete run artifact; registering or promoting an alias is a separate, explicit step.

Install skyulf-core[mlflow] in the loading environment. The MLflow model records exact package requirements, including the Skyulf version; provide the matching Skyulf wheel when the version is unpublished. Certified Spark packages embed an exact-source wheel for isolated worker installation; ordinary local packages retain their existing external-wheel requirement. Only load local pipeline artifacts from trusted producers: pickle can execute code while loading, and the checksum detects corruption rather than authenticating the source.

Portable inference bundles

Skyulf can package an existing InferenceBundle as an MLflow pyfunc model. The package contains the frozen feature state, estimator payload, manifest, and runtime requirements already produced by the bundle workflow. It does not fit again, capture a notebook session, or serialize a Spark session.

Install the optional dependency only in environments that log or load these models:

uv pip install "skyulf-core[mlflow]"

log_model requires the concrete MLflow run ID. Pass the same tracking URI used to create a client-bound track_run when it is not already configured as MLflow's process-wide URI:

import mlflow

from skyulf.inference.bundle import build_bundle
from skyulf.integrations.mlflow import TrackingConfig, track_run
from skyulf.integrations.mlflow.models.model import log_model

bundle = build_bundle(
    fitted_pipeline,
    input_stage="raw",  # use "features" when callers already apply FE
    feature_order=("age", "income"),
)
config = TrackingConfig(
    enabled=True,
    tracking_uri="sqlite:///mlflow.db",
    experiment_name="customer-risk",
)

with track_run(config, run_name="fit-2026-09") as run:
    model_uri = log_model(
        bundle,
        run_id=run.run_id,
        artifact_path="skyulf-model",
        tracking_uri=config.tracking_uri,
    )

mlflow.set_tracking_uri(config.tracking_uri)
loaded = mlflow.pyfunc.load_model(model_uri)
predictions = loaded.predict(request_frame)

The returned URI is runs:/<run_id>/<artifact_path>. Uploading uses an explicit MLflow client and run ID, so an unrelated fluent active run cannot receive the artifact. The raw bundle path applies the frozen FE state before prediction; the features path validates that the caller supplies the saved feature schema. Regression outputs and classification labels, class order, probability columns, and saved threshold decisions remain the bundle's contract.

The model signature uses named tabular columns. MLflow may reorder named input columns and safely cast compatible values before the adapter receives the pandas.DataFrame; undeclared extra columns are ignored by MLflow. Positional NumPy or list input, and inputs missing a declared column, are rejected. Bundle dtypes that MLflow cannot represent exactly, such as int8, are rejected during packaging instead of being silently widened. Input examples are one synthetic row derived from the manifest schema and never a training or production row. This is MLflow's input transport contract; direct predict_local calls still reject different column orders, extra columns and dtype mismatches. See MLflow signature enforcement.

The pyfunc entry point accepts named pandas frames (or named inputs that MLflow can convert). A pipeline trained with Polars can produce the same bundle; convert a Polars request to pandas explicitly for pyfunc prediction. Supported bundle input dtypes are bool, int32, int64, float32, and float64. String classification outputs are supported; raw string FE inputs remain outside the existing bundle contract.

Install the recorded requirements, including the matching Skyulf wheel, before loading the artifact. The wheel itself is not embedded: for an unpublished version, distribute the built wheel through your package delivery process. A worker or subprocess can load it without importing the repository checkout. The estimator payload is a trusted pickle and must only be loaded from a trusted producer; checksums detect corruption but do not make pickle deserialization safe.

Packaging uses run artifacts, not MLflow 3's separate LoggedModel entity. Choose a fresh artifact path for each model; uploading the same run/path is not an atomic publish operation and may overwrite files. Logging failures propagate to the caller even when tracking was configured with warn. Producer project files and the temporary source bundle path are excluded.

Use the registry guide to register a concrete run artifact and manage candidate/champion versions. The Databricks Python SDK adds pinned UC source reads and bounded Delta publication; the Bundle guide describes distributed scoring and deployment settings. Those integrations keep their own schema, context and admission checks; logging a model alone does not deploy it or create a prediction table.