Skip to content

perf(collect): amortize per-pivot tokenize via a rolling forward key cache - #65

Merged
nh13 merged 1 commit into
mainfrom
perf/tokenize-amortize
Jul 6, 2026
Merged

perf(collect): amortize per-pivot tokenize via a rolling forward key cache#65
nh13 merged 1 commit into
mainfrom
perf/tokenize-amortize

Conversation

@nh13

@nh13 nh13 commented Jul 5, 2026

Copy link
Copy Markdown

Third of a three-PR stack. Stacked on #64 (perf/boundary-search) — review #63 then #64 first; this PR's diff is against #64.

What

Amortize the per-pivot tokenize_32mer in SMEM collection. Build the per-read forward 32-mer key cache once (encoding::rolling_forward_keys, one O(read.len()) right-to-left pass) into CollectScratch.fwd_keys, and thread it through the pass-1/pass-2 forward searches so a pivot reads fwd_keys[pivot] instead of re-tokenizing read[pivot..]. Consulted only under a query.len() >= 32 guard: an N clamps the query below 32, so the cache is read only on N-free full 32-mer windows, where the rolling key is byte-identical to a fresh tokenize_32mer.

Adds keyed entry points that accept the precomputed key — mem_search_keyed, mem_search_warmstart_keyed, forward_truncate_below_maximal_keyed (each the existing function taking an Option<u64> key; None re-tokenizes internally, byte-identical). They also reuse the full-32 query key as the maximal match's qm key when match_len >= 32 (qm is a prefix, so the same first 32 bases), dropping a second tokenize per hit.

Wall / instructions

This is a byte-identical work reduction, not a wall-clock speedup: the removed compute is hidden behind the DRAM-miss latency that bounds cold seeding, so the cold wall is neutral. Measured ≈ −4..−8% retired instructions/read on Graviton4 (gate-subtracted, built without instrumentation).

Correctness

Byte-identical. rolling_forward_keys is N-safe (masks each base, never panics) and gated by rolling_forward_keys_* proptests (key == tokenize_32mer at every offset; N-safe and identical on N-free full windows). Per-pivot debug_asserts (cached key == fresh tokenize) run at every guarded call site across the existing collect_smems / zz_* byte-identity corpus.

Summary by CodeRabbit

  • Performance Improvements

    • Reduced repeated re-tokenization during forward portions of sequence matching by reusing a rolling 32-mer key cache.
    • Improved forward search and interval truncation by enabling optional keyed lookup and warm-start paths.
  • Bug Fixes

    • Added safer rolling key construction that avoids crashes on ambiguous/unexpected input bytes.
    • Preserved correct behavior for windows near read ends and for shorter-than-32 query lengths.
  • Tests

    • Expanded coverage for empty, short, and exact-32 reads, plus keyed vs non-keyed consistency and buffer reuse.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75855c7f-5ea7-4b53-8451-e0353d81b849

📥 Commits

Reviewing files that changed from the base of the PR and between 3091fe8 and d8dbd50.

📒 Files selected for processing (3)
  • prmi/src/encoding.rs
  • prmi/src/index/collect.rs
  • prmi/src/index/spectrum.rs

Walkthrough

Adds a rolling forward 32-mer cache in encoding.rs, threads it through collect.rs forward-extension and reseed paths, and introduces keyed mem_search, mem_search_warmstart, and forward_truncate_below_maximal variants in spectrum.rs.

Changes

Keyed forward search via rolling key cache

Layer / File(s) Summary
Rolling 32-mer key cache implementation and tests
prmi/src/encoding.rs
Adds rolling_forward_keys plus _into cache reuse, with unit and property tests covering empty, short, exact-32, and invalid-byte cases.
Keyed mem_search, warmstart, and truncation entrypoints
prmi/src/index/spectrum.rs
Wraps the existing search/truncation entrypoints with keyed variants that accept optional precomputed query keys and re-tokenize only when needed.
CollectScratch fwd_keys cache and per-read computation
prmi/src/index/collect.rs
Adds a per-read fwd_keys buffer, computes it once, and threads it through forward-extension and reseed paths that now call keyed search/truncation APIs.
Test helpers updated for fwd_keys threading
prmi/src/index/collect.rs
Updates pipeline helpers and proptest wiring to compute and pass cached forward keys into the modified call sites.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant collect_smems_unsorted_into
  participant zz_step1
  participant zz_right_emit
  participant mem_search_keyed

  collect_smems_unsorted_into->>collect_smems_unsorted_into: fwd_keys = rolling_forward_keys_into(read)
  collect_smems_unsorted_into->>zz_step1: fwd_keys
  zz_step1->>zz_right_emit: pivot, fwd_keys
  zz_right_emit->>mem_search_keyed: query, optional query_key
  mem_search_keyed-->>zz_right_emit: MemMatch
Loading

Possibly related PRs

  • fg-labs/prmi#17: Both touch prmi/src/index/spectrum.rs’s mem_search pathway and its one-shot maximal-match flow.
  • fg-labs/prmi#40: Both modify forward reseed/search wiring in prmi/src/index/collect.rs and prmi/src/index/spectrum.rs.
  • fg-labs/prmi#48: Both modify forward_truncate_below_maximal in prmi/src/index/spectrum.rs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: caching rolling forward keys to avoid per-pivot tokenization in collect.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/tokenize-amortize

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nh13
nh13 force-pushed the perf/boundary-search branch from 3d8fb77 to 5491c6d Compare July 6, 2026 07:04
@nh13
nh13 force-pushed the perf/tokenize-amortize branch from 1a42aea to 0baae2c Compare July 6, 2026 07:04
Base automatically changed from perf/boundary-search to main July 6, 2026 18:21
@nh13
nh13 force-pushed the perf/tokenize-amortize branch from 0baae2c to 3091fe8 Compare July 6, 2026 18:24
@nh13

nh13 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
prmi/src/index/spectrum.rs (1)

2058-2108: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Byte-identity is correct; guard the caller-key contract in-function.

The qm_key reuse is sound: match_len >= KMER_LENquery.len() >= 32, so query_key == tokenize_32mer(query, 32) == tokenize_32mer(qm, 32). Correct.

The residual footgun: query_key: Option<u64> is used as the actual compare key (not a seed like hint), so a wrong Some(k) from a future caller yields a silently non-byte-identical result in release. The in-tree call sites debug_assert this, but these pub primitives do not. Mirror the assertion inside so misuse is caught regardless of caller:

debug_assert!(
    query_key.is_none()
        || query_key == Some(tokenize_32mer(query, query.len().min(KMER_LEN))),
    "mem_search_keyed: query_key != fresh tokenize"
);

Same applies to mem_search_warmstart_keyed (Line 2215) and forward_truncate_below_maximal_keyed (Line 3171).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prmi/src/index/spectrum.rs` around lines 2058 - 2108, The keyed search
functions trust an optional caller-provided key too much, which can silently
produce incorrect non-byte-identical results if a future caller passes the wrong
value. Add an internal debug_assert in mem_search_keyed to verify query_key
matches a fresh tokenize_32mer over query, and mirror the same caller-key
contract check in mem_search_warmstart_keyed and
forward_truncate_below_maximal_keyed so misuse is caught even when in-tree
call-site assertions are absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@prmi/src/encoding.rs`:
- Around line 94-128: The hot path in `CollectScratch::fwd_keys` is still
allocating because it replaces the buffer with `rolling_forward_keys(read)`
instead of reusing the existing `Vec<u64>`. Add or switch to an in-place fill
API (for example an `*_into` variant in `crate::encoding`) and have
`CollectScratch::fwd_keys` write directly into the existing scratch buffer
rather than assigning a new vector. Use the `rolling_forward_keys` logic as the
source of truth, but preserve and refill the existing `fwd_keys` storage.

In `@prmi/src/index/collect.rs`:
- Around line 587-597: The `collect.rs` read-processing path is still
reallocating `fwd_keys` for every read by replacing the scratch buffer with a
new `Vec<u64>`. Update the `rolling_forward_keys` flow so `collect` reuses the
existing `fwd_keys` storage in place, ideally by adding a
`rolling_forward_keys_into` helper and calling it from the loop where `scratch`,
`fill_next_n`, and `fwd_keys` are handled. Make sure the buffer is cleared or
resized without allocation so the per-read path stays allocation-free once
warmed up.

---

Outside diff comments:
In `@prmi/src/index/spectrum.rs`:
- Around line 2058-2108: The keyed search functions trust an optional
caller-provided key too much, which can silently produce incorrect
non-byte-identical results if a future caller passes the wrong value. Add an
internal debug_assert in mem_search_keyed to verify query_key matches a fresh
tokenize_32mer over query, and mirror the same caller-key contract check in
mem_search_warmstart_keyed and forward_truncate_below_maximal_keyed so misuse is
caught even when in-tree call-site assertions are absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab319c7b-a2b7-4d81-9fb6-18e08492229c

📥 Commits

Reviewing files that changed from the base of the PR and between 9a93116 and 3091fe8.

📒 Files selected for processing (3)
  • prmi/src/encoding.rs
  • prmi/src/index/collect.rs
  • prmi/src/index/spectrum.rs

Comment thread prmi/src/encoding.rs
Comment thread prmi/src/index/collect.rs
…cache

Build the per-read forward 32-mer key cache once (encoding::rolling_forward_keys,
one O(read.len()) right-to-left pass) into CollectScratch.fwd_keys, and thread it
through the pass-1/pass-2 forward searches so a pivot reads fwd_keys[pivot] instead
of re-tokenizing read[pivot..]. Consulted only under a query.len() >= 32 guard: an
N clamps the query below 32, so the cache is read only on N-free full 32-mer
windows, where the rolling key is byte-identical to a fresh tokenize_32mer.

Adds keyed entry points that accept the precomputed key -- mem_search_keyed,
mem_search_warmstart_keyed, forward_truncate_below_maximal_keyed (each the existing
function taking an Option<u64> key; None re-tokenizes internally, byte-identical).
They also reuse the full-32 query key as the maximal match's qm key when
match_len >= 32 (qm is a prefix, same first 32 bases), dropping a second tokenize
per hit.

This is a byte-identical work reduction, NOT a wall-clock speedup: the removed
compute is hidden behind the DRAM-miss latency that bounds cold seeding, so the
cold wall is neutral (measured ~-4..-8% retired instructions/read on Graviton4,
gate-subtracted). Gated by the rolling_forward_keys_* proptests (key == tokenize
at every offset; N-safe and identical on N-free windows) plus per-pivot
debug_asserts (cache key == fresh tokenize) that run across the existing
collect_smems / zz_* byte-identity corpus.
@nh13
nh13 force-pushed the perf/tokenize-amortize branch from 3091fe8 to d8dbd50 Compare July 6, 2026 20:16
@nh13

nh13 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Jul 6, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13
nh13 merged commit c87da8d into main Jul 6, 2026
4 checks passed
@nh13
nh13 deleted the perf/tokenize-amortize branch July 6, 2026 23: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.

1 participant