Skip to content

Coalescing ManifestStore.get_many#1033

Draft
TomNicholas wants to merge 1 commit into
zarr-developers:mainfrom
TomNicholas:manifeststore-coalescing-get-many
Draft

Coalescing ManifestStore.get_many#1033
TomNicholas wants to merge 1 commit into
zarr-developers:mainfrom
TomNicholas:manifeststore-coalescing-get-many

Conversation

@TomNicholas

@TomNicholas TomNicholas commented Jul 1, 2026

Copy link
Copy Markdown
Member

Implements the chunk-coalescing idea from #947: teach ManifestStore to satisfy a batch of chunk requests with fewer, larger reads by deriving an "effective shard index" from the manifests at read time.

ManifestStore.get_many (overriding the new cross-key Store.get_many) resolves the requested chunk keys to (source file, byte range), groups requests by source file, coalesces each group into runs, and serves each run with one ranged read that is sliced back into per-chunk buffers. No file-format assumptions, no spec changes. Keys that aren't plain manifest-backed chunks (metadata, inlined, missing) fall back to per-key get.

Coalescing uses the object_store / async-tiff two-knob model: references merge into one read when their gap is <= coalesce_max_gap_bytes and the merged read stays <= coalesce_max_bytes.

Why coalesce_max_gap_bytes defaults to 0

Benchmarking a 2D map-tile query (5×5 chunks of air_temperature) against a remote Met Office file on Azure, versus the status quo of one get per chunk:

config reads fetched over-read median vs baseline
no coalescing (per-chunk get) — status quo 25 621 KiB 1.00× 273 ms 1.00×
gap=0 (merge only adjacent) 5 621 KiB 1.00× 97 ms 2.8× faster
gap=1 MiB, cap=8 MiB 1 1964 KiB 3.16× 456 ms 1.7× slower
gap=1 MiB, cap=256 KiB 5 621 KiB 1.00× 99 ms 2.8× faster
gap=256 KiB, cap=8 MiB 5 621 KiB 1.00× 96 ms 2.8× faster

A 2D spatial box is contiguous along one axis but strided along the other (row-major chunk layout), so a large gap bridges the row stride and pulls in the chunks between rows — reading ~3× the needed bytes and running slower than no coalescing. Merging only adjacent references is a pure win: zero over-read, ~2.8× faster (collapsing 25 requests into 5). Hence the default gap of 0; coalesce_max_bytes (8 MiB) just bounds the size of any single read. A non-zero gap is available for callers who know their access is contiguous.

Depends on the Store.get_many API and its use in BatchedCodecPipeline in zarr-python (zarr-developers/zarr-python#4112, zarr-developers/zarr-python#4113); until those are released this is a dormant override, tested in isolation.

xref #947

Draft — feedback welcome on the approach and the config surface.

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.80952% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.93%. Comparing base (80edcb4) to head (7761438).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
virtualizarr/manifests/store.py 98.80% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1033      +/-   ##
==========================================
- Coverage   90.07%   89.93%   -0.14%     
==========================================
  Files          36       36              
  Lines        2206     2275      +69     
==========================================
+ Hits         1987     2046      +59     
- Misses        219      229      +10     
Files with missing lines Coverage Δ
virtualizarr/manifests/store.py 92.92% <98.80%> (+2.72%) ⬆️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TomNicholas
TomNicholas force-pushed the manifeststore-coalescing-get-many branch from 834b1d0 to e86e1f1 Compare July 1, 2026 21:30
@TomNicholas
TomNicholas force-pushed the manifeststore-coalescing-get-many branch from e86e1f1 to 3f770c4 Compare July 1, 2026 22:02
Implement Store.get_many on ManifestStore: resolve the requested chunk keys
through the manifests to (source file, byte range), group the requests by
source file, coalesce each group into runs, and serve each run with a single
ranged read that is sliced back into per-chunk buffers. Keys that are not
plain manifest-backed chunks (metadata, inlined, or missing chunks) are
served individually via `get`.

Coalescing uses two knobs (the object_store / async-tiff model): a run merges
references whose gap is <= `coalesce_max_gap_bytes` as long as the resulting
read stays <= `coalesce_max_bytes`. The gap defaults to 0 - merge only
adjacent references, a pure win with no wasted bytes - because benchmarking a
2D map-tile query against a remote Met Office file showed that bridging larger
gaps pulls in the chunks that sit between rows (a 2D box is contiguous along
one axis but strided along the other), reading ~3x the needed bytes and
running slower than no coalescing at all. Merging only adjacent references was
~2.8x faster than the per-chunk baseline with zero over-read. `max_bytes`
(default 8 MiB) bounds the size of any single read.

Depends on the cross-key `Store.get_many` API in zarr-python
(zarr-developers/zarr-python#4112, #4113); until that is released the method
is a dormant override. See zarr-developers#947.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant