From 4caa2eab451f15c440fb5cece3666eeec56e3315 Mon Sep 17 00:00:00 2001 From: Alexis Jeandet Date: Thu, 24 Sep 2026 00:25:27 +0200 Subject: [PATCH] Only take the asarray fast path when blocks are contiguous in the chunk are_partitions_behaved() accepted blocks that divide the chunk but are not C-contiguous runs of it, e.g. (32, 1, 16, 32) in (256, 32, 16, 32). asarray() then wrote C-order chunk data with update_data(), which b2nd reads back block by block, silently scrambling arrays of 16 MB or more. Use the same rule as nchunk_fastpath() in C-Blosc2's b2nd.c. Fixes #723. Co-Authored-By: Claude Opus 5.5 --- src/blosc2/ndarray.py | 14 +++++++++++++- tests/ndarray/test_ndarray.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/blosc2/ndarray.py b/src/blosc2/ndarray.py index eac313882..ee1aa5b07 100644 --- a/src/blosc2/ndarray.py +++ b/src/blosc2/ndarray.py @@ -322,7 +322,19 @@ def check_contiguity(container, part): return False return builtins.all(size % unit == 0 for size, unit in zip(container[:-1], part[:-1], strict=True)) - return check_contiguity(chunks, blocks) and check_contiguity(shape, chunks) + def blocks_contiguous(chunks, blocks): + # A block is a C-contiguous run of its chunk only if its leading dims are 1, + # then one dim divides the chunk's, and the rest are equal to the chunk's. + # Same rule as nchunk_fastpath() in C-Blosc2's b2nd.c. + k = 0 + for i, (size, unit) in enumerate(zip(chunks, blocks, strict=True)): + if size % unit != 0 or (i > k and size != unit): + return False + if i == k and unit == 1: + k += 1 + return True + + return blocks_contiguous(chunks, blocks) and check_contiguity(shape, chunks) def get_flat_slices_orig(shape: tuple[int], s: tuple[slice, ...]) -> list[slice]: diff --git a/tests/ndarray/test_ndarray.py b/tests/ndarray/test_ndarray.py index dac65566a..5f8e631c1 100644 --- a/tests/ndarray/test_ndarray.py +++ b/tests/ndarray/test_ndarray.py @@ -122,6 +122,28 @@ def test_asarray_chunks_larger_than_shape(shape, chunks, blocks): assert not blosc2.are_partitions_behaved(shape, chunks, blocks) +@pytest.mark.parametrize( + ("shape", "chunks", "blocks", "behaved"), + [ + # blocks divide the chunk but are not C-contiguous runs of it + ((256, 32, 16, 32), (256, 32, 16, 32), (32, 1, 16, 32), False), + ((64, 64, 64, 16), (64, 64, 64, 16), (8, 2, 64, 16), False), + # contiguous blocks keep the fast path + ((256, 32, 16, 32), (256, 32, 16, 32), (1, 4, 16, 32), True), + ], +) +def test_asarray_blocks_not_contiguous_in_chunk(shape, chunks, blocks, behaved): + # Above 16 MB, asarray copies whole chunks with update_data, which is only + # valid when every block is a C-contiguous run of its chunk + a = np.arange(math.prod(shape), dtype=np.float64).reshape(shape) + assert a.nbytes > 2**24 + + b = blosc2.asarray(a, chunks=chunks, blocks=blocks) + + np.testing.assert_array_equal(b[:], a) + assert blosc2.are_partitions_behaved(shape, chunks, blocks) == behaved + + def test_asarray_persists_copy_with_urlpath(tmp_path): array = blosc2.asarray(np.arange(10, dtype=np.int64), chunks=(5,), blocks=(2,)) path = tmp_path / "persisted_copy.b2nd"