Repository navigation
Conversation
Change map
Requirement: scoring defaults live in the bundle and can be overridden per predict call (MLOps build). Existing: the sealed bundle with its publication manifest and |
|
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:
|
f41b8f0 to
f191b4f
Compare
d106960 to
3c40f67
Compare
f191b4f to
8bff817
Compare
8bff817 to
ceccac2
Compare
75ef25b to
a3ce235
Compare
ceccac2 to
fddd456
Compare
a3ce235 to
70a332a
Compare
fddd456 to
cc9b389
Compare
70a332a to
48367fa
Compare
cc9b389 to
2ebe796
Compare
48367fa to
4e5b58d
Compare
2ebe796 to
4174661
Compare
4e5b58d to
8bc9770
Compare
4174661 to
8c72160
Compare
8bc9770 to
6cc2f16
Compare
8c72160 to
c7854b6
Compare
ecbcea7 to
31ec9b5
Compare
68401e3 to
40295d5
Compare
31ec9b5 to
6ef5853
Compare
40295d5 to
ecae4a6
Compare
ecae4a6 to
eafd735
Compare
a37b7de to
3d8779d
Compare
eafd735 to
e53f222
Compare
3d8779d to
8dc7f3f
Compare
e53f222 to
7f4539e
Compare
8dc7f3f to
f5eb73f
Compare
7f4539e to
248091b
Compare
f5eb73f to
0d3f388
Compare
248091b to
14d2514
Compare
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: 1
packages/kumo-engine/kumo/engine/trainer/trainer.py: 1
962-963:Minor · Functional Correctness · Quick win Apply the bundle's link count when a query includes
TOP k.For link predictions, the bundle's stored count should fill an omitted
num_classes_to_return, even when the query hasTOP k. The current condition skips the default wheneverTOP kis present, so prediction raises. Only theTOP kprecedence rule is scoped to class counts, not link counts. KeepTOP kin the resolver call so its existing handling is unchanged.🐛 Suggested fix
- if num_classes_to_return is None and parsed.query.top_k is None: + if num_classes_to_return is None and ( + parsed.query.top_k is None or task_type.is_link_pred): num_classes_to_return = training_result.scoring_defaults.num_classes_to_return

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-snowflake-tracking
- File: .coderabbit.yaml
- Commit: 61b7ba6
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
ceacb288-4e90-4d12-af34-7c504d7ba756
Files selected for processing: 12-
packages/kumo-core/src/kumo_core/artifacts.pypackages/kumo-core/tests/test_artifacts.pypackages/kumo-engine/docs/mlops.mdpackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/trainer/__init__.pypackages/kumo-engine/kumo/engine/trainer/_provenance.pypackages/kumo-engine/kumo/engine/trainer/_scoring.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_prediction_completion.pypackages/kumo-engine/test/test_prediction_workers.pypackages/kumo-engine/test/test_scoring_defaults.pypackages/kumo-engine/test/test_semantic_carry_forward.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 1 remain after this review.
-
A threshold tuned on the holdout after fit has nowhere to live: every scheduled score job must pass it again, and a registry version cannot say which threshold it was validated with. TrainingResult.with_scoring_defaults(ScoringDefaults(...)) copies the bundle, adds scoring-defaults.json and returns a TrainingResult for the copy (its own bundle id). Its _provenance.json embeds the parent's ModelManifest in parents.derived_from (the parent's own model parents cut to ids). predict falls back to TrainingResult.scoring_defaults when a call omits the threshold or class count. Trainer.export(config) publishes a fitted or loaded bundle. Verified with test_scoring_defaults.py and kumo-core test_artifacts.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 scoring defaults carried in the model bundle: a threshold or class count chosen after fit is stored in a derived bundle with its own id, and
predictuses it when a call does not pass one.Summary
This PR adds scoring defaults carried in the model bundle: a threshold or class count chosen after fit is stored in a derived bundle with its own id, and
predictuses it when a call does not pass one. It also addsTrainer.export, so a fitted or loaded model can be published outsidefit().Problem. Teams scoring on a schedule run into these:
fit()can publish.Today none of these can be solved without a code change in every job that calls
predict.Cause. The threshold depends on holdout predictions, so it exists only after
fit()has written and sealed the bundle. The bundle has no place for it:Editing a file inside it would change the bundle without anything recording the edit.
predicttherefore needsbinary_classification_thresholdon every binary call, and publishing happens only insidefit().Change.
kumo.engine.ScoringDefaults(binary_classification_threshold, num_classes_to_return). Field names match thepredictarguments.TrainingResult.scoring_defaults: the bundle's defaults (allNonewhen it has none). It is set for a fit's result and forTrainer.load/load_from_connector(trainer.result).TrainingResult.with_scoring_defaults(defaults) -> TrainingResult:predictapplies: a threshold only for binary and multilabel classification and only in [0, 1]; a count only wherepredictaccepts one (multiclass, multilabel, link prediction);float, count toint, numpy scalars accepted), so1,1.0andnp.float32(1)give the same file and the same id;scoring-defaults.json, lists it in the publication manifest and writes the copy's_provenance.json;ScoringDefaults()removes the file, so the id returns to the parent's.Trainer.export(ModelOutputConfig) -> PublishedModelpublishes a fitted or loaded model through the same stepfit(export_config=)uses. It is a plain command: inside or outside akumo.mlopsblock it writes only to the given config, never to the artifact root.remove_bundle_file(bundle_dir, role)next toadd_bundle_file. It also gainsbundle_task_type(artifact_paths), which the scoring defaults, the trainer's warm-start check and kumo-core's warm-start resolver now share instead of each readingprediction-schema.json.docs/mlops.mdgains a "Scoring defaults" section.Derived bundle from a binary fit on a small synthetic graph (
PREDICT COUNT(events.*, 0, 7, days) > 0 FOR EACH ents.ID, 50 entities):scoring-defaults.json:{ "binary_classification_threshold": 0.5, "num_classes_to_return": null }Its publication-manifest entry, so it is hash-checked at load and part of the bundle id:
{"relative_path": "scoring-defaults.json", "role": "scoring_defaults", "sha256": "5bd069d9…", "size_bytes": 78}The derived bundle's
_provenance.json(ModelManifest, #56) records no fit of its own. Its only parent is the original model's manifest, embedded inderived_from, which carries the training table and snapshot (shortened):{ "kind": "model", "id": "5c4f281c…", "fit": null, "fit_mode": null, "parents": { "derived_from": { "kind": "model", "id": "c728c4af…", "fit": {"started_at": "2026-10-24T18:19:46+00:00", "finished_at": "2026-10-24T18:20:04+00:00", "seconds": 17.913}, "fit_mode": "refit_trainval", "parents": { "training_table": {"kind": "training_table", "id": "tt-3d20699a…", "query": "PREDICT COUNT(events.*, 0, 7, days) > 0 FOR EACH ents.ID", "parents": {"snapshot": {"kind": "snapshot", "id": "bca8c681…@2f054e40…", "...": "..."}}, "...": "..."}, "warm_start": null }, "...": "..." }, "training_table": null, "warm_start": null }, "created_at": "2026-10-24T18:20:04+00:00", "environment": {"build_commit": "14d25143", "diskgraph": "0.1.0", "...": "..."}, "schema_version": 1 }id: the derived bundle's id.parents.derived_from: the parent's manifest, embedded the way Record bundle_id and fit provenance in every model bundle #56 embeds a warm-start model. Its own model parents (warm_start,derived_from) are cut to{kind, id, reconstructed: true}, so a chain of re-tunings does not grow each manifest.derived_fromis the stub{kind: "model", id: <parent id>, reconstructed: true}.In that run the parent's id was
c728c4af…and the derived one's5c4f281c…. Clearing the defaults on the derived result gave backc728c4af….How
predictpicks each value:num_classes_to_return)predictargumentTOP kscoring-defaults.json)Back-compat
This PR only adds new calls and a new optional bundle file, and records rather than enforces: no existing call changes behaviour.
predict,fit,load,load_from_connectorcallsscoring-defaults.json(all existing bundles)with_scoring_defaultson a bundle from main (no_provenance.json)derived_fromis a stub with the parent's id.with_scoring_defaultson a bundle whose_provenance.jsonis unreadable or from a newer enginederived_fromis a stub.scoring-defaults.jsonis a listed, hash-checked extra role, which older loaders verify and ignore. Predict then needs explicit arguments, as today.scoring-defaults.jsonpresent but not listed in the manifestwith_scoring_defaultsValueErrorbefore anything is copied.exportto a connector that cannot publish modelsNotImplementedError, the same checkfit(export_config=)makes.Testing Done
test_scoring_defaults.py(6 tests, CPU fit):idand its embeddedderived_from; re-tuning cuts the grandparent to a stub; clearing the defaults restores the parent's id;1andnp.float32(1)give one id;load_from_connector, keeps the defaults and the id;test_artifacts.py. Together: 17 passed on this head.Checklist
docs/mlops.md).🤖 Generated with Claude Code