Skip to content

perf(rag): O(k) integer-array fancy-index gather in _gather_indices (closes #69) - #70

Merged
d-laub merged 8 commits into
mainfrom
worktree-issue-69-ragged-gather-perf
Jul 22, 2026
Merged

perf(rag): O(k) integer-array fancy-index gather in _gather_indices (closes #69)#70
d-laub merged 8 commits into
mainfrom
worktree-issue-69-ragged-gather-perf

Conversation

@d-laub

@d-laub d-laub commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Eliminates the O(n) np.arange(n)[where] allocation in Ragged._gather_indices, so an integer-array fancy-index gather of k rows from an n-row ragged buffer now runs in O(k), independent of n. Closes #69.

The change is a behavior-preserving performance refactor confined to the integer-array else branch of _gather_indices. The slice fast-path, bool-mask branch, Rust _ragged_select kernel, and numpy ImportError fallback are untouched. No public signature change, no maturin rebuild, no SKILL.md change.

Approach

The old branch resolved indices via np.arange(n)[where], which allocated a full-length array over all n rows just to select k of them (and implicitly did negative-normalization + bounds-checking as a side effect). The new branch does direct O(k) vectorized NumPy over only the k selected indices: np.asarray(where) → integer-dtype guard → normalize negatives → explicit bounds-check raising IndexError (the numpy indexing contract).

Results

benchmarks/bench_ragged_gather.py (transitional harness + correctness oracle):

  • Index resolution, k=256: reference grows ∝ n (12.8 µs → 22,712 µs from n=4K to n=4.2M); candidate stays flat (~12 µs) → ~1935× faster at n=4.2M.
  • End-to-end rag[idx]: now flat in n (53 µs @ n=4K vs 57 µs @ n=4.2M), where it previously rose with n. This flat curve is the red→green signal.
  • Oracle: reference_resolve (old arange logic) == candidate_resolve (new O(k) logic) on all representative + edge cases; both raise IndexError on OOB±/float. candidate_resolve's body is the verbatim source of the shipped branch.

Tests

tests/test_ragged_core.py — permanent regression coverage for the integer-array gather path (previously untested for OOB / float): int-array selection, negative-normalization parity, OOB positive/negative, empty int64 array, empty Python list, list-of-one parity, float→IndexError, bool-list rejection. 84 passed.

⚠️ One behavior decision for review

rag[[True, False, True]] (a plain Python list of bools) now raises IndexError instead of being treated as a boolean mask.

Before this branch, a bool-list incidentally worked as a mask (a side effect of delegating to np.arange(n)[where]). It now falls into the integer-index path and is rejected. I kept the rejection deliberately, because _where_is_bool already defines seqpro's mask contract as "a mask is an np.ndarray of bools" — bool-lists working was never designed, and rejecting them (a) is consistent with that contract, (b) fails loudly rather than silently, and (c) keeps this perf fix confined to the integer branch instead of pulling in numpy's bool-indexing quirks. Pinned by test_getitem_bool_list_raises. The supported ndarray-mask path (rag[np.array([...])]) is unaffected.

If you'd rather restore full numpy parity (route bool-lists through the mask path), say so and I'll adjust — it's a small follow-up.

Empty-list indexing (rag[[]] → empty selection; rag[np.array([])] float ndarray → IndexError) is preserved exactly.

🤖 Generated with Claude Code

@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 12 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_bench_baseline_ragged_cres 6.3 ms 7 ms -10.38%
test_bench_baseline_ragged_short_alleles 3.7 ms 1.9 ms +95.34%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing worktree-issue-69-ragged-gather-perf (5185ba9) with main (decb8f5)

Open in CodSpeed

Pre-existing lint (newer clippy) flagged the ACGT byte array in
parallel_lut_matches_serial; use a byte-string literal. Unblocks the
prek CI check. No behavior change.

Verified: cargo fmt --all --check and cargo clippy --all-targets -- -D
warnings both clean in the dev env (the exact hook commands); local
pre-commit cargo hooks skipped due to a stale PYO3_PYTHON in the bare
shell, not a code issue.
@d-laub
d-laub marked this pull request as ready for review July 22, 2026 05:02
@d-laub
d-laub merged commit da23da7 into main Jul 22, 2026
8 of 9 checks passed
@d-laub
d-laub deleted the worktree-issue-69-ragged-gather-perf branch July 22, 2026 05:02
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.

Ragged fancy-indexing: _gather_indices allocates a full np.arange(n) per gather (O(n) instead of O(k))

1 participant