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. |
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.
packages/c-blosc2-blosc1-compat/patches/is a four-patch series, one concern each:The flag is off by default and there is no ABI change. 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. I do not know whether upstream will accept 0002–0004, so the package is self-contained (no numcodecs imports) and can be split out or dropped. See its README and
TODO.md.What changed
packages/c-blosc2-blosc1-compat/: meson build for c-blosc2 (upstream is CMake only), the patch series, a compatibility check (tests/run.py), and tools to keep the wrap in sync and to test the series against a newer c-blosc2.subprojects/: wraps for c-blosc2 (pinned commit, patched 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).diff_filesandUPSTREAMpin stay consistent. Compressors a build leaves out are skipped..github/workflows/blosc2-compat.yaml: checks the wrap copies are 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), and weekly tries the series on upstream c-blosc2main.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..pre-commit-config.yamlnow excludes*.patch, because the whitespace hook would rewrite the patches' context lines.🤖 Generated with Claude Code