Skip to content

Bundles

A bundle contains all the results of a fit that scoring needs. Its form is safe to store, move and read again. A bundle is one bundle.zarr.zip and four JSON sidecars.

fruit-demand-v1/
├── bundle.zarr.zip      # one zarr v3 zip: the whole fitted model as an xr.DataTree
├── manifest.json        # identity, provenance, lineage and the hashes of every file
├── metrics.json         # holdout metrics (computed through the score path)
├── diagnostics.json     # convergence and sanity checks, expert and plain-language
├── plots.json           # rendered, sanitized plots for outputs.plots
└── evaluation/          # evaluate_then_refit: the train-only bundle behind the metrics

Write, read, verify

import tempfile
from pathlib import Path

from mapi_sdk import Bundle

out = Path(tempfile.mkdtemp()) / "fruit-demand-v1"
manifest = bundle.write(out)
print(sorted(p.name for p in out.iterdir()))
# ['bundle.zarr.zip', 'diagnostics.json', 'evaluation', 'manifest.json', 'metrics.json',
#  'plots.json']  - `evaluation/` is the train-only fit that produced the holdout metrics

again = Bundle.read(out, registry)  # every file's size and hashes are verified here
assert again.bundle_id == bundle.bundle_id

Bundle.read calculates the size, CRC32C, MD5 and SHA-256 of each file again. Then it compares them with the manifest. If a bundle is damaged or changed without permission, Bundle.read raises BundleError (INTEGRITY_FAILURE).

What is inside

print(manifest.role, manifest.plugin["id"], manifest.spec_hash[:19])
# production tabular.regressor sha256:...
print(manifest.files["bundle.zarr.zip"])  # size, crc32c, md5 (GCS forms) and sha256
print(manifest.lineage["evaluation_bundle"])  # the train-only fit behind the metrics

tree = again.to_tree()  # the whole bundle as one xr.DataTree
print(sorted(tree.children))
# ['binding', 'fit_coords', 'model', 'native', 'preprocess', 'schema', 'spec']
print(sorted(tree["preprocess"].children))  # ['cal', 'logy']: one node per step
Node Holds
/spec The canonical spec and the resolved spec (pinned versions, the schema of each stage)
/schema/raw, /schema/model The data contract at the input and at the model stage
/binding Canonical ↔ dataset names, reversed on each output
/preprocess/<step> For each step: the learned JSON state, labelled arrays and the context tail for each group
/model The manifest, settings, fit config and learned JSON
/model/<group> ArviZ-named groups: posterior, sample_stats, log_likelihood, observed_data, …
/model/fit_history Fits in steps (ADVI): loss(step), the loss of each optimization step, at most 5,000 steps
/model/metrics_history v0.11: the metrics, params and tags that the model logged during the fit (see below)
/terminal_state Sequential models: the filtered state at the last fitted step. For example, a forecast of ts.bsts (v0.13) starts from it.
/native Safe native artifacts: skops files, xgboost JSON and, from v0.15, the safetensors weights of a network
/derived/<name> Derived quantities cached at fit (contributions, response curves)
/fit_coords/<stage> The fitted coordinates at each stage (CoordPlan checks against them)
/side_tables/<name> pinned side tables, frozen with the fit
/thin The thinning index for draws, for the full bundle

The model groups of a fit in steps and of a state model

Some plugins keep more groups under /model. The standard plots read them:

Group Plugins Holds Plot
fit_history PyMC models with engine: advi loss(step): the negative ELBO of each step. The var attrs give objective and engine. When the fit has more steps than 5,000, the dataset attr thinned_from gives the number of steps. core.loss_curve
hidden_states ts.gaussian_hmm state_prob(chain, draw, animal, step, state) core.state_sequence
regimes statsmodels Markov switching smoothed_prob(time, regime), filtered_prob, the transition matrix core.state_sequence
components unobserved components, ETS, ts.hsgp_trend, ts.bsts (v0.13) One variable for each part of the series. A variable with the attr decomposition: exclude (a trend slope) is not an additive part. core.decomposition

Each data variable of fit_history is one curve over step. The other dims of a variable are labels: the plot draws one line for each level.

The groups of the model packs v0.12

The marketing-mix, causal and deep-learning models keep more groups under /model. Their plots and workflow nodes read these groups, not the posterior:

Group Plugins Holds Read by
lift_tests (v0.12) mmm.bayesian, when the fit used lift tests Each lift test (observed(test), observed_sd(test)) and the estimate of the model for each draw (model(chain, draw, test)) mmm.lift_tests
impact (v0.13) causal.impact, causal.synthetic_control, causal.did actual, counterfactual, effect, cumulative_effect and post, in the units of the target (Measure the effect of a change) causal.impact, causal.pre_fit, model.lift_test
donor_weights (v0.13) causal.synthetic_control weight(sample, unit, donor) and the error before the test, pre_rmse(unit) causal.donor_weights
event_study (v0.13) causal.did coef(sample, event_time), n_cells(event_time) and the average effect after the change, att(sample) causal.event_study
training (v0.15) dl.mlp, dl.cnn_classifier, dl.finetune_classifier Values along epoch: train_loss, val_loss, the training and validation metric (accuracy or rmse) and lr. A classifier also keeps confusion(epoch, true_class, pred_class), and then only the epochs with a confusion matrix. The attrs give best_epoch, stopped_early, epochs_run and steps. The group keeps a maximum of 200 epochs (50 for a classifier), always with the first, the best and the last. The attr thinned_from gives the number of epochs before. ml.training_curves, ml.confusion_epochs
misclassified (v0.15) dl.cnn_classifier, dl.finetune_classifier Up to 32 validation images that the network classified incorrectly at the best epoch, as small thumbnails with the true and the predicted label ml.misclassified
posterior (v0.15) the dl.* models draw_id(chain, draw): one entry for each draw of the Laplace approximation or of MC dropout. The harness thins the draws of a score request from this group. the score path

The dl.* models keep the trained network in /native: weights.safetensors and, for the Laplace draws, laplace.safetensors. These are safetensors files, not pickle. The architecture of the network is in the metadata of the weights file.

The metrics_history group v0.11

When model code logs metrics during a fit (ctx.log_metric, see Track metrics, params and tags), the fit also keeps them in the bundle, in the group /model/metrics_history. The plot core.loss_curve reads this group in the same way as fit_history. The group has this layout:

Part Content
Data variables One variable for each metric key that has steps (a maximum of 64 keys). The attr mapi_dropped_keys counts the keys that are not kept. The variable of a key named step is metric_step (var attr mapi_key), because step is the name of the dim.
Dim step int64, the steps of all the series of the key
Label dims One dim for each label key (for example chain). The coords are the label values, sorted.
Empty cells NaN where a (step, labels) cell has no point
Long series Each series keeps a maximum of 5,000 points, evenly spaced, with the last point. The var attr thinned_from gives the number of points before.
Attr mapi_scalars The points without a step, as JSON text: {key: {labels_key: value}}. labels_key is k1=v1,k2=v2, or "" for no labels.
Attr mapi_params The params, as JSON text
Attr mapi_tags The tags, as JSON text

All the attrs are text, thus the group is safe in zarr. A fit without logged values has no metrics_history group. The group of an evaluate_then_refit fit has only the values of the final fit. The evaluation fit keeps its values in the evaluation bundle.

Pareto k in metrics.json v0.10

When the fit calculates elpd_loo, metrics.json keeps the PSIS Pareto k of each observation in elpd.elpd_loo.pareto_k. The values are in the same sequence as pointwise. The fit keeps them only for data of up to 10,000 points. The fit keeps no pareto_k if one value is not finite. A k of more than 0.7 shows that the LOO estimate of that observation is not reliable. The plot core.loo_pit shows the k of each observation.

{"elpd": {"elpd_loo": {"elpd": -412.3, "se": 11.8, "p": 6.1, "n": 120,
                       "pointwise": [-3.1, -2.9, ...], "pareto_k": [0.12, 0.08, ...]}}}

No pickle, ever

Pickle runs code when it loads. Thus pickle must not be in an artifact that moves between users, services and sandboxes.

  • Model groups are labelled arrays in zarr. Settings, states and manifests are JSON.
  • Native estimators are stored as skops or xgboost JSON. When skops loads a file, it first lists the types in the file. It refuses all types that are not on the allow list of that estimator.
  • Some FitOutput.native entries look like pickle (.pkl, .joblib, .pt, pickle magic bytes, …). The platform rejects these entries when it writes a bundle and when it reads a bundle. The contract kit checks this for each plugin.

Hashes in the forms object stores report

The platform records CRC32C and MD5 in base64, the same as Google Cloud Storage calculates them on upload. The platform binds a signed manifest to the object generations and to the hashes that the server calculated. Thus a person cannot replace or replay an uploaded artifact (see Storage and integrity). The platform also records SHA-256, for content addressing.

Lineage

The manifest records the spec hash, the data hash, the version of the plugin and of each step, and the library versions of the fit. manifest.lineage links a production bundle to its evaluation bundle. Thus you can always find which data and which code made a forecast.

Next: Coord plans and typed errors.