Repository navigation
Conversation
Change map
Requirement: a scheduled script gives the scheduler a machine-readable result on every exit, a 0/non-zero exit code and a retry hint, and its job ids reach every record. Existing: the Python API, #50's scheduler context read from |
|
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 |
63da7fe to
1bea937
Compare
65608c7 to
7014f45
Compare
1bea937 to
eae8b9a
Compare
7014f45 to
7a38c4b
Compare
eae8b9a to
71256af
Compare
7a38c4b to
6b20cb6
Compare
71256af to
1119fef
Compare
6b20cb6 to
10c4a23
Compare
1119fef to
2bee8ea
Compare
10c4a23 to
573186e
Compare
2bee8ea to
db65780
Compare
573186e to
62b64b6
Compare
db65780 to
8ce09a0
Compare
62b64b6 to
04f3ea1
Compare
8ce09a0 to
da0ca24
Compare
04f3ea1 to
913d621
Compare
fit_inputs parsed the snapshot and training-table manifest files by hand. It now reads them with read_manifest as SnapshotManifest and TrainingTableManifest, so an unreadable snapshot manifest no longer drops a custom training table's source. Verified with test_mlflow_tracking.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuilt on the reworked tracking session (one tracker, no Capabilities, no per-trial Lightning logger); the earlier ten commits are squashed. SnowflakeTracking(session, experiment_name=...) logs the fit as one Snowflake run: model-plan params, kumo.* tags as params, trial attempts as trial-<n>-attempt-<m>/ params and per-epoch metrics, final metrics, query.pql / model_plan.json / provenance.json artifacts, and FAILED on a failed fit. A taken run_name gets a suffix instead of resuming a run; an ExperimentTracking already in use in the process is left alone; native_model_lineage keeps the run open as a lineage context. Review changes: the adapter no longer runs ALTER SESSION on the user's Snowpark session (snowflake-ml-python takes no per-statement parameters, and a stuck call already cannot hold the fit past the 30 s drain), and snowflake-ml-python is capped below 3 because the adapter reads ExperimentTracking internals (checked present in 2.0.0 and 2.6.0). Verified with test_snowflake_tracking (incl. no statements issued on the session), test_tracking_contract for MLflow and Snowflake, test_mlflow_tracking and test_optional_imports; uv lock --check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Job scripts read the tracker's run_id for their summary line, but SnowflakeTracking had none, so scheduled fits reported run_id null. SnowflakeTracking.run_id is the latest fit run's name (Snowflake identifies runs by name), set when the run starts and kept after the fit returns. Verified: test_snowflake_tracking checks run_id is the (suffixed) run name; 20 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binary threshold is chosen after looking at the holdout, but a bundle had nowhere to keep it, so every scheduled predict had to pass it again and a fitted or loaded trainer could only be published from fit(). ScoringDefaults(binary_classification_threshold, num_classes_to_return) is exported from kumo.engine. Trainer.with_scoring_defaults validates the defaults against the bundle's task type, copies the bundle (hardlinking unchanged files) to a new scratch directory, writes scoring-defaults.json (role scoring_defaults, part of the bundle_id; empty defaults remove it via a new kumo-core remove_bundle_file), records parents.derived_from = <old bundle_id> in provenance.json (a reconstructed record when the parent has none) and returns a trainer loaded from the copy. Trainer. scoring_defaults exposes them. predict uses the threshold default when the argument is omitted and the count default only when the argument is omitted and the query has no TOP k. A loaded bundle reads the file only when the manifest lists it and warns and ignores it when unreadable or invalid for the task, so older bundles predict as before. Trainer.export publishes a fitted or loaded trainer through the publish step fit() used. Verified with new tests for default vs explicit thresholds, a multilabel count default vs explicit count and TOP k, parent bundle untouched with its id, child id and provenance link, empty defaults restoring the no-defaults id, a parent without provenance, export and connector reload, invalid defaults, and bundles with a stray, unreadable or invalid defaults file; plus remove_bundle_file and the registry, provenance and output-column suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with_scoring_defaults refused a threshold default for multilabel classification although predict accepts one there, wrote 1 and 1.0 as different bytes (so different bundle ids) and rejected numpy scalars, and replaced an unreadable or newer provenance.json with a reconstructed record. Change: defaults are validated and normalised (threshold float, class count int) before the bundle is copied; the threshold applies to binary and multilabel classification. record_derivation leaves a listed but unreadable provenance.json untouched and warns. README documents with_scoring_defaults and Trainer.export. The kumo-core remove_bundle_file unit test is dropped; the empty-defaults bundle id test covers it. Verification: test_scoring_defaults.py, test_trainer_registry.py and test_bundle_provenance.py (47) and kumo-core test_artifacts.py (10) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
fdf8531 to
5255ad9
Compare
b3dea5a to
2c18051
Compare
09c3701 to
19982c1
Compare
|
Closing this PR: its pieces either moved or were dropped.
|
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: Scheduling: #70 → #71
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds
kumo.job(), a context manager that turns a plain Python script into a scheduled job: one JSON result line on stdout however the script ends, a 0 / non-zero exit code with the failure's retry class in the summary, and the scheduler's job ids recorded in every manifest and tracked run the script writes.Summary
This PR adds
kumo.job(), a context manager that turns a plain Python script into a scheduled job: one JSON result line on stdout however the script ends, a 0 / non-zero exit code with the failure's retry class in the summary, and the scheduler's job ids recorded in every manifest and tracked run the script writes.Problem. Teams putting Kumo on a scheduler (Databricks Jobs, SageMaker, Snowflake ML Jobs, Airflow) need answers like these:
Cause. A job script uses the normal Python API. Progress lines, Lightning output, native-library output and warnings all share stdout, so a scheduler cannot tell results from log noise. A failure is a traceback with exit code 1, whatever its cause. Scheduler ids reach the manifests only if the script exports
KUMO_JOB_ID/KUMO_JOB_RUN_IDitself.Change.
job(job_id=None, job_run_id=None)setsKUMO_JOB_ID/KUMO_JOB_RUN_IDwhen given; otherwise the existing variables or the detected platform (Record provenance for every artifact under one MLOps artifact root #50's scheduler context) apply. Every manifest the body writes (snapshot, training table,provenance.json) and every tracked fit run (kumo.scheduler,kumo.job_id,kumo.job_run_id, Log, register and serve fitted models through MLflow pyfunc #65) carries them.logging.basicConfig(level=INFO, stream=stderr)).run.record(bundle_id=, snapshot_id=, run_id=, outputs=)adds to the summary;Noneleaves a field as it was.sys.exit(), SIGTERM), the environment andwarnings.showwarningare restored and one JSON line goes to stdout (both in afinally); then a failed body exits the process.showwarning. When another library wrapskumo.job()'s handler after it is installed (MLflow autologging's warnings controller does this inside a Databricks trial), the re-entered call prints the warning directly instead of recursing; the test simulates that wrapper.error.kindPermanentSourceErrorin the cause chain (e.g. a missing table or no permission)permanent: do not retryTransientSourceError,ConnectionErrororTimeoutErrorin the cause chaintransient: worth a retryunknownunknownunknownsys.exit(n)in the bodynpasses throughnis non-zeroDatabricks, SageMaker and Snowflake retry policies only see zero vs non-zero, so the retry class lives in the summary line rather than in the exit code.
Real stdout of a job script that builds and exports a snapshot of a synthetic
userstable, then of the same script failing on a missing table, withKUMO_JOB_ID=job-7 KUMO_JOB_RUN_ID=run-7(hashes shortened; stderr held the logs):Summary fields:
event(alwayskumo.engine.job),status,exit_code,scheduler(platform,job_id,job_run_id, or null when there is none),bundle_id,snapshot_id,run_id,outputs(what the body recorded; record full URIs or table names, since a scheduler cannot resolve connector-relative paths),warnings(the first 20 distinct warnings raised in the body, 300 characters each; deprecation and future warnings, mostly from third-party libraries, are left out and still go to stderr), and on failureerror(type,kind,messagecut to 500 characters; the traceback goes to stderr).This replaces the
kumo-engineconsole script and its JSON config that earlier revisions of this PR proposed: a job script already expresses graphs, connectors, tracking and registry options in the normal API, so a second config schema was only another surface to keep in sync. The job templates in #71 are plain Python scripts underkumo.job().Back-compat
New API only.
job()KUMO_JOB_ID/KUMO_JOB_RUN_IDalready setjob()is given ids.SystemExit(1).sys.exit()in the bodyjob()entered off the main threadwarnings.showwarningKnown limitations
job(job_id=..., job_run_id=...)from task parameters, or onlyplatform: databricksis recorded.Testing Done
test_job.py(CPU): the summary line with recorded ids, outputs and the scheduler context, and the same scheduler block in a manifest written inside the body; warnings deduplicated, capped at 20 and 300 characters, deprecations left out; each failure class gives exit 1 witherror.kindtransient (in the cause chain, connection reset), permanent (missing source) or unknown, and a long message cut to 500 characters; Ctrl-C,sys.exit(3)andsys.exit(0)still print the summary with the right code and restore the job-id environment; SIGTERM prints the summary, exits 143 and restores the previous handler.test_job.py+test_optional_imports.py: 56 passed. flake8 and isort clean.error.kindupdated to what this head prints.Checklist
🤖 Generated with Claude Code