Skip to content

fix: keep FusedCodecPipeline compute off the event-loop thread - #4194

Open
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:claude/fusedcodecpipeline-zstd-perf-0bb07d
Open

fix: keep FusedCodecPipeline compute off the event-loop thread#4194
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:claude/fusedcodecpipeline-zstd-perf-0bb07d

Conversation

@d-v-b

@d-v-b d-v-b commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

FusedCodecPipeline.read and FusedCodecPipeline.write run on the event loop thread, which means CPU-bound code it spins up can choke out concurrent awaitables queued in the event loop. This PR instead makes read and write send work to a thread via asyncio.to_thread, which offers a performance win for the FusedCodecPipeline.

Although the perf we get here is good, the pattern is problematic: the codec pipeline is taking charge of how its resource-intensive routines are being scheduled in a manner that is opaque to a caller. There are a few better ways of solving the fundamental problem here, but they require classes that look different.

For reviewers

Please check to ensure that this change isn't problematic from a performance POV.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

TODO

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions
  • New/modified features documented in docs/user-guide/*.md
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

based on work by claude: see d-v-b#247

d-v-b added 5 commits July 14, 2026 13:25
* fix: byte-order handling for structured dtypes in the bytes codec

The bytes codec neither byte-swapped structured-dtype fields to its
configured endian on encode (numpy reports byteorder '|' for void
dtypes, so the top-level byteorder comparison never detected a
mismatch) nor honored its endian when decoding, silently corrupting
any structured data whose field byte order differed from the stored
one (e.g. virtual references to external big-endian data).

Encode now detects byte-order mismatches by comparing full dtypes via
newbyteorder, and decode reinterprets raw bytes in the stored byte
order before converting to the data type's declared byte order, so the
stored layout (codec state) and the in-memory layout (array data type)
are independent.

Closes zarr-developers#4141

Assisted-by: ClaudeCode:claude-fable-5

* test: fold structured byte-order cases into existing bytes codec tests

Extend test_endian's parametrization with structured dtypes and
test_bytes_codec_sync_roundtrip with endian/dtype parametrization plus
stored-layout and decoded-dtype assertions, instead of adding parallel
test functions for the same properties.

Assisted-by: ClaudeCode:claude-fable-5

* refactor: rename stored_dtype to view_dtype in BytesCodec decode

The variable is the dtype used to view the raw chunk bytes (byte order
from the codec's endian configuration), not a property of the stored
data or of the returned buffer, which always carries the array's
declared dtype.

Assisted-by: ClaudeCode:claude-fable-5

* docs: note that the decode-side byte-order conversion copies the chunk

Assisted-by: ClaudeCode:claude-fable-5
…op thread

FusedCodecPipeline.read/write ran their synchronous fast path inline on
the coroutine servicing the request — i.e. on the global zarr_io event
loop thread. Single-chunk batches decoded inline on the loop and
multi-chunk batches blocked the loop in pool.map, so every sync-API call
from every user thread serialized behind each other's codec compute.
The blocked window scales with codec cost, which is why users reported
the fused pipeline as "slower for zstd-compressed data" under
multi-threaded (dask-style, one chunk per call) access: at 8 reader
threads on 4 MiB zstd chunks it was 3.4x slower than
BatchedCodecPipeline, and throughput did not scale with threads at all
(336 -> 439 ms from 1 to 8 threads, versus 669 -> 121 ms for batched).

Offload the synchronous batch to a worker thread with asyncio.to_thread:
one hop per batch, not per chunk, preserving the fused pipeline's win
over per-chunk async scheduling while keeping the loop free. After the
fix the same workload scales 625 -> 109 ms from 1 to 8 threads, beating
batched at every thread count; single-threaded performance is unchanged
(the hop costs ~75 us per batch).

The regression test asserts deterministically (no timing) that codec
compute never runs on a thread with a running event loop, covering
single- and multi-chunk reads and writes through the sync API. A new
benchmark covers the many-threads/one-chunk-per-call access pattern.

Assisted-by: ClaudeCode:claude-fable-5
@d-v-b
d-v-b requested a review from ilan-gold July 28, 2026 14:17
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.87%. Comparing base (0b72757) to head (2d3a79e).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4194   +/-   ##
=======================================
  Coverage   93.87%   93.87%           
=======================================
  Files          91       91           
  Lines       12594    12594           
=======================================
  Hits        11822    11822           
  Misses        772      772           
Files with missing lines Coverage Δ
src/zarr/core/codec_pipeline.py 96.22% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@d-v-b
d-v-b marked this pull request as ready for review July 28, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant