I tried using wheels as a model package format
I’ll give it a few years until MLflow dominates the model package format space, with alternatives like SageMaker models fading away, and sharing pure weights becoming an arcane art. But until that dominance is absolute, I’ve been thinking that there’s another quite obvious way to package models: just store them as wheels. Packaging == persisting the trained model.
Let’s create a project to train a model:
$ uv init --lib model
$ mkdir registry # the model registry
Within the model package (src/model/main.py
), we’ll a create dummy
decision tree model, with typical train/predict interface:
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
import joblib
from pathlib import Path
from sklearn.preprocessing import StandardScaler
import numpy as np
from sklearn.pipeline import Pipeline
def train():
# Load dataset
iris = load_iris()
X, y = iris.data, iris.target
# Split the dataset into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Create a pipeline with a scaler and a decision tree classifier
pipeline = Pipeline([
('scaler', StandardScaler()),
('classifier', DecisionTreeClassifier())
])
# Train the pipeline
pipeline.fit(X_train, y_train)
# Dump the trained pipeline to a file
Path(Path(__file__).parent / 'data').mkdir(parents=True, exist_ok=True)
joblib.dump(pipeline, Path(__file__).parent / 'data' / 'model.pkl')
print("Pipeline trained and saved to model.pkl")
def predict():
# Load the trained model
model = joblib.load(Path(__file__).parent / 'data' / 'model.pkl')
print("Model loaded from model.pkl")
# Make a prediction
X_new = [[6.1, 2.8, 4.7, 1.2]]
y_new = model.predict(X_new)
We pin the model dependencies and the Python version (pickling should be backward compatible):
[project]
name = "model"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Lukas Valatka", email = "<email>" }
]
requires-python = ">=3.9"
dependencies = [
"pandas == 2.2.3",
"scikit-learn == 1.6.1",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Train the model:
$ uv run python -c "from model.main import train; train()"
A /data
folder should appear within src/model
, containing the trained model. This is where the magic happens—we bundle the trained model together with the source code. (Yes, it’s a bit of an abomination, but bear with me.)
Register the model:
$ uv build # this step should be part of a training pipeline
$ mkdir ../registry/model
$ mv dist/* ../registry/model
Make our fake model registry go live:
$ cd ../registry
$ python -m http.server 8000 # package index is just /package folders served
that’s it - we should be able to pull-in our trained model and predict:
$ uv run --with model==0.1.0 --index-url http://127.0.0.1:8000 --extra-index-url 'https://pypi.python.org/simple' --index-strategy unsafe-best-match python -c "from model.main import predict; predict()"
we can also add our model to a serving application, as follows:
pyproject.toml
[project]
name = "serving-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Lukas Valatka", email = "<email>" }
]
requires-python = ">=3.9"
dependencies = [
"model == 0.1.0",
"fastapi[standard]",
"pandas",
"pydantic",
]
[project.scripts]
serving-app = "serving_app:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[[tool.uv.index]]
name = "local"
url = "http://localhost:8000"
all dependencies such as scikit-learn and pandas are at exact versions they were used for training, for full reproducibility. Also reflected within the lockfile - for reproducible builds!
If you try to create the venv within an incorrect Python environment e.g. Python 3.8, it will complain:
$ uv run --python 3.8 --with model==0.1.0 --index-url http://127.0.0.1:8000 --extra-index-url 'https://pypi.python.org/simple' --index-strategy unsafe-best-match python
× No solution found when resolving `--with` dependencies:
╰─▶ Because the current Python version (3.8.20) does not satisfy Python>=3.9 and pandas==2.2.3 depends on Python>=3.9, we can conclude that pandas==2.2.3 cannot be used.
And because model==0.1.0 depends on pandas==2.2.3 and you require model==0.1.0, we can conclude that your requirements are unsatisfiable.
So, how does this all compare to the all mighty mlflow model format:
- With MLflow, you need to explicitly list model dependencies in your serving-app OR have some script automating this (pull in model, uv add deps). It’s not a native functionality.
- How do you make sure Python version is compatible between training and serving? Need to manually check this.
Just a thought experiment. I still believe MLflow is the go-to format due to its flavors, and storing large files within your Nexus will make SREs frown big time.
But who knows? Maybe in the future, MLflow might just consider disguising models as standard Python packages—achieving both framework flexibility via its flavors and interoperability with the Python ecosystem.