Repository navigation
Conversation
Change map
Requirement: a scheduled fit must be visible in the team's experiment tracker, its model registered under a movable alias with a pointer back to its bundle, its post-fit threshold carried by the model, and the model scorable from MLflow without the training code. Existing behaviour: at #50, |
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: 2
packages/kumo-engine/kumo/engine/_tracking.py: 1
59-63: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not mark the session as started when
context()raises.
startsetsself._started = Truebefore it builds theFitContext. The build happens inside the_calllambda, and it can raise. Two examples are aKeyErrorontrain_table_spec['tt_id']and aTypeErrorfromto_json_dict. When the build raises,_calllogs the error andTracking.startis never called. The session still forwards later events, andTrainer.fitstill callsfinish. A third-partyTrackingthen getson_eventandfinishwithoutstart, which breaks the lifecycle that theTrackingprotocol documents.MlflowTrackinghandles this case only because_reset()clears its state.Build the context first. Set
_startedonly after the build succeeds. Keep_started = Truebeforeself._tracking.start(...), so thatfinishstill runs cleanup whenstartitself raises.Proposed fix
def start(self, context: Callable[[], FitContext]) -> None: if self._tracking is None: return - self._started = True - self._call(lambda: self._tracking.start(context())) + fit_context = self._call(context) + if fit_context is None: + return + self._started = True + self._call(lambda: self._tracking.start(fit_context))packages/kumo-core/src/kumo_core/train/trainer.py: 1
1443-1445: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle
loggervalues that are lists or booleans.
create_pl_trainerwraps every suppliedloggervalue in a list with the TensorBoard logger. A logger list becomes nested, andlogger=Falsebecomes a logger collection containingFalse. These values do not produce a valid Lightning logger collection.logger=Falsealso no longer disables logging. The fix must preserve that boolean behavior.Suggested fix
-from collections.abc import Callable +from collections.abc import Callable, Iterable ... extra_logger = self.kwargs.pop("logger", None) - if extra_logger is not None: + if isinstance(extra_logger, bool): + pl_logger = extra_logger + elif isinstance(extra_logger, Iterable): + pl_logger = [pl_logger, *extra_logger] + elif extra_logger is not None: pl_logger = [pl_logger, extra_logger]
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
7e4f692d-f9c2-4dc1-9c3a-15926fab9fd4
⛔ Files ignored due to path filters: 1
uv.lockis excluded by!**/*.lock,!**/*.lock
📒 Files selected for processing: 15
packages/kumo-core/src/kumo_core/automl/auto_trainer.pypackages/kumo-core/src/kumo_core/train/trainer.pypackages/kumo-core/src/kumo_core/training/orchestration.pypackages/kumo-core/src/kumo_core/training/runtime.pypackages/kumo-core/tests/test_training.pypackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/_tracking.pypackages/kumo-engine/kumo/engine/integrations/__init__.pypackages/kumo-engine/kumo/engine/integrations/mlflow.pypackages/kumo-engine/kumo/engine/trainer/_runtime.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/pyproject.tomlpackages/kumo-engine/test/test_mlflow_tracking.pypackages/kumo-engine/test/test_optional_imports.pysetup.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.
74477db to
c4c4d43
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Handle concurrent experiment creation without disabling tracking. · mlflow.py:153-158
packages/kumo-engine/kumo/engine/integrations/mlflow.py:153-158
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winHandle concurrent experiment creation without disabling tracking.
When
create_experimentraisesRESOURCE_ALREADY_EXISTS, perform a freshget_experiment_by_namelookup and use the returned experiment. Catch only this duplicate-creation error. Re-raise unrelated MLflow failures.🛡️ Suggested fix
if self.experiment_name is not None: experiment = self._client.get_experiment_by_name( self.experiment_name) - experiment_id = (experiment.experiment_id - if experiment else self._client.create_experiment( - self.experiment_name)) + if experiment is None: + try: + experiment_id = self._client.create_experiment( + self.experiment_name) + except Exception as error: + exceptions = import_provider( + 'mlflow.exceptions', 'mlflow') + databricks_pb2 = import_provider( + 'mlflow.protos.databricks_pb2', 'mlflow') + if not isinstance(error, exceptions.MlflowException) or ( + error.error_code != + databricks_pb2.RESOURCE_ALREADY_EXISTS): + raise + experiment = self._client.get_experiment_by_name( + self.experiment_name) + if experiment is None: + raise + experiment_id = experiment.experiment_id + else: + experiment_id = experiment.experiment_id
🧹 Nitpick comments: 1
packages/kumo-engine/test/test_tracking_contract.py: 1
378-388: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCover global-rank gating in the tracking test.
The test covers only
LOCAL_RANK=1. It does not detect a regression that removesRANKgating while retaining local-rank gating. Add theRANK=1, LOCAL_RANK=0case.🐛 Suggested fix
def test_only_rank_zero_tracks(monkeypatch): monkeypatch.setenv('LOCAL_RANK', '1') recorder = _Recorder() session = TrackingSession(recorder) session.start(lambda: CONTEXT) session.on_event(EVENTS[0]) session.finish(RESULT, None) assert not session.enabled assert recorder.calls == [] +def test_only_global_rank_zero_tracks(monkeypatch): + monkeypatch.setenv('RANK', '1') + monkeypatch.setenv('LOCAL_RANK', '0') + recorder = _Recorder() + session = TrackingSession(recorder) + + session.start(lambda: CONTEXT) + session.on_event(EVENTS[0]) + session.finish(RESULT, None) + + assert not session.enabled + assert recorder.calls == []
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
413901b2-1304-4d92-8208-5f67666de42c
📒 Files selected for processing: 10
packages/kumo-core/src/kumo_core/automl/auto_trainer.pypackages/kumo-core/src/kumo_core/train/trainer.pypackages/kumo-core/tests/test_training.pypackages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/_tracking.pypackages/kumo-engine/kumo/engine/integrations/mlflow.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_mlflow_tracking.pypackages/kumo-engine/test/test_tracking_contract.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.
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:
4675a596-3919-4607-940f-c95403683072
📒 Files selected for processing: 5
packages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/_tracking.pypackages/kumo-engine/kumo/engine/integrations/mlflow.pypackages/kumo-engine/test/test_mlflow_tracking.pypackages/kumo-engine/test/test_tracking_contract.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
| if event.name == 'trial_started': | ||
| self._safe(self._start_trial, key, events) | ||
| elif event.name == 'epoch_ended' and event.epoch is not None: | ||
| self._safe(self._log_steps, key, events, event.epoch) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Close child runs after an epoch-log failure.
If _log_steps raises after _start_trial creates a child run, _safe disables tracking. finish then skips child-run termination because it checks _enabled at Lines 143–145. A transient logging failure can leave the child run marked RUNNING after the fit ends. Terminate known child runs during cleanup even when tracking is disabled. MLflow requires explicit termination for runs created through MlflowClient.create_run. (mlflow.org)
90ddb38 to
50dda4a
Compare
50dda4a to
11f23bd
Compare
11f23bd to
ec7ea6e
Compare
ec7ea6e to
be04f00
Compare
0f327e3 to
6c1943c
Compare
6c1943c to
47cb59d
Compare
47cb59d to
e625375
Compare
e625375 to
d97b285
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tracking builds each run record from a neutral RunSpec, and Trainer.fit adapts its context and result into one. One _store_and_register stores, logs and registers a bundle from its directory and ModelManifest. The manifest lists the model's dependencies: the predictor entry point that the MLflow pyfunc loads, the kumo-engine package, and content-addressed assets such as text-encoder word vectors, stored once under <root>/assets/<asset_kind>/<sha256>/ and resolved with a sha256 check when the model loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aults at fit kumo.MLOps(registry=) takes a ModelRegistryLocation only: MlflowTracking registers an MLflow pyfunc (Unity Catalog, SageMaker MLflow) and SnowflakeTracking registers in the Snowflake Model Registry through its connector. Trainer.load_from_connector(cfg, location=...) loads a registered version back the same way. Runs carry no Kumo run id: the model manifest records the tracker's own run as tracking (platform, run id, URL), read as TrainingResult.tracking, and trial runs are found by name. fit(scoring_defaults=...) stores the defaults in the bundle before it is stored and registered. MlflowTracking(target=) also takes a tracking URI and log_model/load_model take target=; SnowflakeTracking takes the connector or session first and schema_name=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Store failures (snapshot, training table, model, asset) follow on_error like tracking and registration, and a model whose bundle or assets were not stored is never registered. Assets go to object stores without bucket-level calls, so prefix-scoped S3 rights suffice. A second MLflow registration into a Unity Catalog model reads search results whose tags are a method. result.export(cfg) registers a model through the MLOps tracker and returns the version that already holds its bundle. An ambient Databricks target points MLflow at the workspace for each call, so a profile works without environment variables, and a service principal's client ignores the token lent to MLflow on another thread. docs/mlops.md is restructured around a quickstart per platform, with the details in a reference section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on recipes MlflowTracking.client() yields an MlflowClient for the tracker's server with its credentials and tracking URI set for the block, so promotion and run lookups reach the server as the identity kumo-engine uses. The promotion recipes handle the first promotion, and the Snowflake one moves candidate off the new version. kumo.TextEmbedding is exported, and a text column left unencoded says when no word-vector model is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading a model by alias passes download_artifacts(registry_uri=), which MLflow added in 3.5, so on Databricks Runtime 17.3 (mlflow-skinny 3.0.1) it failed with a TypeError. The extra now requires mlflow-skinny 3.5, which %pip installs over the runtime's, and a CI job runs the MLflow suites at that floor. WriteMode is exported from kumo.engine, and the Databricks quickstart shows its install line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pyfunc's snapshot param takes the snapshot's location under the artifact root, which users rebuilt by hand from the snapshot id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fit logged "Text column 'users.BIO' will not be encoded by default" for a column whose encoder the plan set to TextEmbedding, because encoder inference runs for every column before the overrides apply. Fit now drops inference warnings for overridden columns and keeps them for the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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: #53 → #112 → #50 → #60 → #64; #114 on #112; #115 on #64
Merge order: one linear stack; merge bottom to top (#53 first); #114 sits on #112 and merges any time after it; #115 (draft) sits on #64 and merges after it
This PR: Adds experiment tracking and model registration for
Trainer.fit:kumo.MLOps(tracking=..., registry=...)records every fit in MLflow or Snowflake Experiment Tracking, registers its model in a Snowflake, Unity Catalog or MLflow registry under an alias, and logs it as an MLflow pyfunc model that batch jobs and SageMaker endpoints can score.Update (10-29, harness pass): no warning for overridden text columns
users.BIOtoTextEmbedding(...)still loggedText column 'users.BIO' will not be encoded by default: encoder inference runs for every column before the plan's overrides apply.encoder_overridesand keeps them for the rest (no_encoder_inference_warnings(overridden); no kumo-core change).test_embedding_models.py4 passed: the fixture's fit overrides the warned column and logs nothing; the fit without overrides still warns. With the trainer change reverted, the test fails.Update (10-29): usability fix pass
From the first-hour usability tests on Databricks, SageMaker and Snowflake:
download_artifacts(registry_uri=), added in MLflow 3.5; Databricks Runtime 17.3 ships mlflow-skinny 3.0.1, so it failed with aTypeErrorin notebooks. Themlflowextra now requiresmlflow-skinny>=3.5(%pipupgrades the runtime's), and a new CI jobmlflow-floorruns the MLflow suites against mlflow 3.5.0 (four tests that read logs are left out: MLflow 3.5's SQLite migrations reconfigure logging). Locally: 101 passed at 3.5.0.with tracking.client() as client:yields anMlflowClientfor the tracker's server with its credentials and tracking URI set for the block (mlflow.search_runsinside it works too). WithSageMakerMLflow(identity=...)the docs' rawMlflowClient()got 403.client.get_registered_model(name).aliasesfor the first promotion. Snowflake:VERSION champion UNSET ALIAS(skipped on the first promotion),VERSION <v> UNSET ALIAS(dropscandidate), thenSET ALIAS = champion; the old two-line recipe failed both times.kumo.TextEmbedding,kumo.WriteMode(Stypestays inkumoapi.typing; the docs passstype="text");cfg.snapshot_uri(snapshot)gives a stored snapshot's location (the pyfunc'ssnapshotparam).GloVeVectors("glove.6B.zip", dim=100),with_column("bio", stype="text").SnowflakeConnector(role=); torch CUDA note; Databricks install line;TMPDIRnext toscratch_dir.Update (10-28): one registry style, platform run ids, scoring defaults at fit, live-run fixes
kumo.MLOps(registry=ModelRegistryLocation(name, alias=...))only, and the tracker's registry registers it:MlflowTrackingan MLflow pyfunc (Unity Catalog, SageMaker MLflow, any MLflow server),SnowflakeTracking(connector)the Snowflake Model Registry version through its connector.registry=ModelOutputConfig(...)is gone fromMLOps; plainfit(export_config=ModelOutputConfig(...))is unchanged.Trainer.load_from_connector(cfg, location=ModelRegistryLocation(name, alias="champion")), one way on every platform. Re-register:result.export(cfg)/trainer.export(cfg)registers a model through the tracker; a registry that already holds the bundle under that name returns its version (MLflow, Unity Catalog, SageMaker, Snowflake).kumo.run_idandkumo.parent_runtags, trial ids<fit run id>-trial-…andEvent.run_idare gone. The model manifest records the tracker's own run astracking: {platform, run_id, url}, read asTrainingResult.tracking; runs are found in the platform UI or bykumo.bundle_id.fit(scoring_defaults=ScoringDefaults(...))writes the defaults into the bundle before it is stored and registered;with_scoring_defaultsstays for retuning.MlflowTracking(target=)also takes a tracking URI (notracking_uri=) and needsexperiment_nameonly to record;serving_snapshotmoves offMlflowTracking(it stays onlog_model, which now also takes aGraphSnapshotor snapshot id);load_model/log_modeltaketarget=;SnowflakeTracking(connector_or_session, experiment_name=..., schema_name=...).on_errorlike tracking and registration, and a model whose bundle or assets were not stored is never registered; assets are written to object stores without bucket-level calls (prefix-scoped S3 rights suffice); a second MLflow registration into a Unity Catalog model no longer fails onModelVersionSearch.tags; an ambient Databricks target sets the process-wide tracking URI per call, so a~/.databrickscfgprofile works with no environment variables; a service-principal client setsauth_type="oauth-m2m", so the token lent to MLflow on another thread cannot make its auth ambiguous.docs/mlops.mdis restructured: quickstart per platform, id table, train/register/promote, score, credentials, then Reference.Captured outputs further down (event JSON with
run_id, run trees withkumo.run_id/kumo.parent_run,registry=ModelOutputConfig(...),SnowflakeTracking(connector=...),tracking_uri=) predate this update; the bullets above supersede them.Summary
This PR adds experiment tracking and model registration for
Trainer.fit:kumo.MLOps(tracking=..., registry=...)records every fit in MLflow or Snowflake Experiment Tracking, registers its model in a Snowflake, Unity Catalog or MLflow registry under an alias, and logs it as an MLflow pyfunc model that batch jobs and SageMaker endpoints can score. Underneath are structured fit progress events (fit(callbacks=...)), and scoring defaults (a threshold or class count) stored in the model bundle.It builds on #50, which adds the
_provenance.jsonmanifests, the artifact root and thekumo.MLOpsswitch with onlyartifact_rootandstore. #64, the PR above, adds prediction-run manifests, records prediction runs in the same trackers, monitors drift and realized performance, and ships job templates.Problem. Teams running Kumo fits as scheduled Databricks, SageMaker or Snowflake jobs need to answer questions like these:
champion, without redeploying the scoring job?models:/users_churn@championwithout the training code?Today none of these can be answered.
Cause. At #50,
fit()reports progress only as Lightning progress-bar text, overwritten in place on stdout. The engine's training bindings hard-wireNoOpTrainingInstrumentation, andfit()takes no callbacks. The MLOps switch knows only where to store artifacts:Trial history stays in the trainer's scratch directory, which no tracker reads:
A publish through
fit(export_config=ModelOutputConfig(connector, location=...))records only a marker, so listing can tell Kumo versions from other tools' versions.ModelRegistryLocationhas no alias, and a fetch is by exact version or the registry's default:A bundle is sealed by its publication manifest (the bundle id is a hash of it), so a threshold chosen after the fit has no place in it.
predictneedsbinary_classification_thresholdon every binary call. Publishing happens only insidefit(). Nothing logs a model to MLflow.Change.
All examples below come from real CPU runs of this branch on a synthetic graph:
users(200 rows) andorders(3000 rows,user_idforeign key,tstime column) read through aFileConnector, queryPREDICT COUNT(orders.*, 0, 7, days) > 0 FOR EACH users.user_id, two search trials of two epochs. Hashes are shortened and floats rounded; local paths are<data>/<scratch>.1. Progress events
Trainer.fit(..., callbacks=[...])calls each callback with akumo.engine.tracking.Event(frozen dataclass:name,run_id,time,trial,epoch,metrics,params, plusto_dict()). Order:fit_started; per trial attempttrial_started, oneepoch_endedper epoch,trial_ended; thenfit_ended. JSON lines for platform log capture are one callback:Output (lines trimmed: trial 1 only, metrics cut to
val_auroc/train_loss, the searched params cut to three):{"name": "fit_started", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026044.44, "trial": null, "epoch": null, "metrics": {}, "params": {"snapshot_id": "608b12db…@6084e62f9be989de", "query_sha256": "cdf3deff…"}} {"name": "trial_started", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026044.61, "trial": 1, "epoch": null, "metrics": {}, "params": {"optimization.base_lr": 0.001, "optimization.batch_size": 512, "model_architecture.activation": "leaky_relu", "...": "...", "attempt": 1}} {"name": "epoch_ended", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026049.58, "trial": 1, "epoch": 1, "metrics": {"train_loss": 0.7117, "val_auroc": 0.502, "...": "..."}, "params": {}} {"name": "epoch_ended", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026052.48, "trial": 1, "epoch": 2, "metrics": {"train_loss": 0.6832, "val_auroc": 0.5015, "...": "..."}, "params": {}} {"name": "trial_ended", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026053.72, "trial": 1, "epoch": null, "metrics": {"val_auroc": 0.502, "seconds": 9.1058}, "params": {"status": "succeeded", "attempt": 1}} {"name": "fit_ended", "run_id": "1611ba2c…-fit-816ff38d…", "time": 1793026069.26, "trial": null, "epoch": null, "metrics": {"val_auroc": 0.517, "test_auroc": 0.502, "test_refit_auroc": 0.5303, "...": "...", "seconds": 24.82}, "params": {"status": "succeeded"}}run_idis the fit's run id, the name of its scratch directory; every event of one fit shares it.trialandepochcount from 1.fit_started.params: the snapshot id and a SHA-256 of the query (never the query text).trial_started.params: only the hyperparameters the search varied, keyed by model-plan path (non-scalars as strings), plusattempt. An out-of-memory retry is a newattemptof the same trial.epoch_ended.metrics: the epoch'strain_*andval_*metrics.trial_ended: the tune metric andseconds.fit_ended: the finalTrainingResult.metricsplusseconds.status: failedanderror("<Type>: <message>", at most 200 characters) before the exception propagates.TrainingInstrumentation.event(name, **fields)hook (a no-op by default) andlocal_training_bindings(instrumentation=).AutoTraineremits the trial events,AutoTrainingProgressLoggeremitsepoch_ended, and the searcher recordssampled_params.2. Experiment tracking
Tracking is on only through MLOps settings; there is no per-call
tracking=argument.SnowflakeConnector.session()(new) returns the Snowpark session the connector reads through;SnowflakeTracking(connector=...)opens it from the connector's identity.Credentials. Trackers authenticate with the same
Identityobjects as connectors, so a fit on a laptop or VM can record to Databricks, SageMaker or Snowflake:MlflowClient(tracking_uri, registry_uri)per tracker; the engine no longer callsmlflow.start_run,mlflow.pyfunc.log_modelormlflow.register_model, which only reach the process-wide tracking URI. Model logging ispyfunc.save_modelpluscreate_logged_model/log_model_artifacts/finalize_logged_model/log_outputs; registration iscreate_registered_modelpluscreate_model_version; system metrics come from MLflow's CPU/disk/network/GPU monitors on a kumo thread that logs through the client.DATABRICKS_HOST, a short-livedDATABRICKS_TOKENminted by the identity's own client and the process-wide tracking URIdatabricks(MLflow's Unity Catalog store downloads the logged model through the global URI; without it, registration from a laptop failed with "Logged model ... not found") (otherDATABRICKS_*auth variables unset; the client secret never enters the environment); SageMaker gets the process-wide tracking URI (thesagemaker-mlflowplugin signs and routes with it), the identity's boto3 session throughsagemaker_mlflow.session.use_session(per thread), andSAGEMAKER_MLFLOW_ASSUME_ROLE_ARNunset; SageMaker model artifacts go through anS3ArtifactRepositorybuilt with the identity's credentials. Ambient identities (DatabricksIdentity(),S3Identity()) set no credentials; a SageMaker target still sets the process-wide tracking URI for each call (the plugin signs and routes with it), so noMLFLOW_TRACKING_URIis needed, and other threads calling MLflow during a fit see the SageMaker URI. The docs say not to call MLflow or Databricks on other threads during a fit with explicit credentials.kumo.MLOpsis built (Tracking.check(registry), new optional member): MLflow looks up the experiment (or lists one), lists one registered model when it registers, and on Unity Catalog reads the model's schema (GET /api/2.1/unity-catalog/schemas/{catalog}.{schema}, no warehouse) so a missing catalog or schema, or one the principal cannot use, fails before training; Snowflake runsSHOW SCHEMAS LIKE ... IN DATABASE .... An unreachable server or refused credentials raiseRuntimeError: MLflow at <uri> is not usable with these credentials: <error>.Layers and boundaries.
kumo.engine.trackingexportsEvent,TrackingandRunRecord, for writing an adapter. The adapters live inkumo.engine.integrations.mlflowand.snowflake. Nothing is added to the top-levelkumo.enginenamespace exceptScoringDefaults.kumo.engine._tracking(records, session,fit_inputs) andkumo.engine._events. Adapters translate ready-built records into platform calls and never compute names or values. A platform that lacks a feature records less; nothing is emulated.prepare()runs on the fit's thread (MLflow uses it to nest the fit run under the caller's active run). The fit waits at most 30 s at the end; anything left finishes in the background. If the queue overflows, the rest of the fit is untracked, with one warning. While a fit is tracked, MLflow HTTP calls use 2 retries and a 30 s timeout unlessMLFLOW_HTTP_REQUEST_MAX_RETRIES/MLFLOW_HTTP_REQUEST_TIMEOUTare set.finish(None, None)that ends its run as failed, and is not called again for that fit.Runs carry pointers, not lineage. Lineage (snapshot, training table, source table versions, query) lives once, in the model's
_provenance.json(#50). A run repeats none of it as tags or params:kumo.kindfitortrialkumo.run_id<fit run id>-trial-<n>-attempt-<m>kumo.bundle_idmodels/<bundle id>/under the artifact rootkumo.parent_run,kumo.status,kumo.errorkumo.erroralso on a failed fitkumo.dropped_params,kumo.dropped_metricsParams are options: the resolved model plan flattened to dotted keys on the fit run; the searched hyperparameters and
attempton a trial. Usertags=are added under their own names; one namedkumo.*or like a param is dropped with a warning. Artifacts:query.pql,model_plan.jsonand, at finish, the model's_provenance.json.The model's
_provenance.jsonpoints back at the run. A tracked fit'sModelManifestgains (from the run above):tracking.run_idis thekumo.run_idtag of the fit run, not the tracker's own id. It is written only when the fit was tracked, and dropped where the model is embedded in a child manifest (warm start, derived model). After a fit, the tracker object's ownrun_idattribute (cfg.tracking.run_id) is the MLflow run id, or the Snowflake run name, for job summaries.MLflow. One run per fit, nested under an already active run (for example a Databricks notebook's), and one child run per trial attempt linked by
mlflow.parentRunId. Read back withMlflowClient.search_runs/get_metric_historyfrom a local store:fit_inputs(manifest)reads them from the training table's embedded snapshot manifest (plus a custom training table's own source) without opening a file; a stub parent adds none.RunRecord.inputsmaps each alias to{connector, location, version, version_kind}. MLflow logsDATABRICKStables asdelta_tablesources (unquoted name, plus the Delta version when the snapshot recorded anexactone), so Unity Catalog links each table to a model registered from the run;LOCAL_FILE,S3,GCSby URI (local,s3,gs). Snowflake, BigQuery and Custom SQL have no MLflow source type and are not logged; they stay in_provenance.json.@in metric names, so the adapter logs link-prediction metrics such astest_map@10astest_map_at_10(integrations.mlflow.metric_key, also used for the logged model's metrics).kumo.dropped_params; the full plan is inmodel_plan.json. Optional system metrics (CPU, memory, disk, network, GPU withnvidia-ml-py) go on the fit run;log_system_metrics=Falseturns them off.mlflow.pytorch.autolog()is suspended once per fit, so it adds no runs; a user'sdisable_autologging()is left alone.MLFLOW_TRACKING_URI(automatic on Databricks; the tracking server ARN plus thesagemaker-mlflowplugin for SageMaker managed MLflow; MLflow's local store otherwise).Snowflake. Snowflake Experiment Tracking (
snowflake-ml-python) has one active run per process, no nested runs and no tags, andstart_run(name)resumes a run whose name matches as a SQLLIKEpattern. SoSnowflakeTrackingrecords one run per fit with each trial attempt inside it under atrial-<n>-attempt-<m>/prefix, logs tags as params of the same name, and never resumes a run: a takenrun_namegets_<8 hex of the fit run id>with a warning. Two real fits with the samerun_name="fit_1", recorded into the test fake that stands in forsnowflake.ml.experiment(no Snowflake account was used):Calls for the first fit:
set_experiment("users_churn", database_name="ML", schema_name="EXPERIMENTS")→show_runs_in_experiment→start_run("fit_1")→log_params(71) →log_artifact×2 → per triallog_params(12),log_metrics(step=1),log_metrics(step=2),log_metrics(summary),log_params(kumo.status) →log_metrics(17, final) →log_params(kumo.bundle_id) →log_artifact(_provenance.json) →end_run(status="FINISHED").database/schemadefault to the session's current ones (PUBLICwhen onlydatabaseis given), passed explicitly on every fit.$$(they would end Snowflake's$$SQL literal), are counted inkumo.dropped_params/kumo.dropped_metrics.ExperimentTrackingsingleton already has an active run or belongs to another session, the fit is not tracked there, with a warning, and nothing on it is touched. The adapter reads the private_instance,_run,_sessionand_sql_client.show_runs_in_experiment, sokumo-snowflake[sdk]now pinssnowflake-ml-python>=2.0,<3(2.0 forend_run(status=)).native_model_lineage=Trueholds the run open as a context for the whole fit, so a model version registered meanwhile is linked to it through Snowflake lineage. Off by default.ALTER SESSION; the session's parameters are not changed. Snowflake has no dataset inputs or system metrics.params, user tags,kumo.*tagstrial-1-attempt-1trial-1-attempt-1/<name>params and metrics in the fit runstepslog_metrics(step=epoch)artifactslog_textlog_artifactof temp filesinputs3. Scoring defaults
with_scoring_defaultsvalidates the defaults against the bundle's task with the rulespredictapplies (a threshold only for binary and multilabel classification, in [0, 1]; a count only for multiclass, multilabel and link prediction), normalises them (1,1.0andnp.float32(1)give one file and one id), copies the bundle with hardlinks so the parent is never written, adds the new file to the publication manifest, and writes the copy's_provenance.json:parents.derived_fromembeds the parent's manifest; the parent's own model parents are cut to{kind, id, reconstructed: true}, so re-tuning does not grow the chain. A parent without a manifest reads as that stub.ScoringDefaults()removes the file. Clearingb213bcb0…gave back the parent's id52b1ba56….predictprecedence: an explicit argument, then (for the count) the query's ownTOP k, then the bundle default, then today's behaviour (binary raises without a threshold; multilabel uses 0.5).Trainer.export(ModelOutputConfig)(new) publishes a fitted or loaded bundle through the same step asfit(export_config=). It is a plain command: it writes only to the given config, never to the artifact root.remove_bundle_fileandbundle_task_type; the warm-start checks now sharebundle_task_type.4. Registering models
Two registration paths, chosen by the type of
registry:TrainingResult.registered_model(new) is the version the MLOpsregistrycreated;published_modelstays the plainexport_configresult. Registration is independent ofexport=.fit: plainexport_configpublish, root copy, tracking finish, then the connector registry. A failed registration warns and returns the result withregistered_model=None. A connector that cannot publish models, or an invalid alias, raises before training.kumo.MLOps(...)raises at construction for a bareModelRegistryLocationwithout a tracker that haslog_model(MlflowTracking), for a fixedversion(MLflow assigns versions), or for an invalid alias.ModelRegistryLocation.alias(new; exclusive withversion). On publish, the version is created first and the alias set afterwards; a failure to set it warns and the version stays published.kumoapi.plugin.check_model_aliasdefines one syntax every registry accepts: a letter or underscore, then letters, digits or underscores (at most 255), and notDEFAULT,FIRST,LAST,LATESTorv<digits>in any case (which also excludes SageMaker'sSageMakerLifecycle-aliases).Trainer.load_from_connector(connector, location=ModelRegistryLocation(name, alias="champion"))loads whichever version an alias points at.kumoapi.plugin.ModelAliasingSinkprotocol. Snowflake moves an alias by unsetting it on the holder, then setting it; if setting fails it puts it back and raises. Snowflake allows one alias per version, so a version that already holds a different alias is refused. Unity Catalog usesPUT /api/2.1/unity-catalog/models/{name}/aliases/{alias}andGETon the same path. File, S3 and GCS connectors have no aliases.MlflowClient().set_registered_model_alias, orALTER MODEL … VERSION … SET ALIASon Snowflake);docs/mlops.mdhas a "Promote a Kumo model" recipe that compares the new model's test metrics with the current champion's.TrainingResult.export(ModelOutputConfig)(new) publishes that result's bundle exactly asTrainer.exportdoes for the trainer's latest result, so every artifact has<artifact>.export(config)and aload_from_connector;fit(export_config=...)stays as the shorthand.DatabricksConnector. Publishing a bundle whosekumo_bundle_ida READY Kumo version of the model already carries returns that version (an alias then moves to it) and logs<name> version <n> already holds bundle <id>; reused it; no version is created or uploaded.kumo_bundle_ida version of the same name already carries returns that version (the alias and comment still apply) and logs<name> version <n> already holds bundle <id>; reused it; no new version is created.kumo_bundle_id. For the bundle above:Training metrics are not copied into either registry; they stay in
metrics.jsonand on the tracked run.list_modelsand everyfetch_modelpath return the recordedkumo_bundle_idinPublishedModel.metadata(throughkumoapi.plugin.registry_lineage), andload_from_connectorwarns when the fetched bundle's id differs.task=Task.TABULAR_BINARY_CLASSIFICATION,TABULAR_MULTI_CLASSIFICATIONorTABULAR_REGRESSION, from the bundle's prediction schema), which Snowflake Model Monitor requires. Other tasks, or a schema that cannot be read, register without one; nothing raises.Invalid model artifact path returned by Databricks.). The slash is stripped before the immediate-child check.5. MLflow models and serving
With
registry=ModelRegistryLocation(...), orMlflowTracking(log_model=True)to log without registering, the fit logs its bundle as a plain pyfunc model (logged from code, no pickled object, no custom flavor) on the fit run, registers it and sets the alias. From the run above:The model as downloaded from
models:/users_churn@champion:Scoring it:
kumo_bundle_idonly, the same pointer the connector registries record;load_modelwarns if it differs from the loaded bundle.ENTITYtakes the entity key's type,TIMESTAMPis optional, and outputs are the task's prediction columns from the newkumo_core.train.formatting.prediction_output_columns.snapshotis a stored or exported snapshot's URI or path (s3://,gs://,file://, local). An empty frame returns no rows unlesspopulation=true, which scores the query's population atprediction_time. A temporal query needsprediction_timeor aTIMESTAMPcolumn.serving_snapshot=<URI>(onlog_modelorMlflowTracking), each replica loads it when it starts andpredictuses it when nosnapshotparam is given.predictcall works in its own scratch directory (new internalscoped_scratch_dir, a context variableget_scratch_dirreads) and removes it before returning.kumo-engine[s3,gcs]of the training version, plus[text]when the bundle encodes a text column (requirements.txtlists extras sorted asgcs,s3).finishonly 30 s, so a scheduled job could exit mid-upload. After a fit that succeeded,fitqueues the tracker'slog_model(result, snapshot, registry)on the tracking thread and waits for it while that thread is alive, up to an hour, with a 600 s MLflow HTTP timeout during the upload (unlessMLFLOW_HTTP_REQUEST_TIMEOUTis set). On failure, a dead tracking thread, a tracker already dropped earlier in the fit, or the hour passing,fitwarns (Could not log the model: ...), the run endsFAILED, and the fit returns its result withregistered_model=None; an upload that finishes later is not registered.log_model(trainer, snapshot=...)logs a fitted or loaded model in the given, active or a new run, with the bundle's source tables as inputs.mlflow models serve -m <model dir> --env-manager local(on SageMaker endpoints,-m /opt/ml/model -p 8080). MLflow's stock serving containers pip-install the requirements at start and cannot install kumo-engine. SageMaker batch transform takes JSON (dataframe_splitplusparams), not CSV.InferenceAmiVersionon the production variant (for exampleal2023-ami-sagemaker-inference-gpu-4-1). Not yet tested live.ENOSYSforpidfd_open, so every prediction failed withCould not capture stable worker handles: OSError(38, 'Function not implemented')._WorkerHandle.opennow falls back to the duplicated process sentinel fd onENOSYSorEPERM(other errors still raise), andsend_signalusesos.killwhenever there is no pidfd; the live sentinel guards against PID reuse.MlflowTracking(automatic URI)DatabricksConnectorMlflowTracking+ managed MLflowSnowflakeTrackingSnowflakeConnector(Snowflake Model Registry)Packaging. New extra
kumo-engine[mlflow]installsmlflow-skinny>=3.5,<4andnvidia-ml-py. The client alone is enough (3.5 fordownload_artifacts(registry_uri=)), and it does not pull the server's dependencies, so a runtime's pyarrow and cryptography stay as they are (full MLflow upgraded cryptography and broke the preinstalled pyOpenSSL on a Databricks runtime). The pyarrow range DiskGraph accepts is widened in its companion, #124 (pyarrow>=16,<25). New extrakumo-engine[sagemaker]=kumo-engine[mlflow,s3]plussagemaker-mlflow>=0.5.0, the first release withsagemaker_mlflow.session.use_session(0.4.0 has nosession.py); it is a separate extra because the plugin's request-header provider runs on every MLflow request and warns when the process tracking URI is not a SageMaker ARN.mlflowandsnowflake.mlare imported lazily, soimport kumo.engineloads neither. The roottestextra insetup.pyadds fullmlflow>=3.13,<4, a superset of the extra that the tests need for MLflow's local SQLite store, because engine tests run with--no-depsin the root environment.uv.lockadds mlflow, mlflow-skinny and their dependencies and thesnowflake-ml-pythonspecifier; no existing locked version changes.Scheduled jobs and finding things
kumo.MLOps(on_error="warn"|"raise"), default"warn". A tracking or registration failure is logged (traceback only at DEBUG) and reported once: in warn mode as oneUserWarning(Could not log the model: ...,Could not register the model: ..., orTrainer.fit (its model is in trainer.result) finished, but MLOps tracking or registration failed: ...), so notebooks and job logs both show it; with"raise", as oneRuntimeErrorchained to the cause, after the fit has finished and its result is stored, with no warning before it. The cause's full text is kept (it was cut at 200 characters, which hid AWS's "not authorized to perform ..." part).kumo.erroron failed fits. A fit whose model logging or registration failed ends its tracked run FAILED withkumo.error(MLflow tag; Snowflake param). A connector registration failure now also ends the run FAILED.registry=ModelRegistryLocation(...)withMlflowTrackingregisters each version with akumo_bundle_idtag and returns it inresult.registered_model.metadata, as the connector registries do, so the Unity Catalog model page shows which bundle a version is.fit-<UTC start>(for examplefit-20261027T061838Z) unlessrun_nameis given.docs/mlops.mdhas a "Which id is which" table: kumo run id (kumo.run_id, manifesttracking.run_id), bundle id (result.id,kumo.bundle_id), tracker run id (tracking.run_idon the tracker), registry version (registered_model.version), with asearch_runsexample from a bundle id to its fit run.ENTITYand, for temporal queries, the anchor time (repeated rows pair up in order; entities not in the snapshot are left out).Trainer._predicttakes the internalextra_fieldsit needs for the anchor.params={"prediction_time": ...}or aTIMESTAMPcolumn fails withtemporal query: pass params={'prediction_time': ...} or a TIMESTAMP column; a binary model without a threshold or scoring defaults withbinary classification: pass params={'binary_classification_threshold': ...}. The docs example shows both.Registry names and SageMaker
kumo.MLOpsis built, against the target registry: SageMaker managed MLflow (a tracking-server or MLflow-app ARN) takes letters, digits and hyphens (^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,56}$, e.g.users-churn); Unity Catalog takes up tocatalog.schema.model, each part at most 255 characters without spaces,/,.or control characters; Snowflake takes an identifier (letter or_, then letters, digits,_,$, or a quoted name); other MLflow registries add no rule. Before, a bad name failed only after training. The rules conflict (SageMaker forbids the.Unity Catalog needs and the_Snowflake uses), so the docs say which form each registry takes.UserWarning, so notebooks and job logs show it) with theaws sagemaker create-presigned-mlflow-tracking-server-urlcommand (or Studio).sagemaker-mlflow:*permissions,pip install "kumo-engine[mlflow,sagemaker]"andMlflowTracking(target=SageMakerMLflow(arn), ...), the name rule, opening the UI; S3 examples for the artifact root, source tables (data["orders.parquet"]), prediction outputs and the monitoringstore; and "Running from a laptop" with one example per platform and the explicit-credentials caveat.examples/mlops/smoke.pyis a manual live check per platform (not run in CI).6. Internal seams: any bundle, run and predictor
No new public API. The built-in
Traineris now one client of internal, model-agnostic seams, so a later user-supplied training loop can reuse tracking, root storage, registries and serving:Run records.
_tracking.run_record(RunSpec)builds every tracked record from a neutral spec: kind, run id, flat params, metrics, per-step histories, user tags, inputs,parents(pointer tags such askumo.bundle_id), status and error.TrackingSessiontakes spec callables, andtrainer/_run_spec.pyadapts a fit into one. Nothing in_tracking.pyneeds aTrainingResult.Store, log and register.
trainer._store_and_register(bundle_dir, manifest, settings, ...)stores any bundle under<root>/models/<id>/, logs it in the session's run, and registers it. The tracker'slog_modeland MLflow's_log_modeltake(bundle_dir, ModelManifest, snapshot). A bundle without a training spec is logged without a signature.Dependencies.
ModelManifest.dependencieslists what predicting needs besides the bundle and a snapshot:predictorentry point;kumo-enginepackage and its extras;assets, byasset_kind,name,versionandsha256.Bundles without the field read as none, and get the built-in predictor.
_deps.Dependencies.require(asset)records an asset during fit. With MLOps on, it also stores the asset once under<root>/assets/<asset_kind>/<sha256>/with its own_provenance.json.resolve(asset_kind, name)returns a sha256-checked local directory. It looks in explicit locations first, then the recorded location, then the enclosing block's root. A missing asset raisesMissingDependencyError, naming the kind, name and hash. Text encoders are the first assets: a fit with MLOps on stores its word vectors, andTrainer.load, so also the pyfunc and score jobs, resolves them before falling back to the embedding-model store. A bundle never embeds the vectors.FileAssetcovers plain files.Predictors. The MLflow pyfunc loads
load_predictor(bundle_dir): the bundle'spredictordependency, with.load(bundle_dir)returning an object with.predict(rows, snapshot, params).TrainerPredictorwraps today's scoring. A predictor that cannot be imported raises aValueErrorthat names it.Back-compat
Everything here is opt-in and only records: tracking, registration and model logging never change what a fit trains or returns, and their failures are warnings.
fit()with MLOps off, or MLOps with neithertrackingnorregistrytrackingblock in_provenance.json,registered_modelisNone.fit()withoutcallbacks_provenance.json(not part of the id) gainstracking.run_id.fit_endedwithstatus: failed, runs end FAILED withkumo.error, the same exception propagates; no model is logged or registered.registered_model=None.MlflowTracking()with notargetkumo.MLOps(..., tracking=...)check), and raises there if the server or registry target is unusable; MLOps off, or without a tracker, is unchanged.SnowflakeTracking(session, ...)connector=is the new alternative.kumo.MLOps(root, ...)optionsartifact_root(tracking,registry,store,on_error) is keyword-only, so adding fields never shifts a positional argument.predict,load,load_from_connectorcallsscoring-defaults.json(all existing bundles)scoring-defaults.jsonis a listed, hash-checked extra role, which older loaders verify and ignore; predict then needs explicit arguments.scoring-defaults.jsonunlisted, unreadable, or invalid for the taskDatabricksConnectorkumo_bundle_idinstead of creating another; a new bundle id, or no id, publishes as before.kumo_bundle_idinmetadataand no mismatch warning.versionor with no versionmetadatanow carrieskumo_bundle_id.fitand export turn it into a warning).ModelAliasingSinkNotImplementedError(new call only).fit()with anexport_configwhose connector cannot publish modelsNotImplementedError, now beforefit_startedis emitted.snowflake-ml-python< 2.0 or >= 3kumo-snowflake[sdk]now asks for>=2.0,<3; the registry publish works on 2.x.mlfloworsnowflakeextraimport kumo.engineworks;MlflowTracking()/SnowflakeTracking(...)raise an ImportError naming the extra.dependenciesin_provenance.json(all earlier bundles)<root>/assets/text_encoder/<sha256>/; the bundle and its id are unchanged.Trainer.loadof a bundle whose stored assets cannot be found, and that the store does not hold eitherMissingDependencyErrorat load (only for bundles that record a stored copy); otherwise unchanged.fit_endedinstead of before it, sofit_ended'ssecondsno longer includes the upload.pidfd_openworksKnown limitations
snowflake-ml-pythonskip when the library is absent, as it was here and is in CI; privileges, Snowflake's undocumented per-run byte limit and name lookup are unexercised.native_model_lineage=Trueis off by default for the same reason.kumo_bundle_idrecord, were tested against the packages' fakes only.ExperimentTrackingper process).prediction_time.kumo_bundle_id/kumo.bundle_id.log_modeldoes not settask=.mlflow.tracking._tracking_service.utils._tracking_uri;test_mlflow_target.pypins it and the other internals relied on (mlflow.tracking.fluent._get_experiment_id, the system-metric monitor classes, the plugin's per-thread session, the Databricks SDK's variable names).Testing Done
Run at this head, one file at a time, CPU, mlflow 3.17.1, without
snowflake-ml-pythoninstalled (sqlite MLflow store in the tests; Snowflake through the recording fake):test/connector/test_custom_source.pytest_bundle_provenance.pytest_custom_sql.pytest_filestore_sink.pytest_fit_events.pytest_holdout_df.pytest_mlflow_model.pytest_mlflow_target.pytest_mlflow_tracking.pytest_mlops.pytest_optional_imports.pytest_prediction_completion.pytest_prediction_workers.pytest_predictive_query.pytest_query_from_snapshot.pytest_scoring_defaults.pytest_semantic_carry_forward.pytest_snapshot_export.pytest_snapshot_manifest.pytest_snowflake_connector.pytest_snowflake_tracking.pytest_tracking_contract.pytest_trainer_registry.pytest_training_explanations.pytest_training_table_export.pytest_warm_start.pytest_mlflow_target.py,test_mlflow_tracking.py,test_mlflow_model.py,test_snowflake_tracking.py,test_tracking_contract.py,test_trainer_registry.py, withsagemaker-mlflow0.5.0 installed.test_mlflow_target.pycovers the credential scopes with fake Databricks clients and real boto3 sessions (no network), the construction checks against a closed port and a faked Unity Catalog 404, registry reuse on a SQLite store, and pins the MLflow, plugin and Databricks SDK internals the scopes rely on.test_worker_handles.py: 3 passed. kumo-api: 12 passed. kumo-snowflake, kumo-databricks and kumo-bigquery changed tests pass.test_fit_events.py: event order and numbering on a two-trial CPU fit, JSON round trip, a raising callback, a trial that fails after training reports one failedtrial_ended, a failed fit'sfit_ended, non-finite metrics dropped.test_tracking_contract.py: a recorded fake fit (three source tables; two trials, one with an out-of-memory attempt) is replayed throughTrackingSessioninto both adapters; each store is read back and compared withrun_record/trial_record(params, metrics, step histories,kumo.*and user tags, parent links, artifacts). Also a raising tracker, a failed fit, rank > 0, and a hung tracker (stalls the fit for under 1.5 s).test_mlflow_tracking.py: link-prediction metric names (test_map@10) logged astest_map_at_10; a real two-trial fit nested under a user's run with autolog on; a failed fit;log_batcherrors; a user's disabled autologging; a hung MLflow HTTP server; source tables aslocal,s3anddelta_tableinputs (Snowflake skipped), with the Delta input parsed back the way Unity Catalog registration does;fit_inputson embedded, custom, warm-start and stub parents.test_snowflake_tracking.py: the fake's signatures match the installed library (skipped where it is not installed); native lineage; errors; an instance in use; location defaults; a taken orLIKE-matching run name; limits; the user's session is not altered; a hung call.test_mlflow_model.py: a real fit withMLOps(tracking=MlflowTracking(), registry=ModelRegistryLocation('m', alias='champion'))gives one logged model with the fit's metrics, the@championversion, metadata{kumo_bundle_id}, the provenance artifact equal toresult.manifest, typed signature, served predictions andload_model's mismatch warning;log_modelscoring equalsTrainer.predictfor the population, given entities and a prediction time, under a per-call scratch root; the wait bound, the 600 s upload timeout, a late upload not registered; the[text]extra; a tracker dropped earlier in the fit warns that the model was not logged; invalid MLflow registries refused.test_scoring_defaults.py,test_trainer_registry.py, kumo-snowflake and kumo-databricks registry tests, kumo-apitest_plugin_registry.py: as described in sections 3 and 4.test_worker_handles.py: withpidfd_openrefused byENOSYSorEPERM, a worker is still observed alive, killed and seen to exit; other errors still raise.mlflow models serve --env-manager localin an image with kumo-engine preinstalled scored both with asnapshotparam and withserving_snapshot, at about 0.6 s per call for 1 to 100 entities; that run found thepidfd_openfailure fixed above. MLflow's stock SageMaker container failed because it cannot install kumo-engine. Batch transform completed with a JSON request of 2000 entities; CSV input is refused.test_model_agnostic_mlops.py, 6 tests, CPU): a toy run, bundle, predictor andFileAssetgo through each seam. One test trains a tiny model with non-default word vectors (a GloVe-format directory built in the test) and MLOps on. A clean subprocess then serves it from the bundle, an exported snapshot and the stored assets alone, with no store, an emptyHOME, another cwd and scratch, and proxies pointing nowhere, and matches the training process's scores. After the assets are moved, it fails withMissingDependencyError. Rerun with the change:test_tracking_contract.py,test_snowflake_tracking.py,test_mlflow_tracking.py,test_mlflow_model.py,test_trainer_registry.py,test_bundle_provenance.py,test_embedding_models.py,test_mlops.py(CPU, all pass), and the kumo-snowflake and kumo-databricks registry tests. Tests that call the changed internal signatures were adapted mechanically; their assertions are unchanged.test_mlflow_model.py::test_logged_model_scores_the_query_population_and_timed_rowsis flaky on GPU, before and after this change.Checklist
docs/mlops.md).🤖 Generated with Claude Code