Skip to content

feat(index): suffix-array construction via libsais (byte-identical to caps-sa) - #109

Open
BenjaminDEMAILLE wants to merge 5 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/index-libsais
Open

feat(index): suffix-array construction via libsais (byte-identical to caps-sa)#109
BenjaminDEMAILLE wants to merge 5 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/index-libsais

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

The index-construction speedup from the roadmap (Rob's "we already have one for index construction"), implemented with libsais (fast SA-IS) as an alternative to caps-sa.

Benchmark (real genomeGenerate, 48 MB / 24 chromosomes, release, 8 threads)

Builder Construction time Peak RSS SA output
caps-sa (default) 12.8 s ~585 MB 396,000,003 bytes
libsais (RUSTAR_USE_LIBSAIS=1) 4.2 s ~1.49 GB byte-identical

~3.0× faster, SA and SAindex byte-for-byte identical — at ~2.5× the peak RAM. libsais builds the SA in memory (the i64 SA buffer is 2N·8 bytes ≈ 768 MB here + text + packed output); caps-sa's default path spills to disk (external-memory), so its RSS stays bounded. Extrapolated to human (2N ≈ 6.2 B), libsais would need ~50 GB while caps-sa ext-mem stays at a few GB — hence libsais is opt-in and best defaulted only for RAM-sized genomes.

Byte-identical, by construction

A suffix array is unique for a given text. build_libsais sorts the same per-segment sentinel-transformed text the existing caps-sa sentinel arm uses, so its packed output is byte-for-byte identical to build() — and hence to STAR 2.7.11b. No tie-break/ordering risk; correctness reduces to "feed libsais the same transformed text."

Validated at four levels (all green):

  • unitbuild_libsais == caps-sa build() on single-chr, multi-chr-with-N, repetitive fixtures;
  • large-alphabet — the i32 arm forced on small fixtures;
  • end-to-end (unit) — streaming build_libsais exactly as GenomeIndex::build does matches the caps-sa streaming byte stream;
  • end-to-end (real genome) — the 48 MB genomeGenerate run above: identical SA + SAindex.

What's included

  • sa_build::build_libsais with automatic alphabet-width selection: u8/u16 for few segments, i32 large-alphabet for many-segment genomes (many chromosomes + sjdb).
  • Wired into genomeGenerate behind RUSTAR_USE_LIBSAIS=1 (default stays caps-sa); on-disk format unchanged.
  • libsais pinned with default-features = false (no OpenMP); libsais-sys builds its C core via cc. CI green on all 5 platforms, including windows-x86_64.

On making it the default

Output is proven byte-identical and it's ~3× faster, so flipping the default is output-safe. The trade is RAM (above). Recommended follow-up: enable libsais by default for RAM-sized genomes with a size guard that falls back to caps-sa ext-mem above a threshold. Happy to do that here or in a follow-up once we agree the threshold.

Roadmap item PR-15. Based on #107 (CI health); independent of the WASP stack.

🤖 Generated with Claude Code

BenjaminDEMAILLE and others added 4 commits July 23, 2026 20:00
CI's clippy advanced to 1.97 (new lints fail the `-D warnings` gate) and rustsec
flagged several advisories in the dependency tree. Both block all PRs.

Clippy:
- src/index/suffix_array.rs: assert!(x == 0) -> assert_eq! (manual_assert_eq)
- src/io/sam.rs, tests/alignment_features.rs: byte-slice literals -> byte
  strings, e.g. [b'S', b'M'] -> *b"SM" (byte_char_slices)
- src/quant/transcriptome.rs: if-let/else-return-None -> `?` (question_mark)

Security audit (Cargo.lock only, no manifest change):
- memmap2 0.9.10 -> 0.9.11 (RUSTSEC-2026-0186, out-of-bounds pointer offset)
- crossbeam-epoch 0.9.18 -> 0.9.20 (RUSTSEC-2026-0204, invalid pointer deref)
- anyhow 1.0.102 -> 1.0.104 (RUSTSEC-2026-0190, unsound downcast_mut)

No behavioural change. Unblocks CI for open and future PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `sa_build::build_libsais`, an alternative suffix-array constructor using
libsais (fast SA-IS) instead of caps-sa. It sorts the same per-segment
sentinel-transformed text the caps-sa sentinel arm uses; because a suffix array
is unique for a given text, the packed output is byte-for-byte identical to
`build()` (and hence to STAR 2.7.11b). Validated by the
`build_libsais_matches_caps_sa_*` tests over single-chr, multi-chr-with-N, and
repetitive fixtures.

- libsais is depended on with `default-features = false` (no OpenMP), so
  libsais-sys builds the C core via `cc` without an OpenMP toolchain (keeps the
  5-platform CI portable).
- Covers genomes whose per-segment sentinel alphabet fits u8/u16; larger segment
  counts fall back to `build()` until the libsais large-alphabet (i32) path is
  wired. Not yet swapped in as the default builder.

Roadmap PR-15 (index-construction speedup). Next: large-alphabet path + byte-
identical validation on the yeast and human genomes, then switch `build()` over
and benchmark. See docs-old/dev/porting-from-star-rs.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend build_libsais with the i32 large-alphabet arm so libsais covers genomes
whose per-segment sentinel alphabet exceeds u16 (many chromosomes + sjdb),
removing the previous caps-sa fallback for alphabet width. The width is
auto-selected (u8/u16/i32); an internal forceable width lets tests exercise the
i32 arm on small fixtures without a >64K-segment genome. Byte-identical to
build() (build_libsais_i32_matches_caps_sa_* tests).

libsais large-alphabet uses an i32 SA buffer, which bounds this arm to texts
shorter than 2^31; huge sequences (e.g. human) still use caps-sa's external-
memory path. Selecting libsais vs caps-sa by available memory, and switching
build()/build_streaming over, remain roadmap follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
genomeGenerate builds the suffix array with libsais when RUSTAR_USE_LIBSAIS is set
(default stays caps-sa). The libsais SA is written to the SA file through the same
PackedStreamWriter, so the on-disk format is unchanged.

A new test streams build_libsais's entries exactly as GenomeIndex::build does and
asserts the byte stream is identical to the caps-sa streaming build, pinning the
end-to-end genomeGenerate SA output byte-for-byte (on top of the unit-level
build_libsais == build equality for both the u8/u16 and i32 arms).

libsais builds in memory, so the opt-in path targets genomes that fit in RAM; huge
genomes keep the caps-sa external-memory streaming path. Making libsais the default
for RAM-sized genomes awaits a construction-time benchmark (output is already proven
byte-identical, so enabling it is output-safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BenjaminDEMAILLE
BenjaminDEMAILLE marked this pull request as ready for review July 23, 2026 18:38
@BenjaminDEMAILLE BenjaminDEMAILLE changed the title feat(index): byte-identical suffix-array construction via libsais feat(index): suffix-array construction via libsais (byte-identical to caps-sa) Jul 23, 2026
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