Repository navigation
Conversation
Change map
Requirement: training progress must reach experiment trackers and platform log capture as structured data. Existing behaviour: no hook; instrumentation is a no-op. Smallest change: one additive instrumentation hook plus |
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:
e8b43b2e-69aa-4d3f-bea4-e1f3db823fbc
📒 Files selected for processing: 12
packages/kumo-core/src/kumo_core/automl/auto_trainer.pypackages/kumo-core/src/kumo_core/automl/searcher.pypackages/kumo-core/src/kumo_core/callbacks/train_progress_logger.pypackages/kumo-core/src/kumo_core/training/orchestration.pypackages/kumo-core/src/kumo_core/training/runtime.pypackages/kumo-core/tests/test_searched_params.pypackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/_events.pypackages/kumo-engine/kumo/engine/config.pypackages/kumo-engine/kumo/engine/trainer/_runtime.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_fit_events.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.
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 · Keep nonfinite parameters from disabling JSON progress. · _events.py:76-83
packages/kumo-engine/kumo/engine/_events.py:76-83
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winKeep nonfinite parameters from disabling JSON progress.
A positive infinity can reach
trial_started.paramsthrough the tunablebase_lrsearch value._print_jsonpasses that value tojson.dumps(..., allow_nan=False), which raises.EventEmitter.emitthen removes_print_json, so that event has no JSON line and later progress events are no longer printed. Normalize nonfinite parameter values to JSONnullbefore serialization.Suggested fix
+def _json_safe(value: Any) -> Any: + if isinstance(value, float) and not math.isfinite(value): + return None + if isinstance(value, Mapping): + return {key: _json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_json_safe(item) for item in value] + return value + + def _print_json(event: Event) -> None: - line = json.dumps({'event': 'kumo.engine', **event.to_dict()}, + payload = event.to_dict() + payload['params'] = _json_safe(payload['params']) + line = json.dumps({'event': 'kumo.engine', **payload}, allow_nan=False, default=str)
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
bd991a3d-f0da-47b5-91f0-ab8b380c97a4
📒 Files selected for processing: 1
packages/kumo-core/src/kumo_core/automl/auto_trainer.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.
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-core/src/kumo_core/automl/auto_trainer.py: 1
942-942: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winEnd trials when status persistence fails.
If
status.save()raisesOSErroraftertrial_started, thisRuntimeErrorhandler does not emittrial_ended. The outer fit reports failure, but the callback still has an open trial. Add a separate handler that emits a failedtrial_endedfor other exceptions and re-raises them. Keep the existing retry behavior forRuntimeError.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
f8fa803b-a4ee-4cfb-afc6-93d6677fb3ba
📒 Files selected for processing: 6
packages/kumo-core/src/kumo_core/automl/auto_trainer.pypackages/kumo-core/src/kumo_core/callbacks/train_progress_logger.pypackages/kumo-engine/README.mdpackages/kumo-engine/kumo/engine/_events.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_fit_events.py
💤 Files with no reviewable changes: 2
- packages/kumo-core/src/kumo_core/callbacks/train_progress_logger.py
- packages/kumo-engine/kumo/engine/_events.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.
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/_events.py: 1
58-59: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winIsolate event data between callbacks.
Eventis frozen, but itsmetricsandparamsdictionaries are writable. If one callback changes either dictionary, later callbacks receive the changed values. This can make a JSON-lines callback or tracker record incorrect progress without any callback raising an error. Make the event data immutable, or give each callback an independent copy.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: NVIDIA/kumo-engine/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
9993e071-1343-408f-9450-23db06591684
📒 Files selected for processing: 3
packages/kumo-engine/kumo/engine/_events.pypackages/kumo-engine/kumo/engine/trainer/trainer.pypackages/kumo-engine/test/test_fit_events.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.
Squashed copy of vedaant/mlops-fit-events (17329c9) so prediction tracking builds on the fit tracking stack. Review it in #52; drop this commit when #52 merges. Conflicts resolved against this stack: Trainer.fit keeps #52's fit()/_fit() split with started_at set inside _fit for the provenance record; imports and kumo.engine exports are the union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17329c9 to
9618f69
Compare
e2bc43b to
449bd31
Compare
449bd31 to
3687905
Compare
4598c4b to
8496ea1
Compare
3687905 to
e69106f
Compare
8496ea1 to
91af183
Compare
e69106f to
06ca64f
Compare
91af183 to
49bae78
Compare
06ca64f to
6b5f8eb
Compare
49bae78 to
c5b4bc8
Compare
6b5f8eb to
706d781
Compare
706d781 to
43c1b37
Compare
fit(callbacks=[...]) receives each kumo.engine.tracking.Event: fit_started, trial_started, epoch_ended, trial_ended and fit_ended, with status and a short error on ended events. A callback that raises is logged and disabled; it never fails the fit. fit resolves its MLOps settings once on the caller's thread and passes them down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43c1b37 to
7cdc1ef
Compare
|
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: Experiment tracking: #52 → #60 → #68
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds progress events for
Trainer.fit:fit(callbacks=[...])calls each callback with a structuredEventfor the fit, every search trial and every epoch, so a job's logs or an experiment tracker can follow a fit while it runs.Summary
This PR adds progress events for
Trainer.fit:fit(callbacks=[...])calls each callback with a structuredEventfor the fit, every search trial and every epoch, so a job's logs or an experiment tracker can follow a fit while it runs.Problem. Teams running
fit()as a Databricks, SageMaker or Snowflake job need to answer questions like these from the job's logs or tracker while it runs:Today none of these can be answered without parsing progress-bar text.
Cause.
fit()reports progress only as Lightning/tqdm progress-bar text on stdout, for example:That text has no trial number or hyperparameters, is overwritten in place with carriage returns, and rounds metrics. There is no programmatic hook either: the engine's training bindings hard-wire
NoOpTrainingInstrumentation, andfit()takes no callbacks.Change.
kumo.engine.tracking.Event, a frozen dataclass withname,run_id,time,trial,epoch,metricsandparams, plusto_dict().Trainer.fit(..., callbacks=[...]), where a callback is anyCallable[[Event], None]. The default is no callbacks.docs/mlops.md. JSON lines on stdout, for platform log capture, are one callback:Events are emitted in this order:
fit_started; then per trialtrial_started,epoch_endedonce per epoch, andtrial_ended; thenfit_ended.Example: the output of the callback above for a real two-trial, two-epoch CPU fit on a synthetic 400-row
userstable (PREDICT users.spend_next_30d FOR EACH users.user_id). The lines are trimmed to the first trial's events, the second trial'strial_ended, andfit_endedwith its metrics cut to the*_maekeys (the real line also has loss/mse/rmse per split). Hashes are shortened and floats rounded to 4 digits:{"name": "fit_started", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572589.95, "trial": null, "epoch": null, "metrics": {}, "params": {"snapshot_id": "77d088e9…@647e1e0794fba55c", "query_sha256": "cbb5b05e…"}} {"name": "trial_started", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572590.21, "trial": 1, "epoch": null, "metrics": {}, "params": {"optimization.base_lr": 0.0001, "optimization.weight_decay": 5e-06, "optimization.batch_size": 1024, "optimization.lr_scheduler": "LRScheduler(\n name=cosine_with_warmup_restarts,\n interval=step,\n kwargs={warmup_ratio_or_steps=0.1},\n)", "model_architecture.model": "GNNModelPlan(name='GNN', channels=[256], aggregation=[[sum, mean, min, max, std]], dropout=[0.0])", "model_architecture.num_pre_message_passing_layers": 2, "model_architecture.num_post_message_passing_layers": 1, "model_architecture.activation": "relu", "attempt": 1}} {"name": "epoch_ended", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572593.15, "trial": 1, "epoch": 1, "metrics": {"val_loss": 1.0675, "val_mae": 1.4688, "val_mse": 3.5513, "val_rmse": 1.8845, "train_loss": 1.0454, "train_mae": 1.4242, "train_mse": 3.8349, "train_rmse": 1.9583}, "params": {}} {"name": "epoch_ended", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572594.80, "trial": 1, "epoch": 2, "metrics": {"val_loss": 0.8225, "val_mae": 1.2265, "val_mse": 2.6122, "val_rmse": 1.6162, "train_loss": 0.8367, "train_mae": 1.2248, "train_mse": 2.7899, "train_rmse": 1.6703}, "params": {}} {"name": "trial_ended", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572595.49, "trial": 1, "epoch": null, "metrics": {"val_mae": 1.2265, "seconds": 5.2858}, "params": {"status": "succeeded", "attempt": 1}} {"name": "trial_ended", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572600.44, "trial": 2, "epoch": null, "metrics": {"val_mae": 0.9712, "seconds": 4.6662}, "params": {"status": "succeeded", "attempt": 1}} {"name": "fit_ended", "run_id": "f1070de2…-fit-50fe1fe6…", "time": 1792572605.80, "trial": null, "epoch": null, "metrics": {"val_mae": 0.9712, "test_mae": 0.913, "test_refit_mae": 1.0872, "train_mae": 1.142, "seconds": 15.8578}, "params": {"status": "succeeded"}}Here trial 2 (
val_mae0.971) beats trial 1 (1.227), and thefit_endedmetrics are the finalTrainingResult.metrics.A fit that raises ends with a failed
fit_endedbefore the exception propagates. From the same script, with the training-dataset build step patched to raiseValueError('training table has no rows for split "train"'):{"name": "fit_ended", "run_id": "d602ea2b…-fit-8ba42ea7…", "time": 1792572605.81, "trial": null, "epoch": null, "metrics": {}, "params": {"status": "failed", "error": "ValueError: training table has no rows for split \"train\""}}What each field records:
run_idis the fit's run id, the same one that names the run's scratch directory. It is shared by every event of one fit.timeis wall-clock epoch seconds.trialandepochcount from 1 and are null where they don't apply.fit_started.paramsholds the snapshot id and a SHA-256 of the query. The query text itself is never emitted. If these can't be read (malformedfit()arguments), the event has empty params and the fit then raises its usual error.trial_started.paramsholds the hyperparameters searched for that trial, keyed by their model-plan path, plusattempt. Values that are not plain numbers or strings appear as their string form.epoch_ended.metricsholds thetrain_*andval_*metrics logged at the end of that epoch.trial_endedgives the tune metric,secondsandstatus. It is emitted once per attempt: a succeededtrial_endedonly after the trial is recorded, so a trial that trains and then fails during bookkeeping reports one failedtrial_ended, not both. A failed attempt also giveserror, the error type and message (at most 200 characters). An OOM retry is reported as a newattemptof the same trial.fit_endedgives the final metrics plusseconds, orstatus: failedanderror(same format) if the fit raised.In kumo-core, the training instrumentation gains an additive
event(name, **fields)hook (a no-op by default) andlocal_training_bindingsaccepts aninstrumentation. The engine passes an emitter that implementseventand fans each event out to the callbacks.AutoTraineremits the trial events, the existingAutoTrainingProgressLoggeremitsepoch_endedfrom the epoch it just recorded, and the searcher records the values it sampled. With no callbacks the emitter has nothing to call, so no extra work is done.Back-compat
This PR only reports information. It adds no new checks or errors on any existing path.
fit()call, no callbacks (the default)fit(callbacks=...)fit()arguments (e.g. not a training table)fit_startedis emitted with empty params, thenfit()raises the same documented error as without callbacks.fit_endedwithstatus: failedis delivered, then the same exception propagates.fit()with anexport_configwhose connector cannot publish modelsNotImplementedError, now beforefit_startedis emitted.Known limitations
fitonly.predictemits none; with MLOps tracking on, prediction runs are recorded by the tracker instead (Log prediction runs to MLflow and Snowflake through the tracking adapters #73).Testing Done
test_fit_events.py(4 tests):to_dict()JSON round trip, that a raising callback is called once and does not fail the fit, and that a trial whose searcher update fails after training reports exactly one failedtrial_endedwith the error;fit_startedthenfit_endedwithstatus: failedanderror: "ValueError: stop";epoch_endedand does not raise.test_searched_params.py: only searched fields are reported, non-scalar values as strings, JSON-serialisable.fitnow also takesmlops=):test_fit_events.py+ kumo-coretest_searched_params.py: 5 passed;test_bundle_provenance.py(MLOps model store through the samefit): 7 passed.test_fit_validation_metrics.py,test_trainer_registry.py, kumo-coretest_training.py,test_local_training_runtime.py,make check-kumo-core.erroras<Type>: <message>.Checklist
fit()without callbacks behaves as before.🤖 Generated with Claude Code