Repository navigation
Conversation
Change map
Requirement: MLflow pyfunc + registry with signature, LoggedModel and alias loading, registered through |
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 24 minutes. View limit details
Walkthrough
Priority: Merge Risk:
|
d106960 to
3c40f67
Compare
997485f to
d9882b6
Compare
d9882b6 to
63da7fe
Compare
63da7fe to
1bea937
Compare
75ef25b to
a3ce235
Compare
1bea937 to
eae8b9a
Compare
a3ce235 to
70a332a
Compare
eae8b9a to
71256af
Compare
70a332a to
48367fa
Compare
71256af to
1119fef
Compare
48367fa to
4e5b58d
Compare
1119fef to
2bee8ea
Compare
4e5b58d to
8bc9770
Compare
2bee8ea to
db65780
Compare
fbfc9a5 to
c153ef2
Compare
43aca9e to
8ba05ce
Compare
c153ef2 to
fb48237
Compare
8ba05ce to
18fec4c
Compare
fb48237 to
d42d943
Compare
18fec4c to
963b4d9
Compare
d42d943 to
e7a6da7
Compare
963b4d9 to
f804911
Compare
e7a6da7 to
ca4703c
Compare
f804911 to
1ca4d26
Compare
ca4703c to
238a52e
Compare
1ca4d26 to
fdf8531
Compare
238a52e to
18e3b42
Compare
fdf8531 to
5255ad9
Compare
18e3b42 to
b7ddc6c
Compare
5255ad9 to
09c3701
Compare
b7ddc6c to
824fa79
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 3
packages/kumo-engine/kumo/engine/integrations/mlflow.py: 1
385-396:Minor · Functional Correctness · Quick win Warn instead of failing when the alias or comment step fails after registration.
mlflow.register_modelcreates the version first. Ifupdate_model_versionorset_registered_model_aliasthen raises,_registerraises too.TrackingSession.log_modelreports "Could not log the model" and returnsNone.fitreturnsregistered_model=Noneand the run endsFAILED. The version still exists in the registry, so the result does not match the registry state.The connector path in
trainer._publishhandles this case differently. It warns about the missing alias and still returns the published model. Use the same behavior here.🛠️ Proposed fix
version = str(mlflow.register_model(info.model_uri, name).version) client = mlflow.MlflowClient() - if location.comment: - client.update_model_version(name, version, - description=location.comment) - if location.alias: - client.set_registered_model_alias(name, location.alias, version) + try: + if location.comment: + client.update_model_version(name, version, + description=location.comment) + if location.alias: + client.set_registered_model_alias(name, location.alias, version) + except Exception as error: + warnings.warn( + f'Registered {name} version {version} without its comment or ' + f'alias: {error}', UserWarning, stacklevel=2) return PublishedModel(name=name, version=version, uri=f'models:/{name}/{version}')packages/kumo-engine/kumo/engine/trainer/trainer.py: 1
584-586:Minor · Stability & Availability · Quick win Finish the tracking session if the model-logging wait is interrupted.
session.log_modelcan block for up to an hour. Thetry/except BaseExceptionearlier infitcovers onlyself._fit. If a user presses Ctrl-C, or anotherBaseExceptionarrives, during this wait,session.finishnever runs. The results are:
- The MLflow fit run stays
RUNNING.- The process-global
MLFLOW_HTTP_REQUEST_*variables set by_limited_httpare not removed.- The tracking thread keeps running.
Wrap the call so the session always finishes.
🛠️ Proposed fix
- registered, error = session.log_model(result, snapshot, - _mlflow_registry(settings)) - session.finish(result, error) + try: + registered, error = session.log_model( + result, snapshot, _mlflow_registry(settings)) + except BaseException as interrupt: + session.finish(result, interrupt) + raise + session.finish(result, error)packages/kumo-engine/kumo/engine/integrations/_mlflow_model.py: 1
19-19:Minor · Functional Correctness · Quick win Align the declared
PREDICTEDsignature type with classification output.
_DTYPESdeclaresPREDICTEDasbooleanatpackages/kumo-engine/kumo/engine/integrations/_mlflow_model.py:19. The classification path inpackages/kumo-core/src/kumo_core/train/formatting.pybuildsPREDICTEDwithdf.apply(lambda x: 1 if x["CLASS"] == x[target_col_name] else 0, axis=1), then renames the column toPREDICTED. That produces integer 0/1 values, not booleans.MLflow does not enforce output schemas in
pyfunc.predict, so this mismatch is not caught at scoring time. Consumers that derive result types fromsignature.outputscan then receive values that do not match the declared type. One example ismlflow.pyfunc.spark_udfwhen noresult_typeis given.Declare the classification
PREDICTEDoutput with an integer type, such aslong, rather thanboolean. Change only the classification entry. Before editing_DTYPES, confirm that the entry is not shared with other task types. The multilabel path setsPREDICTEDtoNone(formatting.py:477), so one type may not fit every task.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-registry-aliases
- File: .coderabbit.yaml
- Commit: 71815cc
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
8127603b-c8f6-4624-84a5-45c9bd99d389
Files selected for processing: 11-
packages/kumo-core/src/kumo_core/train/formatting.pypackages/kumo-core/tests/test_prediction_output_columns.pypackages/kumo-engine/docs/mlops.mdpackages/kumo-engine/kumo/engine/_mlops.pypackages/kumo-engine/kumo/engine/_scratch.pypackages/kumo-engine/kumo/engine/_tracking.pypackages/kumo-engine/kumo/engine/integrations/_mlflow_model.pypackages/kumo-engine/kumo/engine/integrations/_mlflow_model_code.pypackages/kumo-engine/kumo/engine/integrations/mlflow.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_mlflow_model.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 4 remain after this review.
-
| uri = params.get('snapshot') or self._default | ||
| if not uri: | ||
| raise ValueError("Pass params={'snapshot': <exported snapshot " | ||
| "URI>}.") | ||
| return predict(self._trainer, self._snapshot(uri), model_input, | ||
| params) | ||
|
|
||
| def _snapshot(self, uri: str) -> GraphSnapshot: | ||
| if uri not in self._snapshots: | ||
| connector, path = snapshot_location(uri) | ||
| self._snapshots[uri] = GraphSnapshot.load_from_connector( | ||
| connector, path=path) | ||
| return self._snapshots[uri] |
There was a problem hiding this comment.
Restrict client-supplied snapshot URIs on a served model.
predict takes params['snapshot'] from the caller. _snapshot passes it unchecked to snapshot_location and connector_for_root. It then calls GraphSnapshot.load_from_connector. This causes three problems for any serving endpoint:
- A client can make the replica read from any
s3://,gs://or local path, using the server's credentials. It then loads the downloaded index into DiskGraph. self._snapshotscaches every loaded URI, and nothing evicts entries. Each distinct URI adds disk and memory use for the life of the process. A client can exhaust both.- The
serving_snapshotdefault does not constrain this, because asnapshotparam always overrides it.
Add a way to pin serving to the configured snapshot. For example, take an allow-list of URI prefixes from model_config, or ignore the snapshot param when serving_snapshot is set. Optionally bound the cache.
A model fitted with MLflow tracking cannot be registered in Unity Catalog or SageMaker managed MLflow, nor served by MLflow: the fit run records metrics only. kumo.MLOps(tracking=MlflowTracking(), registry=ModelRegistryLocation( name, alias=...)) logs the fit's bundle as a pyfunc model on the fit run and registers it (one alias), returned as TrainingResult.registered_model. The fit waits for upload and registration (up to an hour, 600 s HTTP timeout for the upload); a failure warns and ends the run FAILED. MLOps construction refuses a bare location without a tracker that has the log_model hook, a fixed version, or an invalid alias. log_model()/load_model() log any fitted or loaded model and restore it; the pyfunc model scores a snapshot URI param or a serving_snapshot loaded at startup, each predict in its own scratch dir. kumo-core gains prediction_output_columns for the signature. Verified with test_mlflow_model.py, test_mlops.py, test_mlflow_tracking.py, test_tracking_contract.py and kumo-core test_prediction_output_columns.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Folded into #60 (tracking & registry), which now contains this PR's final code: the consolidated stack shows only the end state. Review continues there. |
Series: kumo-engine MLOps series: every snapshot, training table, model and prediction run carries a self-contained _provenance.json;
with kumo.mlops(...)stores each artifact under one artifact root, tracks fits and prediction runs in MLflow or Snowflake and registers models; volumes and stages can hold files; prediction runs are monitored for drift and realized performance.Stack: Models, registry and job templates: #57 → #59 → #65 → #71
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds MLflow model logging, registration and scoring for Kumo models.
Summary
This PR adds MLflow model logging, registration and scoring for Kumo models. With
kumo.MLOps(tracking=MlflowTracking(), registry=ModelRegistryLocation(name, alias=...)), each fit logs its bundle as an MLflow pyfunc model on the fit run (#60), registers it, points the alias at the new version, and can be scored later frommodels:/name@alias. It is the MLflow counterpart of the connector-registry registration in #59.Problem. Teams that run MLflow (Databricks Unity Catalog, SageMaker managed MLflow, a self-hosted server) want to:
@championand roll back by moving the alias;Today none of these can be done. MLflow sees a fit's runs, metrics and source-table inputs (#60), but never the model.
Cause. With #60, a tracked fit records the run, its trials, its source tables as inputs and three artifacts. The bundle reaches the artifact root (#56) but never MLflow:
Change.
From a CPU fit of
PREDICT ents.Y FOR EACH ents.IDon a synthetic 400-row table, local MLflow store (hashes shortened, local paths dropped):The logged model, downloaded from
models:/users_spend@champion:Excerpt of
MLmodel:What each part records and does:
MLOps(registry=ModelRegistryLocation(...)). It needs a tracker with thelog_modelhook (MlflowTracking).kumo.MLOps(...)raises in three cases: a bare location without such a tracker, a fixedversion(MLflow assigns versions), or an alias that Add model registry aliases and a bundle id pointer on registered versions #59'scheck_model_aliasrefuses (which also refuses SageMaker's reservedSageMakerLifecycle-prefix). One alias is set withset_registered_model_alias, andModelRegistryLocation.commentbecomes the version's description. The version comes back asTrainingResult.registered_model(Add model registry aliases and a bundle id pointer on registered versions #59), andpublished_modelstays the plainexport_configresult.MlflowTracking(log_model=True)logs the model without registering it.kumo_bundle_idonly, the same pointer Add model registry aliases and a bundle id pointer on registered versions #59 records on connector-registry versions. Lineage stays in the bundle's_provenance.json, which is both a fit-run artifact (Record fits in experiment tracking and publish models to registries #60) and a model artifact.log_modeloutside a fit logs the same inputs into the run it logs to, read with Record fits in experiment tracking and publish models to registries #60'sfit_inputs(result.manifest).requirements.txtpinskumo-engine[gcs,s3]==<training version>, the extras that loading a snapshot from S3 or GCS needs.signatureis typed per bundle:ENTITYtakes the type of the entity table's key, andTIMESTAMPis optional. Outputs are the task's prediction columns, from the newkumo_core.train.formatting.prediction_output_columns. For example, a binary model outputsENTITY, TARGET_PRED, False_PROB, True_PROB.snapshotis the full URI or local path of a stored or exported snapshot (s3://,gs://,file://or local, through Record provenance for every artifact under one MLOps artifact root #50'sconnector_for_root). The others areprediction_time,binary_classification_threshold,num_classes_to_returnandpopulation.model_id, somlflow.search_logged_models(..., order_by=[{"field_name": "metrics.val_mae"}])ranks fits.finishat most 30 s before the fit returns, so a scheduled job could exit mid-upload, leaving nothing registered and the runRUNNING. Model logging is therefore not part offinish.Trainer.fitqueues the tracker's optionallog_model(result, snapshot, registry)on the tracking thread, so it never races the tracker's other calls. The fit waits for it while that thread is alive, up to 1 hour. Thenfinish(record, error)runs under the usual 30 s limit.fitwarns (Could not log the model: ...) andfinishgets the error, so the run endsFAILED. The fit still returns its result, withregistered_model=None.MLFLOW_HTTP_REQUEST_TIMEOUTis 600 s instead of the fit's 30 s, and only when the user has not set it.Scoring and loading:
Snapshots.
predictloads each snapshot once per model instance and keeps every one it has loaded for the life of the process (no eviction). Each serving replica, and each Spark executor ofspark_udf, downloads it once. Withserving_snapshot=<snapshot URI>(onlog_modelorMlflowTracking), the model loads that snapshot inload_context, i.e. when a replica starts, andpredictuses it when nosnapshotparam is given.Scratch. Each call scores under its own scratch root: a per-call temporary directory set through a context variable that
get_scratch_dirreads (new internalscoped_scratch_dir(path)), removed when the call returns or raises.What a call scores. An empty frame returns no rows unless
population=true, which scores the query's own population atprediction_time. Scoring goes throughtrainer.predict(snapshot, entities=..., prediction_time=...)(Score a snapshot with Trainer.predict(snapshot) and TrainingResult.predict #51).Where it runs. The model is for batch jobs where kumo-engine is installed, not for MLflow Model Serving. MLflow's serving-input check, which cannot pass without a snapshot, is silenced during logging.
Outside a fit.
kumo.engine.integrations.mlflow.log_model(trainer, snapshot=...)logs a fitted or loaded model in the given, active or a new run.load_model(uri) -> Trainerrestores it and warns if the model'skumo_bundle_iddiffers from the bundle it loaded.docs/mlops.mdgains an "MLflow models" section and a "Which registry" table:DatabricksConnectorregistryIt also states the MLflow minimum: a 3.13 or later client, because DiskGraph pins pyarrow 24.
Back-compat
Model logging is opt-in. It only adds records to MLflow; it never changes what a fit trains, writes or returns.
fit()without MLOps, or MLOps withoutregistry=/log_model=TrueMLOps(tracking=MlflowTracking(), registry=ModelRegistryLocation(...))fitreturns;fitwaits up to 1 hour. A failed fit logs no model.FAILED, and the fit returns its result withregistered_model=None.ModelRegistryLocationwithout an MLflow tracker, a fixed version, or an invalid aliasValueErrorwhenkumo.MLOpsis built, before any work.Trackingadapterslog_model(result, snapshot, registry)is optional; adapters without it are unaffected._provenance.jsonlogged withlog_modelMLFLOW_HTTP_REQUEST_TIMEOUTset by the userrequirements.txtpins the training version;load_modeldoes not check it.Known limitations
kumo_bundle_id/kumo.bundle_id; MLflow has no native link to a registry it did not create.FAILEDand the model, if it finishes uploading later, is not registered.kumo.MLOps(registry=...), registered and scored live on SageMaker and Databricks.Testing Done
CPU, mlflow 3.17.1:
test_mlflow_model.py:MLOps(tracking=MlflowTracking(), registry=ModelRegistryLocation('m', alias='champion')):@championversion's source is that model, andresult.registered_modelnames it;kumo.*tags are exactly kind, run id and bundle id;{kumo_bundle_id}, and its_provenance.jsonequalsresult.manifest;pyfunc.predictreturnsENTITY, TARGET_PREDper entity;load_modelreturns the same bundle id and warns on a mismatchedkumo_bundle_id.log_model. A model logged withlog_model:kumo-engine[gcs,s3]==<version>and loads itsserving_snapshotwhen it is loaded;Trainer.predict, also with nosnapshotparam;finish, and a failinglog_modelwarns and reachesfinishas the error.finishas the error.kumo.MLOps.test_mlflow_model.py+test_mlops.pygave 14 passed andtest_mlflow_tracking.py+test_tracking_contract.pypassed. kumo-coretest_prediction_output_columns.pypassed. flake8 and isort are clean.Checklist
docs/mlops.md).🤖 Generated with Claude Code