Repository navigation
Conversation
packages/c-blosc2-blosc1-compat holds a meson build for c-blosc2 (upstream is CMake only), four patches that let c-blosc2 write chunks C-Blosc 1.x can decode, and a check that decodes 735 c-blosc2 chunks with C-Blosc 1.x and the reverse. subprojects/ has the c-blosc2 wrap (patched through packagefiles, kept in sync by tools/sync_wrap.py) and WrapDB wraps for zstd, lz4 and zlib. A workflow checks the sync, runs the compatibility check, and weekly tries the series on upstream main. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-Dblosc_backend=blosc2 builds blosc.pyx against the patched c-blosc2 instead of c-blosc 1.x (still the default). A small C shim gives both libraries one API. zstd, lz4 and zlib are resolved separately from Blosc, so the zstd and lz4 codecs share them, and -Dblosc_zstd/-Dblosc_zlib=disabled leave them out of Blosc only. Chunks are always written in the Blosc1 format. Also ports the frame validation and empty-buffer handling from zarr-developers#854, the zero-itemsize fix from zarr-developers#870 and the shuffle round-trip test from zarr-developers#864, clamps a requested blocksize like c-blosc 1.x does, and adds tests for Blosc1 output and for builds without zstd or zlib. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The compatibility workflow assumed the wrapped zstd/lz4/zlib static libraries were always built; c-blosc2 uses a system copy when it finds one, so it now links whichever exists. Keep the pre-commit whitespace hook away from .patch files, whose context lines must keep their trailing space, and make the package's scripts pass ruff (including the executable bit for shebangs). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pyproject.toml's license-files still lists the c-blosc submodule's licenses, so the build fails without it even with the c-blosc2 backend. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #879 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 27 27
Lines 914 914
=========================================
Hits 914 914 🚀 New features to boost your workflow:
|
…osc2 package Cover the decompress errors for a short buffer, an invalid header and a truncated frame, blocksizes that are larger than the buffer or not a multiple of the typesize, and the library attributes. A new test module checks that the wrap copies match packages/c-blosc2-blosc1-compat, that diff_files lists the patch series, that the wrap matches UPSTREAM, and that sync_wrap.py detects and fixes a stale copy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Performance: c-blosc 1.x vs c-blosc2 backendSingle thread (
What this shows:
Scriptimport sys, time
import numpy as np
sys.path.insert(0, sys.argv[1])
import numcodecs.blosc as b
b.use_threads = False
print(b.LIBRARY, b.VERSION_STRING)
def mbs(f, n, reps=40):
f(); best = 1e9
for _ in range(reps):
t = time.perf_counter(); f(); best = min(best, time.perf_counter() - t)
return n / best / 1e6
rng = np.random.default_rng(0)
cases = [("uint16 1MiB", np.cumsum(rng.integers(0, 3, 2**19)).astype('u2')),
("float32 1MiB", np.cumsum(rng.normal(scale=1e-3, size=2**18)).astype('f4')),
("float64 1MiB", np.cumsum(rng.normal(scale=1e-3, size=2**17))),
("uint16 16MiB", np.cumsum(rng.integers(0, 3, 2**23)).astype('u2'))]
for cname in ("lz4", "blosclz"):
for name, a in cases:
raw = a; n = a.nbytes
enc = b.compress(raw, cname.encode(), 5, b.SHUFFLE)
e = mbs(lambda: b.compress(raw, cname.encode(), 5, b.SHUFFLE), n)
d = mbs(lambda: b.decompress(enc), n)
_, _, memcpyed = b._cbuffer_metainfo(enc)
print(f"{cname:8s} {name:13s} ratio {n/len(enc):5.1f} memcpy={memcpyed} encode {e:7.0f} decode {d:7.0f} MB/s")Run it as Two traps I hit while writing it, in case someone repeats the measurement: passing |
|
Follow-up to the performance comment above: the encode slowdown it shows for 2-byte data, and where it comes from. What is slower. On the c-blosc2 backend, encoding 2-byte types (uint16) is about 25% to 45% slower than with c-blosc 1.x at the same codec, level and shuffle (lz4 725 vs 1137 MB/s, blosclz 1158 vs 1964 in the table above). 4- and 8-byte types are comparable or faster, and decode is much faster everywhere I measured. Against c-blosc 1.x with #864's NEON applied, c-blosc2 is slower on encode in most rows (see my comment on #864). Cause: a different automatic block size, not slower code. In a C-level test (1 MiB, typesize 2, clevel 5, one thread), c-blosc 1.x stores 256 KiB blocks and c-blosc2 stores 128 KiB blocks:
At the same block size c-blosc2 produces the same ratio and is as fast or faster. The two libraries compute the default differently for splittable codecs: c-blosc 1.x uses Correction: I first guessed on #864 that the Blosc1 output mode's block-split rule was responsible. That was wrong. Stock c-blosc2 with its default Blosc2 output is equally slow, and the patched Blosc1 mode is within a few percent of it. What this means for the PR.
Caveats: one machine (Apple Silicon), one buffer size for the block-size experiment, synthetic data, no x86_64. Drafted with Claude Code. |
|
so did they avoid supporting the old blosc1 format header to encourage people to switch to blosc2? |
|
It did not seem to encourage us very much here, did it? |
|
this is a big change so I'd like some eyes from @zarr-developers/python-core-devs. my pov: this effort already revealed one bug in blosc2. I wonder if there are more? for me, a perf improvement isn't worth a moderate chance of data corruption / bugs. I personally am not qualified to evaluate the c code or the build backend changes so I would like to see some very comprehensive property-based tests that fuzz the new codec and ensure that the range of bytes payloads, with important edge cases, that we see in |
|
Thanks for the ping to maintainers. I am surprised that there would be a push to implement this in C with a massive PR rather than call the python or C API - is the overhead really that crushing? This has always puzzled me about numcodecs specifically for blosc; at least the cython-on-Clib codecs are reasonable. Generally, the comparison with what blosc does internally versus zarr's sharding filter is super interesting for me. (yes, I realise here we are in numcodecs/zarr1 land) I feel like the two libraries are sort of doing the same thing and it may be better to (hope to) converge. blosc has done some interesting things with in-memory compressed arrays and operation fusion that zarr has not been interested in or left to other frameworks. cc @FrancescAlted , might find this interesting. |
|
@martindurant The C code patches included in this pull request add a function which c-blosc2 nor its Python wrapping actually does by itself, which is writing write conpatible c-blosc (v1) frames that c-blosc (v1) can decode. Forward compatibility has long been a blocker here inhibiting the migration to c-blosc2. Other pull requests started to pull code from c-blosc2 for ARM Neon support, as an example, which I think is more unsustainable than just using c-blosc2 which already maintains such support. Regardless of depedency and integration mechanism, continued dependence on c-blosc (v1) has become unsustainable, untenable, and irresponsible in any form. |
|
the blosc v1 codec is baked in to the zarr v3 spec. so we are on the hook for supporting it. |
As currently written this pull request actually changes nothing by default. The default is to use the c-blosc (v1) "backend". It must be enabled by a compiler flag whichis disabled by default. This pull request allows for the very testing you are requesting to begin. |
I think what I was saying is that, given the scale of the intended change (swapping out our vendored blosc1 for a patched blosc2), I would need to see those tests before I can think about supporting this effort. |
The blosc v1 codec is distinct from the c-blosc (v1) code base, which is seeing less and less developer attention. I have submitted issues and pull requests to the v1 code base in the past but decided it was no longer worthwhile to do about a year ago. The patches included here show a path to supporting the v1 codec from the c-blosc2 code base. That said the lack of upstream support for writing the c-blosc v1 format suggest that we should begin deprecating writing with that codec. |
|
I agree with @martindurant that the best outcome would be for us to just import all this stuff from a published package. @FrancescAlted would applying the patches here to blosc2 be in scope for the project? then we could just import blosc2 from pypi, which would be far simpler than what we have here. |
Just let me make clear that we are not supporting old blosc1 very actively mainly for two reasons:
Having said that, it is completely true that we encourage people to switch to blosc2. We really think that this is the way to go forward, and for me, adding support for blosc2 in zarr makes a lot of sense. In blosc2 we are actively supporting backward compatibility with blosc1, and I think this could facilitate a lot the transition to blosc2. |
Absolutely. But IMO, providing a way to generate blosc1-compatible format inside blosc2 has little sense, because for me, focusing on the new format should bring more value for users (it is way more flexible) than keep trying using just the old one. Having said that, if you find that this is paramount for you, I am fine with that (within our finite resources). |
Thank you for replying, @FrancescAlted . What is holding adoption of c-blosc2 back here is that c-blosc2 does not support writing c-blosc v1 compatible chunks. What I have done here is provide patches for c-blosc2 which allow for that - write chunks that c-blosc1 can read to enable the transition to the c-blosc2 code base. This seems more practical than trying to backport ARM Neon patches from c-blosc2 to c-blosc v1. The question Davis has is "Would you accept patches that enable writing c-blosc v1 formatted chunks in c-blosc2?" |
Yep. Although this does not make a lot of sense to me. See my reply to him above. |
|
I have opened a pull request to apply the last three patches to c-blosc2 directly. |
Point the c-blosc2 wrap at the blosc1-format-writer branch of mkitti/c-blosc2 (Blosc/c-blosc2#815), pinned to its head commit, instead of a release tarball plus a four-patch series. The reader fix (patch 0001) is already upstream and the branch contains the writer (patches 0002-0004, reworked and with tests). - subprojects/c-blosc2.wrap is now a wrap-git; the patches, make_wrap_diff_files.py and rebase.py are removed, sync_wrap.py only copies the meson overlay - UPSTREAM records the branch and commit; the test checks the wrap agrees - drop the scheduled rebase job from the workflow and the .patch pre-commit exclude - README.md, TODO.md and the release notes describe the branch and what to do once zarr-developers#815 is released (the wrap must not stay on a personal fork for a release) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review feedback on zarr-developers#879 asked for very comprehensive property-based tests that the range of payloads seen in zarr survives the swap to c-blosc2. The compressed bytes cannot be identical (the libraries pick different default block sizes), so check the thing that matters: chunks written by numcodecs decode with C-Blosc 1.x to the original bytes, and the reverse. python-blosc, which bundles C-Blosc 1.x, is the reference. tests/test_blosc1_compat.py has hypothesis properties over compressors, levels, shuffles, typesizes 1-300, buffer sizes at the edges that matter (32 bytes, multiples of 8 and 128 elements, leftover bytes), block sizes and data kinds, plus an exhaustive grid. Both numcodecs code paths are covered, the global context and numcodecs' own context (use_threads=False, other threads, forks), which write Blosc1 chunks differently on the c-blosc2 backend: switching the context path off fails 1506 of the tests, which is how a first version that missed it was found. .github/workflows/blosc1-compat.yaml runs it on the c-blosc2 backend on Linux (x86_64, arm64), macOS and Windows, and on the default c-blosc 1.x backend as a control. It is self-contained and can be disabled without touching anything else: set the repository variable BLOSC1_COMPAT_CI to 'false', delete the file, or deselect the blosc1_compat marker. The test module skips itself when python-blosc or hypothesis is missing, so the other workflows are unchanged. Also format packages/c-blosc2-blosc1-compat/tests/run.py with ruff, which pre-commit.ci flagged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Re: the request for tests that the payloads zarr sees come out "byte-identical after this swap" (comment). I added property-based tests and CI for compatibility with C-Blosc 1.x (e014c8f), but they check that the decoded bytes are identical, not that the compressed bytes are. The compressed bytes will not be identical, and cannot be made so without reimplementing C-Blosc 1.x's choices inside c-blosc2. Why. The Blosc1 format specifies how a chunk is decoded, not how it must be encoded. A writer is free to choose the block size, whether to split blocks, and what the codec emits. Any C-Blosc 1.x reader decodes every valid choice. c-blosc2 makes different choices from c-blosc 1.x in the places below, and the Blosc1 mode in Blosc/c-blosc2#815 deliberately only changes what a C-Blosc 1.x reader needs (the version byte, the bitshuffle layout of blocks whose element count is not a multiple of 8, and never splitting a block the reader would not split).
How often, in practice. I compared the two writers on the 1050 deterministic cases of the grid test with at least 32 bytes (blosclz, lz4, lz4hc, zlib, zstd; 3 shuffles; typesizes 1 to 300; sizes 32 B to 100 KB; level 5, automatic block size). 213 are byte-identical and 837 differ:
For the 1 MiB uint16 example above, numcodecs on c-blosc2 gives 416,503 bytes and C-Blosc 1.x gives 481,483. This is also not specific to c-blosc2: the compressed bytes of any Blosc writer depend on its codec versions and heuristics, so they are not something readers can rely on, and a version bump of c-blosc 1.x's own codecs can change them too. What the tests do guarantee, on every backend (c-blosc 1.x as a control, c-blosc2 on Linux x86_64/arm64, macOS and Windows), with python-blosc (which bundles C-Blosc 1.x) as the reference, in both directions:
It runs 300 examples per property on a pull request and 5000 weekly, and it can be switched off without a code change (repository variable Drafted with Claude Code. |
|
thanks for adding those tests. the inability to match byte-for-byte is probably not a blocker, given the advantages of getting on a more secure dependency footing, but it is a change that will affect people and potentially require them to update their code. Anyone who assumes a fixed relationship between the blosc configuration and a compressed payload will need to update that assumption after that change, and potentially alter any code paths that rely on the size or contents of the compressed payload. So we probably need to socialize this change a bit in advance, so folks can prepare for it. I know a few tests in |
|
Follow-up to my comment on byte-identical output: what the public API can do about it, and one result worth recording, which is what the split mode does. Finding:
Passing C-Blosc 1.x's own block size to c-blosc2 as Why a single mode is not the answer. C-Blosc 1.x decides per codec: it splits every codec except zstd (provided the typesize is at most 16 and the block has at least 128 elements, which the Blosc1 mode in Blosc/c-blosc2#815 already enforces on top of whatever mode is requested). I checked the flag in real chunks: blosclz, lz4, lz4hc and zlib split, zstd does not. c-blosc2's default (
For the 1 MiB buffers alone it goes from 87 to 291 of 540. The reimplemented block size formula agrees with C-Blosc 1.x's header in 4212 of the 4320 cases. What does not move. Two things the API cannot change keep it from going further:
What I would and would not do with this. The tests in this PR check decoded bytes, which is what compatibility needs, and I do not think numcodecs should chase byte-identical output: the stream differences above are outside our control, and the remaining 66% differ for reasons the format allows. If it is still wanted, the cheapest step is to pass C-Blosc 1.x's split rule through the backend: numcodecs would call Caveats: one machine (x86_64), synthetic data, level 5 for the first grid and levels 1/5/9 for the second, and python-blosc's bundled C-Blosc 1.21.7.dev as the reference. Drafted with Claude Code. |
Move the wrap to c919bfe6, the head of blosc1-format-writer after the review on Blosc/c-blosc2#815: a typesize guard, a codec whitelist, the filter accepted in any slot, blosc2_ctx_get_blosc1_format(), and multi-threaded tests. numcodecs only passes a positive typesize, the shuffle filter in the last slot and a supported codec, so nothing changes for it; the compatibility tests pass on this commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
That doesn't seem like something we would promise? I don't know, but I would not be surprised if an update in Zstd, lz4 etc might change the default output for a given array. |
No I don't think it's something we should promise. But it is an expected consequence of the change here, so worth keeping it in mind. |
Blosc/c-blosc2#815 (the Blosc1 format writer) is merged, so the wrap no longer needs the fork's branch. Pin it to the merge commit on main. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Moves from the Blosc/c-blosc2#815 merge commit to d77f4b9 (four later commits: DELTA filter fixes, and recording the real block size in the header). Same version (3.3.6.dev); the source list is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
All patches have been upstreamed. |
Summary
Adds c-blosc2 as an opt-in build backend for the
Blosccodec (-Dblosc_backend=blosc2). The default stays c-blosc 1.x, so nothing changes for existing builds.Chunks written through this backend are in the Blosc1 format and decodable by C-Blosc 1.x. This is the point of the PR and it is not true of stock c-blosc2. Related: #864, #865, #854, #870, #463.
Forward compatibility
c-blosc2's README says its buffers are not readable by C-Blosc 1.x. I checked whether that still holds through the
blosc1_*API (c-blosc 1.21.6 vs c-blosc2 3825863, 735 cases: blosclz/lz4/lz4hc/zlib/zstd × no/byte/bit shuffle × typesizes 1–16 × sizes 1 B–100 KB, including sizes that are not a multiple of the typesize):blosc1_compress, with or withoutBLOSC_BLOSC1_COMPAT, orblosc2_compress_ctx) → C-Blosc 1.xThe gap is in the writer: the format version byte (5 vs 2), the bitshuffle layout for blocks whose element count is not a multiple of 8, and the block-split threshold. The reader bug is fixed upstream (issue #812, PR #813, merged). The writer is merged upstream in Blosc/c-blosc2#815, which adds an opt-in
blosc2_ctx_set_blosc1_format()(and makesBLOSC_BLOSC1_COMPATenable it forblosc1_compress()); the output decodes with C-Blosc 1.x but is not byte-identical to what it writes. Only plain shuffle/bitshuffle with blosclz, lz4, lz4hc, zlib and zstd are supported in this mode; anything else is an error.#815 is merged but not yet in a c-blosc2 release, so the c-blosc2 wrap points at the merge commit on c-blosc2
main(a98177412d3f). That has to move to an upstream release before numcodecs is released with this backend;packages/c-blosc2-blosc1-compat/TODO.mdlists the steps. The package is self-contained (no numcodecs imports) and can be split out. See its README.What changed
packages/c-blosc2-blosc1-compat/: meson build for c-blosc2 (upstream is CMake only), a compatibility check (tests/run.py), and a tool to keep the wrap's copy of the build files in sync.subprojects/: a git wrap for c-blosc2 (main, pinned to a commit, with the meson build copied in through packagefiles) and for zstd, lz4 and zlib from WrapDB.blosc_backendoption. zstd, lz4 and zlib are resolved separately from Blosc, so thezstdandlz4codecs share them.-Dblosc_zstd=disabled/-Dblosc_zlib=disabledleave them out of Blosc only.src/numcodecs/_blosc_shim.hgives both libraries one C API, whichblosc.pyxuses.numcodecs.blosc.LIBRARYreports which one is in use.blosc.pyxalso picks up the frame validation and empty-buffer handling from Fix empty Blosc buffer round-trips #854, the zero-itemsize fix from Use a typesize of 1 in Blosc for zero-itemsize buffers #870, and clamps a requested blocksize to the buffer size and a multiple of the typesize as C-Blosc 1.x does (c-blosc2 writes unreadable chunks otherwise).UPSTREAMpin stay consistent. Compressors a build leaves out are skipped..github/workflows/blosc2-compat.yaml: checks the wrap copy of the build files is in sync, runs the forward-compat check against a real C-Blosc 1.21.6, runs the whole test suite on the c-blosc2 backend (Linux x86_64 and arm64, macOS arm64, Windows, and free-threaded 3.14t).Performance
#864's NEON copy is not needed for this backend: c-blosc2 builds its own NEON shuffle on aarch64 (the
shuffle_neonsymbols are in the extension). Single thread, Apple Silicon, 1 MiB arrays,SHUFFLE, decode, c-blosc 1.x → c-blosc2: lz4 uint16 1.5 → 4.6 GB/s, lz4 float32 1.6 → 8.9, blosclz uint16 1.8 → 20.4, blosclz float64 2.3 → 7.1. Encode is mixed (lz4 uint16 725 vs 1180 MB/s, others comparable or faster); one machine, not explained.Notes for reviewers
snappy, and the wrapped zstd needs a C++ compiler (as in build: replace c-blosc git submodule with meson wrap subprojects #865).c-bloscsubmodule or change CI for the default backend; that belongs with build: replace c-blosc git submodule with meson wrap subprojects #865. Expect a rebase between this PR, perf(blosc): use NEON SIMD for byte-shuffle on aarch64 #864 and build: replace c-blosc git submodule with meson wrap subprojects #865: all touch the blosc part ofsrc/numcodecs/meson.buildandmeson.options. perf(blosc): use NEON SIMD for byte-shuffle on aarch64 #864 is still relevant for the default c-blosc 1.x backend.pyproject.toml'slicense-filesstill lists the c-blosc submodule's licenses, so a c-blosc2 wheel does not yet ship c-blosc2's own. build: replace c-blosc git submodule with meson wrap subprojects #865 reworks this.🤖 Generated with Claude Code