Repository navigation
Conversation
Change map
Requirement: a snapshot built on one machine must be usable on another, and a scoring job must be able to tell whether it is current. Existing behaviour: the index is machine-local. Smallest change: export/load keyed by snapshot id through the existing object-store sinks, plus a metadata-only freshness report. Verification: tests above, a real FileConnector export/load in separate processes on the current head, and a live S3 run (before the DiskGraph checksum layout and later changes). Stopping condition: round trip works on S3/GCS/file; local reuse unchanged; nothing refused on existing paths. |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
7e54c006-738e-49c6-850a-5f0efb2258e0
📒 Files selected for processing: 11
packages/kumo-core/src/kumo_core/object_store_sink.pypackages/kumo-core/src/kumo_core/sources/object_store.pypackages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/_snapshot_io.pypackages/kumo-engine/kumo/engine/graph.pypackages/kumo-engine/test/conftest.pypackages/kumo-engine/test/test_snapshot_export.pypackages/kumo-engine/test/test_snapshot_export_live.pypackages/kumo-s3/src/kumo_s3/backend.pypackages/kumo-s3/src/kumo_s3/sink.pypackages/kumo-s3/tests/test_sink.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.
c1979f0 to
d151486
Compare
There was a problem hiding this comment.
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/kumo/engine/graph.py: 1
248-249: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winReject comparisons across different source connectors.
If two file connectors use different roots but the same table name and version stamp,
same_sourceis true.source_changes()then reportsunchangedfor data from a different connector.Graph.iddistinguishes those connectors, but this comparison does not. Returnunknownwhen the supplied graph does not match the snapshot’s graph identity, or record and compare connector identity for each source.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
02bb012f-3d39-4f33-bcd8-359e4cd17a04
📒 Files selected for processing: 4
packages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/_snapshot_io.pypackages/kumo-engine/kumo/engine/graph.pypackages/kumo-engine/test/test_snapshot_export.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.
8adc47c to
1661366
Compare
1661366 to
70e3212
Compare
There was a problem hiding this comment.
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/test/test_snapshot_export.py: 1
519-522: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe S3 option assertion fails when pyarrow does not support
allow_delayed_open.
sink._parallel_upload_options()returns{'background_writes': False}whenS3FileSystemrejectsallow_delayed_open. In that case,option['allow_delayed_open']raisesKeyError. This makestest_object_store_round_trip[s3]fail on pyarrow versions that the packages declare as supported:kumo-s3declarespyarrow>=14, andkumo-coredeclarespyarrow>=15,<25. The production code handles these versions correctly, but the test does not.Compare each recorded option set with the probed result. Do not hard-code the delayed-open flag.
💚 Proposed fix
if scheme == 's3': - assert seen and all( - option['allow_delayed_open'] and not option['background_writes'] - for option in seen) + from kumo_s3 import sink + expected = sink._parallel_upload_options() + assert seen and all(option == expected for option in seen)
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
9749e921-9993-459f-8487-5daa7be1b286
📒 Files selected for processing: 8
packages/kumo-core/src/kumo_core/object_store_sink.pypackages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/_snapshot_io.pypackages/kumo-engine/kumo/engine/graph.pypackages/kumo-engine/test/test_snapshot_export.pypackages/kumo-s3/src/kumo_s3/backend.pypackages/kumo-s3/src/kumo_s3/sink.pypackages/kumo-s3/tests/test_sink.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.
70e3212 to
dfad21f
Compare
dfad21f to
75f9e80
Compare
75f9e80 to
a748d54
Compare
dfad21f to
4079849
Compare
aaron-kumo
left a comment
There was a problem hiding this comment.
P2 — Reject unsupported export formats instead of interpreting them as format 1.
read() currently warns on an unknown format and continues using format-1 semantics. A future format can change field meanings or required verification while retaining a superficially similar JSON structure.
Please reject unsupported format versions clearly, while continuing to accept unknown optional fields within supported formats. This does not require changing the current format or dropping support for existing snapshots/model bundles. Future engines can retain readers for older formats.
P2 — Do not use the inventory hash as an exact build fingerprint.
index_manifest hashes _MANIFEST.json, which contains filenames and sizes rather than file-content digests. Different materializations with identical filenames and sizes can therefore share this fingerprint.
This matters for interrupted-export resumption: existing files are skipped by size, while checksums are calculated from the current local files. Resuming against another materialization can commit mismatched files and checksums, leaving an export that cannot load.
Please make reuse depend on content identity or another reliable materialization identity, with appropriate verification of resumed files. Per-range CRCs can remain the transfer checks; the inventory hash should not be described or used as proof of exact build identity.
P2 — Preserve unsupported provenance when carrying an artifact forward.
A newer snapshot-manifest.json schema currently reads as None; exporting that snapshot then writes "snapshot": null. An older engine can consequently discard provenance it does not understand while successfully copying the index.
Please preserve that metadata opaquely during export/re-export, or explicitly refuse the operation if it cannot be preserved safely. Ignoring unsupported provenance for interpretation should not erase it from the next artifact.
Discussion — Should multiple materializations coexist for one logical snapshot?
Currently, exporting a rebuilt snapshot to an existing logical snapshot location keeps the previous export, even when its build differs. What should happen after an engine/storage-format upgrade when the same graph and source versions produce a new materialization?
Would it be better to distinguish logical snapshot identity from materialization identity and allow both builds to coexist? That would support upgrades, rollback, and exact replay without making users delete the previous export. I’m open to a smaller initial approach, but think we should define this behavior before making the logical-id destination layout a durable contract.
4274048 to
1320948
Compare
A graph index can only be used on the machine that built it, so building on a CPU box and training on a GPU box means re-reading every source. GraphSnapshot.export(connector, path) copies the index to <path>/<graph id>/<data version[:16]>/ through any S3, GCS or file connector and returns that location; GraphSnapshot.load_from_connector (connector, path=...) copies it into the local snapshot store. Transfers run in parallel with a CRC32 per range, an interrupted export resumes, a load stages and renames so it completes or leaves nothing, and identical racing exports both succeed. Re-running an export over a committed one re-uploads any file whose size no longer matches. kumo_snapshot.json, written last, embeds the snapshot manifest; load writes it back with the loading machine's formats under `loaded`. Load compares no versions: a graph mismatch or unknown layout format warns, and an index DiskGraph cannot open fails naming the recorded and installed formats. Loading again reuses the local copy unless it fails the DiskGraph integrity check. Graph.snapshot() reuse is unchanged. Warehouse connectors raise NotImplementedError. Ported from the platform's snapshot export, with fixes: free space must cover the full size, malformed manifests raise ValueError, the load path always runs the integrity check, and a racing export cannot fail on the marker delete. Verified with the export/load tests (round trip, resume, corruption, hostile and malformed manifests, free space, concurrent and late identical exports, repair, recorded differences, unchanged in-place reuse, object-store fakes), the S3 sink test, and the existing snapshot, kumo-s3 and kumo-core object store suites. Live S3/GCS tests are env-gated and were not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connector image's build context is an allowlist that left out kumo_core/object_store_sink.py, so the new kumo-s3 sink test failed to import it and the s3 suite stopped at collection. Admit the module. Verified by running the s3 suite against a tree filtered by the allowlist: 49 passed (collection error reproduced without the entry). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A loaded snapshot carried the source versions it was built from, but a scoring job had no cheap way to tell whether its sources had moved on. GraphSnapshot.source_changes(graph) asks each table's connector for its metadata-only version stamp (the value Graph.snapshot() records) and compares it with the manifest: unchanged, changed, or unknown for view and fallback hour stamps, Custom SQL, missing or reconstructed manifests and read errors. It never stages data and never raises. load_from_connector(..., graph=...) warns once naming changed tables. Docs add export routes for Databricks Volumes, Snowflake stage volumes and external volumes or stages, and the freshness rule. Verified with new tests (unchanged/changed, unknown cases, no staging calls, warning only with graph) plus the export and manifest suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot manifest now records each table as connector, full location and version, so source_changes matches the current table against the recorded connector type and location instead of the old "<type>:<name>" string. A table at a different location still reports unknown. Verified with test_snapshot_export.py and test_snapshot_manifest.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A snapshot rebuilt on another machine has the same id but a different DiskGraph _MANIFEST.json (run-specific paths and timings), so exporting it to a location already holding that id raised "differs from this one". Exporting a loaded snapshot also copied its `loaded` block into the export record, and corrupt-export errors did not say how to recover. A committed export with the same snapshot id is now final: export logs at info and returns its location; the fingerprint only decides whether a partial upload resumes or a committed one is repaired. Only a different id is refused. The export record drops `loaded`; corrupt and checksum errors name the location to delete and re-export. README and source_changes document that a table now read from another connector or location is `unknown`. Verified: new rebuild-and-re-export test plus the existing export and manifest suites; the reviewer's rebuild script now re-exports and loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sink always passed allow_delayed_open to S3FileSystem, which pyarrow releases inside the pinned range do not accept, so every S3 export and load raised TypeError there. The default no-connection filesystem also lacked the retry strategy the other branches set. The sink probes once whether S3FileSystem accepts the option and drops it otherwise; the no-connection branch always builds S3FileSystem with the retry strategy. Verified: sink test's fake now rejects unknown keywords and runs with and without the option; kumo-s3 unit tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export, load and source_changes docstrings and the README repeated the same guidance several times, and the manifest-validation tests ran duplicate cases of one check. Cut the docstrings to a line plus Args, tighten the README section, and drop redundant hostile/malformed manifest cases and a call-count assertion. test_snapshot_export(.py, _live.py): 38 passed, 2 skipped (live); kumo-s3 test_sink.py: 4 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export/load PR carried per-warehouse error essays, a format-note and corrupt-manifest message layer, defensive branches in source_changes, a long README section, and tests of internal race and retry sequences. Collapse unsupported connectors into one error naming the connectors to use, report corrupt exports through one message, simplify the unreadable-index error to the two format sets, trim the README to usage and limits, and keep tests of user-visible export, load, back-compat and source_changes behaviour. No documented field or API changed. Verified: test_snapshot_export, test_snapshot_export_live (skipped without live roots), test_snapshot_manifest, test_snapshot_cleanup, test_snapshot_staging_cleanup and kumo-s3 tests pass; flake8 and isort clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export and load computed per-range CRC32s in Python over every index byte, kept a second file list and commit protocol beside DiskGraph's _MANIFEST.json/_SUCCESS contract, and re-implemented helpers kumo-core already has (parallel runner, cleanup, directory fsync, snapshot verification, source location). DiskGraph gains snapshot_range_checksums next to verify_snapshot_integrity (shared manifest reader, rayon, 1 MiB buffers), exposed through the pybind module. Export computes it alongside the upload; load downloads, then runs verify_snapshot_integrity and the checksum pass on the staged copy. The export now lists files only in index/_MANIFEST.json and commits with index/_SUCCESS written last; kumo_snapshot.json keeps the snapshot metadata plus range_bytes and a crc32 map. Transfers run on kumo-core's run_in_parallel; a load pre-allocates files with posix_fallocate and removes a damaged copy before staging; _loaded uses LocalSnapshotStore.verify + rebased; source_changes reuses source_table and checks tables in parallel; GraphSnapshot gains graph_key and data_version. Verified: cargo test -p diskgraph-ingest snapshot_mirror (21 passed), pybind test_exception_exports (2), test_snapshot_export + test_snapshot_manifest (39), kumo-s3 (51 passed, 1 skipped). On a 13 GB index the Rust checksum pass takes 0.55 s versus 1.98 s for 64 Python threads with zlib; local export and load times are unchanged within noise (load is disk-bound) and load peak RSS is +65 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebased snapshot manifest no longer records format versions and DiskGraph no longer exposes format_versions(). A loaded snapshot's manifest now records only the loading environment; an index this DiskGraph cannot open surfaces SamplerEngine's own error with the exporting kumo-engine version, and directory fsyncs use kumo-core's fsync_directory. Verified: test_snapshot_export + test_snapshot_manifest (42 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A snapshot built or loaded by this package sits at <graph id>/<data version> in the store, but a GraphSnapshot constructed elsewhere need not, so read the graph id from the id itself. Export still checks that the id and the index directory agree. Verified: test_snapshot_export (23 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export resumed by file name and size and reused an existing export by a hash of the file listing, so an interrupted export resumed against a different build could commit mismatched files. An unknown export format was read as format 1, a newer snapshot manifest was dropped on export, and source_changes guessed hour buckets from round timestamps. Export now removes kumo_snapshot.json and the rest of the location first, uploads every file, and writes kumo_snapshot.json with the range CRCs last; there is no resume, reuse or index/_SUCCESS. snapshot-manifest.json is copied byte for byte (a reconstructed one is written when the snapshot has none) and restored unchanged on load. Loads reject unknown formats. source_changes compares only exact or timestamp versions, using the snapshot's record_locations choice. Verified with the snapshot export, manifest and kumo-s3 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export passed allow_delayed_open and background_writes to S3 (with a pyarrow capability probe) and rebuilt every anonymous S3 source read with an explicit retry strategy. Neither was measured to help export, and the anonymous change touched every S3 read, so both go: ObjectStoreSinkBackend.filesystem() returns the sink's own filesystem. Verified with kumo-s3 tests and test_snapshot_export.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two exports to one location could interleave so that one committed kumo_snapshot.json while the other had just wiped the index; if the second export then died, the location claimed a complete export whose files were gone. Export now deletes the marker again after its wipe, and after committing lists the index: files another export removed are uploaded again and the marker rewritten, so a committed marker always covers complete files with their checksums. Re-export still rewrites everything. Also follows #50's record_sources rename. Verified with test_snapshot_export.py, including a wipe injected just before the commit (fails without the repair). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#50 types a Parquet or CSV directory's version as "listing" (a fingerprint of its parts' paths, sizes and mtimes). source_changes treats it as comparable, like a file's mtime, instead of reporting such tables as unknown. Verified with test_snapshot_export.py (new directory case). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the kumo-s3 sink test, removed with the S3 upload tuning, needed kumo_core/object_store_sink.py in the public connector image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…estination Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot manifest is now a SnapshotManifest, so source_changes reads its source tables by attribute, the reconstructed manifest an export ships is built from the dataclass, and the export manifest stores the environment as its dict. The exported files are unchanged. Verified with test_snapshot_export.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cope Snapshot export took (connector, path), gated on object-store sinks, and was the only place a snapshot reached durable storage. - GraphSnapshot.export(SnapshotOutputConfig(connector, path)), in the style of the other output configs; any sink that holds files at the path (FileStoreSink, #112) can take it, and every upload tells a chunked store its size so files up to one part stay plain. - Graph.snapshot(mlops=): with MLOps on, the snapshot is exported under <root>/snapshots/ unless a complete export of the same format and id is there; failures warn. - ConcurrentExportError is a RuntimeError (no retry role). GraphSnapshot.graph_key/data_version are private. The README section moves to docs/mlops.md. Verified: test_snapshot_export.py (30: config form, a filesystem-only chunked sink round trip, stored once in a block, existing export, retry and repair cases), snapshot manifest and training-table suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b6394e2 to
46465ff
Compare
|
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: Snapshot portability: #54 → #51
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds snapshot export and load:
GraphSnapshot.export(SnapshotOutputConfig(...))copies a built graph index to any file destination (S3, GCS, a file path, or a volume or stage once those sinks hold files, #112), andGraphSnapshot.load_from_connector()copies it back onto any machine as a usable snapshot.Summary
This PR adds snapshot export and load:
GraphSnapshot.export(SnapshotOutputConfig(...))copies a built graph index to any file destination (S3, GCS, a file path, or a volume or stage once those sinks hold files, #112), andGraphSnapshot.load_from_connector()copies it back onto any machine as a usable snapshot. The export carries #50's snapshot manifest with the index. With MLOps on (#50), each snapshot is stored under the artifact root when built.Problem. Teams running Kumo as scheduled jobs need to answer questions like these:
Today none of these are possible: an index can only be used on the machine that built it.
Cause.
Graph.snapshot()writes the index under the local scratch directory and has no way to move it:When the machine goes away, so does the index. Copying the directory by hand does not help either:
_SUCCESSrecords the absolute path it was built at, sosnapshot()treats a moved index as missing and rebuilds it from the sources.Change. Two new calls on
GraphSnapshot:exportwrites to<path>/<graph_id>/<data_version[:16]>/, which can be derived fromsnapshot.id, and returns that location. Only files listed in DiskGraph's_MANIFEST.jsonare copied intoindex/, plus #50's_provenance.json(the series' one manifest name) next to it. Machine-local files (_SUCCESS, generated training tables) stay behind:The list of files to copy is
index/_MANIFEST.json, DiskGraph's own file list; there is no second list.load_from_connectorputs the index in the local snapshot store, at the same locationGraph.snapshot()would have built it:So a later
graph.snapshot()on the loading machine reuses the loaded index instead of re-ingesting, and a secondload_from_connectorreuses the local copy if it passes DiskGraph'sverify_snapshot_integrity. If it fails that check, the copy is downloaded again.kumo_snapshot.jsonfrom an export of a two-table synthetic graph (users50 rows,events500 rows) through aFileConnector(hashes shortened,graph_configand the checksum map trimmed):{ "format": 1, "snapshot_id": "def5ae30…@4699bb2f72c3a16f", "graph_key": "def5ae30…", "data_version": "4699bb2f…(full 64 hex)", "index_manifest": "724c0a28…", "graph_config": {"name": "def5ae30…", "tables": [{"table_name": "users", …}, {"table_name": "events", …}]}, "environment": {"kumo_engine": "1.0.0.dev0", "build_commit": "447de176", "kumo_core": "0.1.0.dev1", "diskgraph": "0.1.0", "diskgraph_build_commit": "023fa815cd62", "torch": "2.12.0+cu130", "python": "3.14.7"}, "range_bytes": 67108864, "crc32": { "_MANIFEST.json": [371284983], "col_stats.json": [10832008], "diskgraph_config.json": [553928727], … } }What each field records:
formatis the export layout version (1). Load rejects any other value with aValueErrornaming the format and saying to load it with the kumo-engine that exported it. Unknown extra fields are ignored, so a later engine can add optional fields within format 1.snapshot_id,graph_key,data_versionname the snapshot and where it goes in the local store. Load refuses ids that are not the 64-hex pairs this package produces.index_manifestis the SHA-256 of the uploadedindex/_MANIFEST.json. Load checks it before trusting that file's list of names and sizes. It is not a build identity and nothing is reused on it.graph_configis the portable graph config, so the loadedGraphSnapshotis complete without the sourceGraph.environmentis the engine that ran the export.range_bytesandcrc32hold one CRC32 per 64 MiB range of every index file (including_MANIFEST.json). DiskGraph computes them (snapshot_range_checksums) from the local index while the files upload. Load downloads ranges in parallel, then recomputes and compares them._provenance.jsontravels as a file, byte for byte: export copies the local file, and load writes the downloaded bytes into the loaded index unchanged. Because the engine never parses it on the way through, a manifest with a newerschema_versionthan the exporting or loading engine understands is preserved rather than dropped. If the snapshot has no manifest (an index built before #50), export writes one with"reconstructed": true, emptysource_tablesandnullcreated_at/environment, so the export says explicitly that its provenance is incomplete.Transfer:
kumo_snapshot.jsonat the location (so it is not loadable), delete everything else there, deletekumo_snapshot.jsonagain (another export may have committed during the wipe), upload every index file, then writeindex/_MANIFEST.json,_provenance.jsonand finallykumo_snapshot.json, which carries the checksums, in one write. After committing, export lists the index: if another export to the same location removed files in the meantime, it uploads them again and rewrites the three files, so akumo_snapshot.jsonnever stays over missing files; if its marker is gone, a later export has taken the location over and decides the outcome. Every export uploads every file; there is no resume and no "already exported" shortcut. An interrupted export is never loadable, and re-running it starts from scratch. Exporting over a completed export replaces it the same way.ConcurrentExportError(aRuntimeError), with the message: runGraphSnapshot.exportagain once the other export finishes.<target>.loadingand checks there is free space for the full size, because any old copy stays until the rename, and pre-allocates the files. It then runs DiskGraph'sverify_snapshot_integrityand the checksum pass, checks that DiskGraph can open the index, and only then renames into place. A failed load leaves nothing behind. A missingkumo_snapshot.jsonsays to re-run export; a short file, a checksum mismatch or a malformedkumo_snapshot.jsonraises oneValueError: the export at<path>is corrupt (with the detail), delete it and runGraphSnapshot.exportagain.FileStoreSink.holds_files, Let any sink hold files: FileStoreSink protocol and a chunked filesystem #112: for example a warehouse table name) raises oneNotImplementedError:<Connector> cannot hold a snapshot export: export needs a file destination (artifact root), such as an S3Connector, GCSConnector or a FileConnector on a volume.Routes on Databricks and Snowflake compute, until their sinks hold files (#114 for Unity Catalog volumes, #115 for Snowflake stages):
FileConnectoron a mounted volume, e.g.FileConnector(root_dir="/Volumes/<catalog>/<schema>/<volume>")on a Unity Catalog Volume, or a stage volume mounted into a Snowflake service (not yet tested with multi-GB indexes).Artifact root.
Graph.snapshot(mlops=...), or akumo.mlopsblock (#50), also exports the snapshot under<root>/snapshots/. A complete export of the same format and id already there is reused rather than uploaded again; a failure warns. With MLOps off nothing is stored.Is a snapshot still current?
snapshot.source_changes(graph)compares, per table alias, the version the snapshot recorded (parents.source_tables.<alias>.versionin the manifest) with the version the source reports now, provided the table still reads from the recordedconnectorandlocation. It calls the connector'sget_data_version, which is the same stampGraph.snapshot()records and is read from metadata only: a catalog query on a warehouse, a listing on an object store. It never stages or reads rows and never raises. Output from the loaded snapshot above, in the loading process, afterevents.parquetwas rewritten with 520 rows:{"users": {"status": "unchanged", "recorded": "1792609776", "current": "1792609776"}, "events": {"status": "changed", "recorded": "1792609776", "current": "1792609788"}}unknownmeans there is nothing reliable to compare:version_kind(Record provenance for every artifact under one MLOps artifact root #50) ishour_bucketorunknown(exact,timestampand a directory'slistingare compared). Views and sources without a readable version are stamped with the current hour, so they always getunknown; a real modification time that happens to fall on the hour is compared normally;reconstructed;record_sourceschoice, so a location-free manifest still compares table names);load_from_connector(..., graph=graph)makes the same metadata call for every table, so the loading machine needs source credentials. If any table ischanged, it issues oneUserWarning:Nothing is refused. The rule for scoring jobs, in the docs: either build a fresh snapshot, or load one whose
source_changesare allunchanged. Loading an older snapshot on purpose, for example to reproduce past scores, is legitimate.Supporting changes: export and load reach the destination through
kumo.engine.connector.file_store(#112), and every upload passes its size toopen_sized, so a chunked store keeps files up to one part as plain objects. kumo-core's object-storecopiesworker sizing becomes public, so export and load reuse it. DiskGraph gainssnapshot_range_checksums(index_path, range_bytes), next toverify_snapshot_integrityand sharing its manifest reader; it checksums in Rust with rayon.GraphSnapshotgains the private_graph_keyand_data_version, read from the snapshot id.Back-compat
This PR only adds two calls and records what it copies. It adds no checks or errors on any existing path.
Graph.snapshot()without export or loadload_from_connectorrewrites_SUCCESSfor the new location._provenance.jsonmarked"reconstructed": truewith emptysource_tables; the loaded copy has that manifest.schema_version, exported or re-exportedNone(with #50's warning) but never drops it.graph=whose id differs from the exported graphload_from_connectorwithoutgraphsource_changesis not called.load_from_connectorwithgraphformatValueErrornaming the format. Unknown optional fields in format 1 are ignored. A malformedkumo_snapshot.jsonraises the corrupt-exportValueError(delete and re-export).GraphSnapshot.exportagain, and leaves no partial copy.nullconnector or location (it could not be determined at build time)source_changesreports that table asunknown.Known limitations
kumo_snapshot.jsonand re-running export repairs it.source_changesreports Custom SQL tables asunknowneven though their dependency versions are recorded (Record provenance for every artifact under one MLOps artifact root #50); comparing those is left for later._- or.-prefixed path (for example a Delta_delta_logcheckpoint) always reportschanged: kumo-core's metadata stamp lists those files, while the stamp taken during the read skips them.Live S3 run
Synthetic data, exported to a dev S3 bucket, loaded into an empty scratch dir in a new process. This run predates the later changes on this branch (DiskGraph checksums, the atomic export protocol and layout,
_provenance.jsoncarried as a file,source_changesonversion_kind, and the shortened error messages). It was not re-run live after them.graph.snapshot()on the loading side reused the loaded index (0 ingest calls). Predict on the loaded snapshot equals the original (200 rows). A second load reused the local copy (0 downloads).source_changesreported allunchanged, thenordersaschangedafter its parquet was rewritten (andload_from_connector(graph=...)warned), thenunchangedafter the file was restored.Performance
Checksumming moved from Python (64 threads, zlib) to DiskGraph (Rust, rayon, 1 MiB buffers). On a 13 GB, 350-file index:
FileConnectorexportTesting Done
test_snapshot_export.py(30 cases) covers:kumo.mlopsblock stores a snapshot once (a secondsnapshot()does not export again) and a block that does not export snapshots stores nothing;_provenance.jsonbyte-identical after export, load and re-export of the loaded snapshot, and reuse by a second load and bygraph.snapshot()with no ingest;Graph.snapshot()but downloaded again by load;ConcurrentExportError;source_changes(with and withoutrecord_sources):unchangedincluding a timestamp that falls on the hour,changedafter a source parquet is rewritten, a Parquet directory (listingversion)unchangedthenchangedafter a part is added,unknownforhour_bucketversions, a version read error, and an absent or reconstructed manifest, with staging patched to fail throughout;graph=and names only the changed table.test_snapshot_export.py,test_snapshot_manifest.py,test_snapshot_export_live.py,test_snapshot_cleanup.py(59 passed, 2 skipped live); kumo-s3tests/pass; DiskGraphtest_exception_exports.py(3 passed);cargo clippy --workspace --all-targets -D warningsclean.cargo test -p diskgraph-ingest snapshot_mirrorwas run by the implementer on the commit that addedsnapshot_range_checksumsand not re-run.source_changesreportsunknownwhen the connector type or location differs.kumo_snapshot.jsonexample was captured from a real export on the current head (engine and kumo-core versions shown as released-style values); thesource_changesexample is from the earlier run.Checklist
docs/mlops.md).🤖 Generated with Claude Code