Skip to content

fix(chunk-grids): make array creation O(1) in per-dimension chunk count - #4218

Open
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:claude/optimistic-diffie-2ef839
Open

fix(chunk-grids): make array creation O(1) in per-dimension chunk count#4218
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:claude/optimistic-diffie-2ef839

Conversation

@d-v-b

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

Copy link
Copy Markdown
Contributor

Summary

this is a claude-authored fix of ##4174. in ChunkLayout, instead of materializing all chunk specifications, even regular ones, as numpy array, we specifically model regular chunk grid dimensions as a single shape + extent. This means ChunksTuple can just disappear, and we can re-use our chunk grid abstractions (FixedDimension and VaryingDimension), which is nice and simple. This avoids o(num chunks) memory problems and avoids some unnecessary abstractions.

this is basically what @maxrjones suggested here: #3899 (comment). My instinct to make the API "uniform" came at the cost of an inefficient representation, so I'm pulling back on the commitment to "uniformity" here.

Here's claude's summary:

Chunk normalization now returns a ChunkGrid whose uniform dimensions are stored as FixedDimension (size + extent) instead of one array entry per chunk, so create_array(shape=(262,), chunks=(1,)) succeeds instantly instead of raising "array is too big", and chunks=(1, 1) on a (231, 2**31) array no longer allocates ~17 GB per dimension.

Explicit per-chunk lists collapse to FixedDimension when they describe a regular grid; genuinely irregular lists become VaryingDimension. Both variants bind chunk sizes to their extent, so the two forms carry the same invariants. The intermediate ChunksTuple type and as_regular_shape helper are removed; create_chunk_grid_metadata consumes the grid directly and serializes uniform dimensions of mixed rectilinear grids as the spec's bare-int step-size shorthand.

Creation-time counterpart of the gh-4174 indexing fix.

Assisted-by: ClaudeCode:claude-fable-5

[Describe what this PR changes and why, in your own words.]

For reviewers

Check if this purported simplification is in fact simpler, and confirm that we don't see any unpleasant performance side effects.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

TODO

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/user-guide/*.md
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

@d-v-b
d-v-b requested a review from maxrjones July 30, 2026 10:24
@d-v-b
d-v-b marked this pull request as ready for review July 30, 2026 10:24
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.20%. Comparing base (7729d92) to head (ed26f62).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4218      +/-   ##
==========================================
+ Coverage   94.19%   94.20%   +0.01%     
==========================================
  Files          92       92              
  Lines       12825    12830       +5     
==========================================
+ Hits        12080    12087       +7     
+ Misses        745      743       -2     
Files with missing lines Coverage Δ
src/zarr/core/array.py 98.05% <100.00%> (+0.10%) ⬆️
src/zarr/core/chunk_grids.py 96.91% <100.00%> (-0.06%) ⬇️
src/zarr/core/common.py 90.47% <100.00%> (+0.59%) ⬆️
src/zarr/core/metadata/v3.py 94.21% <100.00%> (+0.10%) ⬆️
src/zarr/testing/strategies.py 95.45% <100.00%> (+0.02%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@d-v-b d-v-b added the benchmark Code will be benchmarked in a CI job. label Jul 30, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 11.82%

⚡ 1 improved benchmark
✅ 108 untouched benchmarks
🆕 4 new benchmarks
⏩ 37 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime test_read_array[latency=0.03-batched-memory-Layout(shape=(100000000,), chunks=(100000,), shards=(100000000,))-None-repeated] 572.2 ms 511.7 ms +11.82%
🆕 WallTime test_read_array_concurrent[latency=0-local-None-batched] N/A 43.7 ms N/A
🆕 WallTime test_read_array_concurrent[latency=0-local-None-fused_full_threaded] N/A 30.3 ms N/A
🆕 WallTime test_read_array_concurrent[latency=0-local-zstd-batched] N/A 70.2 ms N/A
🆕 WallTime test_read_array_concurrent[latency=0-local-zstd-fused_full_threaded] N/A 61.9 ms N/A

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing d-v-b:claude/optimistic-diffie-2ef839 (8c192b2) with main (80e00ae)2

Open in CodSpeed

Footnotes

  1. 37 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (6f9724c) during the generation of this report, so 80e00ae was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

d-v-b added a commit to d-v-b/zarr-python that referenced this pull request Jul 30, 2026
…k_grid_metadata

The defensive TypeError branch is unreachable through the public API, so
exercise it directly with a stub dimension object. This was the only
genuinely uncovered patch line in zarr-developers#4218; the other lines codecov flagged
came from a partial coverage upload.

Assisted-by: ClaudeCode:claude-fable-5
@d-v-b d-v-b mentioned this pull request Aug 12, 2026
14 tasks
d-v-b added 4 commits August 26, 2026 14:20
Chunk normalization now returns a ChunkGrid whose uniform dimensions are
stored as FixedDimension (size + extent) instead of one array entry per
chunk, so create_array(shape=(2**62,), chunks=(1,)) succeeds instantly
instead of raising "array is too big", and chunks=(1, 1) on a
(2**31, 2**31) array no longer allocates ~17 GB per dimension.

Explicit per-chunk lists collapse to FixedDimension when they describe a
regular grid; genuinely irregular lists become VaryingDimension. Both
variants bind chunk sizes to their extent, so the two forms carry the
same invariants. The intermediate ChunksTuple type and as_regular_shape
helper are removed; create_chunk_grid_metadata consumes the grid
directly and serializes uniform dimensions of mixed rectilinear grids
as the spec's bare-int step-size shorthand.

Creation-time counterpart of the zarr-developersgh-4174 indexing fix.

Assisted-by: ClaudeCode:claude-fable-5
Since normalize_chunks_nd returns ChunkGrid dimensions instead of int64
arrays, is_regular_1d only ever receives plain Python sequences; the
vectorized numpy path was unreachable in production code. Remove it and
narrow the signatures of is_regular_1d / is_regular_nd to Sequence[int].

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

The defensive TypeError branch is unreachable through the public API, so
exercise it directly with a stub dimension object. This was the only
genuinely uncovered patch line in zarr-developers#4218; the other lines codecov flagged
came from a partial coverage upload.

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

The stateful hypothesis tests convert generated chunk grid metadata back
into a create_array chunks= argument. Bare-int dimensions — the spec's
step-size shorthand, now produced when a uniform dimension of a mixed
grid collapses — were wrapped as single-element lists, turning "repeat
to cover the axis" into "exactly one chunk" and failing the sum-to-span
check (e.g. chunks=[1] for span 3). Extract the conversion into
chunks_param_from_rectilinear, pass bare ints through unchanged, and
widen ChunksLike to admit mixed int | sequence per-dimension specs,
which the normalizer already accepted.

Assisted-by: ClaudeCode:claude-fable-5
@d-v-b
d-v-b force-pushed the claude/optimistic-diffie-2ef839 branch from a6d27c7 to 72af8cb Compare August 26, 2026 12:51
@d-v-b

d-v-b commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Rebased onto current main (7729d92) and added a fix for #4272 on top. Summary of what changed:

Rebase

The conflict was with #4257 ("accept numpy integers as chunk sizes"), which landed after this branch was cut and rewrote the same region of normalize_chunks_1d. Its semantics are preserved inside the new FixedDimension/VaryingDimension code:

  • scalars are narrowed with numbers.Integral (not int) and coerced via int(), so numpy integer scalars take the uniform-chunk path;
  • the -1 sentinel check lives inside the integral branch, so numpy-array chunk specs never hit an ambiguous-truth-value error on chunks == -1;
  • the improved TypeError for non-integer, non-iterable specs is kept, along with fix: accept numpy integers as chunk sizes #4257's tests (translated to the FixedDimension/VaryingDimension expectation format).

normalize_chunks_1d no longer needs main's span == 0 special case — FixedDimension handles zero extents natively. In zarr/testing/stateful.py the branch's chunks=-reconstruction hunk was dropped in favor of main's newer zarr.from_array(..., write_data=False) approach; chunks_param_from_rectilinear stays, since the arrays() strategy still uses it.

New commit: explicit per-chunk lists stay rectilinear (fixes #4272)

The explicit-list branch of normalize_chunks_1d previously collapsed uniform (or uniform-plus-short-tail) lists to FixedDimension, so an explicitly rectilinear spec like [[10, 10, 4]] was silently stored as a regular grid — which changes resize semantics (a regular grid grows by extending the uniform pattern; a rectilinear grid appends an edge chunk), breaking append-oriented layouts like (168,) * 13 + (24,). That collapse is now removed: the grid kind follows the input syntax, matching 3.2.x — scalar specs (ints, numpy integers, -1) produce FixedDimension → regular grid (the O(1) path this PR is about), explicit lists always produce VaryingDimension → rectilinear grid. Uniform dimensions of mixed scalar/list specs still serialize as the spec's bare-int step-size shorthand. New tests cover the syntax→kind mapping and the resize divergence (the rectilinear append touches exactly one new chunk; the regular pattern-extension straddles two).

Relation to #4290

#4290 fixes the same issue on current main by threading a requested_rectilinear flag from the raw input into create_chunk_grid_metadata, because main's ChunksTuple normalization erases the form of the request. With this PR's representation the flag isn't needed — FixedDimension vs VaryingDimension records the request's form, so create_chunk_grid_metadata dispatches on the grid with no call-site plumbing, and rectilinear shards= producing a rectilinear outer grid falls out automatically. End behavior matches #4290 (including uniform nested specs now hitting the array.rectilinear_chunks gate). Credit to #4290 for the touched-chunk-keys assertion style, which the resize regression test here adopts.

Drop the regular-grid collapse for explicit list input in
normalize_chunks_1d: a per-chunk size list now always produces
VaryingDimension, even when the sizes are uniform or uniform plus a
short tail. Scalar specs (ints, numpy integers, and the -1 sentinel)
still produce FixedDimension, which is the path that makes array
creation O(1) in per-dimension chunk count; explicit lists are already
O(n) in the input, so nothing is lost.

The grid kind now follows the input syntax, matching 3.2.x behavior:
previously an explicitly rectilinear spec whose edges happened to look
regular was silently stored as RegularChunkGridMetadata, which changes
resize semantics — a regular grid grows by extending the uniform
pattern while a rectilinear grid appends an edge chunk, breaking
append-oriented layouts like (168,) * 13 + (24,). This is resolution
option 1 from zarr-developersgh-4272.

Uniform dimensions of mixed scalar/list specs still serialize as the
spec's bare-int step-size shorthand; explicit lists serialize as edge
lists.

The touched-chunk-keys assertions in the resize regression test follow
the approach from zarr-developersgh-4290, which fixes the same issue on main via a
requested_rectilinear flag.

Fixes zarr-developers#4272

Co-authored-by: Shurong Cao <CAOShurong@users.noreply.github.com>
Assisted-by: ClaudeCode:claude-fable-5
@d-v-b
d-v-b force-pushed the claude/optimistic-diffie-2ef839 branch from 72af8cb to 535fa0a Compare August 26, 2026 13:02
…me grid

The stateful hypothesis tests in CI failed in from_array on a
rectilinear source whose edges happen to be uniform (e.g.
chunk_shapes=((3,),)): _parse_keep_array_attr and _info guarded
data.chunks / data.shards with the runtime ChunkGrid.is_regular, but
the runtime grid collapses uniform rectilinear dimensions to
FixedDimension as an optimization, while metadata.chunks raises for
any array whose stored grid is rectilinear. Before explicit per-chunk
lists stopped collapsing at creation, the two notions of regularity
always agreed for created arrays, so the mismatch was unreachable.

Add _stored_chunk_grid_is_regular, which dispatches on the metadata
kind, and use it at the three sites that read .chunks/.shards. A
uniform-edged rectilinear source now round-trips through from_array
with its grid kind intact.

Assisted-by: ClaudeCode:claude-fable-5
@d-v-b

d-v-b commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@maxrjones could you review this when you get a chance? It fixes two issues and so I consider it high priority for our next release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmark Code will be benchmarked in a CI job.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant