镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Lazy expressions and reductions could skip special (zero / repeated-value) chunks #761

Open

Description

@FabianIsensee

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 (

# 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)
), but the float16 numbers suggest it is not reached there. I have not traced which path float16 takes, and the typed evaluator in #741 may change it.

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 * inf is NaN, z + 1 gives a chunk of ones, z > 0.5 an 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions