Skip to content

Faster core (0.2.1) - #7

Merged
kholdrex merged 5 commits into
mainfrom
faster-core
Oct 11, 2026
Merged

kholdrex merged 5 commits into
mainfrom
faster-core

Conversation

@kholdrex

Copy link
Copy Markdown
Member

About 2.5× faster on one core with identical scores. Profiling showed the Rust core, not the Python wrapper, dominated the time per pair.

  • Tokens go into one buffer per text and are interned without a string per token; stemming works in place; ASCII fast path in the rouge-score tokenizer.
  • rouge1 uses dense counts, rouge2 packed bigram keys, both with a single decrementing map.
  • rougeL uses Hyyrö's bit-parallel LCS. Masks exist only for tokens present in both texts, with a linear-memory fallback above 8 MiB. rougeLsum keeps the table because it needs rouge-score's exact backtracking.
  • The binding builds result dicts in Rust (interned keys, Score via tuple.__new__).
  • Fixes from review: bounded hash-map reservations, checked_mul in the mask-size guard, GC traversal for the native scorer, pickles from 0.1.0 and 0.2.0 load.

Parity: the full suite against rouge-score and NLTK passes; new tests compare the bit-parallel LCS and the n-gram counts with the straightforward algorithms on random sequences and unpickle real 0.1.0 and 0.2.0 pickles.

Real data (CNN/DailyMail, XSum, PubMed, 2,000 pairs each, M1 Pro): 29.7–80.6× faster than rouge-score on one core (was 10.6–39.4×), 82.6–352.5× with 8 threads (was 39.7–201×). On the ROUGE-1/2/L subset without stemming it is now faster than rouge-rust on every workload: 1.3–4.6× per call and 1.4–3.3× with 8 threads.

Profiling showed the Rust core, not the Python wrapper, dominated the
time per pair: a String per token, two hash maps per n-gram order, and
an O(nm) table for rougeL.

- Tokens are written into one buffer per text; stemming works in a
  reused byte buffer.
- rouge1 counts in a dense array indexed by token id, rouge2 packs
  bigrams into u64 keys, and both use one map with decrementing counts.
- rougeL uses Hyyro's bit-parallel LCS length; rougeLsum keeps the table
  because it needs rouge-score's exact backtracking.
- The binding returns float triples and Python adds the cached names.

Scores are unchanged: the parity suite against rouge-score and NLTK
passes, and new tests compare the LCS and n-gram counts with the
straightforward algorithms on random sequences.
Building the result dicts in Rust removes the Python-side loop that
wrapped each row in a Score; the binding creates the namedtuples with
tuple.__new__ and interned keys. Reserving the token buffers and the
vocabulary up front, an ASCII fast path in the rouge-score tokenizer
and a single index array for the LCS masks remove most allocations,
which dominated short texts.

Review fixes:
- The LCS mask table only holds tokens present in both texts and falls
  back to the linear-memory table above 8 MiB, instead of growing with
  the number of distinct tokens times the length.
- Scorers pickled by 0.2.0 load again: the score class is optional in
  the native constructor.
- score_documents interns sentences only when rougeLsum is requested.
- Occurrence counts are usize again.
- Hash maps reserve for at most 4096 entries, since repetitive texts
  have far fewer distinct tokens than occurrences.
- The LCS mask-size check uses checked_mul, so it cannot wrap on 32-bit
  targets and skip the table fallback.
- The native scorer reports its Python references to the cycle
  collector.
- Scorers pickled by 0.1.0, which had no cache key, load and score.
- The type stub matches the optional constructor arguments.
Real-data results after the rewrite: 30-80x faster than rouge-score on
one core, 80-350x with eight threads, and faster than rouge-rust on
every workload of its supported subset.
@kholdrex
kholdrex merged commit 8ddaec4 into main Oct 11, 2026
7 checks passed
@kholdrex
kholdrex deleted the faster-core branch October 11, 2026 04:21
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