Repository navigation
Conversation
Change map
Requirement: a stable bundle id tied to the scoring files, and a record of the training table, snapshot, source tables, engine build, timing and shipped fit mode that survives without scratch. Smallest change: reuse the existing verifier's digest and add one listed file; |
33f889d to
b1a607d
Compare
d106960 to
3c40f67
Compare
75ef25b to
a3ce235
Compare
d6a5b87 to
6afff95
Compare
a3ce235 to
70a332a
Compare
6afff95 to
02b13a4
Compare
70a332a to
48367fa
Compare
02b13a4 to
2ab9712
Compare
48367fa to
4e5b58d
Compare
2ab9712 to
996fa16
Compare
4e5b58d to
8bc9770
Compare
f7c62c3 to
7858ace
Compare
A fitted model bundle had no stable id and recorded no fit time, seed, trials, library versions or parent ids. verify_model_bundle already computed a 16-hex content digest that every caller discarded. verify_model_bundle now returns the 64-hex bundle_id: sha256 over the sorted [relative_path, sha256] pairs of the verified files (plus an unlisted training-spec.json, as before), excluding provenance.json. TrainingResult.bundle_id and Trainer.bundle_id carry it from fit and load; older bundles get an id at load without being rewritten. A new kumo-core add_bundle_file lists a written file in the publication manifest and replaces the engine's own training-spec amend. Fit writes provenance.json (role provenance, integrity-covered, outside the id): parents (training table id, snapshot id, warm-start bundle id), seed, fit times, selected trial, per-trial status/seconds/best epoch/val metrics/varying plan params, environment and scheduler. TrainingResult.provenance reads it only when listed. Provenance and id failures warn; the bundle still publishes. Verified with new tests for the id formula and exclusion, add_bundle_file, a two-trial fit's provenance, id stability across load and copy, a bundle without provenance loading and predicting identically, a stray unlisted provenance file, a failing provenance write and the warm-start parent id; plus the registry, compatibility, warm-start, holdout, snapshot, carry- forward, explanation, training-table export, Databricks and Snowflake registry suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Any bundle-id failure now warns instead of only OSError/ValueError. Provenance drops the always-null training-table content digest and records a training_table object (id, derived_from, modified) copied from the training-table manifest. Missing trial logs yield an empty trials list so parents, seed, times and environment are still written. Single-element trial params are unwrapped. new_manifest accepts a null id. Verified with the bundle provenance (incl. a new missing-trial-logs test), trainer registry, warm start and kumo-core artifact suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Training results built without a bundle_id field (test doubles) made the property raise; return None for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
metrics.json holds test_* (the fit model) and test_refit_* (the
refit_trainval model) and nothing says which, if any, belongs to the
shipped checkpoint; a refit_full model has none of its own.
Record published_model ('fit', 'refit_trainval' or 'refit_full') from the
best trainer's refit flags, the order kumo-core publishes in; null when
unknown. metrics.json is unchanged.
Verified with a two-trial fit and each combination of refit flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#55 no longer records derived_from or modified for a reloaded table, so provenance.json's training_table block stopped having anything to copy. The block now records the table id, whether it was generated (pql) or loaded (custom), the snapshot it was generated on or validated against, and, for custom tables, the location and data version they were loaded from. The bundle stays self-contained without the scratch staging directory; query and plans stay only in training-spec.json. Verified with test_bundle_provenance.py (pql fit, custom-table stub, manifest-less stub) and test_trainer_registry.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seed copied model/config.json, bundle_format_version restated training-spec.json's spec version, and trials scraped AutoML-internal status, metric and plan files that experiment tracking already records. All three are dropped; selected_trial stays, read from the bundle's own model/config.json, and write_provenance no longer needs the run's home directory. The training_table block of a custom table now copies connector, location and version from the table manifest's parents.source_table, the same keys the snapshot manifest uses for its sources. Verified with test_bundle_provenance.py and test_trainer_registry.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
published_model in provenance.json held the shipped checkpoint's fit mode while TrainingResult.published_model is the registry record; it is now fit_mode. selected_trial indexed AutoML log directories that the bundle does not ship, numbered differently from fit events, and had no reader; it is dropped, as is training_table.id, which repeated parents.training_table. The fit snapshot's parents.source_tables (connector, location, version per table) is copied into provenance.json, so a bundle still says which source versions it was trained on after the scratch index is gone. Trainer.provenance returns the record for a fitted or loaded trainer. Verified with test_bundle_provenance.py, test_trainer_registry.py and test_warm_start.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The provenance tests fitted their own copy of the shared fitted-model fixture, restated manifest fields, tested the private fit-mode helper case by case and spread one back-compat scenario over three tests. Reuse the conftest fitted fixture, merge the load/back-compat tests, fold the missing-manifest case into the custom-source stub test, reduce the kumo-core bundle-id test to the provenance-exclusion contract, drop the fit-mode helper test, find a listed bundle file through one listed_file helper, and shorten docstrings. Behaviour is unchanged. Verified: test_bundle_provenance, test_trainer_registry and kumo-core test_artifacts pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add_bundle_file inlined its own atomic write, the engine re-walked the trainer's refit flags to name the published fit mode, and provenance.json repeated the fit snapshot that training-spec.json already records as snapshot_id. add_bundle_file now uses kumo_core.io.filesystem.atomic_write_bytes. kumo_core's Trainer gains published_fit_mode(), the order _set_fit_mode_to_use already used, and fit records it as fit_mode. provenance.json drops parents.snapshot; training_table.snapshot stays because a table may be generated on a different snapshot than the fit's. training_table.source is DiskGraph's source_mode (pquery or custom_tt). Verified with the bundle provenance, trainer registry, warm start, training-table export, snapshot manifest and kumo-core artifact/training suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fit re-read and re-hashed every bundle file to compute bundle_id right after publication and add_bundle_file had hashed them; on a bundle with a 1 GiB checkpoint that is about 0.76 s of SHA-256 per fit. kumo-core gains bundle_id_from_manifest, the same id formula over the digests the publication manifest already lists; verify_model_bundle uses the shared formula and still fully verifies bundles on load and fetch. Verified with the kumo-core artifact suite (the two ids agree) and the bundle provenance and warm-start suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
provenance.json picked connector, location and version from the training table's source entry and dropped version_kind and Custom SQL dependencies that the training-table manifest now records. The whole entry is copied, so provenance mirrors the training-table manifest the way source_tables mirror the snapshot manifest. Verified with the bundle provenance and training-table export tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
provenance.json recorded "id": null, so a provenance file copied out of its bundle (logged to a tracker or registry) could not be matched to the bundle it describes. The id excludes provenance.json, so it is now computed first and written as the provenance id. Verified with test_bundle_provenance.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TrainingResult.provenance and Trainer.provenance returned a dict. They now return a BundleProvenance (parents, fit timing, the training table record and source tables) built on the shared manifest dataclasses. provenance.json on disk and the bundle id are unchanged; the training table record still writes its source table's keys inline. Verified with test_bundle_provenance.py and test_warm_start.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dels Bundle provenance copied parts of the training table and snapshot into bespoke fields (training_table source/snapshot, source_tables) and named the warm-start model by id only. - ModelManifest (was BundleProvenance), kind model, file _provenance.json: parents.training_table embeds the training-table manifest (with its snapshot), parents.warm_start the warm-start model's manifest with its own model parents cut to stubs, so retrain chains stay flat; a missing parent is a stub. The bundle id still leaves the manifest out (old and new names). - fit(mlops=): with MLOps on, the bundle is stored under <root>/models/<bundle id>/; a complete copy is kept, a partial one replaced; failures warn. Each fit returns a new TrainingResult (.id, .manifest); Trainer.result and Trainer.manifest expose it. Verified: test_bundle_provenance.py (manifest contents and round trip, stubs, flat warm-start chains, new result per fit, root store and repair, write failure), warm start, registry, holdout and kumo-core artifact suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4274048 to
1320948
Compare
Order per the series: a plain export_config publish first, then the artifact-root store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 1
packages/kumo-engine/docs/mlops.md: 1
48-48:Minor · Maintainability & Code Quality · Quick win State that artifact-root storage can fail without failing the fit.
_store_modelwarns and letsfitreturn when an upload fails. This sentence can lead a user to treat a successful fit as proof that the bundle is durable under the artifact root. State that storage is attempted and that users must check its warning or confirm the stored copy when durability matters.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-training-table-manifest
- File: .coderabbit.yaml
- Commit: 732f4c3
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
03cd3b4e-0949-41c8-b95f-5f90b1388acd
Files selected for processing: 9-
packages/kumo-core/src/kumo_core/artifacts.pypackages/kumo-core/tests/test_artifacts.pypackages/kumo-engine/docs/mlops.mdpackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/_manifest.pypackages/kumo-engine/kumo/engine/trainer/_provenance.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_bundle_provenance.pypackages/kumo-engine/test/test_warm_start.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 0 remain after this review.
-
| if remote: | ||
| filesystem.delete_dir_contents(root, missing_dir_ok=True) | ||
| upload_bundle(filesystem, root, str(result.bundle_dir)) |
There was a problem hiding this comment.
🔎 Supported by static analysis
-
Script executed:
sed -n '195,245p' packages/kumo-engine/kumo/engine/trainer/trainer.pyRepository: NVIDIA/kumo-engine
Length of output: 2123
Script executed:
# Locate upload_bundle and the bundle-id computation, then show their bodies. rg -n -F -e 'def upload_bundle' -e 'def compute_bundle_id' -e '_provenance.json' -- packages/ || test "$?" -eq 1 for f in $(rg -l -F -e 'def upload_bundle' -- packages/ 2>/dev/null); do echo "== $f" ast-grep outline "$f" --view expanded done
Repository: NVIDIA/kumo-engine
Length of output: 5838
Script executed:
sed -n '20,62p' packages/kumo-core/src/kumo_core/object_store_sink.py echo "== artifacts.py bundle id / provenance exclusion" rg -n -F -e 'PROVENANCE_FILE' -e 'def ' -- packages/kumo-core/src/kumo_core/artifacts.py || test "$?" -eq 1
Repository: NVIDIA/kumo-engine
Length of output: 3133
Stage the bundle overwrite; delete stale files only after a successful upload.
_store_model deletes the remote copy before it uploads. A remote copy with the same bundle ID but a different _provenance.json size fails the size comparison, so the code deletes it. Provenance is excluded from the bundle ID, so the model files in that copy are complete and loadable. If the upload then fails, the warning leaves models/<bundle id>/ empty or partial, and the loadable copy is gone.
Uploading over the existing files first keeps a loadable copy in place during the upload. The model files have the same digests, so overwriting them does not change what loads. _provenance.json is uploaded last. Remove remote files that are absent locally only after the upload succeeds.
🐛 Suggested fix
-
--- "a/packages/kumo-engine/kumo/engine/trainer/trainer.py" +++ "b/packages/kumo-engine/kumo/engine/trainer/trainer.py" @@ -226,11 +226,11 @@ FileSelector(root, recursive=True, allow_not_found=True)) if info.type is FileType.File } if remote == local: return - if remote: - filesystem.delete_dir_contents(root, missing_dir_ok=True) upload_bundle(filesystem, root, str(result.bundle_dir)) + for name in set(remote) - set(local): + filesystem.delete_file(f'{root}/{name}') except Exception as error: warnings.warn( f"Could not store model {result.bundle_id} under the artifact "
Committable suggestion
-
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.Suggested changeif remote: filesystem.delete_dir_contents(root, missing_dir_ok=True) upload_bundle(filesystem, root, str(result.bundle_dir)) upload_bundle(filesystem, root, str(result.bundle_dir)) for name in set(remote) - set(local): filesystem.delete_file(f'{root}/{name}')
|
Folded into #50 (artifacts & provenance), which now contains this PR's final code: the consolidated stack shows only the end state. Review continues there. |
Series: kumo-engine MLOps series: every snapshot, training table, model and prediction run carries a self-contained _provenance.json;
with kumo.mlops(...)stores each artifact under one artifact root, tracks fits and prediction runs in MLflow or Snowflake and registers models; volumes and stages can hold files; prediction runs are monitored for drift and realized performance.Stack: Base (lineage, MLOps switch): #50 → #55 → #56
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds a stable
bundle_idand a self-contained_provenance.jsonmanifest to every model bundle.Summary
This PR adds a stable
bundle_idand a self-contained_provenance.jsonmanifest to every model bundle. The id says which fitted model a bundle is. The manifest records what the model was trained from and how: the training table's manifest from #55 (which embeds the snapshot's from #50, with its source table versions), the warm-start model, which fit mode was shipped, timing and engine build. With MLOps on (#50), each bundle is also stored under the artifact root.Problem. Teams running Kumo in production need to answer questions like these:
metrics.json, if any, belong to the model that was shipped?Today none of these can be answered from the bundle.
Cause. A bundle has no id.
verify_model_bundlealready computed a 16-hex content digest on load, but every caller discarded it. The bundle holds only what scoring needs:None of these files record:
fit(trained on train),refit_trainval(retrained on train+val) andrefit_full(retrained on train+val+test). The bundle ships the one trained on the most data that was enabled:refit_full, elserefit_trainval, elsefit.metrics.jsonhastest_*keys, which are thefitmodel's, andtest_refit_*keys, which are therefit_trainvalmodel's (_refit_metricsin kumo-core's training orchestration). Nothing says which of them, if any, belongs to the shipped model. Forrefit_fullnone do: it trained on the test split, so it has no test metrics of its own.Change. Fit now writes
_provenance.json(the series' one manifest name) and lists it in the publication manifest with roleprovenance. Nothing else in the bundle changes:Example from a fit on a three-table synthetic graph (
users200 rows,items20,orders1,000; queryPREDICT COUNT(orders.*, 0, 14, days) > 0 FOR EACH users.USER_ID), captured from this branch. Hashes are shortened, environments elided, and the embedded snapshot'ssource_tablesshown once:{ "kind": "model", "schema_version": 1, "id": "55dded33…", "parents": { "training_table": { "kind": "training_table", "schema_version": 1, "id": "tt-6a9795e8…", "parents": {"snapshot": { "kind": "snapshot", "schema_version": 1, "id": "01d8ad46…@b0d7459155debdeb", "parents": {"source_tables": {"orders": {"connector": "LOCAL_FILE", "location": "<data>/orders.parquet", "version": "1792862945", "version_kind": "timestamp"}, "...": "…"}}, "record_sources": true, "environment": {"...": "…"}, "created_at": "2026-10-24T17:38:34+00:00"}}, "query": "PREDICT COUNT(orders.*, 0, 14, days) > 0 FOR EACH users.USER_ID", "generation_plan": {"split": "inferred", "timeframe_step": 14, "...": "…"}, "task_type": "binary_classification", "split_seed": null, "environment": {"...": "…"}, "created_at": "2026-10-24T17:38:34+00:00"}, "warm_start": null}, "fit": {"started_at": "2026-10-24T17:38:34+00:00", "finished_at": "2026-10-24T17:38:50+00:00", "seconds": 15.291}, "fit_mode": "refit_trainval", "environment": {"kumo_engine": "1.0.0.dev0", "build_commit": "13209487", "...": "…"}, "created_at": "2026-10-24T17:38:50+00:00" }fitreturned aTrainingResultwithid=bundle_id=55dded33…andmanifest= the record above. Withfit_mode: "refit_trainval", thetest_refit_*keys in the unchangedmetrics.jsonare the shipped model's.What each field records:
parents.training_tableis the training table's Record training-table lineage across export and reload #55 manifest, embedded. It includes the generating snapshot's manifest with its source tables' connector, location and version, and, for a table loaded withfrom_source_table, the source table the rows came from. The bundle therefore stays self-contained once scratch is gone. Embedded copies leave out statistics and fields written later. If the table has no manifest, a stub{kind, id, reconstructed: true}takes its place.parents.warm_start, forfit(warm_start=...), is the parent model's manifest. Its own model parents are cut to stubs ({kind, id, reconstructed}), so a chain of daily warm-start retrains keeps each manifest the same size. A parent bundle without a manifest becomes a stub with its bundle id.fitgives the wall-clock start, end and duration offit().fit_modeis the fit mode of the checkpoint the bundle ships:fit,refit_trainvalorrefit_full. It comes from kumo-core'sTrainer.published_fit_mode(), the same function (and order) that picks the checkpoint predictions use by default. Read it withmetrics.json:fitmeans thetest_*keys describe the shipped model,refit_trainvalmeans thetest_refit_*keys do, andrefit_fullmeans none do.environmentandcreated_atuse Record provenance for every artifact under one MLOps artifact root #50's format.idis the bundle id (below). It is computed before this file is written, since the file is excluded from it.Seeds, per-trial outcomes and a bundle-format version are not recorded: nothing reads them, or another bundle file already holds them.
Artifact root. With MLOps on (
fit(mlops=...)or akumo.mlopsblock, #50), the bundle is also stored under<root>/models/<bundle id>/(upload_bundle, #112, writes_provenance.jsonlast). A copy already there with every file at the right size is kept; a partial one is replaced; a failure warns. With MLOps off nothing is stored.export_config=publishes exactly as before, independently.bundle_id.verify_model_bundlenow returns the full 64-hex id instead of a discarded 16-hex digest, and a fresh fit computes the same id with a new kumo-corebundle_id_from_manifestfrom the digests the publication manifest already lists, instead of re-reading every file (about 0.76 s of SHA-256 per fit for a bundle with a 1 GiB checkpoint, now about 0). Loading and fetching still verify every file fully. The id is the SHA-256 of the sorted[relative_path, sha256]pairs of every verified file in the publication manifest, plustraining-spec.jsonif an older manifest left it unlisted. The manifest file is excluded (both_provenance.jsonand the earlier nameprovenance.json). Its timings and environment fields change from run to run without changing which model the bundle is. It is still size- and digest-checked like every other listed file. Role names are no longer part of the payload.model/config.jsoncarry run-specific paths. Equal ids therefore mean "the same fitted artifact", not "equal weights from two runs".Reading:
fitreturns a newTrainingResultper call. It hasid(the bundle id),bundle_idandmanifest(aModelManifest, exported askumo.engine.ModelManifest, orNone).Trainer.resultis the latest fit's result, or the loaded bundle's.Trainer.bundle_idandTrainer.manifestread it. All are set at fit and atTrainer.load.test_manifest_round_trips_unchanged).add_bundle_file(bundle_dir, role, relative_path)lists a file in the publication manifest. It replaces the engine's own training-spec amend.Trainer.published_fit_mode()returns the fit mode predictions use by default (refit_full, elserefit_trainval, elsefit;Nonebefore fitting).Back-compat
This PR only records and exposes information. It adds no new checks or errors on any existing path, and predictions are unchanged.
_provenance.jsonwritten and listed.bundle_idset on the result and the trainer.metrics.jsonis unchanged.Trainer.loadbundle_idis computed at load from the files it already has.manifestreturnsNone. Predictions are identical._provenance.jsonis an extra listed role. The older loader verifies its digest and ignores it (checked against the v1.0.0 loader).bundle_idasNone.schema_versionmanifestreturnsNone. Unreadable or newer: reads asNonewith a warning.Testing Done
test_artifacts.py: the manifest file (new and old name) is excluded from the bundle id, and the id from the publication manifest equals the verified id.test_bundle_provenance.py(7, behaviour tests):snapshot.manifest;idequals the bundle id on the result and trainer;Trainer.loadgives the sameresult.idandmanifest;kumo.mlopssettings object stores the bundle once and repairs a damaged copy, while a block that does not export models stores nothing;test_warm_start.pyasserts the embedded warm-start parent.test_trainer_registry.py(27),test_semantic_carry_forward.py(20),test_holdout_df.py(11) and kumo-coretest_artifacts.py(10).Checklist
🤖 Generated with Claude Code