Skip to content

perf(svar2): consume genoray's sparse range stream in the writer - #416

Merged
d-laub merged 3 commits into
mainfrom
feat/sparse-range-source
Sep 16, 2026
Merged

d-laub merged 3 commits into
mainfrom
feat/sparse-range-source

Conversation

@d-laub

@d-laub d-laub commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #405.

What

_write_from_svar2 built its region-CSR range cache by materializing genoray's dense (samples, ploidy, regions, 2) chunk and then throwing almost all of it away. At the All of Us chr22 grid only 0.45% of (region, sample, ploid) windows hold a variant, the dense intermediate is ~128 GB per contig, and the np.nonzero scan that recovered the useful cells was 71% of the per-chunk write kernel.

genoray now emits exactly those cells (d-laub/genoray#206), so the writer consumes SparseVar2._find_ranges_chunked_sparse. The scan and the intermediate both disappear; nonempty_entries collapses into a CSR expansion plus four column assignments and is deleted.

The output does not change

Byte for byte. The two paths already agreed on every convention:

convention old nonempty_entries genoray sparse
filter predicate end > start per channel, OR'd same
cell_id slot * ploidy + ploid, selection-absolute same
empty channel raw start, len = 0 (not zeroed) same
order region-major, cell_id ascending same

test_write_svar2_chunked_matches_unchunked compares the on-disk table across chunk boundaries and still passes, with its spy now confirming the sparse stream is the one being driven.

Also in here: the fixtures move to genoray's grouped pipeline args

The same version bump carries genoray's #153/#200 refactor, which made the _core pipeline entry points keyword-only with RegionSpec / FieldSpec / PlanSettings. GVL called them positionally at 12 sites across 9 test files, so all 12 are ported.

One guard intentionally removed

nonempty_entries's shape check defended against a mis-transpose. There is no transpose left — the chunk arrives region-major — so that axis order is now unrepresentable rather than validated. Its int32 cell-id overflow guard is already enforced by _SparseWriter's span >= 2**31 check.

Pinned to genoray 5.0.0

The release landed as 5.0.0, not the 4.1.0 this branch first anticipated, so the original >=4.1.0,<5 pin excluded the very release it was waiting for. Now pinned >=5,<6 in pyproject.toml and pixi.toml, relocked, and both Cargo.toml git deps (svar2-codec, genoray_core) moved to the 5.0.0 tag commit 714f876.

5.0.0's breaking changes — the GENORAY_* environment variables, the reader_workers= parameter, and resolve_log_level — all land on APIs genvarloader never calls, so none of them required a change here.

Re-checked the empty-cell contract at the new rev, as the Cargo.toml comment asks: gather_haps_readbound_impl (genoray:src/query/gather.rs:869 at 5.0.0) still consumes every range purely as a slice bound (d_snp_pos[ss..se], snp_positions[vs..ve]), so a len == 0 range yields an empty slice and its start is never dereferenced.

Testing

Against the real genoray 5.0.0 wheel, full suite (after pixi run gen):

1315 passed, 43 skipped, 4 xfailed, 0 failed — pytest tests -p no:randomly --ignore=tests/benchmarks, 3m06s.

cargo check --all-targets is clean at the new rev, and all prek hooks (ruff check/format, pyrefly, pixi lock, commitizen) pass.

Unblocks d-laub/genoray#204 (deleting genoray's dense chunk path).

🤖 Generated with Claude Code

d-laub and others added 3 commits September 16, 2026 05:42
`_write_from_svar2` built its region-CSR cache by materializing genoray's
dense `(samples, ploidy, regions, 2)` chunk and then discarding it: at the
All of Us chr22 grid only 0.45% of `(region, sample, ploid)` windows hold a
variant, the dense intermediate is ~128 GB per contig, and the `np.nonzero`
scan that recovered the useful cells was 71% of the per-chunk kernel.

genoray 4.1.0 emits those cells directly (d-laub/genoray#206), so the scan
and the intermediate both go away. `nonempty_entries` collapses into a CSR
expansion plus four column assignments and is deleted.

The on-disk cache is unchanged, byte for byte. genoray filters on the same
predicate (`end > start` in either channel), emits the same absolute
`slot * ploidy + ploid` cell ids in the same region-major order, and carries
the same raw start with length 0 for a cell non-empty in only one channel.

The nine `_core.run_conversion_pipeline` fixtures move to genoray's
keyword-only, struct-grouped signature (d-laub/genoray#153), which the same
version bump requires.

`nonempty_entries`'s shape check went with it. It guarded a transpose that
no longer exists — the chunk arrives region-major — so the axis order it
defended is now unrepresentable rather than validated. Its int32 cell-id
overflow guard is already enforced by `_SparseWriter`'s `span >= 2**31`
check.

Refs #405.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_write_from_svar2` now calls `SparseVar2._find_ranges_chunked_sparse` and
the test fixtures call the keyword-only `_core.run_conversion_pipeline`.
Neither exists before 4.1.0.

`pixi.lock` is deliberately not regenerated and the `pixi-lock` / `pyrefly`
hooks are skipped for this commit: 4.1.0 is not on PyPI yet, so pixi cannot
resolve this range at all. Run `pixi lock` and re-run the hooks once the
release lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… entry points

genoray 5.0.0 is out, carrying both changes this branch needs: the sparse
range emission from d-laub/genoray#206 and the keyword-only, grouped-argument
pipeline entry points from d-laub/genoray#200. The previous pin
(`>=4.1.0,<5`) anticipated a 4.1.0 that was never cut, so it excludes the
release it was waiting for.

Bump the pin in `pyproject.toml` and `pixi.toml` to `>=5,<6`, relock, and move
both `Cargo.toml` git deps (`svar2-codec`, `genoray_core`) to the 5.0.0 tag
commit 714f876.

5.0.0's breaking changes -- the `GENORAY_*` environment variables, the
`reader_workers=` parameter and `resolve_log_level` -- all land on APIs
genvarloader never calls, so nothing here needed rewriting for them.

What did need rewriting: #200 made the `_core` pipeline entry points
keyword-only and grouped their arguments into `RegionSpec` / `FieldSpec` /
`PlanSettings`. The twelve test call sites that build svar2 fixtures now pass
keywords and those dataclasses instead of a positional argument list.

Re-checked the empty-cell contract at the new rev, as the comment in
`Cargo.toml` asks: `gather_haps_readbound_impl` still consumes every range
purely as a slice bound, so a `len == 0` range's `start` is never
dereferenced.

Full suite against the real 5.0.0 wheel: 1315 passed, 43 skipped, 4 xfailed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@d-laub
d-laub marked this pull request as ready for review September 16, 2026 22:28
@d-laub
d-laub merged commit f317148 into main Sep 16, 2026
8 checks passed
@d-laub
d-laub deleted the feat/sparse-range-source branch September 16, 2026 22:28
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.

genoray: emit sparse ranges from find_ranges_chunk

1 participant