spikes/rust-forecast ports build_features (P.1 path) and HistGradientBoosting inference to Rust and checks them against the pandas/sklearn output on two fixtures (2024 record flood, summer 2026): 244,800 feature cells and every regression prediction bit-identical, classifier heads to the last ulp; 0.25 ms vs 158 ms per station. docs/RUST_REWRITE.md: what a rewrite would mean (Rust service, Python training), costs, phased plan, Python alternative.
44 lines
2.1 KiB
Markdown
44 lines
2.1 KiB
Markdown
# Rust forecast spike
|
|
|
|
Answers one question: can the flood forecast be served from Rust without
|
|
train/serve skew, while training stays in Python (scikit-learn)?
|
|
Assessment and migration plan: [docs/RUST_REWRITE.md](../../docs/RUST_REWRITE.md).
|
|
|
|
- `export/export_fixture.py`: dumps `models/flood_P.1.joblib` (all 9 HGB heads) as
|
|
flat tree arrays, plus two fixtures from `models/cache` (Aug-Oct 2024 record
|
|
flood, Jun-Aug 2026): the observed hourly grid of P.1 and its 7 upstream
|
|
gauges, rain, the pandas feature matrix and every head's sklearn prediction.
|
|
- `src/features.rs`: port of `src/ml/features.py::build_features` (P.1 path:
|
|
lags, rises, rolling stats, upstream leads, season, rain).
|
|
- `src/hgb.rs`: HistGradientBoosting inference (~90 lines).
|
|
- `src/main.rs`: compares bit-for-bit and times serving-shaped work.
|
|
|
|
```bash
|
|
cd spikes/rust-forecast/export
|
|
uv run --project ../../.. python export_fixture.py # writes ../data/*.json (gitignored)
|
|
cd .. && cargo run --release
|
|
uv run --project ../.. python export/time_python.py # Python timing for comparison
|
|
```
|
|
|
|
Result (model hgb-v3+cbb3bf7, 3,600 hourly rows):
|
|
|
|
| | Result |
|
|
| --- | --- |
|
|
| Features, 68 columns x 3,600 rows | 244,800 / 244,800 cells bit-identical |
|
|
| Regression heads (max_6/12/24) | bit-identical on every row |
|
|
| Classifier heads (warn/danger) | equal to the last ulp (<= 2.2e-16, `exp` vs scipy `expit`) |
|
|
| One station: 336 h window -> features + 9 heads | Rust 0.25 ms, Python 158 ms |
|
|
| Model load | Rust 16 ms (JSON), Python 199 ms (joblib) |
|
|
|
|
Two traps the spike had to get past, both of which flip tree branches because
|
|
HGB thresholds often equal real data values exactly:
|
|
|
|
1. `serde_json` parses floats best-effort (can be 1 ulp off) unless the
|
|
`float_roundtrip` feature is on.
|
|
2. pandas `rolling().sum()/.mean()` are incremental with Kahan compensation,
|
|
a "run of identical values" shortcut and a sign clamp. A plain window sum
|
|
differs in the last bits; `rolling_kahan()` replicates pandas exactly.
|
|
|
|
Not covered yet: dam features (Mae Ngat), HII gap fill, Open-Meteo catchment
|
|
mean, the heuristic fallback, stage probabilities, the other 14 stations.
|