Skip to content

Artifacts, serving and ONNX export

How a fitted model leaves the training process: a versioned artifact directory for serving with honestml, and an optional ONNX bundle for external runtimes. Every python block on this page is self-contained: copy any one of them and it runs as-is, and every block is executed on each CI run, so the examples cannot rot.

Saving an artifact

save_artifact(model.fitted_, directory) serializes the shipped model (fitted_ is the FittedModel behind the facade) into a self-contained versioned directory: manifest.json (versioned metadata plus provenance — the winner, the equivalence band, calibration, holdout score), schema.json (the serialized preprocessing schema, the single source of truth that keeps train and inference identical), the model body, and leaderboard.json. The manifest carries a checksums block — sha256 of every file plus a digest of the manifest payload — so corruption or naive file substitution is detected before anything is deserialized. An optional sign= hook signs the manifest digest, enabling an authenticated verify= on load.

import json
import tempfile
from pathlib import Path

from sklearn.datasets import make_classification

from honestml import AutoML, save_artifact

X, y = make_classification(n_samples=150, n_features=6, n_informative=4, random_state=0)
model = AutoML(task="binary", models=("baseline", "linear"), random_state=0).fit(X, y)

art = Path(tempfile.mkdtemp())
save_artifact(model.fitted_, art)

print(sorted(p.name for p in art.iterdir()))
manifest = json.loads((art / "manifest.json").read_text(encoding="utf-8"))
print(manifest["best_model_id"], manifest["checksums"]["algo"])

Loading and predicting

load_artifact(directory) verifies the checksums first, then returns a FittedModel — the exact inference path the facade itself uses — with predict, predict_proba (classification) and score. Prediction does not need the training stack: preprocessing is rebuilt from schema.json, and a standalone load that only predicts never resolves the AutoML metric or any selection machinery. require_integrity=True turns a missing checksums block (an artifact written by an older build) from a warning into an error.

import tempfile

import numpy as np
from sklearn.datasets import make_classification

from honestml import AutoML, load_artifact, save_artifact

X, y = make_classification(n_samples=150, n_features=6, n_informative=4, random_state=0)
model = AutoML(task="binary", models=("baseline", "linear"), random_state=0).fit(X, y)

art = tempfile.mkdtemp()
save_artifact(model.fitted_, art)
loaded = load_artifact(art)

print(np.array_equal(loaded.predict(X), model.predict(X)))  # True
print(loaded.best_model_id, loaded.predict_proba(X[:3]).shape)

The trust model: pickle vs native

The default body is model.joblib — joblib, which is pickle, and unpickling executes code: load only artifacts you trust. The sha256 manifest detects corruption and naive substitution, not malice — a malicious author can embed code with matching digests, so authenticity needs the sign=/verify= hooks and a trusted source. For boosting models, model_format="native" stores the body in the library's stable structural format instead (XGBoost UBJSON, CatBoost cbm, LightGBM text): no pickle in the body, and the file survives library upgrades that break pickle. Two disclosed caveats: anything without a native format (sklearn models, a shipped ensemble) transparently stays joblib, and a fitted probability calibrator is always stored as calibrator.joblib — pickle even inside an otherwise native artifact.

import json
import tempfile
from pathlib import Path

import numpy as np
from sklearn.datasets import make_classification

from honestml import AutoML, load_artifact, save_artifact

X, y = make_classification(n_samples=150, n_features=6, n_informative=4, random_state=0)
model = AutoML(task="binary", models=("lightgbm",), random_state=0).fit(X, y)

art = Path(tempfile.mkdtemp())
save_artifact(model.fitted_, art, model_format="native")

manifest = json.loads((art / "manifest.json").read_text(encoding="utf-8"))
print(manifest["model_type"], manifest["model_file"])  # lightgbm model.txt
print(np.array_equal(load_artifact(art).predict(X), model.predict(X)))  # exact round-trip

Slim serving with honestml[inference]

The inference extra installs only what loading and predicting needs (numpy, pandas, polars, scikit-learn, pydantic, joblib) — no optuna, no boosting libraries, no training-only dependencies. import honestml pulls just the pure core, and load_artifact(...).predict(...) resolves lazily, so the serving process never executes the training stack. A native boosting artifact records its required_extra in the manifest; the serving box then additionally needs that one library (for example pip install lightgbm), and a missing runtime surfaces as MissingDependencyError before any deserialization.

pip install "honestml[inference]"   # slim runtime: load artifacts + predict

from honestml import load_artifact

loaded = load_artifact("artifacts/churn-v3")
predictions = loaded.predict(X_new)

ONNX export

export_onnx(model.fitted_, directory, sample=...) converts a supported estimator (linear, lightgbm, xgboost, catboost) into an export-only bundle for external runtimes: model.onnx, schema.json, onnx_manifest.json and a README.md. A boosting model trained with native categorical features is the exception — it cannot be represented as a numeric-design-matrix graph, so export_onnx raises NativeCategoricalONNXUnsupportedError before writing anything; keep such models in joblib or native format. The graph is the raw estimator over the numeric design matrix — it contains neither preprocessing (the consumer reproduces the design matrix from the bundled schema: columns in the manifest's feature_order, categoricals fed as ordinal codes) nor calibration (disclosed in the manifest). sample is required because the export is parity-gated: before any file is written, the converted graph is compared against the native estimator on that sample, and a divergence raises instead of shipping a silently wrong graph — only a near-tie label flip within float32 noise is downgraded to a WARNING and recorded in the manifest. The bundle is one-way: it is not loadable back into honestml, so round-trips stay with save_artifact.

pip install "honestml[onnx]"   # onnx, onnxruntime, skl2onnx, onnxmltools

from honestml import export_onnx

parity = export_onnx(model.fitted_, "onnx_bundle", sample=X[:100])
print(parity)   # e.g. {'proba_max_abs': 1.2e-07, 'label_verdict': 'ok', ...}

Where the model came from and how it was selected is recorded next to it — see run reports and experiment tracking.