Repository navigation
Conversation
Change map
Observed requirement: scheduled/MLflow scoring jobs hold a snapshot and a bundle but no |
52c934b to
b4f46b8
Compare
dfad21f to
75f9e80
Compare
b4f46b8 to
a5ad423
Compare
dfad21f to
4079849
Compare
b4f46b8 to
2f97f33
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/README.md: 1
409-410: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winClarify snapshot reuse in the reload guidance.
The graph-free scoring path is documented earlier, but the reload section still instructs every scoring job to rebuild the live graph and snapshot. Distinguish graph-based scoring from graph-free scoring.
Suggested documentation fix
-For an exported model, use -`Trainer.load_from_connector(connector, path="model/version")`, or -`location=ModelRegistryLocation(...)` for Snowflake Model Registry. Rebuild -the live graph and snapshot before generating prediction inputs. Bundles -contain trained artifacts, not source data or graph storage. +For an exported model, use +`Trainer.load_from_connector(connector, path="model/version")`, or +`location=ModelRegistryLocation(...)` for Snowflake Model Registry. If the +scoring job has `Graph` code, rebuild the live graph and snapshot before +generating prediction inputs. A graph-free scoring job can instead load the +exported snapshot with `GraphSnapshot.load_from_connector(...)` and bind the +query with `PredictiveQuery.from_snapshot(snapshot, trainer.training_query)`. +Bundles contain trained artifacts, not source data or graph storage.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
4372abd0-2cd3-4a74-a511-01be4ef9315b
📒 Files selected for processing: 3
packages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/graph.pypackages/kumo-engine/kumo/engine/pquery/predictive_query.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.
aaron-kumo
left a comment
There was a problem hiding this comment.
Discussion — Confirm the compatibility boundary and scoring ergonomics.
PredictiveQuery.from_snapshot() and Trainer.training_query look consistent with the existing explicit, stage-oriented interface. I would keep these entry points.
Could we clarify that the graph-id check establishes membership between the query binding and supplied snapshot, rather than compatibility with the trained model? Future model compatibility should compare the trained requirements—features, types, relations, encoders, query contract, and temporal settings—with the scoring snapshot. Requiring equal training/scoring graph ids would unnecessarily restrict compatible graphs from another environment.
Also, callers must remember to pass trainer.training_table_generation_plan when generating prediction inputs. Would a future trainer helper that supplies the saved query/settings and performs compatibility checks be useful? I wouldn’t require that convenience API in this PR, but documenting the intended direction would help avoid multiple competing scoring paths.
4079849 to
3d4b9a7
Compare
2f97f33 to
b63a463
Compare
3d4b9a7 to
447de17
Compare
7c07303 to
b6394e2
Compare
91af183 to
49bae78
Compare
aaron-kumo
left a comment
There was a problem hiding this comment.
Re-reviewed at 49bae78. Approved. PredictiveQuery.from_snapshot() and Trainer.training_query remain explicit entry points, and Trainer.predict_snapshot() supplies the saved query and table-generation plan for both generated and supplied entities. The docs correctly distinguish query/snapshot graph membership from model compatibility, with full model-compatibility checks explicitly deferred; a different training graph produces a warning rather than an unconditional refusal.
No additional issue identified in this PR's changes. Validation was source review and inspection of the added regression coverage; the full integration suites were not rerun because this checkout lacks the required Python environment.
b6394e2 to
46465ff
Compare
A job that loads a model bundle and a stored snapshot has no Python Graph, but PredictiveQuery required one, so it could not compile a prediction table. Prediction-table compilation only needs the snapshot's graph config. PredictiveQuery.from_snapshot(snapshot, query) binds the query to a GraphSnapshot: parsing uses the snapshot's graph_config and the snapshot match check uses the graph id embedded in snapshot.id, so a snapshot of a different graph is still refused. PredictiveQuery(graph, query) is unchanged. Verified: a snapshot-bound query generates prediction tables (generated and supplied rows) that Trainer.predict scores identically to the Graph-bound path; existing predictive-query and custom-source tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A job scoring a loaded model had no public way to get the query it was trained on, so binding it with PredictiveQuery.from_snapshot meant copying the text out of training-spec.json. Trainer.training_query now returns it. The snapshot check accepted a snapshot whose id equals a Graph's bare id; equality now applies only to a snapshot-bound query, so a Graph-bound query keeps the original prefix check. Verified with test_query_from_snapshot.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The from_snapshot change carried docstring prose that restated the code, and its graph-check test called a private helper. Shorten the module note and training_query docstring, drop the restating _parsed paragraph, and exercise the snapshot check through the public suggest_model_plan instead of _require_matching_snapshot. test_query_from_snapshot.py: 2 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PredictiveQuery kept its own normalized-graph cache and parsed the graph id out of snapshot ids. GraphSnapshot now offers native_graph (cached, like Graph.native_graph), and the snapshot check compares graph_key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A graph-free scoring job had to bind the saved query to the snapshot, pass the saved generation plan to generate_prediction_table and then call predict; each caller (MLflow model, job scripts) repeated that. Trainer.predict_snapshot(snapshot, *, entities=None, prediction_time=None, ...) does it in one call with predict's options. The docs now say the query's graph check only ties a snapshot to the query's graph, not to a trained model. Verified with the query-from-snapshot and predictive-query tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports the fitted snapshot, loads it into a fresh scratch directory without a Graph, reloads the model from its bundle and scores with predict_snapshot and with PredictiveQuery.from_snapshot; both match the graph-bound scores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
predict_snapshot bound the saved query to whatever snapshot it was given, so scoring a snapshot of a different graph than the model was trained on went through silently. It now warns (and still scores) when the snapshot's graph_key differs from the graph key of the snapshot recorded at fit/load. Verified with test_query_from_snapshot.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
predict_snapshot sat next to predict, so scoring had two entry points with overlapping arguments. - Trainer.predict(snapshot, *, table=, entities=, prediction_time=, ...) scores a snapshot with the trained query and generation plan (entities or a generated table instead when given) and warns when the snapshot is of another graph. main's predict(prediction_table, snapshot=...) still works, positionally or by keyword. Argument checks run first, as before. - TrainingResult.predict(...) scores with the model that produced or loaded it (TrainingResult._trainer, set by fit and load). - predict_snapshot is gone (never released); Trainer.training_query and GraphSnapshot.native_graph are private. Docs move to docs/mlops.md. Verified: test_query_from_snapshot.py (snapshot, entities, table and legacy forms, other-graph warning, a fresh scratch scoring an exported snapshot through a loaded result), prediction workers, output columns, completion, preflight, registry, warm start, embedding and file connector suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49bae78 to
c5b4bc8
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Folded into #50 (artifacts & provenance), 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: Snapshot portability: #54 → #51
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Makes
Trainer.predict(snapshot)the one scoring entry point, addsPredictiveQuery.from_snapshot(snapshot, query)andTrainingResult.predict, so a job that has a trained model bundle and a graph snapshot, but noGraphcode, can score in one call.Summary
This PR makes
Trainer.predict(snapshot)the one scoring entry point, addsPredictiveQuery.from_snapshot(snapshot, query)andTrainingResult.predict, so a job that has a trained model bundle and a graph snapshot, but noGraphcode, can score in one call.Problem. Teams running Kumo in production need to answer questions like these:
predict()build a prediction table when nothing in the package can rebuild theGraph?Today none of these work:
PredictiveQuerycan only be bound to aGraph, and a loadedTrainerkeeps the training query privately.Cause. A scoring job has these on hand, and no
Graph:GraphSnapshotalready carries everything needed to compile a query:graph_configis the same portable graph config the sourceGraphproduces, andTrainer.predictalready scores against it. ButPredictiveQueryreadself.graph.native_graphto parse, and checked a snapshot withsnapshot.id.startswith(f'{self.graph.id}@'). Passing aGraphSnapshotin place of theGraph, onmaintoday:Change. No new files or on-disk changes.
PredictiveQuery.graphwidens fromGraphtoGraph | GraphSnapshot,from_snapshotbinds a snapshot, andTrainer.predicttakes the snapshot first and uses the query of the last fit or load (read from the bundle'straining-spec.jsonon load):Before, the job must rebuild the graph:
After, the snapshot is enough. It is loaded from the export the training job wrote (
locationis whatgraph.snapshot().export(SnapshotOutputConfig(store, "snapshots"))returned there):predict(snapshot, *, table=None, entities=None, prediction_time=None, output_config=None, num_classes_to_return=None, num_workers=1, binary_classification_threshold=None)binds the saved query to the snapshot, builds the prediction table with the saved table-generation plan (trainer.training_table_generation_plan), and scores it.table=scores a prediction table generated beforehand instead. Withoutentitiesit scores the query's population atprediction_time(latest source time when omitted);entitiesis a source table ofENTITY1(andTIMESTAMP) rows to score instead, and passing both raisesValueError. When the snapshot's graph key (the graph id half ofsnapshot.id) differs from the graph key of the snapshot the model was fitted or loaded with, it warns (UserWarning) and still scores: the model may be scoring a graph it was not trained on. It is the one scoring path the MLflow model (#65) and the job scripts (#71) call. main's form,predict(prediction_table, snapshot=...), keeps working, positionally or withprediction_table=as a keyword. ATrainingResultfromfitor a loaded trainer'strainer.resultalso scores (result.predict(snapshot, ...)), with the model that produced it. The pieces stay public for jobs that need them:On a synthetic users/orders graph (50 users, 3,000 orders, one-epoch CPU fit, bundle published to a
FileConnector), a fresh process running the "after" code (with the query text passed explicitly; this run predates the mergedpredict(snapshot), which the tests below cover) returned 50 rows identical to the Graph-bound scores from the training process:How it works:
graph_configin kumo-core's normalized form (a private cachedGraphSnapshot._native_graph) instead ofgraph.native_graph. AGraph-bound query still uses theGraph's own.Graph-bound query. A snapshot-bound query also accepts its own snapshot, or any snapshot with the samegraph_key, so a snapshot of the same graph over newer data is accepted; a snapshot of a different graph is refused withValueErrorfor both bindings.get_task_type,validate,suggest_model_plan,generate_training_table,generate_prediction_tableandPredictionTable/TrainingTable.from_source_tableall work on a snapshot-bound query.docs/mlops.md("Scoring without the graph") says where a graph-free job gets its snapshot and model, scores withtrainer.predict(snapshot), and states what the graph check does and does not mean.Back-compat
This PR only adds an entry point. It adds no new checks or errors on any existing path.
PredictiveQuery(graph, query)graph.native_graph, same snapshot check. Only the refusal message is reworded ("was not materialized from this query's graph (id ...) -- pass a snapshot of that graph.").PredictiveQuery.from_snapshot(snapshot, query)snapshot.graph_config. Accepts any snapshot with the same graph key (the graph id half ofsnapshot.id).ValueError, for both bindings.@Trainer.predict(snapshot)(new form)RuntimeError. Otherwise the same result as building the table by hand and callingpredict. A snapshot of a different graph than the training snapshot gets aUserWarningand is still scored.trainer.predict(prediction_table, snapshot=...)(positional or keyword)num_workers,num_classes_to_return) still run first.TrainingResult.predictRuntimeError.pquery.graphGraphSnapshotfor a snapshot-bound query;Graphotherwise.Known limitations
predict, aValueErrorinPredictiveQuery). It does not check that the snapshot's features, types or encoders match what the model was trained on.predict(snapshot)needs the trained query and plan, so it works only on a trainer that has been fitted or loaded from a bundle; a hand-builtTrainingResultcannot predict.mlops=onpredict(storing and tracking prediction runs) comes with Record a manifest and output score statistics for every prediction run #67 and Log prediction runs to MLflow and Snowflake through the tracking adapters #73.Testing Done
test_query_from_snapshot.py(4 tests, run at the current head: 4 passed):FileConnectorand loaded into a fresh scratch directory with noGraph; the model is reloaded from its bundle, and bothresult.predict(snapshot)andPredictiveQuery.from_snapshot+generate_prediction_table+predictreproduce the Graph-bound scores;predict(snapshot)returns the same scores as the Graph-bound path with and without suppliedentities, with a pre-generatedtable=, and in main'spredict(prediction_table=..., snapshot=...)form; it refusesentitiestogether withprediction_time;@accepts itself;predicton a snapshot whose graph key differs from the training snapshot's warns and returns the same scores.test_prediction_workers.py(55),test_prediction_output_columns.py(25),test_prediction_completion.py(14),test_prediction_output_preflight.py(4),test_trainer_registry.py(27),test_warm_start.py(6),test_embedding_models.py(4),test_semantic_carry_forward.py(20),connector/test_file_connector.py(77).test_accelerator.pyhas 2 failures that also fail on Export and load graph snapshots through any file destination #54's head (environment).Checklist
PredictiveQuery(graph, query)andpredict(prediction_table, snapshot=...)behave as before.docs/mlops.md).🤖 Generated with Claude Code