Repository navigation
Description
Hi Francesc,
Many arrays in our medical imaging work are mostly special chunks: masks and the prompt channels of interactive segmentation are created with blosc2.zeros() and stay zero almost everywhere, and c-blosc2 stores every all-zero chunk that is written as a special ZERO chunk. Operations on such arrays cost as much as on dense data whenever the evaluation does not take the fast block-wise path. blosc2 4.14.1, Ryzen 9 7950X (blosc2.nthreads 28), shape (1100, 512, 512), chunks (64, 128, 128), blocks (32, 32, 32), single runs:
| float32 | float16 | |
|---|---|---|
(z * s).compute(), z all ZERO chunks |
20 ms | 828 ms |
the same with 6 small boxes of data in z |
21 ms | 783 ms |
z.sum() |
21 ms | 462 ms |
z.max() |
21 ms | 488 ms |
blosc2.full(shape, 3).sum() (VALUE chunks) |
20 ms | 434 ms |
float32 is fast already, but float16 goes through a path that decompresses and computes every chunk. get_chunk() has a shortcut for ZERO chunks in reductions (
python-blosc2/src/blosc2/lazyexpr.py
Lines 1343 to 1350 in a712c65
| # First check if the chunk is a special zero chunk. | |
| # Using lazychunks is very effective here because we only need to read the header. | |
| if reduc: | |
| # Reductions can treat zero scalars as zero chunks | |
| chunk = arr.schunk.get_lazychunk(nchunk) | |
| special = blosc2.SpecialValue((chunk[31] & 0x70) >> 4) | |
| if special == blosc2.SpecialValue.ZERO: | |
| return np.zeros((), dtype=arr.dtype) |
Proposal: when every operand chunk of an output chunk is special (ZERO, VALUE or NaN) and the chunk grids match:
- Element-wise expressions: evaluate the expression once on the operands' values and store the result as a special chunk (ZERO if the result is all zero bits, else a VALUE chunk as
blosc2_chunk_repeatval()makes), with nothing decompressed, computed or compressed. Evaluating the expression on the value, instead of assuming zero in, zero out, keeps the semantics:0 * infis NaN,z + 1gives a chunk of ones,z > 0.5an all-False chunk. (Today(blosc2.full(shape, 3.0) * 2).compute()stores ordinary compressed chunks, not VALUE chunks.) - Reductions: ZERO and VALUE chunks reduce in closed form (
sum= value times the number of items,max/min= value,any,all,count_nonzero), without decompressing them. - In-place updates (see In-place operators on NDArray (a *= 2) return a LazyExpr and leave the array unchanged #760): a chunk whose result equals its input, such as a ZERO chunk times a finite scalar, needs no write at all.
For scale: nnInteractive rescales its prompt channels (8 x 1100 x 512 x 512 float16, mostly zero) every hundred or so interactions. Skipping the ZERO chunks at the application level (via iterchunks_info()) took that from 9.2 s to 27 ms for 7 channels. It would be nice if blosc2 did this for everyone.
Since this touches the evaluator you are reworking in #741, I am opening it as an issue first rather than a PR. I am happy to do the work where it fits your plans.
AI disclosure: I used Claude Code (Anthropic) to measure this and to draft this issue, and Claude Code opened it on my behalf. I checked the examples and numbers.
Best,
Fabian
Activity