Repository navigation
Conversation
Change map
Requirement: exported predictions can be traced to their run, bundle and snapshot, opt-in. Smallest change: stamp constant columns in the scorer's existing Parquet writer (replaces the earlier export-time DiskGraph Parquet splicer, which is removed). Stopping condition: lineage on adds exactly three string columns everywhere; lineage off output is unchanged. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
2f8be9c to
1e2ca0e
Compare
d106960 to
3c40f67
Compare
1e2ca0e to
4f41238
Compare
4f41238 to
8b49e08
Compare
75ef25b to
a3ce235
Compare
8b49e08 to
94d5f00
Compare
a3ce235 to
70a332a
Compare
94d5f00 to
1057df9
Compare
70a332a to
48367fa
Compare
1057df9 to
814023c
Compare
48367fa to
4e5b58d
Compare
814023c to
1b084ef
Compare
4e5b58d to
8bc9770
Compare
1b084ef to
72c6211
Compare
72c6211 to
2b4a71f
Compare
Three readers each re-parsed the publication manifest to find one role's file, the tests split one derived-bundle scenario over several tests and ran a second real fit, and the README repeated the docstrings. Add one listed_file helper for provenance and scoring-defaults reads, merge the derived-bundle, provenance-fallback and back-compat tests, drop the second real fit and the private validator matrix, and cut README and docstrings to usage and limits. Behaviour is unchanged. Verified: test_scoring_defaults, test_bundle_provenance, test_trainer_registry and kumo-core test_artifacts pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundle's task type was read from prediction-schema.json in three places (scoring defaults, the trainer's warm-start check, kumo-core's warm-start resolver), and scoring-defaults.json had its own JSON writer. Add kumo_core.artifacts.bundle_task_type next to the bundle layout and use it everywhere; write the defaults with the engine manifest writer. Verified: test_scoring_defaults, test_bundle_provenance, test_warm_start, kumo-core test_artifacts and warm-start tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
with_scoring_defaults copied the parent's provenance.json into the derived bundle, so its id named the parent. Set it to the derived bundle's own id (computed from the publication manifest, which excludes provenance.json). Verified with test_scoring_defaults.py and test_bundle_provenance.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A derived bundle's parent now goes in BundleParents.derived_from, and a derived bundle whose parent had no provenance gets a reconstructed BundleProvenance that still writes only the fields it knows. Bundle fields absent from such a record are optional. write_manifest also takes the plain dict scoring-defaults.json is written from. Verified with test_scoring_defaults.py and test_bundle_provenance.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Unity Catalog Files API lists directories as '.../1/model/', so the immediate-child check in _download rejected every real model bundle and Trainer.load_from_connector(DatabricksConnector, location=...) raised 'Invalid model artifact path returned by Databricks.' Strip the trailing slash before the check and the recursive download. The fake Files API in the registry tests now lists directories the same way, so every fetch test covers it. Verified: kumo-databricks test_registry and test_uc_registry pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A version published to Snowflake or Databricks could not be loaded by a stable name, there was no way to promote one, and nothing on the version said which bundle it held. ModelRegistryLocation gains alias (exclusive with version): an export publishes, then points the alias at the new version (a failed alias step warns and returns the published model); a fetch loads the version the alias points at. kumo.engine.set_model_alias promotes explicitly through a new ModelAliasingSink protocol. Each version records the kumo_engine marker and kumo_bundle_id only (Snowflake version metrics, which keep the finite training metrics; Databricks version tags); provenance.json in the bundle holds the lineage. list_models and fetch_model return kumo_bundle_id, and load_from_connector warns when the fetched bundle differs from it. kumoapi.plugin shares the marker, check_model_alias and registry_lineage between the registries. Snowflake fetch accepts DEFAULT/FIRST/LAST; Trainer.export of a derived bundle publishes the child's bundle id. Verified: kumo-api test_plugin_registry, kumo-snowflake and kumo-databricks registry tests, test_trainer_registry and test_scoring_defaults pass. No live Snowflake or Databricks calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Snowflake alias move unset the alias on the old version before setting it on the new one, so a failure in between left no version holding it. Restore it on the old version and log a warning. Export aliases were only checked after publishing; check them in fit()/export() before any work, with one syntax every registry accepts (letters, digits, underscores; DEFAULT/FIRST/LAST/LATEST/v<digits> reserved). Snowflake versions no longer record every training metric: registry versions carry only the kumo_engine marker and kumo_bundle_id on both Snowflake and Databricks; metrics stay in the bundle and the tracked run. Verified with the registry, plugin and trainer registry tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An MLflow model signature needs the columns a scored frame carries for each task type, and a served model needs a connector for a snapshot URI. format_prediction kept its long-format task set inline, and nothing mapped an s3://, gs://, file:// or local root to a connector. kumo_core.train.formatting.prediction_output_columns(task_type) returns the prediction columns after the entity column, sharing the task sets format_prediction uses; kumo.engine.connector.connector_for_root(uri) builds the S3, GCS or file connector for a root. Verified: kumo-core test_prediction_output_columns and kumo-engine test_connector_for_root pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fitted model could be published to a connector registry, but not to
MLflow, where Databricks Unity Catalog and SageMaker managed MLflow
expect models; and a tracked fit run did not say which bundle it made
or which scheduler job ran it.
kumo.engine.integrations.mlflow gains log_model(trainer, snapshot=...)
(a pyfunc model wrapping the bundle, scored with params={'snapshot':
uri} or a serving_snapshot loaded once per replica at startup; metadata
kumo_bundle_id only; provenance.json as an artifact; pip requirement
kumo-engine[s3,gcs]==<version>) and load_model(uri). MlflowTracking(
log_model=, registered_model_name=, aliases=, serving_snapshot=) logs,
registers and aliases the fit's model on the fit run through an
optional log_model(result, snapshot) tracker member. Trainer.fit runs
it on the tracking thread and waits for it without the 30 s finish
limit, so the model is registered (or the run ends FAILED with a
warning) before fit returns.
Fit runs gain the pointer tags kumo.bundle_id and, under a scheduler,
kumo.scheduler, kumo.job_id and kumo.job_run_id; a tracked fit's
provenance.json records "tracking": {"run_id": <kumo run id>}.
Verified: test_mlflow_model, test_mlflow_tracking, test_tracking_contract,
test_snowflake_tracking, test_bundle_provenance, test_trainer_registry,
test_scoring_defaults, test_fit_events and test_optional_imports pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pyfunc model's predict deleted every directory that appeared under the shared prediction_tables/ and the trainer's runs/ during the call, so concurrent calls (other threads, or other serving processes on the same disk) lost directories they were still using; and it never removed <trainer>/predict_output/<run>, which holds the scored rows. The README said kumo-engine needs MLflow 3.11 while the extra said 3.0. - kumo.engine._scratch gains scoped_scratch_dir(path): a context variable that get_scratch_dir() reads before the configured root. - predict runs inside a TemporaryDirectory under the trainer's scratch dir with that scope, so everything the call creates (prediction table, run dir, predict output) is removed with it, on success or error, and nothing else is touched. - README: the mlflow extra needs MLflow 3.13 or later (DiskGraph pins pyarrow 24; older MLflow releases exclude it), stated once. Verified: test_mlflow_model passes; its predict test now also checks a directory another call creates meanwhile survives and the trainer's scratch dir is left empty (fails on the previous cleanup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fit waited for the tracker's model logging with no limit, so a stuck upload or a dead tracking thread hung the fit; and the fit's 30 s MLflow HTTP timeout also applied to the model upload. - TrackingSession.log_model waits while the tracking thread is alive, up to MODEL_SECONDS (1 h). If the thread stops or the hour passes it warns, ends the tracker's run as failed from the fit thread and returns the error; fit still returns its result. - MlflowTracking.log_model raises MLFLOW_HTTP_REQUEST_TIMEOUT to 600 s for the upload only, and only when the fit set the 30 s limit (the user had not set it), then puts the limit back. A model whose upload finishes after its run was ended is not registered or aliased (warning). Verified: test_mlflow_model (slow upload and dead thread both return within the bound and end the run; the upload sees 600 s, the run 30 s, nothing is left set and a late upload is not registered), test_mlflow_tracking, test_tracking_contract and test_snowflake_tracking pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
log_model's dataset inputs now come from BundleProvenance's source tables and training table record, and the scheduler tags from SchedulerContext. The tracked run id is BundleProvenance.tracking (a TrackingRef); the provenance.json an MLflow model carries is unchanged. Verified with test_mlflow_model.py and test_bundle_provenance.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scheduler runs a Python script and needs two things back: one machine-readable result line, and an exit code that says whether a retry can help. A plain kumo-engine script prints logs, progress and warnings to stdout and exits 1 for every failure, and its scheduler job ids reach no manifest unless the script exports them. kumo.job() is a context manager for the script body. It records job_id/job_run_id (else KUMO_JOB_ID/KUMO_JOB_RUN_ID or the detected platform) so manifests and tracked runs carry them, sends logs to stderr, and on exit prints one JSON line to stdout: status, exit_code, the scheduler context, the ids and outputs passed to run.record(), warnings raised, and error on failure. A failed body exits 10 (a source refused the request), 20 (transient source, connection or timeout error), 30 (anything else) or 143 (SIGTERM). Verified: test_job covers the summary line, every exit-code class, SIGTERM, and the scheduler ids reaching a manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kumo.job() restored the environment and printed its summary only after an Exception or SIGTERM, so Ctrl-C or sys.exit() inside the body left no summary line and the job ids set in the environment. Do both in finally, for every way the body ends. Exit codes are now 0 on success, 1 on failure, 143 on SIGTERM (a body's own sys.exit code passes through). The transient/permanent split moves from the 10/20/30 codes to the summary's error.kind. Verified with test_job.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The summary line collected every Python warning in full, so a run that hit pandas FutureWarnings printed kilobytes of repeated array dumps in one line. Keep the first 20 distinct messages, 300 characters each. Verified with test_job.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third-party DeprecationWarning and FutureWarning messages (optuna, lightning, pandas) filled the summary's warnings. Record only the other categories; deprecations still reach stderr. Verified with test_job.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scheduler_context() now returns a SchedulerContext, so the job summary line writes its dict; the printed JSON is unchanged. Verified with test_job.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Snowflake job that reads through a SnowflakeConnector also needs a Snowpark session for SnowflakeTracking and the model registry, and had to build a second one from the same SPCS token and target. SnowflakeConnector.session() returns the session the connector reads through: its identity, pointed at its warehouse, database and schema, from the reader's session cache. Verified: test_snowflake_connector passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Teams scheduling kumo-engine had no starting point for a nightly fit and an hourly score on their platform: how the job installs the engine, where snapshots and models live, how tracking and the registry are wired, and how a scoring job finds the promoted model. examples/mlops holds two plain Python job scripts under kumo.job() (fit_job.py, score_job.py) and a shared jobs.py, with per-platform wiring: a Databricks asset bundle (MLflow on Unity Catalog), a SageMaker pipeline (SageMaker managed MLflow), and Snowflake ML Jobs plus a task (Snowflake Model Registry and Experiment Tracking). Scoring jobs load the model by its champion alias, which the fit moves; every score passes a binary threshold. test_job_templates checks the scripts and configs parse and stay consistent. Verified: test_job_templates passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a registry the score job loaded the model from its own run directory, which only works when fit and score share a run id; real schedulers give each run its own id. The score job now loads the path KUMO_MODEL_PATH names under KUMO_ARTIFACT_ROOT (the fit summary's outputs.model). The SageMaker pipeline requires --instance-type instead of defaulting to a placeholder, and the Snowflake template is marked experimental because it is built on preview APIs. The exit-code notes follow kumo.job()'s 0 / 1 / 143 codes. Verified with test_job_templates.py (fit as run-1, score as run-2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job scripts recorded outputs relative to their connector roots
("predictions/", "snapshots/..."), which a scheduler cannot resolve.
Record them under KUMO_ARTIFACT_ROOT as full URIs. The SageMaker README
now records the live run, drops the advice to pin the MLflow client to
the server version, and lists the CUDA stack only for GPU instances.
Verified with test_job_templates.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both need the prediction-run PRs, which are not below this one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Databricks spark_python_task runs the job script without __file__, so fit_job.py and score_job.py failed on their first line with NameError and printed no summary. Fall back to sys.argv[0], which is the script path there. The Asset Bundle now installs a requirements file (starting with --prefer-binary) before the kumo-engine wheel and ships a default target; the README records the live Databricks run. Verified with test_job_templates.py (scripts executed without __file__). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without KUMO_REGISTRY the fit job publishes to the artifact store, whose PublishedModel.version is the object path without its scheme, and the summary reported that as outputs.model.version. fit_job sets outputs.model.version to null when no registry is configured; outputs.model.uri keeps the full path. README updated. Verified: test_job_templates (the local fit summary has version null); 11 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Databricks GPU nodes need a GPU ML runtime, whose preinstalled packages conflict with kumo-engine's pins; the standard runtime runs on CPU nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exported predictions carried no record of which run, model bundle and graph snapshot produced them. PredictionOutputConfig gains lineage_columns (default False). Passed to Trainer.predict, it makes the scorer's Parquet writer append constant KUMO_RUN_ID, KUMO_BUNDLE_ID and KUMO_SNAPSHOT_ID string columns to every row group it writes (StreamingFrameWriter constant_columns, plumbed through score_prediction and StreamingPredOutputWriter), so result.files, result.frame and every export carry them with no extra pass over the output. Unknown ids are omitted. export() copies whatever columns the result has and warns when asked for lineage on a result written without it. PredictionResult records bundle_id and snapshot_id. On 10M rows: Parquet +0.3% (dictionary-encoded), ~181 B/row once decoded or loaded into a warehouse, +1.7 s writer time. Verified: kumo-core test_train_output, test_prediction, test_prediction_artifact_stats (121 passed); engine test_prediction_output_columns + test_prediction_completion (41 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing: this PR's pieces landed elsewhere or were dropped in the final MLOps design.
|
Series: kumo-engine MLOps series: every snapshot, training table, model bundle and prediction run records where it came from; snapshots and models move between machines and registries; fits and prediction runs are tracked in MLflow or Snowflake; plain Python job scripts run on a scheduler through a small job helper; prediction runs are monitored for drift and realized performance.
Stack: Prediction runs and monitoring: #58 → #64 → #67 → #69 → #73
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds opt-in lineage columns on prediction outputs:
KUMO_RUN_ID,KUMO_BUNDLE_IDandKUMO_SNAPSHOT_IDwritten into every output row by the scorer, so a row in a downstream table can be traced back to the prediction run, model bundle (#56) and graph snapshot (#50) that produced it.Summary
This PR adds opt-in lineage columns on prediction outputs:
KUMO_RUN_ID,KUMO_BUNDLE_IDandKUMO_SNAPSHOT_IDwritten into every output row by the scorer, so a row in a downstream table can be traced back to the prediction run, model bundle (#56) and graph snapshot (#50) that produced it.Problem. Teams consuming Kumo predictions downstream need to answer questions like these:
Today none of these can be answered from the exported table.
Cause. An exported predictions Parquet carries only the prediction columns:
The run, bundle and snapshot ids exist in the process that ran
predict, but nothing writes them into the output.Change.
PredictionOutputConfig(lineage_columns=False)is a new flag. With it set toTrueand the config passed toTrainer.predict, the scorer's Parquet writer (kumo_core.train.io.StreamingFrameWriter, newconstant_columns) appends the three ids as constant string columns to every row group it writes, for predictions and embeddings. There is no extra pass over the output: the columns are added in the same write, soresult.files,result.frameand every export (predict(output_config=...)and laterresult.export(...)) carry them.Exported predictions Parquet from a binary-classification fit on a small synthetic users/items/orders graph (
PREDICT COUNT(orders.*, 0, 7, days) > 0 FOR EACH users.ID, 50 rows), withlineage_columns=True:First rows (ids shortened):
791f22bc…-predict-c26d64d5…c584b62b…52f642b6…@b747cbf1bf62f69d791f22bc…-predict-c26d64d5…c584b62b…52f642b6…@b747cbf1bf62f69d791f22bc…-predict-c26d64d5…c584b62b…52f642b6…@b747cbf1bf62f69dThe export directory itself is unchanged (
part-00000-00000.parquet,_KUMO_EXPORT,_SUCCESS); only the Parquet columns differ.What each column records:
KUMO_RUN_IDis the prediction job id (<trainer id>-predict-<run uuid>). The export identity in_KUMO_EXPORTis derived from it.KUMO_BUNDLE_IDis the 64-hexbundle_idof the model that scored (from Record bundle_id and fit provenance in every model bundle #56). It matchesTrainer.bundle_idand survives registry round trips, so it identifies the promoted model.KUMO_SNAPSHOT_IDis the snapshotid(<graph key>@<first 16 hex of the data version>), which matchessnapshot-manifest.json(Record provenance for every artifact under one MLOps artifact root #50) and leads to the source table versions.Details:
bundle_idon a bundle that predates Record bundle_id and fit provenance in every model bundle #56) is omitted rather than written as null.result.export(config)copies whatever columns the result already has; it does not add or remove them. Asking forlineage_columns=Trueon a result written without them emits aUserWarningand exports the result as it is.PredictionResultnow recordsbundle_idandsnapshot_id. Both are optional and excluded from equality and hashing.MetadataField, which is part of the hosted API.Cost. Writing 10M rows of a 3-column predictions table through the scorer's writer with the three ids (run id 50 characters, bundle and snapshot ids 64):
Parquet files barely grow, but anything that decodes the rows (
result.frame, CSV exports, warehouse tables) pays roughly 200 bytes per row for the three strings. This is the main reason the flag is off by default.Why opt-in. Turning it on changes the destination's column set. On a warehouse table created without lineage columns:
lineage_columns=TrueOVERWRITEAPPENDAPPENDAPPENDSo lineage should be enabled on a new table or directory. The README lists the per-sink behaviour above; the
PredictionOutputConfigdocstring says to enable it only on a new table or directory. Every other column is written exactly as without lineage, on every sink, because the export path is unchanged.Back-compat
This PR only records and exposes information. With the flag at its default (
False) the scorer writes exactly what it wrote before.predict/exportcall (flag not set)result.frameand sink calls identical to before.lineage_columns=Trueon a new destinationlineage_columns=Trueon an existing warehouse tableAPPENDdrops the columns.result.export(lineage_columns=True)on a result written without lineageUserWarning; export proceeds without the columns.PredictionResultbuilt without ids (older code path)Known limitations
Testing Done
test_prediction_output_columns.py): one prediction with lineage on and one with it off, for predictions and embeddings. Column lists differ only by the three columns; every original column keeps its Arrow type; row counts are equal; values equal the run, bundle and snapshot ids;result.framecarries them; a later export without the flag copies them unchanged. A second test checks the warning for lineage on a result written without it, and that result equality and hashing ignore the new ids.test_train_output.py): the streaming writer appends the constant columns to every rotated part, keeps the source columns equal, and its recorded schema fingerprint and column count match the written files.test_prediction_output_columns.py+test_prediction_completion.py+test_holdout_df.py: 52 passed; kumo-coretest_train_output.py+test_prediction.py+test_prediction_artifact_stats.py: 121 passed (CPU).Checklist
🤖 Generated with Claude Code