perf: optionally seed phase 1 with segment-aware packed keys - #15
Conversation
Phase 1 sorts each subarray from singletons, the case the merge kernel handles worst: every leaf merge starts at `m = 0` and orders two suffixes by scanning the text at two random addresses. Sorting by a packed fixed-depth key first resolves the leading `k` symbols with no text access at all, and hands back the LCP between adjacent runs for free from the key difference. Only suffixes agreeing through the whole key reach the merge kernel, on the short slice they occupy. Those group merges take phase 1's own task-local choice, so the seed never nests rayon inside a task that 0.7.0 deliberately made task-local. The key is segment-aware, which is what makes it reach a splice-junction index at all. It packs `min(k, lim_at(p))` symbols, so it never reads into the next segment, and pads with a reserved sentinel placed on the side the provider asks for through the new `LimitProvider::boundary_rank`. `ShorterFirst` pads below every real code, `LongerFirst` above, which is STAR's spacer-as-largest convention. Keys that tie still defer to `boundary_order`, so a position tie-break needs no representation in the key: the key must only avoid contradicting the convention, never reproduce it. `boundary_rank` defaults to `None`, so every existing implementation stays on the comparison path until it opts in. `PlainText` and `SegmentedText` answer `ShorterFirst`. The cross-run LCP is capped by both suffixes' limits: a sentinel field can agree with a real symbol's field past the end of the shorter suffix, so the raw count can overstate the LCP, and a wrong LCP silently corrupts the order at the next merge level. Measured through the rustar-shaped harness on an Apple M4 Max, 12 threads, output checksum-identical in every run: chr21 + GENCODE, 9,952 segments 3.588 s -> 2.401 s -33% GRCh38 + GENCODE, 773,358 segments 454.7 s -> 342.3 s -24.7% User CPU on the second falls from 4,065.7 s to 2,770.9 s and peak RSS lands within 0.3% of the baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The one line rustar-aligner adds on its side: `StarSegmentedText` decides boundary ties purely by which suffix ended first, so it can answer `boundary_rank`. `CAPS_SA_BENCH_NO_RANK=1` measures the same build with the provider declining, which is what a provider that has not opted in gets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`radix.rs` carried a phase-1 subarray seed alongside the in-memory doubling path, and that seed took its comparator as a `runs::Cmp`, the run-skipping wrapper closed as COMBINE-lab#10. It is also unreachable on 0.7.0, whose phase 1 fuses sorting and distribution and never called it. The seed is worth having, but as a re-derivation against the phase 1 that exists, which is COMBINE-lab#15. Removing it here leaves this module with the in-memory doubling path alone, which is what this PR is about, and drops the last dependency on the closed work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
One configuration finding worth folding into this PR, since it changes what a Every number above is with memoization off, which is the default. It is not
So The two act on different phases, which is why they compose: the seed cuts |
|
Thanks, Ben — I independently audited and benchmarked this at the current PR head ( My complete fixture is the production-shaped ruSTAR construction from GENCODE Human v50: GRCh38 primary assembly, all 698,597 prepared/deduplicated junctions, After warm-up, three interleaved measured rounds gave these medians:
So I measure:
That last pair is the clearest evidence that the two mechanisms genuinely compose. I did not reproduce the exact -24.7% / -26.2% figures in the PR, but the difference is unsurprising across hardware and fixtures, and the independent effect remains far outside noise. Controls were also positive: the focused chr21-backbone plus all v50 annotation flanks improved 21.7%, while chr21 with no GTF/junctions improved 27.7%. Every full run emitted the same count and ordered 128-bit stream hash ( I found the algorithm itself careful and sound: exact- I do want to change a few integration details before merging:
I am going to push these revisions to the PR branch under the enabled maintainer-edit permission. My proposed caller-facing shape is an explicit On |
|
I pushed the maintainer revisions discussed above as The important API/design changes are now concrete:
I also strengthened the tests to use a direct adjacent-LCP oracle, including LCPs just below, at, and above the packed-key depth; added external-output, allocation-policy, environment-policy, and early-gating tests; removed the incomplete The revised code retains the measured benefit on the focused ruSTAR chr21 backbone plus all GENCODE-v50-derived splice flanks (32 physical cores, one warm-up and three interleaved measured runs):
That is a 21.8% total improvement and 65.2% phase-1 reduction on the focused annotated fixture. The declined result is within run noise of The focused fixture does not benefit from geometric memoization (6.080 s with both versus 6.025 s with the seed alone), but the complete GRCh38 + GENCODE v50 run does: 171.205 s with memoization alone versus 134.618 s with both. This supports the original conclusion that the optimizations compose, while also showing that memoization's incremental value is workload-dependent. Local validation is green: 88 debug and release tests, all targets, Clippy with warnings denied, rustdoc with warnings denied, doctests, formatting, the Astro documentation build, exact-output comparison, signed- I have intentionally left a |
Summary
Adds an opt-in, segment-aware packed-prefix seed for external-memory phase 1. Each phase-1 subarray is first ordered by a fixed-depth
u64key; only equal-key runs invoke the complete LCP merge comparator. Keys stop atLimitProviderboundaries and reserve a boundary code on the side declared by the provider, so the optimization supports both standard generalized-SA ordering and STAR's spacer-as-largest convention.PR #14, the standalone ruSTAR replay harness, has now merged. This branch is refreshed onto that
mainand contains only the feature, its policy/API, tests, and documentation changes relative to the merged harness.Public API and fallback behavior
Activation is separate from comparator semantics:
PackedPrefixSeedPolicy::Disabledis the default.DenseAlphabetOnlyrequires symbols already encoded as0..alphabet_sizeand never allocates a second text-sized buffer. This is the intended ruSTAR policy forA=0, C=1, G=2, T=3, N=4, spacer=5.remap(max_extra_bytes)permits an order-preserving ranked copy for a gapped byte alphabet only within the explicit allocation budget.LimitProvider::boundary_rank()declares semantic eligibility only. Built-in providers returnShorterFirst; ruSTAR's provider returnsLongerFirst.u8symbols, finite contexts, providers without a representable boundary order, 256-symbol byte alphabets, insufficient remap budgets, and failed remap reservations automatically use the existing comparison sort.All cheap policy/type/context/provider gates run before the one alphabet scan. The boundary rank is computed once and shared by the phase-1 tasks.
Independent ruSTAR measurements
Complete production-shaped fixture: GENCODE Human v50 GRCh38 primary assembly, all 698,597 prepared/deduplicated splice junctions,
sjdbOverhang=100, ruSTAR padding and junction append, forward/reverse-complement layout, 1,397,582 segments, STAR boundary ordering, ACGT-start filter,u64, 8,192 partitions, and 32 pinned AMD EPYC 9555 physical cores. The text has 6,557,611,930 symbols and emits 6,176,694,310 suffixes.One warm-up followed by three interleaved measured runs; values are medians:
The seed reduces phase 1 by 75.1%, improves the memoized ruSTAR configuration by 21.4%, and improves the full stack by 32.3% over un-memoized v0.7.0. Geometric memoization changes phase 4 by 19.25% without the seed and 19.23% with it, confirming that the mechanisms compose.
The additional 366–376 MiB peak RSS (about 4.1%) matches one
(u64, u64)key record per selected suffix in each active phase-1 task. Dense ruSTAR input does not allocate a ranked-text copy.Refreshed-head correctness and performance gate
After merging current
mainand the PR #14 harness cleanup, the focused chr21 genomic backbone plus every GENCODE-v50-derived splice flank was rerun at 32 physical cores:6d13e13009418fd6efbe4a73d7721e2e.Targeted coverage includes exact adjacent-LCP oracles below, at, and above the packed-key depth under both boundary conventions; simultaneous boundary ties; the 255/256-symbol reserved-code edge; dense and budgeted-remap alphabets; signed
i8; finite contexts; randomized segmented/filter constructions; policy-disabled and provider-decline paths; and composition with geometric memoization.Validation
cargo package --locked --offline --allow-dirtyverifies the packaged crate.--packed-prefix-seed,--no-packed-prefix-seed, and--no-boundary-rankcontrols.The
bonbuilder migration remains intentionally separate: it is a broader API decision and is not required for this optimization.