Skip to content

Add opt-in lineage columns, stamped by the prediction writer - #58

Closed
Vedaant-J wants to merge 134 commits into
vedaant/mlops-job-templatesfrom
vedaant/mlops-lineage-columns
Closed

Vedaant-J wants to merge 134 commits into
vedaant/mlops-job-templatesfrom
vedaant/mlops-lineage-columns

Conversation

@Vedaant-J

@Vedaant-J Vedaant-J commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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_ID and KUMO_SNAPSHOT_ID written 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_ID and KUMO_SNAPSHOT_ID written 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:

  • A row in the scores table looks wrong. Which prediction run wrote it?
  • Which model bundle produced that score, and was it the one we promoted?
  • Which graph snapshot (and so which source table versions) was it scored against?
  • The table is appended to daily. Which rows came from which run?

Today none of these can be answered from the exported table.

Cause. An exported predictions Parquet carries only the prediction columns:

ENTITY: int64
TARGET_PRED: bool
False_PROB: float
True_PROB: float

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 to True and the config passed to Trainer.predict, the scorer's Parquet writer (kumo_core.train.io.StreamingFrameWriter, new constant_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, so result.files, result.frame and every export (predict(output_config=...) and later result.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), with lineage_columns=True:

ENTITY: int64
TARGET_PRED: bool
False_PROB: float
True_PROB: float
KUMO_RUN_ID: string        # new
KUMO_BUNDLE_ID: string     # new
KUMO_SNAPSHOT_ID: string   # new

First rows (ids shortened):

ENTITY TARGET_PRED False_PROB True_PROB KUMO_RUN_ID KUMO_BUNDLE_ID KUMO_SNAPSHOT_ID
0 True 0.060519 0.939481 791f22bc…-predict-c26d64d5… c584b62b… 52f642b6…@b747cbf1bf62f69d
8 True 0.061139 0.938861 791f22bc…-predict-c26d64d5… c584b62b… 52f642b6…@b747cbf1bf62f69d
40 True 0.062343 0.937657 791f22bc…-predict-c26d64d5… c584b62b… 52f642b6…@b747cbf1bf62f69d

The export directory itself is unchanged (part-00000-00000.parquet, _KUMO_EXPORT, _SUCCESS); only the Parquet columns differ.

What each column records:

Details:

  • The columns are constant per prediction run. An unknown id (for example no bundle_id on 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 for lineage_columns=True on a result written without them emits a UserWarning and exports the result as it is.
  • PredictionResult now records bundle_id and snapshot_id. Both are optional and excluded from equality and hashing.
  • The lineage ids are part of the prediction's source identity only when lineage is on, so the default identity is unchanged.
  • Not added to 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):

Writer time Parquet size Decoded (Arrow / warehouse row)
lineage off 0.91 s 89.7 MB 12.4 B/row
lineage on 2.59 s 90.0 MB (+0.3%, dictionary-encoded) +190 B/row

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:

Sink and write mode Behaviour with lineage_columns=True
Snowflake OVERWRITE Fails.
Snowflake APPEND Succeeds, but silently drops the new columns.
BigQuery APPEND Fails.
Databricks APPEND Fails.
New table or directory Lineage columns written.

So lineage should be enabled on a new table or directory. The README lists the per-sink behaviour above; the PredictionOutputConfig docstring 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.

Situation Behaviour
Existing predict/export call (flag not set) Output files, result.frame and sink calls identical to before.
lineage_columns=True on a new destination Three string columns appended after the existing ones; other columns keep their types; row count and row groups unchanged.
lineage_columns=True on an existing warehouse table See the table above: most sinks fail, Snowflake APPEND drops the columns.
result.export(lineage_columns=True) on a result written without lineage UserWarning; export proceeds without the columns.
PredictionResult built without ids (older code path) Equal and hash-equal to one with ids.
Output read by an older engine or other tools Extra columns are ordinary string columns; nothing in the engine reads them.

Known limitations

  • Lineage is chosen at prediction time. A result scored without it cannot gain the columns on a later export; score again with the flag on.
  • Each decoded row pays ~200 B for the three ids (see Cost).

Testing Done

  • Engine (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.frame carries 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.
  • kumo-core (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.
  • On the current head (on a merge of the PRs below it): engine test_prediction_output_columns.py + test_prediction_completion.py + test_holdout_df.py: 52 passed; kumo-core test_train_output.py + test_prediction.py + test_prediction_artifact_stats.py: 121 passed (CPU).

Checklist

  • No breaking changes (default off).
  • README updated.
  • Unit tests added.

🤖 Generated with Claude Code

@Vedaant-J

Vedaant-J commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Change map

File Function / scope What changed Why
packages/kumo-core/src/kumo_core/prediction.py _PredictionWorkerConfig, _score_prediction_shard, score_prediction New constant_columns carried to each scoring worker Spawned scorers write the same columns
packages/kumo-core/src/kumo_core/train/diskgraph_nested_trainer.py DiskgraphNestedTrainer.predict, _predict_lp New output_constant_columns, forwarded to every StreamingPredOutputWriter Node and link prediction scorers both stamp
packages/kumo-core/src/kumo_core/train/io.py StreamingFrameWriter.__init__, _file_schema, _stamped, _serialize, close New constant_columns: string columns appended to each row group in the serializer before write_table; file schema and recorded stats schema include them Stamp lineage in the one write the scorer already does, with no extra pass
packages/kumo-core/src/kumo_core/train/output.py StreamingPredOutputWriter.__init__, write_embedding Passes constant_columns to the predictions and embeddings writers Both artifacts carry the same lineage
packages/kumo-core/tests/test_train_output.py test_constant_columns_are_appended_to_every_part New test: columns on every rotated part, source columns equal, stats fingerprint matches the files Writer contract
packages/kumo-engine/README.md Prediction output Documents lineage_columns, per-sink effect on existing tables Users must enable it on a new destination
packages/kumo-engine/kumo/engine/trainer/output_config.py PredictionOutputConfig New lineage_columns: bool = False and docstring Opt-in switch
packages/kumo-engine/kumo/engine/trainer/trainer.py PredictionResult, PredictionResult.export, _has_lineage_columns, Trainer.predict predict builds the run/bundle/snapshot ids and passes them as constant_columns (and into the source identity only when on); result records bundle_id/snapshot_id; export warns when lineage is asked of a result without it Ids come from the predicting process; export never re-encodes files
packages/kumo-engine/test/test_prediction_output_columns.py test_lineage_columns_are_written_by_the_scorer, test_lineage_export_of_a_result_without_them_warns Lineage on vs off for predictions and embeddings; later export copies columns; warning path; equality ignores ids User-visible behaviour of the flag
test/train/test_kumo_pred_ttng.py test_ttng_link_pred_predict_receives_runtime_and_top_k expects output_constant_columns=None in the forwarded link-prediction call the new keyword reaches _predict_lp

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.

@copy-pr-bot

copy-pr-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 20, 2026 22:12 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 2f8be9c to 1e2ca0e Compare October 20, 2026 22:36
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 20, 2026 22:36 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from d106960 to 3c40f67 Compare October 20, 2026 23:29
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 1e2ca0e to 4f41238 Compare October 20, 2026 23:29
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 20, 2026 23:29 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 4f41238 to 8b49e08 Compare October 21, 2026 00:45
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 00:45 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from 75ef25b to a3ce235 Compare October 21, 2026 07:18
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 8b49e08 to 94d5f00 Compare October 21, 2026 07:18
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:18 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from a3ce235 to 70a332a Compare October 21, 2026 07:57
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 94d5f00 to 1057df9 Compare October 21, 2026 07:57
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:57 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from 70a332a to 48367fa Compare October 21, 2026 08:47
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 1057df9 to 814023c Compare October 21, 2026 08:47
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 08:47 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from 48367fa to 4e5b58d Compare October 21, 2026 09:01
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 814023c to 1b084ef Compare October 21, 2026 09:01
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 09:01 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-bundle-provenance branch from 4e5b58d to 8bc9770 Compare October 21, 2026 09:58
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 1b084ef to 72c6211 Compare October 21, 2026 09:58
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 09:58 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-lineage-columns branch from 72c6211 to 2b4a71f Compare October 21, 2026 16:24
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 16:24 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J changed the base branch from vedaant/mlops-bundle-provenance to vedaant/mlops-holdout-fit-mode October 21, 2026 16:24
Vedaant-J and others added 29 commits October 22, 2026 18:52
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>
@Vedaant-J

Copy link
Copy Markdown
Contributor Author

Closing: this PR's pieces landed elsewhere or were dropped in the final MLOps design.

This branch was previously deployed

1 inactive deployment
gha-kumo-workflow-tests — 87371977 Deployed Oct 22, 2026 by Vedaant-J via import #633
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant