SPoHF-WP6-Twins

Trained models persist across a deploy, and carry a fingerprint of what fitted them

Status

accepted — supersedes the arrangement described in blue/routes/monitor/soil_forecast.py and red/dashboard.py before this change

Context

Both twins trained models into the container’s own writable layer: red’s DLI model and climate chain under ~/.wp6/models, blue’s soil forecasters under ~/.wp6/blue-models. That layer dies with the pod, so every deploy, OOM kill, node drain and reschedule threw the models away and the dashboard refitted them on boot.

This was a deliberate choice, and it bought something real. An artifact could not outlive the code that wrote it, so a refactor could never serve numbers from a model fitted by different code — the accident enforced correctness. It also meant every restart refitted against more data, which for the climate chain is not incidental: retraining is when it re-decides whether link 3 has seen enough winter to earn a day-of-year term, and which greenhouse-level reference wins on end-to-end skill.

What it cost was a gap after every deploy. Red’s climate chain takes ~55 s to fit, and it is backgrounded so the pod reports Ready before the model exists — so the model-backed pages show “not trained yet” to whoever looks first. That is on top of the dashboard being down for the pod swap anyway, because the deployment is Recreate (it mounts ReadWriteOnce volumes).

Decision

Persist the models on a small per-twin PVC, refit them nightly in-process, and gate every load on a fingerprint of the code contract and the configuration that produced the artifact.

The three parts are one decision; any two without the third is worse than what was there before.

Persist

A <release>-<twin>-models PVC, mounted at /data/models. Separate from manual-uploads because a model is a derived artifact that can be thrown away and rebuilt, and it should not turn up in backups of real uploaded data.

ReadWriteOnce is safe for the same reason the existing volumes are: both dashboards are single-replica with strategy: Recreate, so no two pods hold the volume at once.

Refit on a clock, inside the dashboard

Persisting alone would freeze the models at whenever they were first fitted, which for the climate chain forfeits the self-tuning described above. So a nightly refit restores that rhythm: red at 03:00 UTC, blue at 03:30.

Not a Kubernetes CronJob, though the chart already runs the nightly exports that way, for two reasons:

Training already runs in-process for the boot bootstrap and the admin button, under a lock the scheduled run shares. This adds a clock, not a new place for training to happen.

Fingerprint every artifact

A hand-maintained version constant is not sufficient, because it only catches the changes somebody remembered to declare. Two kinds that bite:

So shared/artifacts.fingerprint hashes the things that must match, and each twin stores the result beside the model:

A mismatch means “no usable artifact”, which every caller already handles: it is the same answer they got when the file was absent, and it routes into the refit that already exists. The version constants stay alongside, for the deliberate break no fingerprint can see — a change in what the stored numbers mean while their shape is unchanged.

Consequences

Alternatives considered