Optimized HealDA data loaders to avoid GIL - #1921
Conversation
Migrate HealDA ZarrLoader remote zarr reads and core filesystem s3:// downloads from fsspec/s3fs to obstore. obstore's Rust client performs object fetches off the GIL with native connection pooling, measuring 7.7x faster concurrent chunk reads against public HRRR data on S3 (384 GETs: 12.4s -> 1.6s median). - ZarrLoader: remote URLs open through zarr.storage.ObjectStore backed by obstore; fsspec-style storage_options keys are translated; falls back to fsspec when obstore is unavailable - core/filesystem: s3:// single-file and recursive downloads stream through obstore; msc:// stays on fsspec; checkpoint write path unchanged - Adds obstore>=0.6.0 dependency, tests for both paths, and a benchmark figure
CODEOWNERS review mapCurrent for commit ⏳ @CharlelieLrt — 1 file(s)
⏳ @coreyjadams — 4 file(s)
⏳ @ktangsali — 2 file(s)
⏳ @pzharrington — 1 file(s)
No CODEOWNER
Comment |
Greptile SummaryThe PR migrates HealDA remote Zarr reads and cached S3 downloads from fsspec/s3fs to obstore to improve concurrent object-fetch performance.
|
| Filename | Overview |
|---|---|
| physicsnemo/core/filesystem.py | Replaces S3 downloads with obstore streaming, but recursive downloads permit destination escape through object-key path traversal. |
| physicsnemo/experimental/datapipes/healda/loaders/zarr_loader.py | Routes remote Zarr stores through a read-only obstore adapter while retaining the existing fsspec fallback when obstore is unavailable. |
| test/core/test_filesystem.py | Adds in-memory coverage for single and recursive downloads, but does not test hostile object keys or destination confinement. |
| test/datapipes/healda/test_zarr_loader_obstore.py | Covers local loading, obstore-backed remote loading, option translation, data correctness, and read-only behavior. |
| pyproject.toml | Adds the obstore runtime dependency. |
| uv.lock | Updates the lockfile to include obstore and its resolved metadata. |
Reviews (1): Last reviewed commit: "Optimize HealDA data loaders with GIL-fr..." | Re-trigger Greptile
| key = meta["path"] | ||
| rel = key[len(prefix) :].lstrip("/") if prefix else key | ||
| dest = Path(destination) / rel | ||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||
| _obstore_download_file(f"{base}/{key}", str(dest)) |
There was a problem hiding this comment.
Recursive download path traversal
When a recursively listed S3 object key contains parent-directory components, rel is joined directly to destination without a containment check, causing the download to overwrite writable files outside the cache entry. Resolve each destination and reject it unless it remains beneath the recursive download root.
How this was verified: The public Package.get(..., recursive=True) path reaches these lines, where the raw object-key suffix is passed to Path(destination) / rel without sanitization or containment validation.
There was a problem hiding this comment.
Is this used in docs somewhere?
pzharrington
left a comment
There was a problem hiding this comment.
HealDA changes look good to me, nice speedup
Refuse listed keys that resolve outside the destination root. Also drop the PR-only benchmark figure from the tree; the PR body now references it by commit SHA.
Description
Migrates the HealDA
ZarrLoaderremote zarr reads ands3://downloads inphysicsnemo.core.filesystemfrom fsspec/s3fs to obstore. obstore's Rust-native client performs object fetches off the GIL with real connection pooling, so concurrent chunk reads no longer serialize on the Python interpreter.Benchmark
Concurrent zarr chunk reads against public HRRR data on NOAA S3 (4 surface fields × 96 chunks = 384 GETs of ~45 KB, cold cache, 3 trials per backend), using the exact
ZarrLoadercode path:This is the many-small-GET regime that HealDA's per-timestep 2D/3D variable reads hit; for large-chunk stores both backends converge toward bandwidth-bound parity.
Changes
ZarrLoader: remote URLs now open throughzarr.storage.ObjectStorebacked byobstore.store.from_url. fsspec-stylestorage_optionskeys (anon,key,secret,token,endpoint_url,region_name) are translated to obstore config names so existing call sites keep working; unknown keys pass through. Falls back to the fsspec path if obstore is not installed.core/filesystem.py:s3://single-file and recursive downloads in_download_cachedstream through obstore (8 MB chunks, layout-preserving recursive fetch,FileNotFoundErroron empty prefixes).msc://stays on fsspec; the checkpoint write path (_get_fs) is deliberately unchanged.obstore>=0.6.0dependency.test/datapipes/healda/test_zarr_loader_obstore.py(local baseline, remote-via-obstore with data verification, storage-options translation, read-only store) and four obstore download tests intest/core/test_filesystem.pyagainst an in-memory store.Notes for reviewers
docs/img/to embed here; happy to drop it from the tree if preferred.Checklist