Repository navigation
Conversation
`one_cut` reset its ambiguity graph with `HashMap::clear()` after a map pre-reserved to `input_len / 10` entries. `clear()` is O(capacity), and it runs once per BFS resolution, so tokenizing long text was quadratic (608k chars: 0.27 s, with a jump when the table doubles in size). The graph is now replaced with a fresh map. `bfs_paths_graph` also cloned the path-so-far for every vertex pushed to the queue, which is quadratic on text that stays ambiguous over a long stretch (e.g. "กรรมกร" repeated). It now records one predecessor per vertex and rebuilds the path once when the goal is reached. Output is byte-identical on the inputs tested. Adds two scaling regression tests. Co-Authored-By: Claude Code <noreply@anthropic.com>
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.
Summary
Removes two sources of superlinear time in
newmmtokenization (one_cut) on the currentmain(2.0.0, unreleased):graph.clear()costs O(capacity).one_cutpre-reservesgraphtoinput_len / 10entries and callsgraph.clear()once per BFS resolution.HashMap::clear()walks the whole table, so each call costs O(capacity), and both the number of calls and the capacity grow with the text. The graph is now replaced with a fresh map (and the upfrontreserveis dropped, since it is discarded at the first reset).bfs_paths_graphcopies the path at every step.path.clone()for each vertex pushed to the queue is quadratic on text that stays ambiguous over a long stretch (e.g."กรรมกร"repeated). It now records one predecessor per vertex and rebuilds the path once, when the goal is reached. The queue holdsCharacterIndexinstead of(usize, Vec<usize>).The visited-set fix from #101 is kept: each vertex is still enqueued at most once, and the returned path is the same first-reached (shortest) path.
Measurements
Rust core,
words_th.txt(62k words), release build, seconds, median of runs. "Paragraph" is a fixed Thai/English paragraph repeated N times."กรรมกร"x2,000 (12,000 chars)"กรรมกร"x8,000 (48,000 chars)"กรรมกร"x32,000 (192,000 chars)On the paragraph,
mainjumps from 0.12 s at 486k characters to 0.27 s at 608k. That is where the reserved table doubles (57,344 entries is the load limit of a 65,536-bucket table, andinput_len / 10goes from 48.6k to 60.8k), which is what points atclear(). For comparison, the 1.4.0 release takes 0.085 s on the 608k paragraph.Verification
Output compared byte for byte between
mainand this branch on: the adversarial texts above, the paragraph x1000, the whole dictionary concatenated in file order, 40,000 shuffled dictionary words, and 20,000 lines mixing three dictionary words with Latin/digits/newlines. 0 differences.cargo test(debug and--release),cargo fmt --check,cargo clippy --all-targets: pass.Two new regression tests in
tests/test_tokenizer.rs, both timing-ratio based rather than absolute:test_newmm_persistent_ambiguity_scaling: 4x longer"กรรมกร"text must take less than 10x as long (about 4x if linear, 16x if quadratic).test_newmm_long_text_scaling: 10x longer ordinary text must take less than 25x as long.Both fail on
main(0.30 s vs a 0.23 s limit, and 0.58 s vs a 0.34 s limit) and pass with this change.Not covered: the Python and Node.js bindings, and
safe_mode/parallel_modetiming, were not run. The existing tests cover their behavior through the Rust API only.🤖 Generated with Claude Code