Repository navigation
perf: read each chunk once when iterating over an array - #4559
Open
bhaskargurram-ai wants to merge 3 commits into
Open
bhaskargurram-ai wants to merge 3 commits into
bhaskargurram-ai wants to merge 3 commits into
Conversation
Array had no __iter__, so `for row in array` fell back to calling `array[i]` for every i, which reads and decodes every chunk once per row it holds. Array.__iter__ now reads one row of chunks (of shards, for a sharded array) along the first dimension at a time and yields the rows of each block, so a full pass reads each chunk once. Rows are copied out of the block, so each one is an independent array, as `array[i]` is. A row of chunks larger than the new `array.iter_block_max_bytes` config value (default 128 MiB) is read in smaller blocks; for a sharded array these are kept to whole rows of inner chunks where possible. Iterating over a 0-d array now raises TypeError, as for NumPy arrays, instead of yielding nothing. Closes zarr-developers#2529
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4559 +/- ##
=======================================
Coverage 94.73% 94.74%
=======================================
Files 94 94
Lines 13706 13728 +22
=======================================
+ Hits 12985 13007 +22
Misses 721 721
🚀 New features to boost your workflow:
|
# Conflicts: # src/zarr/core/config.py # tests/test_config.py
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #2529.
zarr.Arrayhas no__iter__, sofor row in zfalls back to Python's sequence protocol. That protocol callsz[0],z[1], and so on untilIndexError. Each call reads and decodes every chunk that row touches. A chunk holdingnrows is therefore read and decodedntimes per pass, and on a remote store each row becomes a fresh set of requests. zarr v2 avoided this with a chunk-awareArray.__iter__/islice.This PR adds
Array.__iter__, following the direction discussed in the issue: chunk-aware iteration with a size check.self[start:stop]. For a sharded array, that row is a row of shards. It then yields the rows of that block. A full pass reads each chunk (or shard) once._chunk_grid.dimensions[0]), not fromchunks[0]. So rectilinear grids, a partial last chunk, and arrays resized below their last edge all work. Bounds are generated lazily, so nothing is built per chunk up front.array.iter_block_max_bytes, default 128 MiB. A row of chunks larger than this is read in smaller blocks, and its chunks are decoded once per block instead of once per row. For sharded arrays the smaller blocks are rounded to whole rows of inner chunks, so each inner chunk is still decoded once. This is the guard for the "chunks span the whole first axis" case raised in the thread.z[i]returns: the same dtype, the same shape, and the same memory order (C or F, followingconfig.order). For a 1-D array that is a 0-dndarray, as today. Each row is copied out of its block, so it is an independent array likez[i]and does not keep the block alive.TypeError("iteration over a 0-d array"), as NumPy and zarr v2 do. Today it silently yields nothing, becausez[0]raisesIndexErrorand the sequence protocol stops.Benchmark
Each cell times
for x in zover the whole array. The last column is the "read everything once, then iterate in NumPy" reference.z[:]then iterateHow these were measured:
mainat 957a5f6 vs this branch.LocalStorewas on tmpfs.The gap that remains against
z[:]comes from copying each row out of its block, roughly 0.5 µs per row.Benchmark script
Tests
Correctness is checked against
z[i], which is the old code path, and against NumPy iteration over the same data. Store reads are counted with the existingCountingDictstore intests/test_indexing.py.tests/test_indexing.pytest_iter: zarr formats 2 and 3, C and F order; chunks dividing the shape, a partial last chunk, one chunk, a chunk larger than the shape, 2-D and 3-D, and a zero-length trailing axis. For every row it checks the value, dtype, shape and C/F contiguity againstz[i], and the values against NumPy iteration.test_iter_sharded: sharded arrays, including a partial last shard.test_iter_rectilinear: rectilinear chunk edges, the same array after a resize, and a rectilinear shard grid.test_iter_dtypes: bool, uint8, complex128,<U3, a structured dtype, and variable-length strings.test_iter_missing_chunks: chunks that were never written yield the fill value.test_iter_0d: raisesTypeError, as NumPy does.test_iter_empty: an empty first dimension yields nothing.test_iter_rows_are_independent: each row is its own array, not a view into its block.test_iter_reads_each_chunk_once: unsharded, sharded with full-width shards, and sharded with a 2-D shard grid. Each chunk or shard key is read from the store exactly once per pass, and no other key is read.test_iter_reads_lazily: creating the iterator reads nothing. Consuming the first chunk row reads only that row's chunks, once each.test_iter_block_max_bytesandtest_iter_block_max_bytes_sharded: the block bounds for small limits, including the round-down to whole inner-chunk rows for sharded arrays. Values are still correct when blocks are split.tests/test_properties.py::test_iter: a Hypothesis test overblock_test_arrays(), covering regular, sharded and rectilinear grids, random dtypes and the available stores. Iteration must match NumPy iteration, and each row must matchz[i]in shape and dtype.tests/benchmarks/test_indexing.py::test_iterate: a CodSpeed benchmark on memory and latency stores, with and without sharding.tests/test_config.py: the defaults now include the new key.On
main, the read-count, laziness, 0-d and block-bound tests fail. The value-parity tests pass on both, as they should.For reviewers
row_of_chunks_bytes / 128 MiBtimes per pass, not once per row.TypeErrorinstead of yielding nothing..copy(order="K")), so rows stay independent arrays likez[i], keep the same memory order, and don't pin their block in memory. Yielding views would be a little faster (about 0.5 µs per row) and is how NumPy and zarr v2 behaved.AsyncArraystays non-iterable (an__aiter__could reuse_iter_row_blockslater), the v2islice(start, end)method is not restored, and__len__is not added. Happy to add any of these here or in a follow-up.z[i]parity tests cover this.Author attestation
TODO
docs/user-guide/*.mdchanges/