Add indexed queries - #161
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #161 +/- ##
==========================================
+ Coverage 59.75% 60.40% +0.64%
==========================================
Files 75 76 +1
Lines 11494 11963 +469
Branches 1987 2085 +98
==========================================
+ Hits 6868 7226 +358
- Misses 3812 3897 +85
- Partials 814 840 +26
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
nclack
marked this pull request as ready for review
September 14, 2026 22:21
nclack
force-pushed
the
indexed-queries
branch
from
September 24, 2026 22:38
029677b to
61ce41c
Compare
nclack
changed the base branch from
cpu-pipeline
to
fix/cpu-read-buffer-capacity
September 24, 2026 22:38
This was referenced Sep 24, 2026
nclack
added this pull request to stack #170
September 24, 2026 23:07
nclack
force-pushed
the
indexed-queries
branch
from
September 24, 2026 23:13
61ce41c to
f86232c
Compare
added 9 commits
September 24, 2026 16:34
nclack
added a commit
that referenced
this pull request
Sep 24, 2026
CPU input buffers previously held one chunk per decode worker, limiting simultaneous file reads when each merged request contains several chunks. A new CPU executor setting, `chunks_per_input_buffer` (`CpuLimits.chunks_per_input_buffer` in Python, default 256), sets how many chunks each of the two input buffers holds. It must be at least `decode_workers`, so every worker can get a chunk, and at most 16384, the most chunks a batch can have. Values outside that range are rejected, with a message, when the executor is created. The worker pool and per-worker decoder workspaces stay the same. This restores simultaneous reads. Each merged read still holds at most `min(decode_workers, chunks_per_input_buffer)` chunks, the rule used in the measurements below, so a buffer holds several reads at once. With 32 workers, reads are at most 32 chunks, so a full 256-chunk buffer splits into at least 8 reads. With one decode worker, reads do not merge. A separate read-size setting is left for a later PR. This breaks existing budgets. The input reserve is `2 * chunks_per_input_buffer * max_encoded_chunk_bytes`, plus per-chunk bookkeeping. With the defaults, 256 chunks and a 4 MiB encoded bound, that is 2 GiB before decoder workspaces and output buffers, so a 1 GiB `CpuLimits` budget that worked before now fails with `BUDGET` when the pipeline starts. Set `max_encoded_chunk_bytes` to the largest encoded chunk expected, lower `chunks_per_input_buffer`, or raise `max_memory_bytes`. When the budget is too small, starting now logs the bytes it tried to reserve for input buffers, decoder workspaces, output buffers, and the rest; a read plan that cannot fit logs its size too. Documentation shows the cost and raises the example budget to 3 GiB. The native pipeline fixture now uses a 1 KiB encoded bound appropriate for its small test chunks, retaining its 32 MiB budget. The `throughput-cpu` bench scenario no longer fit its 4 GiB budget with the 4 MiB bound; it now sets `chunks_per_input_buffer` to 256 and a 5 GiB budget. The bench reads the setting from a scenario's `pipeline.chunks_per_input_buffer`, default 256. The motivating Turin experiment used 32 decode workers, 64 KiB chunks, 16 KiB Blosc blocks, and sixteen raw 1 GiB shards. In an isolated benchmark patch with persistent shard handles and default readahead, moving from 32 to 256 chunks improved cold translated 256×256 crop output by 47–55% and full-scan output by 71–97% across Blosc-LZ4 and Blosc-Zstd, adding 28.04 MiB of accounted memory at those 64 KiB chunk bounds, not the 2 GiB reserved with the default 4 MiB bound. These are benchmark-patch measurements; this PR does not include the persistent-handle or LZ4 additions and its final binary has not been benchmarked. Regression coverage checks 513 chunks through three input groups of exactly 256, 256, and 1 chunks at 1, 2, and 32 workers; merged reads of at most `decode_workers` chunks, with exact read ranges, shard order, and reads per buffer at 1, 2, and 32 workers; complete output; exact memory-budget admission; oversized-allocation rejection; and rejection of `chunks_per_input_buffer` below `decode_workers` or above 16384, in C and in Python. Private read-plan tests check complete coverage at 256, 257, and 1024 workers without starting hundreds of threads. Formatting and whitespace checks pass, and the strict documentation build passes locally. At `18cee45`, the [CPU workflow](https://github.com/nclack/damacy/actions/runs/36065751602) built without CUDA and passed 24/24 CTest entries on Ubuntu and 25/25 on macOS (the native test executables plus the Python pytest suite), followed by the native dependency audit; the macOS job passed on a rerun after the known `test_scheduler` timing failure. The [GPU-enabled regression workflow](https://github.com/nclack/damacy/actions/runs/36065753652) passed 36/36 CTest entries, the [TSan-labeled check](https://github.com/nclack/damacy/actions/runs/36065755729) passed 1/1, and the [build](https://github.com/nclack/damacy/actions/runs/36065749180) passed. Stacked on #164; the base is `fix/cpu-read-scheduling`. #161 stacks on this branch. --------- Co-authored-by: Nathan Clack <nclack@biohub.org>
nclack
force-pushed
the
indexed-queries
branch
from
September 24, 2026 23:34
f86232c to
b4bab59
Compare
nclack
added a commit
that referenced
this pull request
Sep 24, 2026
Spatial queries can now resolve a fixed output grid against OME-Zarr 0.5 metadata before chunk planning. Resolution chooses a source level using the full affine sampling scale, including rotation and anisotropy. Aligned crops run on either CPU or CUDA; queries that still require resampling fail explicitly when submitted. This PR does not implement a resampler. Load an immutable `NgffImage` through an injected metadata reader, then call `image.resolve(query, shape=output.shape)`. Resolution performs no I/O and depends only on the requested sample shape. Batching, output allocation, and dtype conversion remain downstream. Each query carries its full-rank transform, sampler, boundary behavior, and automatic or explicit level choice. Results own their source URI and geometry independently of the image. Python returns one immutable, serializable value that can be pushed directly. C fills a caller-owned `damacy_spatial_resolution` with directly readable fields and a `clear()` operation; the existing sample conversion is the compatibility bridge to C submission. Public coordinates use reference-level voxel corners, with identity matching integer crops. NGFF center coordinates are converted at metadata loading. Loading also removes floating-point rounding error from each level's scale ratio and origin, so crops built from the reported level values are exact; the resolver itself never rounds a query into a crop. Validation checks consumed fields needed for array layout and coordinate interpretation, including ambiguous duplicate fields and finite transforms. Unused metadata, shared transforms that cancel in reference coordinates, and trailing content are left uninterpreted. Loading caps each `zarr.json` read at the part of `NgffLimits.max_metadata_bytes` still unused; a larger file fails with `DAMACY_BUDGET`. Whole-file metadata reads, capped or not, also fail that way above `UINT32_MAX` bytes. On macOS the metadata reader is the POSIX worker pool from #167, which had no capped read; this PR adds one there with the same behavior as Linux. See the [spatial query API and contracts](https://github.com/nclack/damacy/blob/ngff-resolution/docs/spatial.md) for C/Python examples, coordinate conversion, level selection, ownership, and the next executor operation. Stacked on #161 (`indexed-queries`). Validation: - CI at `382d921`: the [CPU workflow](https://github.com/nclack/damacy/actions/runs/36066539561) passes 26/26 CTest entries on Ubuntu and 27/27 on [macOS](https://github.com/nclack/damacy/actions/runs/36066539561/job/107857506786), where the metadata budget tests use the new capped read. [CUDA](https://github.com/nclack/damacy/actions/runs/36066541831) passes 38/38, including the native and Python spatial tests. [TSan](https://github.com/nclack/damacy/actions/runs/36066544271) passes 2/2, the prefetcher and spatial tests, and the [build](https://github.com/nclack/damacy/actions/runs/36066537440) passes. - Before the review fixes, CPU: 25/25 CTest targets passed; spatial Python tests: 62 passed, 7 CUDA cases skipped. - Before the review fixes, L40: native spatial tests and all 69 Python spatial tests passed; all 7 CUDA memory-check cases passed with zero sanitizer errors. - Before the review fixes, ASan/UBSan: JSON and spatial tests passed, including C result ownership and cleanup. - Ruff, Pyright for the package and spatial tests, strict documentation build, and C example syntax passed. - Regression coverage includes NGFF center/corner conversion, anisotropic level selection, interpolation bounds, permissive handling of unused metadata, invalid consumed fields, result serialization and ownership, fixed output shapes, 128 collapsed transforms, large differences in sampling scale, floating-point rounding in level scales and translations, unrelated keys that do not decode, and image comparison and copies. --------- Co-authored-by: Nathan Clack <nclack@biohub.org>
github-actions Bot
added a commit
that referenced
this pull request
Sep 24, 2026
CPU input buffers previously held one chunk per decode worker, limiting simultaneous file reads when each merged request contains several chunks. A new CPU executor setting, `chunks_per_input_buffer` (`CpuLimits.chunks_per_input_buffer` in Python, default 256), sets how many chunks each of the two input buffers holds. It must be at least `decode_workers`, so every worker can get a chunk, and at most 16384, the most chunks a batch can have. Values outside that range are rejected, with a message, when the executor is created. The worker pool and per-worker decoder workspaces stay the same. This restores simultaneous reads. Each merged read still holds at most `min(decode_workers, chunks_per_input_buffer)` chunks, the rule used in the measurements below, so a buffer holds several reads at once. With 32 workers, reads are at most 32 chunks, so a full 256-chunk buffer splits into at least 8 reads. With one decode worker, reads do not merge. A separate read-size setting is left for a later PR. This breaks existing budgets. The input reserve is `2 * chunks_per_input_buffer * max_encoded_chunk_bytes`, plus per-chunk bookkeeping. With the defaults, 256 chunks and a 4 MiB encoded bound, that is 2 GiB before decoder workspaces and output buffers, so a 1 GiB `CpuLimits` budget that worked before now fails with `BUDGET` when the pipeline starts. Set `max_encoded_chunk_bytes` to the largest encoded chunk expected, lower `chunks_per_input_buffer`, or raise `max_memory_bytes`. When the budget is too small, starting now logs the bytes it tried to reserve for input buffers, decoder workspaces, output buffers, and the rest; a read plan that cannot fit logs its size too. Documentation shows the cost and raises the example budget to 3 GiB. The native pipeline fixture now uses a 1 KiB encoded bound appropriate for its small test chunks, retaining its 32 MiB budget. The `throughput-cpu` bench scenario no longer fit its 4 GiB budget with the 4 MiB bound; it now sets `chunks_per_input_buffer` to 256 and a 5 GiB budget. The bench reads the setting from a scenario's `pipeline.chunks_per_input_buffer`, default 256. The motivating Turin experiment used 32 decode workers, 64 KiB chunks, 16 KiB Blosc blocks, and sixteen raw 1 GiB shards. In an isolated benchmark patch with persistent shard handles and default readahead, moving from 32 to 256 chunks improved cold translated 256×256 crop output by 47–55% and full-scan output by 71–97% across Blosc-LZ4 and Blosc-Zstd, adding 28.04 MiB of accounted memory at those 64 KiB chunk bounds, not the 2 GiB reserved with the default 4 MiB bound. These are benchmark-patch measurements; this PR does not include the persistent-handle or LZ4 additions and its final binary has not been benchmarked. Regression coverage checks 513 chunks through three input groups of exactly 256, 256, and 1 chunks at 1, 2, and 32 workers; merged reads of at most `decode_workers` chunks, with exact read ranges, shard order, and reads per buffer at 1, 2, and 32 workers; complete output; exact memory-budget admission; oversized-allocation rejection; and rejection of `chunks_per_input_buffer` below `decode_workers` or above 16384, in C and in Python. Private read-plan tests check complete coverage at 256, 257, and 1024 workers without starting hundreds of threads. Formatting and whitespace checks pass, and the strict documentation build passes locally. At `18cee45`, the [CPU workflow](https://github.com/nclack/damacy/actions/runs/36065751602) built without CUDA and passed 24/24 CTest entries on Ubuntu and 25/25 on macOS (the native test executables plus the Python pytest suite), followed by the native dependency audit; the macOS job passed on a rerun after the known `test_scheduler` timing failure. The [GPU-enabled regression workflow](https://github.com/nclack/damacy/actions/runs/36065753652) passed 36/36 CTest entries, the [TSan-labeled check](https://github.com/nclack/damacy/actions/runs/36065755729) passed 1/1, and the [build](https://github.com/nclack/damacy/actions/runs/36065749180) passed. Stacked on #164; the base is `fix/cpu-read-scheduling`. #161 stacks on this branch. --------- Co-authored-by: Nathan Clack <nclack@biohub.org> 7ba9200
github-actions Bot
added a commit
that referenced
this pull request
Sep 24, 2026
Spatial queries can now resolve a fixed output grid against OME-Zarr 0.5 metadata before chunk planning. Resolution chooses a source level using the full affine sampling scale, including rotation and anisotropy. Aligned crops run on either CPU or CUDA; queries that still require resampling fail explicitly when submitted. This PR does not implement a resampler. Load an immutable `NgffImage` through an injected metadata reader, then call `image.resolve(query, shape=output.shape)`. Resolution performs no I/O and depends only on the requested sample shape. Batching, output allocation, and dtype conversion remain downstream. Each query carries its full-rank transform, sampler, boundary behavior, and automatic or explicit level choice. Results own their source URI and geometry independently of the image. Python returns one immutable, serializable value that can be pushed directly. C fills a caller-owned `damacy_spatial_resolution` with directly readable fields and a `clear()` operation; the existing sample conversion is the compatibility bridge to C submission. Public coordinates use reference-level voxel corners, with identity matching integer crops. NGFF center coordinates are converted at metadata loading. Loading also removes floating-point rounding error from each level's scale ratio and origin, so crops built from the reported level values are exact; the resolver itself never rounds a query into a crop. Validation checks consumed fields needed for array layout and coordinate interpretation, including ambiguous duplicate fields and finite transforms. Unused metadata, shared transforms that cancel in reference coordinates, and trailing content are left uninterpreted. Loading caps each `zarr.json` read at the part of `NgffLimits.max_metadata_bytes` still unused; a larger file fails with `DAMACY_BUDGET`. Whole-file metadata reads, capped or not, also fail that way above `UINT32_MAX` bytes. On macOS the metadata reader is the POSIX worker pool from #167, which had no capped read; this PR adds one there with the same behavior as Linux. See the [spatial query API and contracts](https://github.com/nclack/damacy/blob/ngff-resolution/docs/spatial.md) for C/Python examples, coordinate conversion, level selection, ownership, and the next executor operation. Stacked on #161 (`indexed-queries`). Validation: - CI at `382d921`: the [CPU workflow](https://github.com/nclack/damacy/actions/runs/36066539561) passes 26/26 CTest entries on Ubuntu and 27/27 on [macOS](https://github.com/nclack/damacy/actions/runs/36066539561/job/107857506786), where the metadata budget tests use the new capped read. [CUDA](https://github.com/nclack/damacy/actions/runs/36066541831) passes 38/38, including the native and Python spatial tests. [TSan](https://github.com/nclack/damacy/actions/runs/36066544271) passes 2/2, the prefetcher and spatial tests, and the [build](https://github.com/nclack/damacy/actions/runs/36066537440) passes. - Before the review fixes, CPU: 25/25 CTest targets passed; spatial Python tests: 62 passed, 7 CUDA cases skipped. - Before the review fixes, L40: native spatial tests and all 69 Python spatial tests passed; all 7 CUDA memory-check cases passed with zero sanitizer errors. - Before the review fixes, ASan/UBSan: JSON and spatial tests passed, including C result ownership and cleanup. - Ruff, Pyright for the package and spatial tests, strict documentation build, and C example syntax passed. - Regression coverage includes NGFF center/corner conversion, anisotropic level selection, interpolation bounds, permissive handling of unused metadata, invalid consumed fields, result serialization and ownership, fixed output shapes, 128 collapsed transforms, large differences in sampling scale, floating-point rounding in level scales and translations, unrelated keys that do not decode, and image comparison and copies. --------- Co-authored-by: Nathan Clack <nclack@biohub.org> 6e87e2b
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.
Add
IndexQueryto CPU and CUDA pipelines, combining ordered index arrays with contiguous slices. Axes form a Cartesian product, preserving order, duplicates, and singleton dimensions. Rectangular and indexed requests can share a batch with the same output shape.Stacked on #168; the base branch is
fix/cpu-read-buffer-capacity. On this base the CPU executor keeps the merged reads from #164 and the input buffers from #168; index gathering happens during assembly, after decoding, so read planning is unchanged. #162 stacks on this branch.Preparation copies index vectors, enumerates only touched shards and chunks, and publishes owned gather plans. Index storage grows with vector lengths, without a planning record for every output voxel. CPU assembly and CUDA dispatch consume those same plans. CUDA keeps each chunk's selected range in a separate per-batch array that only indexed samples use, so rectangular chunk records keep their size.
PlanLimits.max_plan_bytesincludes owned index data.CudaLimits.max_index_bytessets device index storage within the total GPU budget. It must be zero or hold every index a batch can contain, 8 bytes × samples × the sum of the output shape, so an accepted query never runs out of room. A smaller nonzero value raisesBudgetExceededwhen the pipeline is created. With zero,pushraisesBudgetExceededfor anIndexQuery; the query is not consumed and the pipeline keeps running. Out-of-bounds coordinates are found when metadata arrives and raiseInvalidArgumentfrompop, which stops the pipeline.The C API uses
damacy_sample.rankand taggedaxes. Each active axis explicitly selectsDAMACY_AXIS_INTERVALwith.intervalorDAMACY_AXIS_INDICESwith.indices. Zero and unknown tags, invalid intervals, and null or empty index arrays fail validation. Preparation derives the bounding AABB.damacy_tuninggainsmax_index_bytes;damacy_tuning_defaultssets it to 64 MiB, and a zeroed tuning rejects indexed samples. C callers must rebuild and migrate their query initializers and tuning setup. Negative indices are rejected at push and out-of-bounds coordinates at pop; interpolation and NGFF level selection remain separate future work.CUDA decode-gap timing skips the first wave until a previous decode event has been recorded, avoiding an invalid event-handle call.
Validation:
61ce41c: the CPU workflow passes 25/25 CTest entries on Ubuntu and 26/26 on macOS; the macOS job passed on a rerun after the knowntest_schedulertiming failure. CUDA passes 37/37 with the coverage upload, TSan passes, and the build passes.closes #138