fix: keep FusedCodecPipeline compute off the event-loop thread - #247
Open
d-v-b wants to merge 4 commits into
Open
fix: keep FusedCodecPipeline compute off the event-loop thread#247d-v-b wants to merge 4 commits into
d-v-b wants to merge 4 commits into
Conversation
) Bumps the actions group with 3 updates: [actions/labeler](https://github.com/actions/labeler), [actions/attest](https://github.com/actions/attest) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action). Updates `actions/labeler` from 6.2.0 to 7.0.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](actions/labeler@b8dd2d9...bf12e9b) Updates `actions/attest` from 4.1.1 to 4.2.0 - [Release notes](https://github.com/actions/attest/releases) - [Changelog](https://github.com/actions/attest/blob/main/RELEASE.md) - [Commits](actions/attest@a1948c3...f7c74d2) Updates `zizmorcore/zizmor-action` from 0.5.7 to 0.6.0 - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](zizmorcore/zizmor-action@192e21d...6599ee8) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/attest dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#4187) Allows constructing a ZipStore from any seekable binary reader, enabling zip archives on remote storage: - io objects (BytesIO, fsspec file objects) are used directly - minimal readers that are not io.IOBase instances and whose read() may return buffer-protocol objects rather than bytes (e.g. obstore.ReadableFile) are adapted via a small io.RawIOBase wrapper when opened for reading clear()/move() raise NotImplementedError for file-object-backed stores. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…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
Open
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
fix: keep FusedCodecPipeline compute off the event-loop thread
Problem
Users reported that the opt-in
FusedCodecPipelineis slower thanBatchedCodecPipelinefor zstd-compressed data. Investigation showed thefused pipeline is actually ~2x faster in every single-threaded scenario; the
regression only appears under concurrent access from multiple user threads
(the dask/xarray pattern: many threads, each reading roughly one chunk per
call).
Root cause:
FusedCodecPipeline.read/writeran their synchronous fast path(
read_sync/write_sync) inline on the coroutine servicing the request —i.e. on the global
zarr_ioevent-loop thread. Single-chunk batchesdecoded inline on the loop; multi-chunk batches blocked the loop in
pool.mapuntil the whole batch finished. Since every sync-API call fromevery user thread is serviced by that one loop, concurrent operations
serialized behind each other's codec compute. The blocked-loop window is the
codec compute time, so zstd-compressed data (real CPU work per chunk) showed
the regression prominently while uncompressed data barely moved.
Evidence (8192x8192 f32, 64 x 4 MB chunks, zstd level 3, LocalStore; each
user thread reads one chunk per call):
Batched scales with threads; fused was flat-to-degrading — the signature of
everything funneling through one thread. Tracing confirmed decode executed on
the thread named
zarr_iofor single-chunk reads.Fix
Run the synchronous batch on a worker thread via
asyncio.to_thread— onehop per batch, not per chunk, so the fused design's win over per-chunk
async scheduling (the reason this pipeline exists) is preserved, while the
event loop stays free to service concurrent callers.
Benchmarks (after)
Same workload as above, interleaved in-process A/B (medians of 7 rounds;
"inline" = old behavior, "to_thread" = this PR):
Thread-scaling after the fix (same benchmark as the table above):
Fused now scales with reader threads and is at least as fast as batched at
every point. Single-threaded workloads are unchanged: the isolated cost of the
offload hop is ~75 µs per batch (measured with a noop through
sync(asyncio.to_thread(...))), which is noise next to per-chunk decode.(An earlier A/B that appeared to show a ~2x single-thread regression for
sequential single-chunk reads turned out to be a thermal/ordering artifact:
reversing the variant order moved the inflated median to the other variant;
per-call interleaved p50s are 4.78 ms vs 4.89 ms.)
Single-threaded sweep (unchanged, fused/batched time ratios after this PR,
LocalStore): zstd write 0.62, zstd read 0.61, raw write 0.69, raw read 0.85.
Changes
FusedCodecPipeline.read/write: offloadread_sync/write_syncto aworker thread via
asyncio.to_thread, with a comment explaining why inlineexecution is forbidden.
test_sync_api_compute_off_event_loop: deterministic(no timing) — asserts codec compute never runs on a thread with a running
event loop, for single- and multi-chunk reads and writes through the sync
API.
test_read_array_concurrentintests/benchmarks/test_e2e.pycovering the dask-style many-threads/one-chunk-per-call access pattern for
both pipelines, zstd and uncompressed.
changes/247.bugfix.md.🤖 Generated with Claude Code