Skip to content

Version 1 prediction state and bounded native work

Normative source: docs/format-state.md. This page renders that file unchanged; the guides explain it, this text defines it.

Normative section of the format contract. Version 1 is not frozen. Bundles lacking the required prediction-state records fail full claim comparison.

Sklearn state ownership

Skops’ type audit is necessary but does not define which attributes belong to an estimator. mlbundle additionally owns a closed class/attribute catalog in _sklearn/state_fields.json, audited against scikit-learn 1.9.1. It covers public estimator constructor parameters, inherited and fitted attributes, shared fit helpers and the admitted loss/link/MLP optimizer internals. It is package data, never supplied by a bundle and never used to import or construct a class. Unknown object types, attributes, nested estimator attributes, node keys, loaders and unreferenced files are rejected. The supplemental skops trusted list contains Tree, AdamOptimizer, SGDOptimizer, numpy.dtype and the closed functions below (other exact paths in the adapter contract). A catalog entry does not extend skops’ trust or guarantee that every configuration exports.

The audited/tested baseline is scikit-learn 1.9.1 with skops 0.16.0; the dependency floor is not a promise of compatibility with every later version. There is no version whitelist. Other versions can introduce unknown classes or state fields and fail export/load. _sklearn_version also participates in the commitment: a different installed sklearn version changes reserialized state, so loading may reject it as InvalidModelError. After a sklearn/skops InvalidModelError, safe differing declared/installed versions produce EnvironmentMismatchError with both versions and re-export/load guidance. Declarations must match a strict version pattern of at most 64 characters; otherwise the original error remains. Successful loads remain successful: this is diagnosis, not a compatibility gate. It also covers a composite’s skops prefix, without relabeling native leaf failures. Export and load should use the same sklearn version. Other matching-version configurations must satisfy all catalog, trust, dimensions and round-trip checks; they are not a tested range.

Object state must be a dictionary with one string key type per attribute; skops may otherwise truncate attributes silently. Stateless admitted objects may use the native null state. Tree constructor state has exactly node_count, max_depth, nodes, values. Constructor dimensions, buffer shapes and loaded tree reachability/indices are checked before native traversal. Tree capacity must equal node count; maximum depth is recomputed, and maximum class width is derived from the per-output class counts. Missing directions are zero or one. Estimator classes must be the exact installed native class, rather than a subclass impersonating its module and name. This rule also applies before export.

MLPClassifier and MLPRegressor retain their native fitted state for all three solvers, standalone and in pipelines. AdamOptimizer fields are beta_1, beta_2, epsilon, learning_rate, learning_rate_init, ms, t, vs; SGDOptimizer fields are learning_rate, learning_rate_init, lr_schedule, momentum, nesterov, power_t, velocities. Every field uses the same schema/reference/ array budgets and complete commitment as other sklearn state; extra fields fail. Skops 0.16.0 ObjectNode uses object.__new__ then __dict__.update for these classes. They have no custom allocation, __setstate__ or reduce hook; their constructors and training update methods are never called on load. Other loaders for these classes are rejected before skops construction. MLP structure remains null, with coefficients, intercepts, layer/output/activation state, label binarizer, random state, optimizer buffers and training/validation histories committed in model.state_sha256. Pipelines commit their complete nested state the same way.

Every admitted state field participates in a complete state commitment, including fields omitted from visualization and optional training-only state. Deserialization also compares the payload commitment with a fresh skops serialization of the loaded object. Ignored declarations or state that changes during construction are invalid models. Export applies the same rules before publication.

Adapter work belongs to one synchronous export/load, never a global cache. Sklearn validates and digests each original or freshly reserialized constructed payload once, then shares those results with claim/structure extraction before the native model leaves that operation. The received-payload versus constructed- state comparison remains mandatory; independent public calls start fresh. Tree constructor checks reuse numeric headers already checked against actual streamed bytes. Equal digest-only records and scalar encodings are memoized within that digest, with identical commitment bytes and byte ceilings.

Closed scalar and function values

_sklearn/trusted.py owns these lists for export, projection and load; bundle declarations never select or expand them. NumPy scalar type parameters are exactly bool (bool_ alias), int8, int16, int32, int64, uint8, uint16, uint32, uint64, float16, float32, float64, complex64, complex128. Aliases such as intp/int_ use their installed scalar identity/name. These classes use skops’ existing TypeNode defaults; TypeNode resolves the attribute without calling it. Projection checks object identity, not an advertised name.

Fitted dtype values admit those scalar storage representations, either byte order, plus object: categorical SimpleImputer stores an object _fit_dtype. String, datetime/timedelta, void, structured, sub-array and metadata-bearing dtypes are rejected. Numeric dtype storage must be exactly a NumPy ndarray of shape (0,) whose bounded NPY header passes the scalar list. Object dtype storage must be the canonical empty JSON ndarray, with a one-element TupleNode shape containing the JSON integer zero. DTypeNode returns the constructed array’s dtype; only DTypeNode/CachedNode with exact numpy.dtype identity are permitted. Object cells are never loaded with pickle. Native dtype validation also rejects metadata/compound values that serialization could otherwise discard.

Score functions are exactly f_classif, f_regression, chi2, r_regression in sklearn.feature_selection._univariate_selection, and mutual_info_classif, mutual_info_regression in sklearn.feature_selection._mutual_info. NumPy functions are exactly numpy.log, numpy.log1p, numpy.log2, numpy.log10, numpy.exp, numpy.expm1, numpy.sqrt, numpy.square, numpy.absolute (np.abs is the same native object and serializes as absolute). They support FunctionTransformer func and inverse_func. Other functions, including NumPy reductions, lambdas and user functions, remain rejected with unsupported names.

Skops 0.16.0 FunctionNode stores only module/name/loader/identity. Its load path uses getattr(import_module(module), name) and never calls the function. Only FunctionNode/CachedNode are permitted for these names; all other function names and alternative constructor loaders fail before skops auditing/construction. The closed list includes no I/O function. Function execution remains native to the fitted transformer at prediction time. All names, loader identities, dtype values and fitted selector statistics participate in state_sha256.

Commitment encoding

state_sha256 is a lowercase SHA-256 digest. A sklearn structure object requires this field. Sklearn models with null structure, including Pipeline, instead put the digest in model.state_sha256; CatBoost also requires a native commitment there. Composite prefix digests instead live in model.pipeline.transformers.state_sha256; the outer digest is null, with native leaf claims/state and structure verified separately. This optional manifest field defaults to null, and is part of the verified claim projection. Missing commitments on an opaque sklearn model fail full load comparison.

The commitment is a Merkle digest of the bounded skops schema and its buffers. For each dictionary, resolve CachedNode to its definition, omit __id__, replace each child with its digest, then hash b"dict:" + canonical_dumps(record). Lists hash b"list:" + canonical_dumps(child_digest_list); scalars hash b"scalar:" + canonical_dumps(value). A file child uses SHA256(b"file:" + canonical_dumps(buffer_digest)) instead of a filename. Shared definitions and files are memoized; identifiers and ZIP timestamps do not affect the result. Metadata, loader/class identity and array dimensions do.

A numeric buffer hashes canonical [dtype.str, shape] followed by contiguous row-major element bytes. Floating NaNs normalize to NumPy’s NaN before hashing; finite bits, signed zero, dtype and shape are preserved. Structured arrays hash the canonical dictionary of named-field numeric digests; unnamed padding is excluded because native Tree storage leaves it uninitialized. Sparse buffers hash the canonical dictionary of inner member names and their numeric digests. These records bind state; they do not authenticate authorship or training history.

Readable inference composition

Every sklearn structure also requires prediction_state, an object with exactly the applicable fields below. Missing fields are omitted rather than invented. Native arrays keep their shape and canonical numeric representation.

Native state Record
estimators_ estimator_labels: row-major constituent classes_ arrays, or null for a constituent without labels.
estimator_weights_, estimators_features_ The corresponding arrays under their native names.
_x_mean, _x_std Native centering/scaling vectors under their native names.
_loss or _base_loss loss: exactly class, link (class names), n_classes.
init_ initializer: literal native value, or exactly class, parameters, state_sha256 for an estimator. Dummy initializers additionally expose values, with each present classes_, class_prior_, constant_, n_classes_, n_outputs_. learning_rate is also present.
_probA, _probB probability_calibration: exactly A, B native arrays.

A custom supported initializer’s commitment binds all fitted state; it does not manufacture a visualization for that initializer. Linear coefficients are native decision/regression coefficients. Centered models and calibrated classifiers require the composition record to interpret the complete prediction.

Prediction-state coverage

The commitment covers all allowed attributes and nested buffers, not a selection inferred from attribute suffixes. The explicit checks supplement it:

Family Prediction-relevant state and coverage
DecisionTree / ExtraTree All eight node record fields, including missing routing, plus value arrays and native dimensions are in the tree record. Depth, capacity and class width are derived/checked. Parameters, names, owning output counts and labels are claims.
RandomForest / ExtraTrees Constituent trees and order are structure; parent/child output and class counts agree. Parent counts, labels and parameters are claims; remaining state is committed.
GradientBoosting Stage arrangement/counts, constituent trees, initializer, learning rate and loss/link state are structure/composition. Stage width agrees with classes; fitted loss/link class and loss class count agree with the native parameter-derived loss. Initializer feature width, dummy priors and dummy output dimensions agree with fitted state.
AdaBoost / Bagging / tree meta-estimators Trees, native order/shape, weights, feature subsets and constituent labels are structure/composition. Feature subsets address parent input widths. All other state, including output mappings, is committed.
LinearRegression / Ridge / Bayesian / sparse-penalty / robust / SGD / LinearSVR / LDA Native coefficients/intercepts are structure, parameters and labels/counts are claims, all fitted state is committed. Native probability transforms remain library-owned.
PLS / CCA Coefficients/intercepts plus centering/scaling are structure/composition; fitted output-shape flags and remaining state are committed.
GLMs Coefficients/intercepts plus loss/link are structure/composition. Fitted loss/link class and class count must agree with the parameter-derived loss.
SVC / NuSVC / SVR / NuSVR Kernel parameters, support data, native/private coefficients, sparse flag, effective gamma and calibration are committed. Linear coefficients and calibration are structure/composition when available. Public and inference dual coefficients/intercepts must agree, including sklearn’s binary sign reversal. Support counts/indices, coefficient/calibration dimensions and sparse storage agree.
Naive Bayes / QDA / neighbors / nearest centroid / MLP / semi-supervised / opaque meta-estimators Their complete fitted statistics, data, class/output mappings, distance/activation state and nested models are committed in the manifest when structure is null. Statistics/likelihood/prior dimensions agree with classes/features; neighbor sample counts agree with fitted arrays. MLP layer sizes, output counts/activation and label-binarizer classes agree with public parameters/labels. Existing probability/count/trust constraints still apply.
All-sklearn Pipeline All fitted steps/order/names/parameters are committed; structure stays null.
Composite Pipeline Prefix skops/state/parameters use the same owners as standalone sklearn. Leaf claims and complete native structure use its adapter. Shared data-free widths/names and checked tree-boundary record describe the reconstructed Pipeline; outer options/final name are declarations. See pipelines.

Unsupported custom types, functions/types outside these lists, HistGradientBoosting storage, unsupported XGBoost/CatBoost placements, multilabel/chain probability layouts and untrusted nested internals remain rejections. For example, actual fitted csr_matrix neighbor state is outside the unchanged trust list; merely declaring its loader as the trusted base spmatrix does not make its state survive the native construction comparison.

Work and allocation ceilings

Before skops’ type audit or construction: at most 100,000 schema nodes, 100,000 CachedNode references, 100,000 expanded dictionary/list visits, and nesting 32. Expanded work is calculated once per shared definition, with capped sums; cycles, missing references and conflicting numeric/scalar definitions are rejected. Sklearn parameter projection separately caps visits at 100,000 and ancestry at 16, within the existing 1 MiB/depth-16 hyperparameter contract.

Nested ZIP directories are count-checked before ZipFile creates entries: at most 100,000 outer skops entries and nine inner sparse NPZ entries. Numeric headers have rank at most 32, positive item size, no object storage, dimensions at most 512 MiB, and exact product(shape) * itemsize bytes, checked against both ZIP metadata and actual streamed bytes before NumPy loads anything. Inner sparse archives have a closed member set, ratio at most 200 and checked format, shape, index/pointer/block/coordinate/diagonal consistency. Sparse declared dense bytes are at most 512 MiB. Aggregate decoded buffers and aggregate materialized buffer references are each at most 512 MiB; repeating a file under new identities cannot bypass this bound. All these are fixed ceilings in addition to archive limits.

Canonical JSON preflight calculates expanded minimum size/depth of shared native containers with memoization before normalization, rejecting over-budget expansion and cycles. Native arrays are size-checked before tolist. Wire JSON has no references and remains bounded by its stored bytes/depth. Lexical depth and Unicode escape checks precede decoding; object hooks reject duplicate keys and invalid float tags. The C decoder handles ordinary numbers, followed by finite float checks (bulk checks for flat arrays); integers retain the 4,096-digit check. No native-value normalization or shared-container expansion pass is needed for decoded wire values.

Canonical encoding may be deferred until comparison when its byte budget is already proven: strings/integers cannot grow from their wire spelling, and a finite binary64 float has at most 24 canonical bytes for at least three wire token bytes. Thus eight times the stored byte length is a conservative bound. Otherwise the canonical encoder checks the exact size immediately. This does not raise any ceiling. Equality of validated stored bytes to freshly recomputed canonical bytes already proves exact canonical equality; otherwise comparison encodes the decoded value with the same sorted keys, types and numeric spellings.

XGBoost, CatBoost and ONNX

XGBoost’s structure additionally requires native_model, the complete native gradient-booster model returned by bounded UBJSON decoding of save_raw. Numeric buffers become arrays without decimal rounding. It includes every tree field, tree_info, iteration pointers, parallel-tree counts, category encodings, categorical splits/default directions and optional dropout weights. Learner base score and objective fields remain verified hyperparameters. Tree dumps alone are insufficient to describe this prediction state.

XGBoost retains bounded, validated immutable UBJSON snapshots within the operation. Equal payload bytes reuse preflight; structure reuse requires exact native booster byte-span equality plus feature names/types and the importance setting. Every native copy and loaded model is still checked through freshly serialized bytes. Dump JSON is decoded as one byte/depth-bounded array before canonical structure encoding. Cached canonical structure bytes are decoded afresh for each result.

Booster attribute keys are closed: mlbundle, best_iteration, best_score, scikit_learn. The last three are preserved in hyperparameters.attributes; the binding is independently checked. Best iteration is a decimal integer with at most six digits, in [0, native_round_count). Best score is finite and at most 256 characters; sklearn identity has only the native classifier/regressor key. Unknown wrapper attributes and unknown/missing native record fields fail. Native wrapper objective and supplied feature types must agree with native state. The wrapper booster is null or a supported tree alias (gbtree, or dart, which XGBoost 3.4 normalizes to gbtree). Wrapper declarations cannot reinterpret the native predictor. Native iteration increments equal num_parallel_tree * max(1,num_class). Objective and learner class counts agree; dropout weights have native tree count, are finite and nonnegative. Category indices are at most 1,000,000; split segments are checked and aggregate declared categorical bitsets are capped at 512 MiB. Category dictionary segments and sorted-index permutations agree with stored values; string offsets must address the actual stored byte vector. UBJSON has nesting 32, at most 100,000 object collection items and 1,000,000 non-buffer values; numeric buffer counts require actual stored bytes.

ONNX has no additional mutable fitted state. The CPU session is created from the exact original bytes; signatures, metadata, kind, labels and graph/environment claims are recomputed from those bytes. No initializer override, custom function, domain or model-provided session configuration is admitted. Its descriptor scan has no shared-reference mechanism, bounds fields/depth before construction, and tensor dimensions require actual stored values before a session. See the ONNX contract for execution limits.

CatBoost’s complete numeric CBM snapshot and metadata use the commitment and preflight rules in the CatBoost contract. Its mandatory structure exposes every symmetric tree without truncation.

These checks are not a native-code sandbox. Runtime-computed allocations, dependency memory safety and prediction row/CPU/process limits remain the host’s responsibility. No claim of exhaustive native-parser memory safety is made.