Skip to content

perf(merge bpe): improvement suggestions - #2275

Open
SBrandeis wants to merge 5 commits into
perf/bpe-mergefrom
perf/bpe-merge-review
Open

perf(merge bpe): improvement suggestions#2275
SBrandeis wants to merge 5 commits into
perf/bpe-mergefrom
perf/bpe-merge-review

Conversation

@SBrandeis

Copy link
Copy Markdown
Contributor

No description provided.

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

- Use a trait generic instead of a const generic
- 2 Specialized struct instead of mangling all into SymbolSink
- fn that had no reason to live in impl PipelineBpe are moved oout
@SBrandeis
SBrandeis force-pushed the perf/bpe-merge-review branch from 544c602 to e9fe3ca Compare August 3, 2026 19:13

@ArthurZucker ArthurZucker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks a lot for the great PR 🤗

//! A pass builds the new word in the same array that holds the old one, using two cursors that both start at index 0.
//! The read cursor marks the start of what is left of the old word.
//! The write cursor marks the end of the new word built so far.
//! Every step writes exactly one symbol: a copy moves both cursors by one, and a merge writes one symbol but consumes two, so the read cursor moves ahead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
//! Every step writes exactly one symbol: a copy moves both cursors by one, and a merge writes one symbol but consumes two, so the read cursor moves ahead.
//! Every step writes exactly one symbol: a copy (a no merge for a pair just results in copying the id that was just read) moves both cursors by one, and a merge writes one symbol but consumes two, so the read cursor moves by 2.

//! The read cursor marks the start of what is left of the old word.
//! The write cursor marks the end of the new word built so far.
//! Every step writes exactly one symbol: a copy moves both cursors by one, and a merge writes one symbol but consumes two, so the read cursor moves ahead.
//! The write cursor never gets ahead of the read cursor, so a write only ever lands on a slot that was already read.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
//! The write cursor never gets ahead of the read cursor, so a write only ever lands on a slot that was already read.

Comment on lines +78 to +80
//! ┌───┬───┬────┬───┐
//! │ h │ e │ ll │ o │
//! └───┴───┴────┴───┘

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would leave the last slot since it will just get a u32::MAX

Comment on lines +90 to +92
//! pass 2 target (h,e): [ h │ e │ ll │ o ] -> [ he │ ll │ o ] lowest written pair: (ll,o)
//! pass 3 target (ll,o): [ he │ ll │ o ] -> [ he │ llo ] lowest written pair: (he,llo)
//! pass 4 target (he,llo): [ he │ llo ] -> [ hello ] no pair left to rank: done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here! Its a nit but important IMO to show that there is "padding"

//!
//! # Batching and the `SAFE` bit
//!
//! The target can occur several times in the word. When its merge is `SAFE`, one pass merges every occurrence.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
//! The target can occur several times in the word. When its merge is `SAFE`, one pass merges every occurrence.
//! The target merge can occur several times in the word (for example "hello lots", the pair "lo" appears twice). When its merge is `SAFE`, one pass merges every occurrence.

write_cursor: 0,
target_merge,
batched,
has_merged: false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

was_merged?

Comment on lines +207 to +215
/// Looks up the pair at the read cursor and writes one symbol: the pair's product id when
/// its value equals the target and the pair may still merge, the left symbol otherwise. A
/// merge consumes both symbols of the pair, a copy only the left one.
///
/// `&mut [u32]` rather than `&mut Vec<u32>` so the length is a local and the reads can have
/// their bounds checks removed..
/// The return value becomes the next call's `known_pair_value`. After two copies in a row,
/// the pair the second write ranks is the (`left_symbol`, `right_symbol`) the first call
/// already looked up, so the second write reuses that value instead of looking it up again.
/// A merge returns `None`: the product id it writes is a new symbol, and no pair containing
/// it has been looked up yet.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

badly formulated, but yes you are winning one lookup! let's put this in better framing!

known_pair_value = state.step(tables, symbols, known_pair_value);
}
if state.read_cursor < len {
state.copy_last(tables, symbols, known_pair_value);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not sure we need the copy last func but nit

let pair_value = tables.get_value(&left_symbol, &right_symbol);
let should_merge = pair_value == self.target_merge && (self.batched || !self.has_merged);
if should_merge {
self.has_merged = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
self.has_merged = true;
self.was_merged = true;

let merge_rank = self.tables.get_value(&to_merge[write_id - 1], &written);
running_min = std::cmp::min(running_min, merge_rank);
self.read_cursor += 1;
self.write(tables, symbols, left_symbol, known_pair_value);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

known_paire_value naming is a bit weird / not explicit that it will just not be looked up and None will be?

@ArthurZucker
ArthurZucker marked this pull request as ready for review August 4, 2026 03:58
ArthurZucker added a commit that referenced this pull request Aug 4, 2026
Brings in the BPE merge work from #2241 and #2275: the multipass and
two-tier-queue engines, the rank tables and the pretoken->rank conversion.

`models/bpe/model.rs` is gone -- #2241 split it into `bpe_model.rs`,
`bpe_scratch.rs` and the merge engines -- so the word cache and the batched
`tokenize_spans` this branch added to it are re-wired onto the new engine:
`BpeScratch` carries `word_cache` again, and both pipeline entry points emit
through `tables.unmap` instead of `Word::get_chars_iter`.
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.

3 participants