MLOps
Model Packaging and Runtime Contracts
Package models with explicit signatures, dependencies, initialization behavior, resource needs, and compatibility guarantees.
By the end you can
- Define the contents and boundaries of a deployable model package
- Specify input, output, initialization, and error contracts for inference
- Choose between portable formats, framework-native artifacts, and custom runtimes
- Validate package compatibility across environments and serving modes
Example
The class labels moved but the tensor shape stayed valid
A ten-class classifier is exported to a portable runtime. The probability vector that comes back is a row of numbers. Nothing inside it says which column is which class. That mapping belongs to the fitted estimator, not to the array. scikit-learn's documentation says so for LogisticRegression.predict_proba: “Returns the probability of the sample for each class in the model, where classes are ordered as they are in self.classes_.” The same page adds that “The returned estimates for all classes are ordered by the label of classes”. The order is the model's, never the caller's.
- Matching shape: The output is still a vector of length ten. Every check on rank, length and data type passes. None of them can see a label.
- Changed mapping: The serving application reads position three against an alphabetically sorted label list. The scores were written in the order held in the estimator's classes_ attribute, and that is the only order they were ever written in.
- Silent failure: Every score lands on the wrong class. The request succeeds, the latency is normal, and the monitoring on shape and dtype stays green.
- Missing contract: The class vocabulary lived in a separate repository with no immutable link. The position-to-label map could be edited without touching the artifact that depends on it.
- Repair: Bind the vocabulary into the package. sklearn-onnx, the ONNX exporter for scikit-learn, does this by default through its ZipMap: “By default, sklearn-onnx converts that matrix into a list of dictionaries where each probabily is mapped to its class id or name”. The exported artifact emits an output_label output alongside it, and an option emits the class labels as a separate array.
A weight file is not a deployable contract
A serialized model loads successfully and produces nonsense, because serving applies a different normalization and a different class order. The bytes are intact. The package omitted the assumptions required to interpret them.
Packaging converts learned state into an operational component. It should declare how inputs are validated, how preprocessing is bound, what outputs mean, which runtime is required, and how initialization or failure behaves.
The Python documentation is blunt about the format most weight files use. “The pickle module is not secure. Only unpickle data you trust.” It adds that malicious pickle data can execute arbitrary code during unpickling. scikit-learn's model persistence page repeats the warning: loading a pickle “can execute arbitrary code”, and “You should never load a pickle file from an untrusted source”. joblib says the same of its own loader: “arbitrary Python code can be executed when loading a serialized object with joblib.load()”. Three independent projects point at the format itself, not at one library that got it wrong.
There is an incident record behind the warning. Two malicious models sat on Hugging Face, stored as PyTorch pickle archives compressed with 7z instead of the default ZIP. The compression was the point: Hugging Face's Picklescan could not parse the archive. Inside was a platform-aware reverse shell to the hardcoded address 107.173.7.141, placed at the start of the pickle stream so that it executed before the reader reached the broken portion. ReversingLabs disclosed the pair as nullifAI on 6 February 2025. Hugging Face removed the models in under 24 hours, and Picklescan was updated to handle broken pickles. The disclosure states the mechanism plainly: “Pickle is considered an unsafe data format, as it allows Python code to be executed during ML model deserialization.” A model file from an unverified source is code, and it runs on load.
The platform-side scanner is not the contract either. Sonatype disclosed four CVEs in picklescan on 11 March 2025 — CVE-2025-1716, CVE-2025-1889, CVE-2025-1944 and CVE-2025-1945 — all addressed as of picklescan 0.0.23. This is the scanner Hugging Face runs over uploaded pickles. The GitHub Advisory Database rates the first of them CVSS 5.3 Moderate, published on 3 March 2025, affected versions up to 0.0.21, patched in 0.0.22. The flaw was that pip was not treated as an unsafe global, which let a model install a malicious PyPI package through pip.main(). Hugging Face's own pickle scanning documentation had already disclaimed the guarantee: “this is not 100% foolproof. It is your responsibility as a user to check if something is safe or not.” Provenance is part of the package contract. No scanner supplies it on your behalf.
Visual
A deployable package carries more than parameters
The package should make its assumptions executable. Five layers have to travel together. The learned state: weights, trees, vocabularies, calibration or index snapshots. The transformation logic: required preprocessing, tokenization, postprocessing and class mapping. The interface contract: input and output schemas, batch semantics, errors and version compatibility. The runtime contract: framework, libraries, hardware capabilities, memory and initialization. The evidence metadata: training origin, validation scope, limitations, owner and artifact digest.
Every layer left outside the package becomes a thing that can move without the artifact noticing. The class vocabulary in the case above is the transformation layer taken out and stored somewhere editable.
Learned state
Weights, trees, vocabularies, calibration, or index snapshots.
Transformation logic
Required preprocessing, tokenization, postprocessing, and class mapping.
Interface contract
Input and output schemas, batch semantics, errors, and version compatibility.
Runtime contract
Framework, libraries, hardware capabilities, memory, and initialization.
Evidence metadata
Training origin, validation scope, limitations, owner, and artifact digest.
Package boundaries determine what can drift independently
Bundling preprocessing with the model reduces one class of skew. It also makes the feature logic harder to share and to inspect. Keeping transformations outside improves reuse, but only under strict compatibility contracts.
There is no universal boundary. The right package groups components that must change together, and exposes stable interfaces for the components allowed to evolve independently. The question a boundary answers is not aesthetic. It decides which pieces can be replaced by someone who never reads your training code, and which will break silently when they are.
Comparison
Packaging strategies trade portability for control
The format should fit the runtime and the evidence requirements. A framework-native artifact preserves framework behavior and custom components. It is the fastest path for a known runtime, and it carries a large dependency surface whose custom code can be hard to audit. A portable graph or standard format targets multiple runtimes through a constrained operator set, at the price of conversion validation and fallbacks for unsupported operators. A custom serving package combines model, transformations, policies and optimized kernels for maximum control, and takes on a higher maintenance and security burden in exchange.
One of these boundaries has been audited in public, which makes it worth reading closely. EleutherAI, with Hugging Face and Stability AI, commissioned Trail of Bits to assess the safetensors library. The engagement ran from 20 to 24 March 2023 — two consultants, two person-weeks — and the final report was delivered on 3 May 2023. It recorded ten findings: 0 high, 3 medium, 3 low, 3 informational and 1 undetermined, nine of them in the category Data Validation and one in Patching. None of them was an arbitrary-code-execution flaw in the serialization format itself. Hugging Face and EleutherAI announced the result jointly on 23 May 2023: “No critical security flaw leading to arbitrary code execution was found.”
The interesting part sat immediately beside the format. Finding 8 was “PyTorch conversion utility is vulnerable to arbitrary code execution”. Finding 9 was “Python dependencies are not semantically versioned”. The bytes were cleared. The tool that produced them, and the dependency pins around it, were not. That is the packaging boundary this section is arguing about, drawn by an auditor rather than by a preference. Choosing a safe format buys you the format. Everything you bundled next to it still needs its own contract.
Framework-native artifact
Preserves framework behavior and custom components.
- Fastest path for known runtime
- May carry large dependency surface
- Custom code can be difficult to audit
- Use when framework fidelity matters
Portable graph or standard format
Targets multiple runtimes through a constrained operator set.
- Improves deployment portability
- Requires conversion validation
- Unsupported operators need fallback
- Use when runtime diversity matters
Custom serving package
Combines model, transformations, policies, and optimized kernels.
- Maximum control over behavior
- Higher maintenance and security burden
- Interface must be documented carefully
- Use for specialized latency or hardware needs
Steps
Write an inference contract
The contract should let a consumer integrate the package without reading training code. Five moves, in order.
1. Define input semantics — names, shapes, units, missingness, ranges and batch behavior. You do not have to invent the envelope. KServe's Open Inference Protocol (V2) already specifies six required HTTP endpoints: GET /v2/health/live, GET /v2/health/ready, GET /v2, GET /v2/models/<model_name>[/versions/<model_version>], GET /v2/models/<model_name>[/versions/<model_version>]/ready and POST /v2/models/<model_name>[/versions/<model_version>]/infer. The version segment in brackets is explicitly optional. NVIDIA Triton Inference Server implements that protocol rather than a private one: “Triton exposes both HTTP/REST and GRPC endpoints based on standard inference protocols that have been proposed by the KServe project.”
2. Bind transformations — package or version preprocessing, vocabularies and postprocessing. The ZipMap in the case above is the move: the label vocabulary goes into the exported artifact instead of staying an index convention.
3. Define outputs and errors — labels, score meaning, abstention, invalid input and timeout behavior. Note what the protocol above separates. A model-ready endpoint and a model-metadata endpoint are distinct from the inference endpoint, because "can this thing answer" and "what does this thing accept" are different questions from "here is a request".
4. Declare runtime needs — dependencies, hardware, memory, initialization and concurrency assumptions. An ambiguously resolved dependency name is a live failure mode of this step, not a hypothetical one. Between 25 and 30 December 2022, a malicious torchtriton package sat on PyPI under the same name as the one on PyTorch's own nightly index. The project's advisory is direct: “PyTorch-nightly Linux packages installed via pip during that time installed a dependency, torchtriton, which was compromised on the Python Package Index (PyPI) code repository and ran a malicious binary.” The binary read /etc/hosts, /etc/passwd, $HOME/.gitconfig, $HOME/.ssh/* and the first 1,000 files in $HOME. It sent them out over DNS queries to *.h4ck[.]cfd. ReversingLabs analysed the same attack on 4 January 2023 and added that the exfiltrated data was scrambled but not encrypted, and that the malicious package saw roughly 2,500 installations. The fix was a naming fix: PyTorch renamed the dependency to pytorch-triton and registered a dummy package on PyPI so the substitution could not recur. Five days, one unpinned name.
5. Validate compatibility — run semantic fixtures and resource tests in every supported target, not only in the one you built on.
1. Define input semantics
Specify names, shapes, units, missingness, ranges, and batch behavior.
2. Bind transformations
Package or version preprocessing, vocabularies, and postprocessing.
3. Define outputs and errors
State labels, score meaning, abstention, invalid input, and timeout behavior.
4. Declare runtime needs
Record dependencies, hardware, memory, initialization, and concurrency assumptions.
5. Validate compatibility
Run semantic fixtures and resource tests in every supported target.
Key idea
Successful conversion is not behavioral equivalence
An exported graph can load and execute while changing numerics, unsupported preprocessing, dynamic shapes or edge-case behavior. Conversion success is a build result. Equivalence is an evaluation claim.
There is a measured base rate for the gap between the two. A study presented at ISSTA 2024 surveyed 92 engineers and analysed 200 ONNX converter issues in PyTorch and TensorFlow. Jajal and eight colleagues report both numbers in the abstract: “We find that the node conversion stage of a model converter accounts for ∼75% of the defects and 33% of reported failure are related to semantically incorrect models.” Louloudakis and colleagues, an independent group, restate the same two figures at EASE 2025.
Read the second figure against your test suite. A third of the reported failures were not build errors that a pipeline would have caught. They were models that converted, loaded, ran and returned an answer of the right shape — and meant something else. No exception was raised for anyone to catch.
Validate representative and adversarial fixtures, batch shapes, missing values, extreme values, latency, memory, and threshold decisions in the target runtime.
A package is accepted only after the target runtime demonstrates the declared behavior: a third of reported conversion failures are models that loaded and ran.
The package is a promise to its consumers
A consumer should know what the model accepts, what it returns, which failures are expected, and which version combinations are supported. Hidden assumptions become production incidents.
Treat the inference contract as a versioned public interface even when the only consumer is another internal team.
MLflow turns that promise into a file layout. Its documentation calls an MLflow Model “a standard format for packaging machine learning models that can be used in a variety of downstream tools”. One saved model carries several flavors, and each flavor is what a particular tool knows how to read. Databricks documents the same convention. The convention travels with the artifact, which is the whole point of having one.
“Which version combinations are supported” is a solved shape too, not an aspiration. ONNX separates three versioned entities: IR version, operator-set (opset) version per domain, and model version. Its versioning document defines the middle one precisely: “An operator set represents a specific version of a domain, indicated by a pair (domain, version)”. A model declares the opsets it requires inside itself, in ModelProto.opset_import. The same document carries the released-versions table from ONNX 1.0 to 1.23.0.
The implementer answers with a window, not with a guarantee. ONNX Runtime's compatibility page states: “ONNX Runtime supports all opsets from the latest released version of the ONNX spec. All versions of ONNX Runtime support ONNX opsets from ONNX v1.2.1+ (opset version 7 and higher).” It publishes a table mapping each ONNX Runtime release to the ONNX version and opset it supports. That is what a version contract looks like when both sides write it down. A declaration carried inside the artifact, and a bounded support window published by whoever has to load it.
Key takeaways
- A model package must make preprocessing, outputs, and runtime assumptions explicit; scikit-learn orders probability columns by the estimator's classes_, so the label map belongs in the artifact.
- Package boundaries determine which components can evolve independently — the safetensors format cleared arbitrary code execution while the conversion utility beside it did not.
- Shape compatibility does not guarantee semantic compatibility: 33% of reported ONNX converter failures are semantically incorrect models, not build errors.
- Portable formats require conversion and target-runtime equivalence tests, because the node conversion stage alone accounts for about 75% of converter defects.
- Inference contracts should define valid inputs, errors, abstention, and resource needs; KServe's Open Inference Protocol and Triton already share six named endpoints for exactly that.
- Treat internal model packages as versioned interfaces with real consumers — ONNX declares IR, opset and model versions in the artifact, and ONNX Runtime publishes the window it will accept.