Add insitubatch streaming ERA5 data source and hindcast recipe#962
Add insitubatch streaming ERA5 data source and hindcast recipe#962emfdavid wants to merge 11 commits into
Conversation
Greptile SummaryThis PR adds an optional insitubatch-based ERA5 streaming path. The main changes are:
|
| Filename | Overview |
|---|---|
| earth2studio/data/insitu.py | Adds the streaming feed and batch conversion logic; shifted leads need bounds validation against the requested init window. |
| pyproject.toml | Adds the optional insitubatch data-extra dependency for supported Python versions. |
| test/data/test_insitu.py | Adds offline coverage for feed shape, values, deduplication, transpose behavior, persistent cache, and lead-step validation. |
| recipes/insitubatch_hindcast/stream_score.py | Adds a streaming versus dense scoring benchmark using Persistence. |
| recipes/insitubatch_hindcast/bench_hindcast.py | Adds a before/after hindcast verification-read benchmark. |
| recipes/insitubatch_hindcast/bench_cache.py | Adds a cold/warm persistent-cache benchmark. |
| recipes/insitubatch_hindcast/README.md | Documents setup, benchmark usage, results, and caveats for the new recipe. |
| CHANGELOG.md | Documents the new optional streaming feed. |
Reviews (1): Last reviewed commit: "Sort recipe imports for the current ruff..." | Re-trigger Greptile
| row = [] | ||
| for v in self.variables: | ||
| label = f"{v}#{li}" | ||
| geometries[label] = opened[vmap[v]].shift(int(k)) |
There was a problem hiding this comment.
Shifted Leads Escape Sample Window
When sample_range starts at the first init and lead_times includes a history lead like -6h, this shift reads before index 0; when the range reaches the store tail, positive verification leads read past the last time step. The feed accepts those documented lead values, so iteration can fail or return boundary samples from the wrong stored time instead of rejecting the invalid init window.
|
Nick - I will update the branch anytime there are substantive conflicts but chasing the change log seems silly. I will fix that when we get close to merging. |
New optional data module `earth2studio.data.insitu` that feeds initial conditions to `earth2studio.run` workflows without going through xarray. - `batch_to_xcoords`: pure converter from an insitubatch numpy `Batch` to the exact `fetch_data(legacy=True)` contract -- an `(time, lead_time, variable, lat, lon)` tensor plus the matching 5-key `CoordSystem` OrderedDict -- so it drops straight into `prognostic.create_iterator` after `map_coords`. - `InSituForecastFeed`: prefetched IC iterator over a contiguous window of an analysis store. Reads with insitubatch (bounded-fan-out async prefetch and a read plan that de-duplicates chunks shared across init times) instead of the per-`(time, variable)` `DataSource -> xr.DataArray -> fetch_data` path, whose gather is unbounded and re-reads overlapping chunks. Emits a single 0 h lead (an initial condition) today; multi-step history and verification-lead offsets are the next step. insitubatch is imported lazily and only required when this module is used.
Rework InSituForecastFeed from a single-lead initial-condition feed into a verification-capable feed: - Unify history (lead_time <= 0) and verification (lead_time > 0) into one lead axis. Each lead is a sample-axis `shift` view of one stored array, stacked into `(time, lead_time, variable, lat, lon)`, so the `(init, lead)` grid decodes each shared chunk exactly once. - Take an injected `store: Store` instead of building obstore from a URL, so a caller can pick any insitubatch backend (e.g. anonymous public buckets over gcsfs). - Decode the CF `time` coordinate via `cftime` (an existing dependency) rather than assuming a datetime64 encoding. - `transpose_inner` swaps a store's `(lon, lat)` field layout to the contract's `(lat, lon)`. `self.dataset` exposes the underlying InSituDataset cache counters.
Two runnable benchmarks that feed ERA5 into an Earth2Studio prognostic via the insitubatch feed instead of the dense `fetch_data` grid, with a README framing the results: - bench_hindcast.py: verification-read de-duplication over a hindcast grid (WB2 fat-chunk / ARCO chunk-1), before (E2S fetch) vs after (insitubatch feed). - stream_score.py: streaming vs dense verification materialization, scored against ERA5 with Persistence; reports wall, peak RSS, and matching RMSE. Both read anonymous public GCS over gcsfs on both sides, so the delta isolates the loader (not an obstore-vs-gcsfs artifact). Motivated by recipes/eval's predownload sentinel, which exists because live fetch_data is too slow.
insitubatch 0.1.0 is on PyPI and now exposes InSituDataset and the framework adapters at the package root, so: - earth2studio/data/insitu.py imports them from `insitubatch` (public surface) instead of reaching into `insitubatch.source` / `.frameworks` / `.types`. - pyproject: add `insitubatch>=0.1.0` to the `data` extra, gated `python_version>='3.12'` (insitubatch requires 3.12; E2S supports 3.11) — mirrors the existing intake-esgf marker. Resolves from PyPI (uv.lock updated).
insitubatch is now a declared earth2studio `data`-extra dependency (insitubatch>=0.1.0, gated Python>=3.12), not a manual side install. Point the Setup at `earth2studio[data]` (or a direct pinned `insitubatch>=0.1.0`).
The earth2studio tree is uv-managed and the recipe runs in-tree, so match the repo convention (`uv sync --extra <name>`) instead of pip; the data extra brings insitubatch.
Cover InSituForecastFeed / batch_to_xcoords / decode_cf_time against a synthetic on-disk store (fat time-chunk, CF-encoded time coord): the (x, coords) contract, byte-correct sample-axis shift views, the decode-once dedup property (cache_misses), transpose_inner, and the integer-multiple-of-dt lead guard. No network / live bucket. Addresses the tests + CHANGELOG items of the PR checklist.
cache_dir now sets persist=True, so decoded chunks survive across runs (the flag was passed through but never persisted). Add a cold-vs-warm bench (bench_cache.py), a cross-run persistence test, and a README section framing it as a predownload replacement. Measured cold->warm: WB2 33->0 cloud fetches (~1.4x wall), ARCO 54->0 (~2.2x wall).
Clarify that bench_cache cold includes the one-time persist write, so it runs slightly above the persist-off de-dup figure in section 1.
Rebasing onto NVIDIA main picked up its import-grouping rules; re-sort bench_hindcast.py and stream_score.py (stdlib / third-party / first-party).
Greptile flagged that a history (<0) or verification (>0) lead near a store boundary reads outside the requested init window: the engine silently drops those edge anchors, so a scoring campaign would cover a shorter window than asked without any signal. Validate the requested sample_range against valid_anchor_range(lead_steps, n_samples) and raise an actionable ValueError; when sample_range is unset, default to the in-bounds init window. Covered by boundary tests (past-end, before-start, none-defaults).
Earth2Studio Pull Request
Closes #961
Description
A recipe for streaming ERA5 into an Earth2Studio prognostic with
insitubatch — a streaming, read-planning cloud-zarr
loader — instead of the dense
fetch_datagrid, for IO-bound hindcast / scoring campaigns.How it plugs in — around the model, not inside the xarray loop. insitubatch delivers
tensor batches and never builds
xr.DataArray. The newearth2studio.data.insitu.InSituForecastFeedreads the analysis store → DLPack →(torch.Tensor, CoordSystem)and feedsprognostic.create_iterator(x, coords)directly, whereeach forecast lead is a sample-axis
shiftview of one array (decode-once, no reshard). Itdoes not touch the
DataSource → xr.DataArray → fetch_datapath, so E2S's lexicon / coords /regrid machinery is untouched — this is a complementary, opt-in feed, not a replacement.
Why it helps an IO-bound campaign. A scoring grid needs ERA5 at
valid = init + leadforevery
(init, lead); consecutive inits share valid times and a fat time-chunk holds severalsteps, so the requested reads collapse onto far fewer stored chunks. insitubatch's read planning
de-duplicates those into one decode per stored chunk under a bounded
max_inflightbudget,and streams lead-by-lead so scoring never materializes a dense verification tensor.
What's in the PR
earth2studio/data/insitu.py— theInSituForecastFeedadapter (guarded optional import ofinsitubatch; yields(torch.Tensor, CoordSystem)).recipes/insitubatch_hindcast/— two runnable benchmarks + a README:bench_hindcast.py— verification-read de-duplication vs per-initfetch_data.stream_score.py— streaming vs dense materialization (three modes, identical RMSE).Benchmark results
Preliminary, one
n2-standard-8-class box (15 GB RAM), cold reads, gcsfs anon on both thebefore and after side — so the delta isolates insitubatch's read-planning + streaming, not an
obstore-vs-gcsfs artifact. Public WeatherBench2 and ARCO ERA5 stores.
1. Verification-read de-duplication (
bench_hindcast.py) — BEFORE = per-initfetch_data,AFTER = the insitubatch feed:
chunks=(8,240,121)(fat time-chunk)chunks=(1,721,1440)(chunk-1)2. Streaming vs dense materialization (
stream_score.py, N = 120 inits × 40 leads) — allthree modes produce identical RMSE:
e2s— dense predownload (redundant reads)dense— insitubatch,batch_size=Nstream— insitubatch,batch_size=WStreaming peak memory is flat at ~1.9 GB across N = 120 / 240 / 480, while the dense grid is
7.53 GB at N = 120 and OOMs a 15 GB box by ~N = 240. Dense scales with campaign size;
streaming does not — that bounded-memory property, not just throughput, is the point for a long
campaign. (Persistence is used as a checkpoint-free model that exercises the real
create_iteratorseam on CPU; a real NVIDIA checkpoint — SFNO/FCN — is a drop-in with the samecode on a GPU.)
Cross-run persistent cache
InSituForecastFeed(cache_dir=...)persists decoded chunks to local disk, replacing the evalrecipe's
predownload.pysentinel: the first run decodes each shared chunk once and cachesit; a re-score of a different model against the same ERA5 reads from disk, fetching the cloud zero
times. No predownload step, no reshard, only the chunks touched; a static reanalysis store never
goes stale. Measured cold→warm (cache on NVMe): WB2 33→0 cloud fetches (~1.4× wall), ARCO
54→0 (~2.2× wall). Fetch elimination is the deterministic win; the wall speedup scales with how
IO-bound / metered the source is and is understated by this box's cheap same-region reads. The cold
wall includes the one-time persist write, so it sits slightly above the persist-off de-dup figures above.
Honest boundary
Not a universal speed win. On a chunk-1 store with large fields, against an unbounded
concurrent gather, insitubatch's bounded-inflight scheduling trails per byte (ARCO ~2×; the reads
are already minimal, but the dense output the model consumes must still be assembled, and E2S's
flat gather saturates bandwidth on 4 MB chunks). The sweet spot is streaming with bounded
memory, and the large wins land when the chunk layout maps many samples onto shared chunks
(overlapping windows, verification grids, fat chunks).
Scope / caveats
target infrastructure.
t2m,u10m,v10m); pressure-level indexing is not yet wired inthe adapter.
insitubatchis an optional dependency (the module raises a clear install hint if absent);nothing in the core E2S path changes.
Checklist
RE: Docs - Added recipe readme - please advise on API docs etc
Dependencies
insitubatch>=0.1.0(PyPI) — added to thedataextra, gated
python_version>='3.12'. Optional; the adapter guards its import.