Skip to content

fix: shape the value buffer for coordinate selections in sharded writes - #4284

Open
dylanpulver wants to merge 3 commits into
zarr-developers:mainfrom
dylanpulver:fix-sharding-orthogonal-multi-array-set
Open

fix: shape the value buffer for coordinate selections in sharded writes#4284
dylanpulver wants to merge 3 commits into
zarr-developers:mainfrom
dylanpulver:fix-sharding-orthogonal-multi-array-set

Conversation

@dylanpulver

Copy link
Copy Markdown
Contributor

Summary

Nightly Slow Hypothesis CI on main filed #4280 on 2026-08-22 and hit it again on 2026-08-25 (run 32792180919): ValueError: shape mismatch: value array of shape (3,1) could not be broadcast to indexing result of shape (3,). An orthogonal set on a sharded array with two array-indexed dimensions reproduces it:

a = zarr.create_array(MemoryStore(), shape=(4, 4), chunks=(2, 4), dtype="int32",
                      serializer=ShardingCodec(chunk_shape=(2, 2), codecs=(BytesCodec(),)))
a.oindex[np.array([3, 1, 2]), np.array([0, 2])] = np.arange(6).reshape(3, 2)

OrthogonalIndexer hands such a chunk selection down as an np.ix_ pair (indexing.py:989). get_indexer reads it back as a coordinate selection, whose projections address shard_array flat while the caller shaped it like sel_shape. _decode_partial_single reshapes out to sel_shape on the way out (sharding.py:1085); this does the same on the way in. Reads were unaffected.

For reviewers

Guarded on shard_array.shape == sel_shape, so a flat value passes through. Checked against a numpy np.ix_ oracle over 1568 combinations of chunk grid, sharding nesting, per-dimension selector: 96 failures on main, 0 after, every one a write with two array-indexed dimensions.

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
  • Changes documented as a new file in changes/

@github-actions github-actions Bot added needs release notes Automatically applied to PRs which haven't added release notes and removed needs release notes Automatically applied to PRs which haven't added release notes labels Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.12%. Comparing base (d44f9f9) to head (77ce2c2).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4284   +/-   ##
=======================================
  Coverage   94.12%   94.12%           
=======================================
  Files          92       92           
  Lines       12831    12835    +4     
=======================================
+ Hits        12077    12081    +4     
  Misses        754      754           
Files with missing lines Coverage Δ
src/zarr/codecs/sharding.py 96.19% <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 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Code review

Found 1 issue:

  1. The fix is applied to _encode_partial_single (async, BatchedCodecPipeline) but not to its sync twin _encode_partial_sync (used by FusedCodecPipeline), which derives its indexer the same way and still fails with the same ValueError for this PR's own regression scenario when codec_pipeline.path is set to FusedCodecPipeline. The decode side applies the sel_shape reshape in both twins (_decode_partial_single and _decode_partial_sync), so the sync encode path needs the matching reshape too. Parametrizing the new test over both pipelines (as test_sharding_vlen_inner_codec_roundtrip does) would cover it.

Missing-fix location:

indexer = list(
get_indexer(
selection,
shape=shard_shape,
chunk_grid=ChunkGrid.from_sizes(shard_shape, self.chunk_shape),
)
)

The reshape added on the async side:

shard_indexer = get_indexer(
selection,
shape=shard_shape,
chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape),
)
# A coordinate indexer flattens the selection, so its projections address
# `shard_array` as 1-D while the caller shaped it like `sel_shape`. This
# mirrors the reshape `_decode_partial_single` applies on the way out.
sel_shape = getattr(shard_indexer, "sel_shape", None)
if sel_shape is not None and shard_array.shape == sel_shape:
shard_array = shard_array.reshape(shard_indexer.shape)
indexer = list(shard_indexer)

Sync decode twin already carrying the mirrored reshape:

if hasattr(indexer, "sel_shape"):
return out.reshape(indexer.sel_shape)
return out

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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.

2 participants