Skip to content

fix(indexing): make sparse selections O(npoints) instead of O(nchunks) - #262

Open
d-v-b wants to merge 2 commits into
mainfrom
claude/zarr-python-issue-4174-77043e
Open

fix(indexing): make sparse selections O(npoints) instead of O(nchunks)#262
d-v-b wants to merge 2 commits into
mainfrom
claude/zarr-python-issue-4174-77043e

Conversation

@d-v-b

@d-v-b d-v-b commented Jul 29, 2026

Copy link
Copy Markdown
Owner

🤖 AI text below 🤖

Fixes zarr-developers#4174

Problem

CoordinateIndexer (both the general path and the sorted-1D searchsorted fast path) and IntArrayDimIndexer built a dense per-chunk histogram with np.bincount(..., minlength=nchunks) / np.zeros(nchunks), followed by a full-length np.cumsum. Memory and time were proportional to the total number of chunks in the array, regardless of how few points were selected. The reporter's 3-point set_coordinate_selection on a (10_000_000, 13_854) array with (1, 1) chunks (~1.4e11 chunks) tried to allocate 1 TiB and raised MemoryError. get_coordinate_selection, vindex, mask selections (via MaskIndexer), and orthogonal integer-array selections (per-dimension) were all affected.

Fix

The dense histogram was only ever consumed by __iter__ to find each occupied chunk's start/stop range in the chunk-sorted point list — and the points are already sorted (or verified pre-sorted) by raveled chunk id. The new sorted_run_ends helper computes the occupied chunk ids and a compressed cumsum (one entry per occupied chunk) directly from run boundaries in the sorted ids; __iter__ indexes it by position instead of by raveled chunk id. Cost is now O(npoints).

Measured with the scaled-down repro from the investigation (3-point selection, 1e8 chunks): peak allocation drops from 1.6 GB to 0.1 MB for both get and set, on both the general and fast paths.

Tests

  • End-to-end coordinate + orthogonal set/get on a (2**22, 2**22) array with (1, 1) chunks (2**44 chunks — any dense per-chunk allocation fails outright).
  • Indexer-level projection tests for CoordinateIndexer (general sorted, general unsorted, and dense-duplicates fast path) and IntArrayDimIndexer (increasing/decreasing/unordered) on a grid with 2**62 chunks.
  • All tests were written first and observed failing at the dense allocation sites before the fix.

Out of scope, noted during investigation

normalize_chunks_1d materializes np.full(n_chunks_along_dim, size) at array-creation time, so creating an array with an astronomically chunked single dimension is still slow/impossible (opening one is fine — ChunkGrid.from_metadata builds O(1) FixedDimensions). That is a separate creation-time issue and is not addressed here.

API compatibility

Although zarr.core is documented private, the indexer attributes are introspectable, so the change is conservative: the compressed offsets live under a new name (chunk_run_ends, aligned with chunk_rixs / dim_chunk_ixs), and the pre-existing dense attributes — CoordinateIndexer.chunk_nitems_cumsum, IntArrayDimIndexer.chunk_nitems, IntArrayDimIndexer.chunk_nitems_cumsum — remain as deprecated properties that lazily rebuild the original dense arrays (with ZarrDeprecationWarning). External code that read them sees identical values; the O(nchunks) allocation is only paid on access. A GitHub-wide code search found no importers of these attributes (only vendored copies and ports), so removal after a deprecation cycle should be safe.

🤖 Generated with Claude Code

d-v-b added 2 commits July 29, 2026 17:10
CoordinateIndexer (general path and sorted-1D fast path) and
IntArrayDimIndexer built a dense per-chunk histogram via
np.bincount(..., minlength=nchunks) / np.zeros(nchunks) plus a
full-length cumsum. For arrays with very many chunks this allocates
memory proportional to the total chunk count regardless of how few
points are selected — a 3-point write to an array with 1.4e11 chunks
tried to allocate 1 TiB and raised MemoryError.

Replace the dense histogram with run boundaries computed directly on
the sorted raveled chunk ids (sorted_run_ends), storing a compressed
cumsum aligned with the occupied chunks, and index it positionally in
__iter__. Memory and time now scale with the number of selected
points.

Fixes zarr-developers#4174

Assisted-by: ClaudeCode:claude-fable-5
…rties

The compressed per-occupied-chunk cumsum introduced for zarr-developersgh-4174 changed
the observable semantics of chunk_nitems_cumsum (and removed
chunk_nitems on IntArrayDimIndexer). Although zarr.core is documented
as private API, external code is known to introspect these indexers, so
be conservative: store the compressed offsets under a new name
(chunk_run_ends, aligned with chunk_rixs / dim_chunk_ixs) and restore
chunk_nitems / chunk_nitems_cumsum as properties that lazily rebuild
the original dense arrays, warning with ZarrDeprecationWarning. The
O(nchunks) cost is now only paid if someone actually accesses them —
which was the status quo before the fix.

Assisted-by: ClaudeCode:claude-fable-5
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.

set_coordinate_selection runs out-of-memory for large arrays

1 participant