perf: speed up dataset-suggestion minimizer search#1773
Draft
ivan-aksamentov wants to merge 6 commits into
Draft
perf: speed up dataset-suggestion minimizer search#1773ivan-aksamentov wants to merge 6 commits into
ivan-aksamentov wants to merge 6 commits into
Conversation
The per-hit loop scanned all references and tested membership via a linear Vec::contains, costing O(n_refs * mz.len()) per hit. Each map entry already lists exactly the references carrying that minimizer, so iterate them directly for O(mz.len()). Output is unchanged; isolates a ~75% reduction in hit-count cost on a mixed 235-sequence query set.
Itertools::unique allocates a SipHash-keyed HashSet to dedup surviving minimizers. A wider minimizerIndex.cutoff admits proportionally more hashes, and this dedup dominated the resulting slowdown. Sort + dedup on u64 keys removes the allocation and hashing overhead; the consumer needs only the distinct set, so iteration order is irrelevant and output is unchanged.
Pin the sorted-unique postcondition of get_ref_search_minimizers and verify that a minimizer shared by several references increments all of them exactly once per distinct query minimizer, and only them.
Self-contained criterion benchmark for run_minimizer_search. Splits per-query cost into the hashing/dedup stage and the map-lookup/hit-count stage, and varies only the global params.cutoff (1<<28 vs 1<<31) over an index built at 1<<28, modelling one dataset raising the global cutoff. A BTreeMap-vs-HashMap lookup comparison isolates the cost of the extra missing c31 query k-mers.
Records the profiling result (wider-cutoff slowdown is dominated by the SipHash dedup, not by map lookups as hypothesized in the PR thread), the two fixes landed on this branch, the remaining hash-map lookup proposal with its design axes, and the per-dataset cutoff feasibility analysis.
…mation dev/profile builds the profiling cargo profile and samples a binary with samply (browser UI) or perf (--report, text). Used to confirm on the c31 sort workload that the pre-fix wider-cutoff cost was the SipHash HashSet behind Itertools::unique (hashbrown rehash + SipHash), that BTreeMap::get lookups are a secondary ~10-12%, and that the post-fix dominant cost is the intrinsic per-k-mer get_hash.
5 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.
The
sortdataset-suggestion step hashes each query into minimizers and looks them up against a shared index. Raising a dataset'sminimizerIndex.cutoffwidens the globalparams.cutoff(the client hashes each query once, before it knows which dataset matches, so it must hash at the widest cutoff any dataset uses). The wider cutoff admits several times more query k-mers and slows this step for every query and every dataset, not only the one that raised its cutoff. Where that added cost came from was unverified: extraBTreeMap::getlookups, a larger key span in the map, or something else.This fixes two hot-path inefficiencies in
run_minimizer_search, together cutting its time by 18% at the default cutoff and 22% at the widest, with byte-identicalsortoutput at both cutoffs.What was fixed
get_ref_search_minimizersdeduplicated the surviving query minimizers withItertools::unique, which fills a hash set that grows with the wider cutoff. It now sorts the small per-query list and drops adjacent duplicates instead [src]. The consumer needs only the distinct set, so iteration order is irrelevant.Vec::contains, costingO(references * entries)per hit. Each map entry already lists exactly the references carrying that minimizer, so it now increments those directly,O(entries)per hit [src].Both are behavior-preserving: the counted hits and the distinct minimizer set are identical, so
sortoutput is unchanged (verified byte-identical at both cutoffs).How it was measured
Single-threaded
sorton one production index (103 references, 44508 entries, k=17), varying onlyparams.cutoff(1<<28vs1<<31) so the stored index is identical between runs. Two independent methods agree:run_minimizer_search[src], isolated from CLI startup and IO, splitting each query into the hashing/dedup stage and the map-lookup/hit-count stage.cargo bench --bench bench_minimizer_searchreproduces the split and aBTreeMap-vs-HashMaplookup comparison.samply/perfon a release build with debug symbols, before and after the fix, for symbol-level confirmation.Findings
Full
run_minimizer_searchtime (criterion, mixed query set, ms):End-to-end CLI wall-clock (includes startup and index parse): default cutoff 0.84 s -> 0.75 s, widest cutoff 1.04 s -> 0.82 s.
The wider-cutoff penalty was not the map lookups. Splitting the pre-fix +89 ms by stage (both cutoffs pre-fix):
90% of the added cost is in
get_ref_search_minimizers. The per-k-mer hashing runs for every k-mer regardless of cutoff, so the cutoff-scaling cost is deduplicating the several-times-larger set of surviving minimizers:Itertools::uniquestores seen values in aHashSet[doc] keyed with the standard-library defaultSipHash, and that set grows with the number of surviving minimizers. The profiler confirms it (inclusive share of samples at cutoff1<<31,run_minimizer_searchat ~95%):Itertools::uniquededup (SipHashHashSet)hashbrownresize/rehashSipHashhashingBTreeMap::get(lookup)get_hash(per-k-mer, cutoff-independent)The
HashSetspends most of its time inhashbrownreserve/rehash as it grows.BTreeMap::getis a stable 10-12%, secondary and independent of key magnitude (O(log n)in entry count, which barely changes when a single dataset raises its cutoff).Remaining cost
After the fix, the dominant cost is the intrinsic per-k-mer
get_hash(the char-membership"ACGT".containspath), which is cutoff-independent and is the target of the bit-packing in #1649. Because the wider-cutoff penalty was the dedup rather than anything fundamental to hashing more k-mers, wider cutoffs are now cheap, and scoping the client cost per dataset is unnecessary.Work items
Itertools::uniquedev/profile)Possible improvements
BTreeMaplookups with a hash map, measured -44% on the lookup stage (perf-minimizer-search-lookup-map.md)