Skip to content

feat: train-encode split - #2119

Open
McPatate wants to merge 57 commits into
mainfrom
feat/train_encode_split
Open

feat: train-encode split#2119
McPatate wants to merge 57 commits into
mainfrom
feat/train_encode_split

Conversation

@McPatate

@McPatate McPatate commented Jun 18, 2026

Copy link
Copy Markdown
Member

splitting training and encoding logic into two separate crates. Should allow for overall perf improvements and reduce crate size significantly.

New additions that come with the PR:

  • Trainable trait (tk-train/src/trainable.rs): Holds the type Trainer + get_trainer() that was stripped off Model.
  • TokenizerTrainExt trait (tk-train/src/train_ext.rs): The train / train_from_files bodies existed before as inherent methods on TokenizerImpl
  • get_model_mut() (tk-encode TokenizerImpl): So the train extension can hand &mut model to a trainer across the crate boundary.
  • pre_tokenize_for_training() (tk-encode TokenizerImpl): Extracted verbatim from the old train-method bodies (normalize → pre-tokenize → splits). Keeps the closure on one &self borrow instead of poking 3 private fields.
  • new tokenizers/src/lib.rs file: Re-export tree that rebuilds the old tokenizers::… paths.

The rest should be unchanged apart from the import changes! EDIT: this PR is the base of the tokenizers v1 refactor now!

PipelineTokenizer benchmark

10 / 10 models supported — PipelineTokenizer vs tokenizers v0.23.1 (latest release) · ~10 kB inputs · add_special_tokens on · single thread + 1/2/4/8/max-thread sweep

f3bf3e820 · 2026-08-04 13:58 UTC · Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz · 16 cores

Per-model encode throughput vs latest release

vs base branch (c299c1ae0) — per-model geomean ×speedup of this PR's PipelineTokenizer against the base branch's; regressions in red.

Per-model encode throughput vs base branch

Per-model memory footprint

Minimal encode binary size

Decode

Round-trip: v0.23.1 encode_fast produces the id streams (same fixtures, add_special_tokens=true); both implementations decode those SAME ids with skip_special_tokens=false. MB/s counts decoded text bytes.

Per-model decode throughput vs latest release

Per-model decode memory footprint

bert-base-uncased — normalizer-heavy WordPiece · ×5.15 vs v0.23.1 · ×0.71 vs base · decode pending bert-base-uncased speedup bert-base-uncased stage decomposition bert-base-uncased thread scaling bert-base-uncased decode speedup bert-base-uncased decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 12+0 (peak 12) · Pipeline 8+0 (peak 17)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 5.3 19.7 ×3.70 ×0.72 3% (1.8) 75% (39.8) 11% (5.8) 9% (4.9) 1% (0.5) match
arb_Arab lang 2.8 17.0 ×6.15 ×0.69 3% (1.7) 66% (40.4) 8% (4.8) 22% (13.1) 1% (0.8) match
ben_Beng lang 4.0 24.4 ×6.09 ×0.71 4% (1.6) 65% (27.8) 10% (4.3) 21% (9.0) 0% (0.1) match
cmn_Hani lang 2.5 12.6 ×5.05 ×0.68 1% (1.2) 69% (56.3) 12% (9.9) 17% (14.1) 0% (0.3) match
ell_Grek lang 2.5 16.7 ×6.68 ×0.71 3% (1.7) 66% (41.6) 8% (5.3) 22% (14.1) 0% (0.2) match
eng_Latn lang 2.8 12.4 ×4.48 ×0.68 4% (3.1) 72% (59.7) 7% (5.4) 19% (15.5) 0% (0.0) match
heb_Hebr lang 2.8 13.5 ×4.77 ×0.69 2% (1.7) 72% (54.6) 8% (5.8) 19% (14.5) 0% (0.0) match
hin_Deva lang 4.4 19.5 ×4.43 ×0.72 3% (1.7) 72% (38.0) 8% (4.4) 17% (8.7) 0% (0.0) match
jpn_Jpan lang 2.9 19.0 ×6.56 ×0.70 3% (1.7) 66% (34.6) 12% (6.3) 20% (10.3) 0% (0.0) match
kat_Geor lang 4.0 19.4 ×4.79 ×0.69 3% (1.7) 73% (38.3) 8% (4.0) 16% (8.1) 0% (0.2) match
kor_Hang lang 1.6 11.6 ×7.33 ×0.66 2% (1.7) 59% (51.9) 10% (9.1) 28% (24.5) 1% (1.3) match
rus_Cyrl lang 2.4 16.8 ×6.97 ×0.72 3% (1.7) 65% (40.3) 7% (4.5) 24% (15.1) 1% (0.6) match
tam_Taml lang 4.7 26.9 ×5.71 ×0.70 4% (1.7) 71% (26.9) 10% (3.7) 16% (6.2) 0% (0.0) match
tha_Thai lang 5.4 23.3 ×4.32 ×0.71 4% (1.7) 80% (35.4) 7% (3.1) 9% (4.0) 0% (0.1) match
added_normalized_dense modalities 4.4 12.9 ×2.91 ×0.65 2% (1.8) 84% (72.1) 2% (1.3) 11% (9.4) 2% (1.6) match
added_normalized_sparse modalities 3.5 12.4 ×3.49 ×0.67 2% (2.4) 60% (60.9) 10% (10.2) 13% (13.6) 14% (14.4) match
added_special_dense modalities 3.7 25.2 ×6.88 ×0.65 31% (8.1) 28% (7.2) 33% (8.5) 9% (2.3) 0% (0.0) match
added_special_sparse modalities 2.8 14.8 ×5.27 ×0.69 7% (4.8) 62% (45.1) 19% (14.0) 9% (6.7) 4% (2.6) match
agentic-traces modalities 2.6 12.4 ×4.83 ×0.69 3% (2.9) 70% (59.0) 8% (6.6) 20% (16.4) 0% (0.0) match
agentic_swe modalities 2.7 13.1 ×4.82 ×0.68 3% (2.4) 74% (59.3) 6% (4.5) 18% (14.0) 0% (0.0) match
code_mixed modalities 3.1 18.7 ×5.98 ×0.99 3% (2.7) 73% (59.6) 5% (4.3) 20% (16.8) 0% (0.0) match
math_latex modalities 2.8 14.4 ×5.14 ×0.79 9% (5.4) 61% (36.1) 8% (4.7) 83% (49.4) 0% (0.0) match
deepseek-v4 — deepseek 3-regex split-heavy byte-level BPE · ×4.72 vs v0.23.1 · ×0.77 vs base · decode pending deepseek-v4 speedup deepseek-v4 stage decomposition deepseek-v4 thread scaling deepseek-v4 decode speedup deepseek-v4 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 62+0 (peak 68) · Pipeline 82+0 (peak 82)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 2.9 26.3 ×8.96 ×0.76 2% (0.9) 0% (0.0) 14% (5.2) 83% (31.8) 0% (0.2) match
arb_Arab lang 2.6 13.2 ×5.08 ×0.78 1% (0.6) 0% (0.0) 5% (3.4) 84% (56.7) 10% (6.8) match
ben_Beng lang 4.4 13.4 ×3.05 ×0.73 1% (0.9) 0% (0.0) 5% (3.7) 94% (68.3) 0% (0.0) match
cmn_Hani lang 2.6 12.8 ×5.00 ×0.75 1% (1.1) 0% (0.0) 5% (4.0) 94% (76.1) 0% (0.0) match
ell_Grek lang 3.2 14.1 ×4.44 ×0.76 1% (0.9) 0% (0.0) 6% (3.9) 93% (65.8) 1% (0.4) match
eng_Latn lang 2.7 12.7 ×4.78 ×0.95 3% (2.1) 0% (0.0) 7% (5.1) 138% (107.6) 0% (0.0) match
heb_Hebr lang 2.4 10.6 ×4.43 ×0.80 1% (0.6) 0% (0.0) 3% (3.5) 70% (75.4) 27% (28.9) match
hin_Deva lang 4.0 16.0 ×3.98 ×0.71 1% (0.9) 0% (0.0) 6% (4.0) 93% (57.9) 0% (0.0) match
jpn_Jpan lang 2.8 13.4 ×4.72 ×0.73 1% (1.0) 0% (0.0) 5% (3.7) 93% (65.4) 0% (0.2) match
kat_Geor lang 5.4 16.9 ×3.13 ×0.93 1% (0.9) 0% (0.0) 5% (3.5) 94% (67.6) 0% (0.2) match
kor_Hang lang 3.0 20.3 ×6.73 ×0.99 1% (0.9) 0% (0.0) 7% (4.2) 92% (58.0) 0% (0.0) match
rus_Cyrl lang 3.0 11.7 ×3.97 ×0.79 1% (0.9) 0% (0.0) 5% (3.8) 94% (79.0) 0% (0.2) match
tam_Taml lang 4.3 13.9 ×3.22 ×0.76 1% (0.9) 0% (0.0) 5% (3.3) 93% (67.6) 1% (0.8) match
tha_Thai lang 4.9 12.1 ×2.48 ×0.83 1% (0.9) 0% (0.0) 3% (2.8) 95% (79.1) 0% (0.3) match
added_normalized_dense modalities 3.9 16.1 ×4.11 ×0.66 2% (1.1) 0% (0.0) 6% (3.4) 93% (56.4) 0% (0.0) match
added_normalized_sparse modalities 3.6 13.9 ×3.91 ×0.68 2% (1.6) 0% (0.0) 6% (4.4) 91% (64.3) 0% (0.3) match
added_special_dense modalities 2.5 24.9 ×9.92 ×0.67 24% (9.0) 6% (2.4) 24% (9.3) 46% (17.3) 0% (0.0) match
added_special_sparse modalities 2.7 14.4 ×5.24 ×0.68 8% (5.0) 1% (0.6) 15% (9.4) 77% (50.2) 0% (0.0) match
agentic-traces modalities 1.8 11.2 ×6.12 ×0.76 2% (2.1) 0% (0.0) 7% (6.3) 90% (79.8) 1% (0.6) match
agentic_swe modalities 2.0 11.4 ×5.63 ×0.75 2% (1.6) 0% (0.0) 5% (4.7) 92% (81.0) 1% (0.7) match
code_mixed modalities 2.4 11.8 ×4.96 ×0.74 2% (1.8) 0% (0.0) 6% (5.5) 91% (76.8) 1% (0.5) match
math_latex modalities 1.9 10.9 ×5.85 ×0.76 2% (2.2) 0% (0.0) 7% (6.2) 91% (83.0) 0% (0.0) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 3.39 3.67 5.19 5.47 63.0 29.8 9.3 12.1× / 11.5× 5.7× / 5.4× 1.8× / 1.7×
arb_Arab 1.86 3.34 3.37 4.84 73.2 33.3 15.0 21.8× / 15.1× 9.9× / 6.9× 4.4× / 3.1×
ben_Beng 2.34 3.15 3.72 4.53 50.9 22.6 11.4 13.7× / 11.2× 6.1× / 5.0× 3.1× / 2.5×
cmn_Hani 1.38 2.61 3.96 5.19 89.2 50.3 21.9 22.5× / 17.2× 12.7× / 9.7× 5.5× / 4.2×
ell_Grek 0.57 2.76 3.92 6.11 47.7 21.3 9.7 12.2× / 7.8× 5.4× / 3.5× 2.5× / 1.6×
eng_Latn 0.09 0.90 5.09 5.89 66.6 39.7 18.4 13.1× / 11.3× 7.8× / 6.7× 3.6× / 3.1×
heb_Hebr 1.75 3.30 3.54 5.09 78.5 35.6 18.4 22.2× / 15.4× 10.1× / 7.0× 5.2× / 3.6×
hin_Deva 2.17 3.35 4.01 5.18 56.1 26.6 12.6 14.0× / 10.8× 6.6× / 5.1× 3.1× / 2.4×
jpn_Jpan 2.30 3.74 3.66 5.09 76.3 39.2 18.0 20.9× / 15.0× 10.7× / 7.7× 4.9× / 3.5×
kat_Geor 2.29 2.91 3.53 4.15 48.0 21.0 10.7 13.6× / 11.6× 5.9× / 5.1× 3.0× / 2.6×
kor_Hang 1.75 2.98 4.17 5.39 73.4 39.9 17.5 17.6× / 13.6× 9.6× / 7.4× 4.2× / 3.2×
rus_Cyrl 2.03 3.24 3.81 5.03 69.5 30.1 13.9 18.2× / 13.8× 7.9× / 6.0× 3.7× / 2.8×
tam_Taml 1.25 3.22 3.27 5.25 45.6 18.5 10.0 13.9× / 8.7× 5.6× / 3.5× 3.0× / 1.9×
tha_Thai 2.27 2.89 2.80 3.43 39.3 12.9 8.2 14.0× / 11.5× 4.6× / 3.8× 2.9× / 2.4×
added_normalized_dense 0.10 1.77 3.38 5.05 68.5 32.2 13.9 20.3× / 13.6× 9.5× / 6.4× 4.1× / 2.8×
added_normalized_sparse 0.25 1.77 4.36 5.88 77.5 37.9 16.7 17.8× / 13.2× 8.7× / 6.4× 3.8× / 2.8×
added_special_dense 0.10 1.77 9.26 10.93 267.6 156.7 61.6 28.9× / 24.5× 16.9× / 14.3× 6.7× / 5.6×
added_special_sparse 0.10 1.77 9.45 11.12 157.4 88.7 36.0 16.7× / 14.2× 9.4× / 8.0× 3.8× / 3.2×
agentic-traces 1.02 1.78 6.29 7.05 124.5 81.7 29.4 19.8× / 17.7× 13.0× / 11.6× 4.7× / 4.2×
agentic_swe 1.02 1.73 4.72 5.42 149.8 102.4 36.7 31.8× / 27.6× 21.7× / 18.9× 7.8× / 6.8×
code_mixed 0.10 1.73 5.46 7.08 115.8 81.8 27.9 21.2× / 16.4× 15.0× / 11.5× 5.1× / 3.9×
math_latex 1.17 1.76 6.22 6.81 122.7 71.9 28.6 19.7× / 18.0× 11.6× / 10.6× 4.6× / 4.2×
gemma-4 — byte-fallback BPE, Metaspace-style split (gemma-4) · ×1.99 vs v0.23.1 · ×0.86 vs base · decode pending gemma-4 speedup gemma-4 stage decomposition gemma-4 thread scaling gemma-4 decode speedup gemma-4 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 304+0 (peak 371) · Pipeline 274+0 (peak 370)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 9.0 24.8 ×2.76 ×0.84 2% (0.7) 4% (1.4) 0% (0.0) 94% (33.9) 0% (0.1) match
arb_Arab lang 6.3 11.2 ×1.78 ×0.79 1% (0.6) 2% (1.6) 0% (0.0) 98% (75.8) 0% (0.0) match
ben_Beng lang 7.9 16.2 ×2.04 ×0.86 1% (0.6) 2% (1.0) 0% (0.0) 96% (54.1) 1% (0.6) match
cmn_Hani lang 10.0 30.6 ×3.08 ×0.81 2% (0.6) 1% (0.2) 0% (0.0) 98% (28.8) 0% (0.0) match
ell_Grek lang 6.9 13.8 ×2.02 ×0.88 1% (0.7) 2% (1.6) 0% (0.0) 94% (64.1) 2% (1.6) match
eng_Latn lang 3.3 4.8 ×1.47 ×0.84 1% (2.3) 2% (3.1) 0% (0.0) 99% (194.0) 0% (0.0) match
heb_Hebr lang 6.6 15.2 ×2.29 ×0.86 1% (0.6) 3% (1.6) 0% (0.0) 101% (59.6) 0% (0.0) match
hin_Deva lang 7.5 15.8 ×2.10 ×0.82 1% (0.6) 2% (1.4) 0% (0.0) 92% (57.5) 5% (3.0) match
jpn_Jpan lang 9.0 25.2 ×2.79 ×0.81 2% (0.6) 1% (0.2) 0% (0.0) 98% (33.9) 0% (0.0) match
kat_Geor lang 10.8 23.6 ×2.18 ×0.90 2% (0.6) 2% (0.9) 0% (0.0) 96% (38.6) 0% (0.2) match
kor_Hang lang 8.4 24.6 ×2.93 ×0.88 2% (0.6) 4% (1.6) 0% (0.0) 102% (38.8) 0% (0.0) match
rus_Cyrl lang 6.1 9.6 ×1.58 ×0.80 1% (0.6) 1% (1.5) 0% (0.0) 98% (97.6) 0% (0.0) match
tam_Taml lang 9.4 18.1 ×1.92 ×0.90 1% (0.6) 2% (0.8) 0% (0.0) 98% (50.9) 0% (0.0) match
tha_Thai lang 10.8 22.0 ×2.03 ×0.89 2% (0.6) 1% (0.5) 0% (0.0) 99% (41.7) 0% (0.0) match
added_normalized_dense modalities 4.8 7.2 ×1.51 ×0.93 1% (0.8) 1% (1.9) 0% (0.0) 97% (132.5) 1% (0.9) match
added_normalized_sparse modalities 4.3 6.2 ×1.44 ×0.91 1% (1.4) 2% (3.0) 0% (0.0) 96% (147.5) 2% (2.4) match
added_special_dense modalities 4.6 18.7 ×4.06 ×0.84 18% (9.1) 15% (7.7) 22% (10.9) 44% (22.1) 0% (0.1) match
added_special_sparse modalities 6.4 9.6 ×1.49 ×0.93 5% (5.1) 7% (7.1) 6% (6.2) 83% (82.8) 0% (0.0) match
agentic-traces modalities 3.8 5.8 ×1.53 ×0.88 1% (2.1) 2% (2.8) 0% (0.1) 93% (161.8) 4% (7.0) match
agentic_swe modalities 3.6 6.4 ×1.77 ×0.87 1% (1.4) 3% (4.1) 0% (0.1) 96% (147.4) 0% (0.3) match
code_mixed modalities 3.8 6.1 ×1.58 ×0.88 1% (1.7) 2% (3.9) 0% (0.0) 99% (161.3) 0% (0.0) match
math_latex modalities 3.6 5.3 ×1.45 ×0.85 1% (2.1) 2% (3.1) 0% (0.0) 98% (183.2) 0% (0.0) match
gpt2 — gpt2 ByteLevel regex · ×9.02 vs v0.23.1 · ×0.99 vs base · decode pending gpt2 speedup gpt2 stage decomposition gpt2 thread scaling gpt2 decode speedup gpt2 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 25+2 (peak 27) · Pipeline 28+0 (peak 28)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 3.3 46.3 ×13.94 ×0.88 3% (0.7) 0% (0.0) 18% (3.7) 79% (15.9) 0% (0.0) match
arb_Arab lang 3.3 26.3 ×7.93 ×1.02 2% (0.6) 0% (0.0) 6% (2.3) 92% (33.8) 0% (0.1) match
ben_Beng lang 2.5 47.1 ×18.52 ×1.00 3% (0.6) 0% (0.0) 15% (3.1) 82% (16.6) 0% (0.0) match
cmn_Hani lang 3.4 29.0 ×8.65 ×0.99 2% (0.6) 0% (0.0) 7% (2.3) 90% (31.1) 1% (0.3) match
ell_Grek lang 3.8 29.9 ×7.85 ×0.99 2% (0.6) 0% (0.0) 6% (2.2) 91% (30.4) 1% (0.2) match
eng_Latn lang 3.0 14.1 ×4.72 ×0.99 3% (2.1) 0% (0.0) 4% (3.0) 93% (64.9) 0% (0.0) match
heb_Hebr lang 3.4 30.0 ×8.74 ×1.01 2% (0.6) 0% (0.0) 7% (2.4) 92% (30.4) 0% (0.0) match
hin_Deva lang 2.8 42.1 ×15.16 ×1.05 3% (0.6) 0% (0.0) 13% (3.2) 83% (19.8) 2% (0.4) match
jpn_Jpan lang 4.1 22.4 ×5.42 ×1.00 1% (0.6) 0% (0.0) 5% (2.1) 94% (42.4) 0% (0.2) match
kat_Geor lang 4.1 73.8 ×17.95 ×1.00 5% (0.6) 0% (0.0) 16% (2.1) 80% (10.7) 0% (0.0) match
kor_Hang lang 2.9 44.7 ×15.55 ×1.02 3% (0.6) 0% (0.0) 11% (2.5) 86% (18.8) 0% (0.0) match
rus_Cyrl lang 3.9 27.7 ×7.07 ×0.96 2% (0.6) 0% (0.0) 6% (2.1) 92% (32.8) 0% (0.0) match
tam_Taml lang 2.4 68.8 ×28.41 ×0.94 4% (0.6) 0% (0.0) 19% (2.7) 77% (11.1) 0% (0.0) match
tha_Thai lang 3.1 37.9 ×12.22 ×1.03 2% (0.6) 0% (0.0) 10% (2.5) 88% (23.1) 0% (0.0) match
added_normalized_dense modalities 5.5 26.5 ×4.79 ×0.98 2% (0.8) 0% (0.0) 3% (1.2) 94% (34.7) 0% (0.1) match
added_normalized_sparse modalities 4.6 22.2 ×4.80 ×0.98 3% (1.4) 0% (0.0) 5% (2.0) 93% (40.7) 0% (0.0) match
added_special_dense modalities 3.8 46.2 ×12.18 ×0.97 25% (5.3) 1% (0.2) 21% (4.3) 52% (10.9) 0% (0.1) match
added_special_sparse modalities 3.9 23.4 ×6.02 ×0.95 8% (3.3) 1% (0.2) 11% (4.4) 79% (32.2) 1% (0.5) match
agentic-traces modalities 2.7 16.5 ×6.10 ×0.98 3% (1.9) 0% (0.0) 6% (3.6) 91% (54.3) 0% (0.1) match
agentic_swe modalities 3.0 25.3 ×8.43 ×0.99 4% (1.4) 0% (0.0) 6% (2.5) 90% (35.2) 0% (0.0) match
code_mixed modalities 3.2 21.0 ×6.58 ×0.99 4% (1.7) 0% (0.0) 6% (3.0) 90% (42.1) 0% (0.1) match
math_latex modalities 2.7 15.3 ×5.67 ×0.99 3% (2.1) 0% (0.0) 5% (3.4) 91% (58.8) 0% (0.1) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 2.61 3.07 3.68 4.14 28.5 21.0 6.0 4.8 7.8× / 6.9× 5.7× / 5.1× 1.6× / 1.5× 1.3× / 1.2×
arb_Arab 0.99 2.75 2.32 4.08 32.1 26.4 6.9 5.2 13.8× / 7.9× 11.4× / 6.5× 3.0× / 1.7× 2.2× / 1.3×
ben_Beng 1.47 2.56 3.08 4.18 65.2 57.0 14.1 4.0 21.2× / 15.6× 18.5× / 13.6× 4.6× / 3.4× 1.3× / 1.0×
cmn_Hani 1.13 2.05 2.34 3.25 26.1 21.1 6.1 2.4 11.2× / 8.0× 9.0× / 6.5× 2.6× / 1.9× 1.0× / 0.7×
ell_Grek 0.57 2.76 2.16 4.34 28.4 22.1 6.1 4.7 13.2× / 6.5× 10.2× / 5.1× 2.8× / 1.4× 2.2× / 1.1×
eng_Latn 0.10 0.89 3.03 3.83 43.5 43.2 12.3 3.8 14.3× / 11.4× 14.2× / 11.3× 4.1× / 3.2× 1.2× / 1.0×
heb_Hebr 0.99 2.79 2.42 4.22 31.8 26.6 7.0 2.9 13.1× / 7.5× 11.0× / 6.3× 2.9× / 1.7× 1.2× / 0.7×
hin_Deva 1.36 2.72 3.15 4.52 61.4 55.8 13.8 4.2 19.5× / 13.6× 17.7× / 12.4× 4.4× / 3.1× 1.3× / 0.9×
jpn_Jpan 1.56 3.12 2.12 3.68 23.2 17.9 5.1 3.9 10.9× / 6.3× 8.4× / 4.9× 2.4× / 1.4× 1.8× / 1.0×
kat_Geor 1.39 2.22 2.10 2.93 17.4 14.9 4.2 2.0 8.3× / 5.9× 7.1× / 5.1× 2.0× / 1.4× 0.9× / 0.7×
kor_Hang 1.08 2.50 2.46 3.88 31.8 28.3 7.6 3.6 12.9× / 8.2× 11.5× / 7.3× 3.1× / 2.0× 1.5× / 0.9×
rus_Cyrl 1.02 2.76 2.11 3.85 27.1 21.3 5.9 2.4 12.8× / 7.0× 10.1× / 5.5× 2.8× / 1.5× 1.2× / 0.6×
tam_Taml 0.92 2.64 2.74 4.46 69.4 59.4 14.5 3.8 25.3× / 15.6× 21.6× / 13.3× 5.3× / 3.3× 1.4× / 0.8×
tha_Thai 1.36 2.19 2.51 3.34 39.5 32.2 8.9 3.2 15.7× / 11.8× 12.8× / 9.6× 3.6× / 2.7× 1.3× / 1.0×
added_normalized_dense 0.06 0.88 1.16 1.99 23.4 22.8 6.4 1.9 20.1× / 11.8× 19.6× / 11.5× 5.5× / 3.2× 1.6× / 0.9×
added_normalized_sparse 0.06 0.88 1.99 2.81 30.4 30.9 8.4 2.5 15.3× / 10.8× 15.5× / 11.0× 4.2× / 3.0× 1.3× / 0.9×
added_special_dense 0.06 0.88 4.32 5.14 93.8 97.9 20.2 2.9 21.7× / 18.2× 22.7× / 19.0× 4.7× / 3.9× 0.7× / 0.6×
added_special_sparse 0.06 0.88 4.42 5.25 60.0 61.8 14.2 3.4 13.6× / 11.4× 14.0× / 11.8× 3.2× / 2.7× 0.8× / 0.6×
agentic-traces 0.57 0.92 3.61 3.96 58.8 60.6 15.6 4.6 16.3× / 14.9× 16.8× / 15.3× 4.3× / 3.9× 1.3× / 1.2×
agentic_swe 0.52 0.86 2.48 2.83 59.0 70.1 14.9 3.5 23.8× / 20.9× 28.3× / 24.8× 6.0× / 5.3× 1.4× / 1.2×
code_mixed 0.07 0.87 3.01 3.80 57.8 67.7 16.4 4.0 19.2× / 15.2× 22.5× / 17.8× 5.5× / 4.3× 1.3× / 1.1×
math_latex 0.56 0.92 3.40 3.75 51.7 52.8 13.8 4.2 15.2× / 13.8× 15.5× / 14.1× 4.1× / 3.7× 1.2× / 1.1×
gpt-oss — o200k-regex byte-level BPE (gpt-oss) · ×5.19 vs v0.23.1 · ×0.94 vs base · decode pending gpt-oss speedup gpt-oss stage decomposition gpt-oss thread scaling gpt-oss decode speedup gpt-oss decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 241+0 (peak 315) · Pipeline 234+0 (peak 316)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 3.6 23.3 ×6.51 ×0.88 2% (0.7) 0% (0.0) 11% (4.7) 87% (36.4) 1% (0.3) match
arb_Arab lang 3.7 17.9 ×4.83 ×0.93 1% (0.6) 0% (0.0) 6% (3.2) 92% (49.8) 1% (0.8) match
ben_Beng lang 6.2 17.7 ×2.87 ×0.95 1% (0.6) 0% (0.0) 7% (3.6) 91% (50.5) 1% (0.8) match
cmn_Hani lang 4.4 11.9 ×2.73 ×0.94 1% (0.6) 0% (0.0) 4% (3.3) 96% (79.5) 0% (0.0) match
ell_Grek lang 4.8 15.9 ×3.32 ×0.94 1% (0.6) 0% (0.0) 5% (3.2) 93% (57.5) 1% (0.5) match
eng_Latn lang 3.8 41.6 ×11.00 ×0.87 9% (2.1) 0% (0.0) 19% (4.6) 64% (15.1) 8% (1.8) match
heb_Hebr lang 4.2 16.6 ×3.99 ×0.92 1% (0.6) 0% (0.0) 6% (3.3) 94% (54.5) 0% (0.0) match
hin_Deva lang 6.5 27.4 ×4.20 ×0.97 2% (0.6) 0% (0.0) 10% (3.7) 86% (30.9) 1% (0.5) match
jpn_Jpan lang 5.2 12.4 ×2.40 ×0.96 1% (0.6) 0% (0.0) 4% (3.1) 97% (76.7) 0% (0.0) match
kat_Geor lang 6.3 14.1 ×2.26 ×0.99 1% (0.6) 0% (0.0) 4% (2.9) 94% (65.4) 1% (1.0) match
kor_Hang lang 3.6 15.0 ×4.16 ×0.88 1% (0.6) 0% (0.0) 6% (3.7) 98% (62.3) 0% (0.0) match
rus_Cyrl lang 4.5 15.5 ×3.41 ×0.93 1% (0.6) 0% (0.0) 5% (3.1) 94% (59.2) 0% (0.0) match
tam_Taml lang 6.6 13.1 ×1.98 ×0.98 1% (0.6) 0% (0.0) 4% (3.2) 95% (71.4) 0% (0.1) match
tha_Thai lang 7.6 11.2 ×1.48 ×0.98 1% (0.6) 0% (0.0) 4% (3.2) 96% (84.8) 0% (0.0) match
added_normalized_dense modalities 5.2 25.4 ×4.92 ×0.99 2% (0.8) 0% (0.0) 6% (2.2) 92% (35.2) 0% (0.0) match
added_normalized_sparse modalities 5.2 41.5 ×7.95 ×0.97 6% (1.4) 0% (0.0) 14% (3.2) 80% (18.4) 0% (0.1) match
added_special_dense modalities 4.3 73.1 ×17.09 ×0.96 40% (5.1) 2% (0.2) 42% (5.3) 16% (2.1) 0% (0.0) match
added_special_sparse modalities 4.3 77.1 ×17.86 ×0.99 27% (3.2) 1% (0.1) 49% (6.0) 25% (3.0) 0% (0.0) match
agentic-traces modalities 3.3 37.3 ×11.17 ×0.92 8% (1.9) 0% (0.0) 20% (5.0) 68% (17.3) 5% (1.2) match
agentic_swe modalities 3.5 26.8 ×7.73 ×0.93 4% (1.4) 0% (0.0) 11% (3.7) 85% (30.1) 0% (0.1) match
code_mixed modalities 3.6 49.3 ×13.60 ×0.94 9% (1.7) 0% (0.0) 24% (4.4) 69% (12.4) 0% (0.0) match
math_latex modalities 3.2 36.9 ×11.64 ×0.94 8% (2.0) 0% (0.0) 19% (4.9) 73% (18.6) 0% (0.0) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 2.60 3.08 4.68 5.16 29.5 14.2 7.2 4.7 6.3× / 5.7× 3.0× / 2.8× 1.5× / 1.4× 1.0× / 0.9×
arb_Arab 0.99 2.75 3.19 4.95 33.8 16.0 7.6 5.1 10.6× / 6.8× 5.0× / 3.2× 2.4× / 1.5× 1.6× / 1.0×
ben_Beng 1.46 2.57 3.65 4.75 23.7 11.0 5.8 2.8 6.5× / 5.0× 3.0× / 2.3× 1.6× / 1.2× 0.8× / 0.6×
cmn_Hani 1.12 1.99 3.33 4.19 21.3 10.9 5.5 2.5 6.4× / 5.1× 3.3× / 2.6× 1.7× / 1.3× 0.7× / 0.6×
ell_Grek 0.57 2.76 3.20 5.39 30.0 15.4 7.0 5.0 9.4× / 5.6× 4.8× / 2.9× 2.2× / 1.3× 1.6× / 0.9×
eng_Latn 0.10 0.89 4.57 5.36 43.3 29.3 14.5 4.2 9.5× / 8.1× 6.4× / 5.5× 3.2× / 2.7× 0.9× / 0.8×
heb_Hebr 0.99 2.85 3.25 5.11 33.0 17.1 8.0 2.9 10.2× / 6.5× 5.3× / 3.3× 2.5× / 1.6× 0.9× / 0.6×
hin_Deva 1.36 2.73 3.74 5.11 25.5 13.3 6.4 3.2 6.8× / 5.0× 3.5× / 2.6× 1.7× / 1.3× 0.8× / 0.6×
jpn_Jpan 1.56 3.11 3.11 4.65 20.0 9.4 4.8 3.8 6.4× / 4.3× 3.0× / 2.0× 1.5× / 1.0× 1.2× / 0.8×
kat_Geor 1.39 2.22 2.92 3.75 18.3 10.4 4.6 2.1 6.3× / 4.9× 3.6× / 2.8× 1.6× / 1.2× 0.7× / 0.6×
kor_Hang 1.08 2.41 3.66 4.99 32.6 19.3 9.0 3.7 8.9× / 6.5× 5.3× / 3.9× 2.5× / 1.8× 1.0× / 0.7×
rus_Cyrl 1.01 2.75 3.13 4.87 28.6 14.8 6.7 4.7 9.1× / 5.9× 4.7× / 3.0× 2.1× / 1.4× 1.5× / 1.0×
tam_Taml 0.92 2.61 3.17 4.86 18.7 8.8 4.2 2.9 5.9× / 3.9× 2.8× / 1.8× 1.3× / 0.9× 0.9× / 0.6×
tha_Thai 1.36 2.22 3.18 4.04 13.2 5.7 2.8 2.3 4.1× / 3.3× 1.8× / 1.4× 0.9× / 0.7× 0.7× / 0.6×
added_normalized_dense 0.06 0.88 2.20 3.02 33.3 17.5 11.8 2.2 15.1× / 11.0× 8.0× / 5.8× 5.4× / 3.9× 1.0× / 0.7×
added_normalized_sparse 0.06 0.88 3.23 4.06 37.2 21.6 12.2 3.0 11.5× / 9.2× 6.7× / 5.3× 3.8× / 3.0× 0.9× / 0.7×
added_special_dense 0.06 0.88 5.33 6.16 84.3 68.9 24.3 3.2 15.8× / 13.7× 12.9× / 11.2× 4.6× / 4.0× 0.6× / 0.5×
added_special_sparse 0.06 0.88 5.95 6.78 56.1 41.7 17.4 3.7 9.4× / 8.3× 7.0× / 6.1× 2.9× / 2.6× 0.6× / 0.5×
agentic-traces 0.58 0.92 5.01 5.35 52.9 42.8 18.2 4.9 10.5× / 9.9× 8.5× / 8.0× 3.6× / 3.4× 1.0× / 0.9×
agentic_swe 0.66 0.86 3.73 3.94 53.1 49.3 18.6 3.6 14.2× / 13.5× 13.2× / 12.5× 5.0× / 4.7× 1.0× / 0.9×
code_mixed 0.07 0.94 4.38 5.25 52.6 47.5 17.9 4.3 12.0× / 10.0× 10.8× / 9.0× 4.1× / 3.4× 1.0× / 0.8×
math_latex 0.58 0.92 4.93 5.28 50.0 36.9 16.6 4.5 10.1× / 9.5× 7.5× / 7.0× 3.4× / 3.1× 0.9× / 0.9×
glm-5.2 — cl100k-variant regex byte-level BPE (glm-5.2) · ×6.68 vs v0.23.1 · ×0.95 vs base · decode pending glm-5.2 speedup glm-5.2 stage decomposition glm-5.2 thread scaling glm-5.2 decode speedup glm-5.2 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 169+0 (peak 231) · Pipeline 170+0 (peak 232)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 3.8 45.2 ×11.77 ×0.94 6% (1.2) 0% (0.1) 17% (3.7) 77% (16.8) 0% (0.0) match
arb_Arab lang 4.2 17.7 ×4.26 ×0.96 2% (1.3) 0% (0.0) 4% (2.3) 94% (52.9) 0% (0.1) match
ben_Beng lang 4.1 29.1 ×7.01 ×1.00 3% (1.2) 0% (0.0) 9% (3.1) 87% (30.0) 0% (0.1) match
cmn_Hani lang 4.7 13.8 ×2.97 ×0.91 2% (1.2) 0% (0.0) 3% (2.4) 95% (68.1) 0% (0.1) match
ell_Grek lang 4.9 19.2 ×3.89 ×0.93 2% (1.2) 0% (0.0) 4% (2.2) 93% (47.3) 0% (0.1) match
eng_Latn lang 3.7 40.6 ×10.93 ×0.89 12% (2.7) 0% (0.0) 14% (3.1) 74% (16.4) 0% (0.0) match
heb_Hebr lang 4.0 25.8 ×6.43 ×0.98 3% (1.2) 0% (0.0) 6% (2.4) 90% (35.1) 0% (0.2) match
hin_Deva lang 3.8 32.0 ×8.31 ×0.99 4% (1.2) 0% (0.0) 11% (3.3) 85% (26.4) 0% (0.1) match
jpn_Jpan lang 5.4 15.5 ×2.87 ×0.94 2% (1.2) 0% (0.0) 3% (2.2) 95% (61.9) 0% (0.0) match
kat_Geor lang 6.3 22.5 ×3.57 ×1.01 3% (1.2) 0% (0.0) 5% (2.1) 92% (40.6) 1% (0.3) match
kor_Hang lang 3.5 18.2 ×5.27 ×0.97 2% (1.2) 0% (0.0) 5% (2.6) 93% (50.9) 0% (0.0) match
rus_Cyrl lang 4.6 17.9 ×3.93 ×0.92 2% (1.2) 0% (0.0) 4% (2.2) 94% (51.5) 0% (0.0) match
tam_Taml lang 3.7 37.5 ×10.00 ×1.01 4% (1.2) 0% (0.0) 10% (2.7) 85% (22.6) 0% (0.1) match
tha_Thai lang 4.6 21.7 ×4.69 ×0.96 3% (1.2) 0% (0.0) 6% (2.6) 93% (43.0) 0% (0.0) match
added_normalized_dense modalities 5.7 26.9 ×4.70 ×0.98 4% (1.3) 0% (0.0) 3% (1.1) 93% (34.5) 0% (0.1) match
added_normalized_sparse modalities 5.1 42.1 ×8.23 ×0.98 8% (1.9) 0% (0.0) 9% (2.0) 83% (19.4) 0% (0.1) match
added_special_dense modalities 3.9 47.4 ×12.13 ×0.97 58% (12.0) 2% (0.4) 28% (5.8) 14% (2.8) 0% (0.0) match
added_special_sparse modalities 4.2 56.6 ×13.37 ×0.97 40% (6.7) 1% (0.2) 31% (5.2) 28% (4.7) 0% (0.0) match
agentic-traces modalities 3.4 34.9 ×10.24 ×0.92 9% (2.6) 0% (0.0) 13% (3.7) 78% (22.7) 0% (0.0) match
agentic_swe modalities 3.5 26.6 ×7.61 ×0.95 6% (2.1) 0% (0.0) 8% (2.8) 87% (32.1) 0% (0.0) match
code_mixed modalities 3.9 44.5 ×11.53 ×0.91 11% (2.3) 0% (0.0) 15% (3.3) 75% (16.3) 0% (0.0) match
math_latex modalities 3.5 37.0 ×10.47 ×0.86 10% (2.6) 0% (0.1) 13% (3.4) 77% (20.8) 0% (0.0) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 2.67 3.12 3.74 4.19 27.8 16.5 6.0 4.8 7.4× / 6.6× 4.4× / 3.9× 1.6× / 1.4× 1.3× / 1.1×
arb_Arab 0.99 2.75 2.34 4.09 31.6 19.3 6.9 5.2 13.5× / 7.7× 8.3× / 4.7× 3.0× / 1.7× 2.2× / 1.3×
ben_Beng 1.46 2.57 3.12 4.22 46.2 28.0 10.7 3.6 14.8× / 10.9× 9.0× / 6.6× 3.4× / 2.5× 1.2× / 0.9×
cmn_Hani 1.12 1.98 2.42 3.28 19.8 11.7 4.7 2.4 8.2× / 6.1× 4.8× / 3.6× 1.9× / 1.4× 1.0× / 0.7×
ell_Grek 0.57 2.77 2.23 4.43 29.1 17.1 6.3 4.8 13.1× / 6.6× 7.7× / 3.9× 2.8× / 1.4× 2.1× / 1.1×
eng_Latn 0.10 0.89 3.06 3.86 45.8 33.1 12.6 3.8 15.0× / 11.9× 10.8× / 8.6× 4.1× / 3.3× 1.2× / 1.0×
heb_Hebr 0.99 2.84 2.36 4.21 31.8 19.6 7.0 2.9 13.5× / 7.6× 8.3× / 4.7× 3.0× / 1.7× 1.2× / 0.7×
hin_Deva 1.36 2.74 3.27 4.65 48.6 31.0 11.2 4.0 14.9× / 10.5× 9.5× / 6.7× 3.4× / 2.4× 1.2× / 0.9×
jpn_Jpan 1.56 3.17 2.20 3.81 18.4 10.0 4.1 3.8 8.4× / 4.8× 4.5× / 2.6× 1.9× / 1.1× 1.7× / 1.0×
kat_Geor 1.39 2.28 2.12 3.01 17.4 11.5 4.2 2.0 8.2× / 5.8× 5.4× / 3.8× 2.0× / 1.4× 1.0× / 0.7×
kor_Hang 1.08 2.39 2.56 3.87 31.9 21.7 7.6 3.6 12.5× / 8.2× 8.5× / 5.6× 3.0× / 2.0× 1.4× / 0.9×
rus_Cyrl 1.01 2.70 2.18 3.88 27.6 17.0 6.0 2.5 12.6× / 7.1× 7.8× / 4.4× 2.8× / 1.6× 1.1× / 0.6×
tam_Taml 0.92 2.60 2.74 4.41 45.2 26.6 10.3 3.3 16.5× / 10.2× 9.7× / 6.0× 3.8× / 2.3× 1.2× / 0.8×
tha_Thai 1.35 2.20 2.62 3.47 28.7 16.4 6.9 3.0 11.0× / 8.3× 6.3× / 4.7× 2.6× / 2.0× 1.1× / 0.9×
added_normalized_dense 0.06 0.88 1.09 1.92 24.4 16.6 6.6 1.9 22.3× / 12.7× 15.2× / 8.7× 6.1× / 3.5× 1.8× / 1.0×
added_normalized_sparse 0.06 0.88 1.98 2.81 32.3 22.6 8.9 2.7 16.3× / 11.5× 11.4× / 8.0× 4.5× / 3.2× 1.3× / 0.9×
added_special_dense 0.06 0.88 5.76 6.59 98.8 73.3 21.6 3.1 17.2× / 15.0× 12.7× / 11.1× 3.7× / 3.3× 0.5× / 0.5×
added_special_sparse 0.06 0.88 5.16 5.99 62.4 45.1 15.2 3.5 12.1× / 10.4× 8.7× / 7.5× 2.9× / 2.5× 0.7× / 0.6×
agentic-traces 0.57 0.92 3.74 4.08 54.5 46.4 15.2 4.6 14.6× / 13.4× 12.4× / 11.4× 4.1× / 3.7× 1.2× / 1.1×
agentic_swe 0.54 0.86 2.85 3.17 57.1 56.2 15.9 3.4 20.1× / 18.0× 19.7× / 17.7× 5.6× / 5.0× 1.2× / 1.1×
code_mixed 0.07 0.86 3.26 4.06 54.6 53.3 15.7 4.0 16.7× / 13.5× 16.3× / 13.1× 4.8× / 3.9× 1.2× / 1.0×
math_latex 0.56 0.94 3.44 3.82 52.5 39.2 14.2 4.2 15.2× / 13.7× 11.4× / 10.2× 4.1× / 3.7× 1.2× / 1.1×
llama-2 — model-bounded BPE, no pre-tokenizer · ×4.12 vs v0.23.1 · ×0.95 vs base · decode pending llama-2 speedup llama-2 stage decomposition llama-2 thread scaling llama-2 decode speedup llama-2 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 18+0 (peak 23) · Pipeline 23+0 (peak 23)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 4.3 40.1 ×9.22 ×0.97 0% (0.1) 8% (1.9) 0% (0.0) 91% (22.4) 1% (0.3) match
arb_Arab lang 9.2 51.3 ×5.61 ×0.97 0% (0.1) 12% (2.2) 0% (0.0) 88% (16.9) 0% (0.0) match
ben_Beng lang 10.0 85.2 ×8.50 ×0.99 1% (0.1) 14% (1.5) 0% (0.0) 86% (9.8) 0% (0.0) match
cmn_Hani lang 8.8 62.6 ×7.12 ×0.94 1% (0.1) 2% (0.4) 0% (0.0) 98% (14.8) 0% (0.0) match
ell_Grek lang 9.3 59.7 ×6.41 ×0.97 0% (0.1) 14% (2.2) 0% (0.0) 86% (14.0) 0% (0.0) match
eng_Latn lang 3.6 5.9 ×1.63 ×0.89 0% (0.1) 3% (5.1) 0% (0.0) 97% (160.5) 0% (0.5) match
heb_Hebr lang 9.1 64.9 ×7.15 ×0.99 0% (0.1) 15% (2.3) 0% (0.0) 85% (12.8) 0% (0.0) match
hin_Deva lang 10.2 81.8 ×8.00 ×0.99 1% (0.1) 17% (2.0) 0% (0.0) 83% (9.7) 0% (0.0) match
jpn_Jpan lang 12.1 82.2 ×6.78 ×0.97 1% (0.1) 3% (0.3) 0% (0.0) 98% (11.2) 0% (0.0) match
kat_Geor lang 12.7 92.8 ×7.33 ×0.97 1% (0.1) 13% (1.4) 0% (0.0) 86% (8.9) 0% (0.0) match
kor_Hang lang 6.8 53.2 ×7.87 ×0.98 0% (0.1) 12% (2.3) 0% (0.0) 87% (16.0) 0% (0.0) match
rus_Cyrl lang 7.0 16.2 ×2.31 ×0.99 0% (0.1) 3% (2.0) 0% (0.0) 96% (58.9) 0% (0.1) match
tam_Taml lang 11.7 92.5 ×7.87 ×0.98 1% (0.1) 12% (1.3) 0% (0.0) 88% (9.3) 0% (0.0) match
tha_Thai lang 14.4 84.1 ×5.86 ×0.94 1% (0.1) 7% (0.8) 0% (0.0) 93% (10.7) 0% (0.0) match
added_normalized_dense modalities 4.6 8.4 ×1.80 ×0.93 0% (0.1) 2% (2.7) 0% (0.0) 97% (111.5) 1% (0.8) match
added_normalized_sparse modalities 4.1 6.8 ×1.66 ×0.89 0% (0.0) 3% (4.6) 0% (0.0) 98% (138.6) 0% (0.0) match
added_special_dense modalities 3.5 20.6 ×5.97 ×0.95 11% (5.1) 31% (14.3) 2% (1.0) 56% (26.2) 0% (0.1) match
added_special_sparse modalities 4.7 10.5 ×2.24 ×0.98 2% (2.1) 13% (12.5) 0% (0.4) 84% (78.5) 0% (0.2) match
agentic-traces modalities 3.8 6.6 ×1.72 ×0.90 0% (0.2) 3% (4.5) 0% (0.0) 97% (144.8) 0% (0.0) match
agentic_swe modalities 3.3 6.5 ×2.00 ×0.89 0% (0.1) 4% (6.4) 0% (0.0) 95% (144.5) 1% (1.2) match
code_mixed modalities 3.6 6.6 ×1.86 ×0.91 0% (0.0) 4% (5.4) 0% (0.0) 97% (145.6) 0% (0.0) match
math_latex modalities 3.8 6.4 ×1.66 ×0.91 0% (0.1) 3% (4.8) 0% (0.0) 96% (150.9) 1% (1.0) match
llama-3 — cl100k-regex byte-level BPE (llama-3), single regex · ×7.18 vs v0.23.1 · ×1.04 vs base · decode pending llama-3 speedup llama-3 stage decomposition llama-3 thread scaling llama-3 decode speedup llama-3 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 73+0 (peak 95) · Pipeline 93+0 (peak 95)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 4.6 55.0 ×11.93 ×1.09 4% (0.7) 0% (0.0) 21% (3.7) 74% (12.9) 1% (0.2) match
arb_Arab lang 5.0 19.1 ×3.80 ×1.06 1% (0.6) 0% (0.0) 5% (2.3) 94% (49.0) 0% (0.0) match
ben_Beng lang 4.1 32.5 ×7.89 ×0.99 2% (0.6) 0% (0.0) 10% (3.1) 87% (26.7) 0% (0.1) match
cmn_Hani lang 5.5 18.2 ×3.29 ×1.05 1% (0.6) 0% (0.0) 4% (2.4) 94% (50.8) 0% (0.0) match
ell_Grek lang 5.4 20.8 ×3.82 ×1.03 1% (0.6) 0% (0.0) 5% (2.2) 93% (44.3) 1% (0.2) match
eng_Latn lang 4.5 53.0 ×11.76 ×1.11 12% (2.1) 0% (0.0) 17% (3.1) 71% (12.6) 0% (0.0) match
heb_Hebr lang 4.6 27.2 ×5.84 ×0.98 2% (0.6) 0% (0.0) 7% (2.3) 91% (32.6) 0% (0.2) match
hin_Deva lang 4.6 86.8 ×18.69 ×1.09 5% (0.6) 0% (0.0) 28% (3.2) 65% (7.4) 1% (0.1) match
jpn_Jpan lang 6.2 17.7 ×2.85 ×1.06 1% (0.7) 0% (0.0) 6% (3.2) 93% (52.4) 0% (0.0) match
kat_Geor lang 5.8 40.9 ×7.06 ×1.05 3% (0.6) 0% (0.0) 9% (2.1) 88% (21.1) 0% (0.1) match
kor_Hang lang 4.4 20.5 ×4.70 ×1.04 1% (0.6) 0% (0.0) 5% (2.6) 93% (44.0) 0% (0.0) match
rus_Cyrl lang 5.4 17.6 ×3.26 ×1.04 1% (0.6) 0% (0.0) 4% (2.2) 95% (52.7) 0% (0.1) match
tam_Taml lang 4.1 37.5 ×9.10 ×1.03 2% (0.6) 0% (0.0) 11% (2.7) 87% (22.4) 0% (0.1) match
tha_Thai lang 5.6 22.4 ×4.00 ×1.06 1% (0.6) 0% (0.0) 6% (2.6) 93% (40.9) 0% (0.1) match
added_normalized_dense modalities 6.2 27.2 ×4.39 ×0.99 2% (0.8) 0% (0.0) 3% (1.0) 95% (34.0) 0% (0.0) match
added_normalized_sparse modalities 5.6 43.2 ×7.73 ×0.99 6% (1.3) 0% (0.0) 8% (1.9) 86% (19.3) 0% (0.1) match
added_special_dense modalities 4.3 71.4 ×16.70 ×0.95 38% (5.0) 2% (0.3) 39% (5.1) 22% (2.9) 0% (0.0) match
added_special_sparse modalities 4.5 72.1 ×16.05 ×0.99 25% (3.3) 1% (0.1) 36% (4.8) 38% (5.0) 0% (0.0) match
agentic-traces modalities 3.8 41.8 ×11.07 ×1.09 8% (1.9) 0% (0.0) 16% (3.7) 76% (17.7) 0% (0.0) match
agentic_swe modalities 4.2 30.4 ×7.18 ×1.05 4% (1.4) 0% (0.0) 9% (2.8) 87% (28.1) 0% (0.0) match
code_mixed modalities 4.3 52.8 ×12.32 ×1.10 9% (1.7) 0% (0.0) 18% (3.3) 73% (13.5) 0% (0.0) match
math_latex modalities 4.0 47.2 ×11.84 ×1.11 10% (2.0) 0% (0.1) 16% (3.4) 73% (15.3) 1% (0.1) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 2.61 3.07 3.68 4.14 27.1 16.5 5.8 4.8 7.4× / 6.5× 4.5× / 4.0× 1.6× / 1.4× 1.3× / 1.2×
arb_Arab 0.99 2.74 2.34 4.09 31.2 19.3 7.0 5.2 13.3× / 7.6× 8.3× / 4.7× 3.0× / 1.7× 2.2× / 1.3×
ben_Beng 1.46 2.58 3.13 4.24 44.8 28.0 10.2 3.7 14.3× / 10.6× 9.0× / 6.6× 3.2× / 2.4× 1.2× / 0.9×
cmn_Hani 1.12 2.03 2.41 3.32 19.4 12.0 4.7 2.4 8.1× / 5.9× 5.0× / 3.6× 1.9× / 1.4× 1.0× / 0.7×
ell_Grek 0.57 2.76 2.23 4.41 28.7 17.7 6.1 4.8 12.9× / 6.5× 7.9× / 4.0× 2.7× / 1.4× 2.1× / 1.1×
eng_Latn 0.09 0.89 3.09 3.89 45.3 33.4 12.2 3.8 14.6× / 11.6× 10.8× / 8.6× 4.0× / 3.1× 1.2× / 1.0×
heb_Hebr 0.99 2.85 2.34 4.20 31.1 19.6 6.9 2.9 13.3× / 7.4× 8.4× / 4.7× 2.9× / 1.6× 1.2× / 0.7×
hin_Deva 1.36 2.80 3.23 4.67 47.4 31.3 11.0 4.0 14.7× / 10.2× 9.7× / 6.7× 3.4× / 2.4× 1.2× / 0.9×
jpn_Jpan 1.55 3.10 3.16 4.72 18.1 10.0 4.1 3.8 5.7× / 3.8× 3.1× / 2.1× 1.3× / 0.9× 1.2× / 0.8×
kat_Geor 1.39 2.23 2.11 2.96 16.8 11.6 4.1 1.9 8.0× / 5.7× 5.5× / 3.9× 2.0× / 1.4× 0.9× / 0.6×
kor_Hang 1.09 2.40 2.55 3.86 30.9 21.7 7.4 3.7 12.1× / 8.0× 8.5× / 5.6× 2.9× / 1.9× 1.4× / 0.9×
rus_Cyrl 1.01 2.71 2.17 3.87 27.0 16.8 5.9 2.4 12.4× / 7.0× 7.7× / 4.3× 2.7× / 1.5× 1.1× / 0.6×
tam_Taml 0.92 2.62 2.75 4.44 43.6 26.6 9.7 3.3 15.9× / 9.8× 9.7× / 6.0× 3.5× / 2.2× 1.2× / 0.7×
tha_Thai 1.36 2.22 2.59 3.45 28.0 16.8 6.7 2.9 10.8× / 8.1× 6.5× / 4.9× 2.6× / 1.9× 1.1× / 0.8×
added_normalized_dense 0.06 0.88 1.04 1.87 24.7 17.2 6.5 1.9 23.7× / 13.2× 16.5× / 9.2× 6.3× / 3.5× 1.9× / 1.0×
added_normalized_sparse 0.06 0.88 1.91 2.73 32.6 23.5 9.1 2.6 17.1× / 11.9× 12.3× / 8.6× 4.7× / 3.3× 1.4× / 1.0×
added_special_dense 0.06 0.88 5.10 5.93 95.3 74.4 21.2 3.2 18.7× / 16.1× 14.6× / 12.6× 4.1× / 3.6× 0.6× / 0.5×
added_special_sparse 0.06 0.88 4.80 5.63 61.4 47.0 15.5 3.5 12.8× / 10.9× 9.8× / 8.3× 3.2× / 2.7× 0.7× / 0.6×
agentic-traces 0.57 0.92 3.70 4.05 54.8 45.3 15.1 4.6 14.8× / 13.5× 12.3× / 11.2× 4.1× / 3.7× 1.2× / 1.1×
agentic_swe 0.65 0.86 2.82 3.03 56.2 50.9 16.1 3.4 19.9× / 18.5× 18.1× / 16.8× 5.7× / 5.3× 1.2× / 1.1×
code_mixed 0.07 0.86 3.26 4.05 53.7 50.5 15.2 4.0 16.5× / 13.3× 15.5× / 12.5× 4.6× / 3.7× 1.2× / 1.0×
math_latex 0.71 0.93 3.44 3.65 52.4 39.5 14.0 4.2 15.2× / 14.3× 11.5× / 10.8× 4.1× / 3.8× 1.2× / 1.1×
mistral-small-4 — tekken byte-level BPE, 1k added specials (mistral-small-4) · ×5.52 vs v0.23.1 · ×1.03 vs base · decode pending mistral-small-4 speedup mistral-small-4 stage decomposition mistral-small-4 thread scaling mistral-small-4 decode speedup mistral-small-4 decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 152+0 (peak 194) · Pipeline 109+0 (peak 195)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 4.2 50.4 ×12.04 ×1.05 7% (1.2) 0% (0.0) 25% (4.6) 69% (12.9) 0% (0.0) match
arb_Arab lang 5.0 23.6 ×4.70 ×1.03 3% (1.2) 0% (0.0) 8% (3.2) 89% (36.5) 0% (0.0) match
ben_Beng lang 7.2 19.8 ×2.74 ×1.02 2% (1.2) 0% (0.0) 7% (3.6) 91% (45.1) 0% (0.0) match
cmn_Hani lang 5.2 19.2 ×3.66 ×1.05 2% (1.2) 0% (0.0) 6% (3.3) 93% (47.5) 0% (0.0) match
ell_Grek lang 5.4 20.1 ×3.69 ×1.04 2% (1.2) 0% (0.0) 6% (3.2) 91% (45.0) 0% (0.2) match
eng_Latn lang 4.1 45.3 ×11.02 ×1.11 12% (2.7) 0% (0.0) 21% (4.5) 66% (14.3) 0% (0.0) match
heb_Hebr lang 4.9 18.4 ×3.77 ×1.06 2% (1.2) 0% (0.0) 6% (3.3) 92% (49.8) 0% (0.0) match
hin_Deva lang 7.1 27.5 ×3.85 ×1.02 3% (1.2) 0% (0.0) 11% (3.7) 86% (30.3) 0% (0.1) match
jpn_Jpan lang 5.9 17.6 ×2.96 ×1.02 2% (1.2) 0% (0.0) 6% (3.1) 92% (51.0) 1% (0.3) match
kat_Geor lang 6.9 16.3 ×2.34 ×1.07 2% (1.2) 0% (0.0) 5% (2.8) 94% (56.9) 0% (0.0) match
kor_Hang lang 4.4 21.6 ×4.92 ×1.06 3% (1.2) 0% (0.0) 8% (3.6) 89% (40.8) 0% (0.0) match
rus_Cyrl lang 5.2 17.9 ×3.44 ×1.04 2% (1.2) 0% (0.0) 6% (3.1) 92% (50.4) 0% (0.0) match
tam_Taml lang 7.2 16.0 ×2.21 ×1.02 2% (1.2) 0% (0.0) 5% (3.2) 92% (57.8) 2% (1.0) match
tha_Thai lang 8.3 15.7 ×1.90 ×1.04 2% (1.2) 0% (0.0) 5% (3.2) 96% (60.2) 0% (0.0) match
added_normalized_dense modalities 5.5 29.9 ×5.39 ×0.98 4% (1.3) 0% (0.0) 7% (2.1) 89% (28.9) 0% (0.1) match
added_normalized_sparse modalities 5.2 44.9 ×8.60 ×1.00 9% (1.9) 0% (0.0) 14% (3.1) 77% (16.8) 0% (0.0) match
added_special_dense modalities 4.4 71.6 ×16.41 ×0.99 38% (5.1) 2% (0.3) 39% (5.3) 22% (2.9) 0% (0.0) match
added_special_sparse modalities 4.5 64.3 ×14.30 ×0.98 26% (3.7) 1% (0.1) 39% (5.7) 34% (4.9) 0% (0.0) match
agentic-traces modalities 3.4 37.8 ×11.00 ×1.02 10% (2.5) 0% (0.0) 19% (5.0) 70% (18.0) 0% (0.1) match
agentic_swe modalities 3.8 27.0 ×7.18 ×1.06 5% (2.0) 0% (0.0) 10% (3.8) 84% (30.7) 0% (0.1) match
code_mixed modalities 3.9 48.1 ×12.30 ×1.05 11% (2.3) 0% (0.0) 21% (4.3) 67% (13.6) 0% (0.0) match
math_latex modalities 3.8 44.8 ×11.89 ×1.05 12% (2.6) 0% (0.0) 23% (5.0) 65% (14.0) 0% (0.0) match

Pre-tokenize: classify + fsm vs regex engines — ns/byte, lower better. The fsm is the scalar jump-table in both pipe columns; SIMD / scalar is the classify pass (regex pre-tokenizers have no SIMD fsm). ×vs = engine ÷ our pipeline (SIMD / scalar classify); onig & pcre2 (JIT) are C, fancy is pure-Rust fancy-regex, logos is a compile-time DFA lexer (approximate grammar; n/a for deepseek).

Fixture classify SIMD classify scalar pipe (SIMD cls + fsm) pipe (scalar cls + fsm) onig fancy pcre2 logos ×vs onig ×vs fancy ×vs pcre2 ×vs logos
amh_Ethi 2.60 3.08 4.65 5.13 27.4 14.4 6.7 5.9× / 5.3× 3.1× / 2.8× 1.4× / 1.3×
arb_Arab 0.99 2.74 3.21 4.95 31.7 16.4 7.4 9.9× / 6.4× 5.1× / 3.3× 2.3× / 1.5×
ben_Beng 1.46 2.57 3.63 4.74 22.3 11.0 5.4 6.1× / 4.7× 3.0× / 2.3× 1.5× / 1.1×
cmn_Hani 1.12 2.02 3.30 4.20 21.1 11.5 5.6 6.4× / 5.0× 3.5× / 2.7× 1.7× / 1.3×
ell_Grek 0.57 2.76 3.16 5.35 27.8 15.1 6.6 8.8× / 5.2× 4.8× / 2.8× 2.1× / 1.2×
eng_Latn 0.09 0.89 4.46 5.26 40.3 30.1 13.2 9.1× / 7.7× 6.8× / 5.7× 3.0× / 2.5×
heb_Hebr 0.99 2.84 3.27 5.12 30.7 17.2 7.7 9.4× / 6.0× 5.3× / 3.4× 2.4× / 1.5×
hin_Deva 1.36 2.75 3.74 5.13 23.7 12.8 6.1 6.3× / 4.6× 3.4× / 2.5× 1.6× / 1.2×
jpn_Jpan 1.56 3.10 3.08 4.62 19.6 9.6 4.8 6.4× / 4.2× 3.1× / 2.1× 1.5× / 1.0×
kat_Geor 1.40 2.22 2.79 3.61 17.5 10.4 4.5 6.3× / 4.9× 3.7× / 2.9× 1.6× / 1.2×
kor_Hang 1.08 2.46 3.62 5.00 30.7 19.3 8.6 8.5× / 6.1× 5.3× / 3.9× 2.4× / 1.7×
rus_Cyrl 1.01 2.70 3.05 4.75 27.0 15.0 6.4 8.8× / 5.7× 4.9× / 3.2× 2.1× / 1.3×
tam_Taml 0.92 2.61 3.17 4.85 17.9 8.9 4.2 5.6× / 3.7× 2.8× / 1.8× 1.3× / 0.9×
tha_Thai 1.36 2.20 3.19 4.04 13.0 5.8 2.8 4.1× / 3.2× 1.8× / 1.4× 0.9× / 0.7×
added_normalized_dense 0.06 0.88 2.14 2.96 31.3 17.1 11.6 14.6× / 10.6× 8.0× / 5.8× 5.4× / 3.9×
added_normalized_sparse 0.06 0.88 3.09 3.92 32.3 20.5 11.6 10.5× / 8.2× 6.6× / 5.2× 3.7× / 3.0×
added_special_dense 0.06 0.88 5.25 6.08 81.1 68.9 23.2 15.4× / 13.3× 13.1× / 11.3× 4.4× / 3.8×
added_special_sparse 0.06 0.88 5.67 6.49 52.4 40.3 16.1 9.2× / 8.1× 7.1× / 6.2× 2.8× / 2.5×
agentic-traces 0.57 0.92 4.99 5.33 51.4 43.6 16.8 10.3× / 9.6× 8.7× / 8.2× 3.4× / 3.2×
agentic_swe 0.50 0.86 3.77 4.13 55.9 54.0 18.4 14.8× / 13.5× 14.3× / 13.1× 4.9× / 4.5×
code_mixed 0.08 0.87 4.31 5.10 50.9 47.9 16.9 11.8× / 10.0× 11.1× / 9.4× 3.9× / 3.3×
math_latex 0.56 0.92 5.00 5.36 47.9 37.6 15.7 9.6× / 8.9× 7.5× / 7.0× 3.1× / 2.9×
t5-base — Unigram + Metaspace · ×2.57 vs v0.23.1 · ×1.02 vs base · decode pending t5-base speedup t5-base stage decomposition t5-base thread scaling t5-base decode speedup t5-base decode thread scaling

Memory (RSS MB, load+encode): v0.23.1 34+2 (peak 36) · Pipeline 61+1 (peak 66)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 6.1 17.9 ×2.91 ×1.06 1% (0.7) 37% (21.4) 4% (2.4) 61% (34.8) 0% (0.0) match
arb_Arab lang 4.7 13.8 ×2.91 ×1.02 1% (0.6) 29% (26.8) 3% (2.9) 67% (61.9) 1% (0.5) match
ben_Beng lang 7.9 22.1 ×2.81 ×1.03 1% (0.6) 33% (24.4) 2% (1.8) 63% (47.1) 1% (0.9) match
cmn_Hani lang 13.2 24.1 ×1.83 ×0.98 1% (0.6) 21% (16.8) 1% (0.6) 73% (58.0) 4% (3.0) match
ell_Grek lang 5.1 14.7 ×2.86 ×1.04 1% (0.6) 27% (27.1) 3% (2.7) 70% (69.5) 0% (0.1) match
eng_Latn lang 2.8 6.5 ×2.34 ×1.03 1% (2.1) 17% (40.7) 2% (4.9) 83% (202.8) 0% (0.0) match
heb_Hebr lang 4.6 13.9 ×3.06 ×1.02 1% (0.6) 25% (23.7) 3% (3.1) 71% (68.2) 1% (0.9) match
hin_Deva lang 6.7 22.2 ×3.31 ×1.04 1% (0.6) 31% (24.4) 3% (2.4) 66% (52.1) 0% (0.0) match
jpn_Jpan lang 14.1 26.0 ×1.85 ×0.99 1% (0.6) 26% (19.8) 1% (0.5) 72% (54.4) 0% (0.1) match
kat_Geor lang 8.2 21.2 ×2.57 ×0.97 1% (0.6) 27% (18.8) 2% (1.5) 70% (49.3) 1% (0.7) match
kor_Hang lang 4.8 12.8 ×2.68 ×0.99 1% (0.7) 27% (24.4) 3% (2.9) 70% (62.9) 0% (0.0) match
rus_Cyrl lang 4.6 9.0 ×1.95 ×1.02 0% (0.6) 19% (26.7) 2% (2.3) 81% (114.6) 0% (0.0) match
tam_Taml lang 9.2 23.4 ×2.54 ×1.03 1% (0.6) 28% (18.7) 2% (1.4) 71% (48.3) 0% (0.0) match
tha_Thai lang 13.4 27.1 ×2.02 ×1.05 1% (0.6) 29% (18.8) 1% (0.9) 72% (47.4) 0% (0.0) match
added_normalized_dense modalities 4.6 13.0 ×2.79 ×1.02 0% (0.8) 19% (37.4) 2% (3.0) 79% (152.0) 0% (0.0) match
added_normalized_sparse modalities 4.0 13.2 ×3.29 ×1.01 1% (1.4) 21% (38.8) 2% (4.0) 75% (138.2) 1% (1.3) match
added_special_dense modalities 6.6 32.1 ×4.87 ×1.00 11% (5.3) 35% (17.4) 6% (3.0) 47% (23.2) 0% (0.2) match
added_special_sparse modalities 4.3 16.4 ×3.82 ×1.02 3% (3.3) 27% (33.8) 7% (8.7) 63% (79.8) 0% (0.2) match
agentic-traces modalities 3.3 6.2 ×1.91 ×1.02 1% (1.9) 17% (39.7) 2% (3.8) 80% (181.4) 0% (0.5) match
agentic_swe modalities 4.7 9.1 ×1.91 ×1.00 1% (1.4) 20% (36.9) 1% (2.6) 78% (145.8) 0% (0.0) match
code_mixed modalities 4.1 8.2 ×1.99 ×1.00 1% (1.7) 20% (39.0) 1% (2.7) 75% (145.5) 2% (4.3) match
math_latex modalities 2.9 6.6 ×2.32 ×1.00 1% (2.0) 20% (41.4) 2% (4.1) 76% (160.5) 2% (3.3) match

@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.

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
@McPatate
McPatate force-pushed the feat/train_encode_split branch from 6ca91df to 4404c11 Compare June 18, 2026 14:27
Comment thread tokenizers/Cargo.toml Outdated
@@ -1,7 +1,12 @@
[workspace]
members = ["tk-encode", "tk-train"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

genuine question: do we want to also add the python bindings to the workspace?
could help with the DX

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

don't think so, I think it's best we leave the current split as such, but no strong opinion on this, python bindings are a special case binding

Comment thread tokenizers/tk-train/Cargo.toml
SBrandeis and others added 8 commits June 24, 2026 19:44
* fmt

* fix ci

* re-generate readmes

* readme check fix

* restructure rust workflow

* Audit & quality for node bindings

* needs to generate cargo.lock

* cleanup comments

* cargo install fix

* iwip

* commit lock files + harmonize workflows

* cache build artifacts and installed binaries
* factor byte_level constants in utils

* also factor BYTES_CHAR

* move tests

* lint
* feat: impl new pretok for bert

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* refactor: change alphabet signature

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add new whitespace pretok

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* refactor: remove unneeded code & comments

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add lookup table for ascii fast path classification

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* bucket draft commit

* we need a mapp then fo check which token

* we know we have to sort it at the end

* buckets not vec buckets and then copy byte set values

We do have a bit of work to do to mach some of the smallVec features.
For now I am implementing, we'll see what I am doing is most probably
not super optimal (espcially having to copy from slice into a newly
allocated buffer?)

* box dyn ...

* boxed_slice

* start flattening the datastructure

* update

* small updates to try and compile

* compiles!

* fix slice len copy

* nits

* use generic const

* this was a bad idea

* create vocab store

* use ptr_hash

* skip some tests for compilation

* nit

* add id to token

* update

* fix

* well this was ai assisted :)

* default and debug

* learn to stop I have to go

* add match_bytes func

* clippy

* fix index

* clippy

* get vocab bytes

* nits

* fix

* omment

* fix compilation

* cleanup

* fix compilation

* nit

* fix some indexing

* fixes

* more fixes esp en empty entries

* fix ID value

* simpler debug

* first implem :)

* handle many buckets case

* details

* fix

* bunch of prints!

* loop was stuck

* for now build splits out of the split end and start

* use VocabStore in BPE model and UnigramModel

* match bytes needs to return token id and token length!

* poc: fast, light, allocation-free BPE encode prototype + benches

A from-scratch inference-only byte-level BPE encode path and the benchmarks
behind the perf investigation. 14-18x over HF tokenizers (main) on byte-exact
models, ~10x smaller binary, ~3x less RAM, 0 hot-path allocations, and a 56x
multi-turn re-encode prefix cache.

- poc/fast-encode: final encoder + benches (stage profile, model x task sweep,
  parallel scaling, multi-turn prefix cache, splitter shootout). NEON-DFA split,
  MPHF VocabStore, allocation-free hybrid merge, thread-local pretoken cache,
  IREE-style ring buffer.
- poc/edge-minimal: C-free, allocation-free build for on-device (0.37 MB stripped).
- poc/special-token-matcher: MPHF length-probe vs IREE-style scan vs daachorse.
- poc/scripts: tokenizer downloader + apply_chat_template workload generators.
- poc/ENCODE_PERF_CASE.md: write-up + the centralize-in-tokenizers case.

Research prototype; byte-exact on the 12 GPT-2-byte-level models tested.

* where i am at

* updates

* fi

* fix byte match

* fixes

* fix

* nits here and there

* tedious fixes

* nits types.rs

* small update

* loads of todo in constructing the struct ±

* update

* just fix warning for now

* add a manual test :)

* fix logic, add match bytes test

* more tests (esp nibble case)

* fis

* better func

* fix the test !

* add a small todo

* memchr2,3 are slow actually

* bench against daachorse

* big update: faster than daachorse up to 90% density of special tokens

This was fun to work on! The key is that the rejection was still very
slow on startswith(). This was looping and we where potentially storing
too long prefix -> vs now u64 & u64 which is efficiient and fast.
This is the final nail for this splitting being fast.

* renamed buckets<-types and remove POC

* remove dummy

* unused

* add rstrip and lstrip

* nits

* naive matcher is IREE's style match

* clean

* add single word

* updates

* remove the bleuprint

* fix CI: cross-arch compile, clippy, fmt

- gate nibble_match_bytes + its test to aarch64 (x86/cross-compile build was failing on a missing method)
- add Unigram::is_empty (clippy len_without_is_empty)
- clippy --fix: needless_return, redundant_field_names, needless_borrow, len_zero, doc continuations
- cargo fmt --all

Normalized-matching / add_tokens tests still fail by design (extract_and_normalize WIP).

* fix tests

* nit

* small todos

* my comments

* updates

* Apply suggestions from code review

Co-authored-by: Luc Georges <McPatate@users.noreply.github.com>

* nits here and thre

* fix

* skip hand rolled byte checks ....

* up

* add "extract_next" API

* simple is word, lstrip rstrip

* nits

* fix them up

* use merge word char

* isolate added vocab

* clippy

---------

Co-authored-by: Luc Georges <McPatate@users.noreply.github.com>
* lint

(cherry picked from commit 2d8adb2)

* fix vocabstore partialeq

(cherry picked from commit 927334c)

* fix doctest

(cherry picked from commit 2247647)

* no fail-fast in CI workflow

(cherry picked from commit 6371e9c)

* fix export

* refactor: park #2129 fast-encode path, restore legacy Tokenizer on the base

Split #2129's token store in two: a legacy map-backed `VocabStore`
(`crate::vocab_store`) that backs the models on this base, and the verbatim
MPHF store renamed to `BucketVocabStore`. The models are unchanged — their
`use crate::vocab_store::VocabStore` now resolves to the legacy twin, so the
fast store can be swapped back in by the pipeline PR via a one-line alias.

Restore the pre-#2129 PreTokenizedString `AddedVocabulary` as the active
tokenizer AV, and park the #2129 fast path — `BucketVocabStore`, `Buckets`,
and the bucket AV (renamed `bucket_added_vocabulary`) — unwired for the
pipeline PR. The parked modules are not re-exported at the crate root so they
don't collide with the legacy AV. This makes the base the genuine legacy
Tokenizer, a fair A/B baseline against PipelineTokenizer.

Models and the tk-train trainer stay byte-identical to #2129. The only #2129
code removed: its broken `extract_and_normalize` stub (replaced by a working
two-pass), the `encode_special_tokens` default (true->false), and the
legacy-tokenizer wiring of the new AV (reverted). BucketVocabStore is #2129
verbatim plus the on-branch PartialEq/doctest fixes, with expanded tests.

tk-encode: 219 pass / 0 fail / 2 ignored, 20 doctests, clippy clean; workspace builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: park #2129 fast-encode path, restore legacy Tokenizer on the base

Split #2129's token store in two: a legacy map-backed `VocabStore`
(`crate::vocab_store`) that backs the models on this base, and the verbatim
MPHF store renamed to `BucketVocabStore`. The models are unchanged — their
`use crate::vocab_store::VocabStore` now resolves to the legacy twin, so the
fast store can be swapped back in by the pipeline PR via a one-line alias.

Restore the pre-#2129 PreTokenizedString `AddedVocabulary` as the active
tokenizer AV, and park the #2129 fast path — `BucketVocabStore`, `Buckets`,
and the bucket AV (renamed `bucket_added_vocabulary`) — unwired for the
pipeline PR. The parked modules are not re-exported at the crate root so they
don't collide with the legacy AV. This makes the base the genuine legacy
Tokenizer, a fair A/B baseline against PipelineTokenizer.

The parked bucket path stays #2129 verbatim, minus dead/broken bits:
  - `BucketVocabStore` = #2129's store + on-branch PartialEq/doctest fixes,
    with expanded unit tests.
  - the bucket AV's `extract_and_normalize` (and its stub-only tests) is
    dropped — the PipelineTokenizer drives the AV solely through `extract_next`
    (the `PipelinePatternMatcher` trait); `extract_and_normalize` is only the
    legacy TokenizerImpl's entry point and uses the legacy AV.
  - `extract_next` had an inverted vocab selection (#2129 used `self.vocab` for
    normalized text); fixed to search `normalized_vocab` when normalized and
    `vocab` otherwise, and covered with tests (routing, offsets, lstrip/rstrip,
    single_word).

Models and the tk-train trainer stay byte-identical to #2129. The only other
#2129 code removed is the legacy-tokenizer wiring of the new AV, reverted so
the legacy AV is active again.

tk-encode: 216 pass / 0 fail / 1 ignored, 20 doctests, clippy + fmt clean; workspace builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* plumbing: full pipeline

* lint

* extract special tokens twice

* lint

* ai-generated: compare pipelines

* ai-generated: pipeline stage analysis

* wip: no vecs for splits

* ai-generated: examples update

* change special token matching api

* iterator formulation

* rm examples

* lint

* rm unused

* cleanup

* ai-generated: bench + correctness test

* inline loop + comments

* ai-assisted: docstring

* lint

* wire PipelineTokenizer onto the parked bucket AddedVocabulary

Rebased onto feat/train_encode_split, which parked the #2129 fast path
(BucketVocabStore / Buckets / bucket_added_vocabulary) and restored the legacy
Tokenizer as the A/B baseline. This collapses the three stale fixup commits
("rebase", "attempt to fix", "apply normalizer in legacy tokenizer") from the
previous rebase — they targeted the pre-park base — into one coherent step:

- bucket_added_vocabulary.rs: the final fast AddedVocabulary (Buckets-backed,
  extract_next / extract_and_normalize, impl PipelinePatternMatcher).
- PipelineTokenizer uses the bucket AddedVocabulary; the base Tokenizer stays
  legacy. TryFrom<&Tokenizer> rebuilds the bucket AV from the tokenizer's added
  tokens in id order, so ids are preserved (model-present tokens reuse their
  model id) and the pipeline emits the same ids as the reference tokenizer.

pipeline_oracle passes: identical ids on big.txt (English) and wagahai
(Japanese) at 1kB/10kB chunks. Full tk-encode/tk-train suites green; fmt +
clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* remove unused extract_and_normalize from bucket AddedVocabulary

The PipelineTokenizer drives special-token matching through `extract_next`
(`PipelinePatternMatcher`) + `SpecialSegmentIterator`, so the bucket AV's
`extract_and_normalize` / `split_on_matches` pair is dead code (only its own
test used it). Drop them and the now-unused `Range`/`PreTokenizedString`/`Token`
imports. The shared helpers (`is_ws`, `is_single_word`, `skip_whitespace_*`)
stay — `extract_next` uses them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* remove extract_two_pass test harness and its tests from bucket AV

Drop the test-only `extract_segments` / `extract_two_pass` / `owned` helpers and
the 7 extraction tests built on them. `extract_two_pass` reimplemented
`PipelineTokenizer::encode`'s two-pass loop inside the test module (a drift-prone
duplicate); `extract_next`'s matching behavior is exercised end-to-end by
pipeline_oracle. Kept the AV-level unit tests that don't go through the harness:
can_add_tokens, can_add_special_tokens, normalized_tokens_are_stored_by_normalized_form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test bucket AV extract_next through SpecialSegmentIterator

Restore the extract_next coverage dropped with the extract_two_pass harness, but
drive the real `SpecialSegmentIterator` instead of a parallel reimplementation.
Five single-pass tests over a bucket `AddedVocabulary`: raw added-token carving,
single_word, lstrip/rstrip span absorption, the encode_special_tokens toggle, and
raw-vs-normalized matcher selection. `SpecialSegmentIterator::new` is now
pub(crate) so the AV tests can construct it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* update makefile

* ai-generated: comparative fixture bench + CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ai-generated: hardware + timestamp + revision

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ons (#2145)

Move the Tokenizer-vs-PipelineTokenizer report out of a PR comment and into
a marker-delimited section of the PR description, kept current in place
(upsert_pr_section.py). Pushes to feat/train_encode_split now target PR #2119;
/pipeline-bench dispatch targets its own PR.

Add emoji feedback to the comment-triggered flow: 👀 when the trigger fires,
👍 from the bench workflow on success. Add a cancel-in-progress concurrency
group keyed on github.ref so a newer commit/comment supersedes an in-flight
run for the same target without cancelling other PRs' benches.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SBrandeis added a commit that referenced this pull request Jul 3, 2026
Comparative Tokenizer-vs-PipelineTokenizer bench that maintains a
marker-delimited section (graph + table) in the target PR's description,
via .github/scripts/upsert_pr_section.py (replace-in-place, else append).

Triggering: issue_comment workflows only run from the default branch, so a
"/pipeline-bench" comment can't fire before this lands on main. Use a
pull_request:[labeled] trigger ("run-pipeline-bench") instead — it runs the
PR branch's own workflow, so it works pre-main. The run itself is the PR
check (no manual check-run API, no emoji reactions), the label is auto-removed
so re-adding re-runs, and the target PR is resolved from the event (labeled PR
/ dispatch input / #2119 for pushes to feat/train_encode_split). Same-repo PRs
only — fork PRs get no secrets and a read-only token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SBrandeis added a commit that referenced this pull request Jul 3, 2026
Comparative Tokenizer-vs-PipelineTokenizer bench that maintains a
marker-delimited section (graph + table) in the target PR's description,
via .github/scripts/upsert_pr_section.py (replace-in-place, else append).

Triggering: issue_comment workflows only run from the default branch, so a
"/pipeline-bench" comment can't fire before this lands on main. Use a
pull_request:[labeled] trigger ("run-pipeline-bench") instead — it runs the
PR branch's own workflow, so it works pre-main. The run itself is the PR
check (no manual check-run API, no emoji reactions), the label is auto-removed
so re-adding re-runs, and the target PR is resolved from the event (labeled PR
/ dispatch input / #2119 for pushes to feat/train_encode_split). Same-repo PRs
only — fork PRs get no secrets and a read-only token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SBrandeis and others added 4 commits July 3, 2026 12:29
Comparative Tokenizer-vs-PipelineTokenizer bench that maintains a
marker-delimited section (graph + table) in the target PR's description,
via .github/scripts/upsert_pr_section.py (replace-in-place, else append).

Triggering: issue_comment workflows only run from the default branch, so a
"/pipeline-bench" comment can't fire before this lands on main. Use a
pull_request:[labeled] trigger ("run-pipeline-bench") instead — it runs the
PR branch's own workflow, so it works pre-main. The run itself is the PR
check (no manual check-run API, no emoji reactions), the label is auto-removed
so re-adding re-runs, and the target PR is resolved from the event (labeled PR
/ dispatch input / #2119 for pushes to feat/train_encode_split). Same-repo PRs
only — fork PRs get no secrets and a read-only token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ai-geenrated: render script grid

* ai-geenrated: update GH workflow

* ai-geenrated: run bench on several different tokenizers

* ai-generated: restore full info + full tokenizers fixtures
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* feat: `impl pipeline::PreTokenizer for FixedLength`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `FixedLength` to `PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
McPatate and others added 6 commits July 6, 2026 16:04
* feat: `impl pipeline::PreTokenizer for Digits`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `Digits` to `PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add extra test for a string of digits

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* feat: `impl pipeline::PreTokenizer for CharDelimiterSplit`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `Delimiter` to `PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* feat: `impl pipeline::PreTokenizer for UnicodeScripts`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: extend ascii lut table to basic multilingual plane

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `UnicodeScripts` to `PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* feat: `impl pipeline::PreTokenizer for WhitespaceSplit`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `WhitespaceSplit` to PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* feat: handle `Merge*` variants of the `SplitDelimiterBehavior`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* feat: add `Punctuation` to `PipelinePreTokenizer`

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* Implement Cow-based normalizers

* ai-generated: bert normalizer

* lint

* implement precompiled

* fallible + more nocopy

* DRY

* ai-assisted: rewrite bert hotpath
ArthurZucker and others added 4 commits July 10, 2026 17:35
…ushed absolute bar (#2185)

The stage-decomposition chart scaled every fixture's stacked bar to a shared ns/byte
range that included the release's whole-encode tick (1000/baseline_mbps). Because the
released tokenizer is slow, that tick is a huge ns/byte value, so it dominated the scale
and squashed the pipeline's actual stage breakdown into an unreadable sliver.

Make it a 100%-normalized bar instead: each stage is its share of THAT fixture's own
pipeline total, labelled `share% · ns/B`, so the mix is readable no matter the absolute
cost. The magnitude and the comparison aren't lost — the right column keeps `total ns/B ·
×speedup`. Dropped the now-moot absolute scale (`stage_scale`), the release tick
(`baseline_ns_per_byte`), and the `max_total` plumbing.

Also restore per-stage columns to the numbers table as `share% (ns/B)`
(added-token / normalize / pre-tokenize / model), so the split cost is legible as text too.
* bench(pipeline): multi-thread scaling sweep (1/2/4/8/max) vs the release

fixture_bench now runs a per-model multi-thread throughput sweep — the pipeline
vs the released `tokenizers` crate at 1, 2, 4, 8, and device-max threads over the
whole corpus (a private rayon pool per count so the sweep neither perturbs nor is
perturbed by the global pool; thread-spawn/scheduling overhead amortized). Emitted
as `threads: {counts, pipeline_mbps, baseline_mbps}` in the JSON.

render_pipeline_bench.py renders a per-model "Thread scaling" chart — throughput
bars at each thread count, pipeline vs release on a shared axis, with the ×speedup
per count and the 1→max scaling factor — placed in each model's <details> block.
The workflow's SVG->PNG glob (`pipeline_bench_*.svg`) picks the new charts up
automatically, so no workflow change is needed.

* bench(pipeline): add ideal-linear reference to the thread-scaling chart

Per-row tick at single-thread throughput × N on the pipeline bar (bar reaches
tick = linear, falls short = sub-linear), plus the self-scaling % of linear in
the right column and the 1→max scaling factor in the subtitle — so the chart
answers 'are we scaling linearly?' directly, not just 'how fast at N threads'.

* ci(pipeline-bench): fan models out across a matrix to run shards in parallel

The multi-thread sweep pushed the single-job bench to ~15 min. Split the work:
a `bench` matrix (4 shards over contiguous manifest slices) runs each slice on
its own isolated 8-vCPU runner in parallel — throughput + thread sweep + memory
for its models — and uploads a partial JSON. A single `report` job (needs:
bench) concatenates the partials in shard = manifest order, then does binary
size + render + HF chart upload + PR-description update once.

fixture_bench gains `--shard <i> <n>`: bench only the i-th of n contiguous
manifest chunks (absent → (0,1) = the whole manifest, unchanged). Shard slices
are contiguous so concatenating in shard order preserves manifest order.
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
@McPatate
McPatate force-pushed the feat/train_encode_split branch from 2055ff7 to 93650e3 Compare July 13, 2026 13:34
ArthurZucker and others added 15 commits July 13, 2026 17:57
* pipeline-bench: add "vs base branch" comparison

The pipeline benchmark only compared PipelineTokenizer against the latest
released crate. Add a "vs base branch" overview + per-fixture "Δ base"
column so a PR's own wins/regressions against feat/train_encode_split are
visible (regressions in red).

Baseline is cached, not recomputed per PR:
- a push to feat/train_encode_split uploads its merged bench JSON to the HF
  Hub dataset as baselines/pipeline-<sha>.json and force-moves the
  lightweight `pipeline-baseline` git tag to that commit (only after the id
  check passes, so a broken base never becomes the baseline);
- a PR run resolves the tag, downloads the cached JSON, and diffs against it.
No tag / no cached JSON yet -> the base overview is skipped and the release
charts render as before.

render_pipeline_bench.py: generalize overview_svg/scale over a speedups fn;
new base_speedup helpers; --base-bench/--base-ref; base overview (red on
regression) + Δ base table column, both gated on base data being present.

* pipeline-bench: fall back to base-branch artifact when no baseline tag

The "vs base branch" section never showed on the PR that introduces it: the
`pipeline-baseline` tag is only created by a base-branch push running this
workflow, which can't happen until the workflow is merged — so the feature was
invisible during its own review (chicken-and-egg).

Add a fallback: when the tag (or its HF Hub JSON) is absent, resolve the
baseline from the newest successful base-branch bench artifact via `gh run
download`. That artifact is produced by every base-branch run, so the
comparison shows up immediately — no manual seeding, no merge required. The
tag + HF Hub JSON stays the durable primary (survives the 30-day artifact
retention). Needs `actions: read` to download the artifact.
* move some stuff around

* nits

* draft fast-splitting

* small updates

* fix!

* update

* fix the small test

* nits

* start todos ands tuff

* finally I get it!

* update

* small updates

* nits

* update

* update

* nits

* update

* regex vs matcher bench

* update

* run end was a wrong approach, use step!

* update

* add the final specs for the new pretokenization paradigm. its SIMD first, classify first then FSM simd when possible, pure scalar for known patterns!

* update

* push lock

* fix conflicts

* fixuppppppppppppppppppppppp

* update

* turbofish

* chore: move sub crates into main workspace

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* remove

* first draft

* nits

* update

* cool

* commit

* fix(fast_split): correct bitmap is number & split _simple_test in 2

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>

* ai draft need to pulls

* save current cleanup

* peel vs looop

* clean

* multi arch

* ai help

* updae

* wasm SIMD128 classify path

Add simd_wasm_classify.rs: wasm32 SIMD128 port of the classify kernel,
generic over TagScheme. u8x16_swizzle is a 16-entry shuffle with OOB->0
(same as NEON vqtbl), so the subtract trick ports directly; native
unsigned compares (u8x16_ge/le) and per-lane byte shifts make it a near
1:1 map of the NEON body. Same tables, same algorithm, 16 bytes/iter.

Gated on simd128 (WASM has no runtime feature detection); non-simd
builds fall back to classify_scalar via the classify.rs dispatcher.
Cross-compiles clean; runtime validation (== classify_scalar) pending
on a SIMD128 engine.

* update

* cleanup simd code

* update

* small xleanup one by one

* draft

* update

* update

* add cl100k

* update

* proper bench

* update

* up

* remove legacy code

* add deepseek

* nits

* update

* update

* peel 2bytes as well

* update

* update

* update

* update

* readme

* cleanup the unecessary scritpt tables

* no push, write on pre allocated output

* more cleanup

* move tests and etc

* more cleanup, update the readme and the docs

* update

* update

* update

* start to  move away from mcpotato's work

* update

* full update

* update

* update

* update

* tk-encode: route ByteLevel/Delimiter through atomsplit, gate fancy-regex

- ByteLevel now splits via atomsplit fsm_byte_level (byte-exact GPT-2 FSM,
  shared GptFsmPattern adapter) instead of a fancy-regex SysRegex; no more
  regex backend for the byte-level pre-tokenizer.
- CharDelimiterSplit pipeline path uses atomsplit's memchr-backed
  CharDelimiterSplit (byte-exact).
- Split.regex is now Option<SysRegex>, built lazily: recognized GPT patterns
  route to atomsplit (fsm/multi) and need no regex backend; both legacy and
  pipeline paths fall back to atomsplit for GPT patterns when no backend is
  compiled. With a backend present (default) behaviour is byte-identical.
- Stub SysRegex for builds with no fancy-regex/onig; drop the compile_error.
  fancy-regex is now optional and droppable via --no-default-features, needed
  only for arbitrary (non-GPT) Split regexes and the Replace normalizer.
- Remove unused regex-syntax dependency.

* up

* update

* fix mega slowness

* update with clippy and etc

* more cleanup

* update for qwen etc

* update

* nit

* clippy

* docs: refresh atomsplit/bitmap_gen structure docs

- add O200k / fsm_o200k to the implemented-pretokenizer / FSM lists (lib.rs, fsm.rs)
- fsm_deepseek doc: drop the stale `ds_is_cjk_letter` ref; document the closed-unit
  CJK-range handling and the gap-grouping + ALPHA_SYM behavior
- bitmap_gen: a tag is a full u8 (low nibble coarse Atom, high nibble refinement), not a u4
- README: document the high-nibble refinement (o200k case on Letter, ALPHA_SYM on Mark)
- drop two broken `[class_runs_neon]` intra-doc links (pub(crate), unlinkable)

* atomsplit: classify from one current Unicode source; add o200k parity gate

atom() mixed Unicode versions: is_letter/is_mark came from unicode_categories 0.1
(frozen at Unicode 9.0) while is_alphabetic/is_whitespace/is_numeric bound to std
(modern). Post-9.0 letters (e.g. U+9FD6, CJK Ext-C astral) were is_letter=false yet
is_alphabetic=true, so they fell into the ALPHA_SYM branch and got tagged 0x16
(coarse Mark) — deepseek/o200k then routed them to \p{S} instead of the [\p{L}\p{M}]
letter run, diverging from modern-Unicode regex on real modern/astral CJK.

- swap unicode_categories -> unicode-properties (current); derive all general
  categories from general_category(); keep std for the White_Space/Alphabetic/
  Numeric properties (also current) — no stale-version mix
- regenerate atom_tables.rs (self-validated over all 1.1M codepoints)
- verified U+9FD6 / U+2B81D now classify as Letter (0x00); circled letters (Other_
  Alphabetic \p{S}) stay ALPHA_SYM (0x16) so \w is unchanged; SIMD == scalar
- add permanent o200k_parity onig gate (the only regex-shaped FSM that lacked one)

No parity regression: cl100k/o200k/byte_level/deepseek gates all byte-exact.

* Drop redundant tk-encode/Cargo.lock; gitignore member-crate lockfiles

tk-encode is a workspace member, so cargo only ever reads the workspace-root
Cargo.lock — the committed tk-encode/Cargo.lock (~3.1k lines) was dead weight
inflating the diff with zero functional effect. Remove it and add a .gitignore
rule (`/*/Cargo.lock`) so member locks don't get re-committed, while leaving the
tracked workspace-root Cargo.lock untouched.

* fix(pipeline): drop duplicate imports left by the feat/train_encode_split merge

The #2183 merge added standalone `PipelineSequence` / `SplitPattern` imports,
which this branch already imports in the `use crate::{…}` block — E0252 "defined
multiple times", so the merge didn't compile. Keep the block imports (the
branch's style) and drop the standalone duplicates. #2183's WordPiece ->
PipelineWordPiece rename is retained.

* perf(pipeline): reuse a thread-local scratch across pre_tokenize calls

Every pipeline pre-tokenizer (the Split GPT-FSM path, Whitespace, WhitespaceSplit,
Punctuation, and the deepseek Sequence) allocated two fresh Vecs per call —
`vec![0u8; n]` for tags and `vec![(0,0); n+1]` for spans. On a special-token-dense
input the special scan carves the text into many tiny segments, so pre_tokenize
runs once per tiny segment and the per-segment malloc/free dominates the cost.

Route all of them through `pipeline::classify_into_spans`, which classifies and
runs the FSM into a grow-only thread-local scratch reused across calls (no
per-segment allocation). Byte-identical to the old per-call allocation.

~1.5x on tiny-segment pre-tokenization (WhitespaceSplit at 20 B segments:
5.30 -> 3.56 ns/B); unchanged for one-big-segment inputs. Byte-exact with the
reference (pipeline_oracle, both add_special_tokens values, bert-wiki over the
big + wagahai corpora).

* update comments

* start removing the scheme, we have found another solution that is much more lightwheight

This commit and the next are gonna be cleaning up the stupid AI slope

jk

* more refinement

* more cleanup

* clean benches

* more cleanup

* update bitmapgen comments

* nits

* pull

* split: route cl100k/o200k to their native FSM instead of regex fallback

cl100k and o200k ship their pre-tokenizer as Sequence[Split(invert=true,
behavior=Removed, <gpt-regex>), ByteLevel] (the tiktoken-conversion convention).
The pipeline's Split->FSM fast path only fires for (invert=false, Isolated), so
these two — the most common production vocabs — silently fell back to MultiRegex
(regex_automata), spending 11-18% of the whole encode in the regex DFA on plain
text with zero special tokens. gpt2 (bare ByteLevel, synthesized as Isolated)
and deepseek (own FSM path) were unaffected.

For a whole-covering GPT regex, (invert=true, Removed) is byte-exactly
equivalent to (invert=false, Isolated) — the inverted match set is the gaps, and
these patterns leave none. Canonicalize to that form at pipeline build
(Split::canonicalized_for_pipeline) when gpt_fsm recognizes the pattern, so
cl100k/o200k route to fsm_cl100k / fsm_o200k.

Measured on the poc-merge branch (identical split/pipeline code): single cold
pass +20..63% (cl100k, o200k); regex_automata share 18% -> 0.0%; byte-exact
(reference token-stream checksums unchanged, incl. Japanese).

* refine doc a bit more :)

* more details and more tables

* update

* update

* refactor the tail / run end to make it re-usable, cleaner and self contained

* small fixes, doc updates

* lol

* explanation

* update

* update

* more comments, better perfs by removing bound checks

* update

* update

* update

* cargo

* wasm cjk update

* nitw

* revert deepseek specific code.

* start to revert the multiregex

* more cleanup

* remove regex automatad

* unify span and splits

* thinner deps

* update

* small up

* update: split the different unrolled fsm for readability

* update

* update

* nits and ai disclaimer

* update

* cleanup some slop0e

* update

* unify benches

* clean

* more unified source of truth for unrolled regex

* update

* nits

* update

* update

* doc updates

* use jit for fair pcre benches

* add logos to the bench

* local bench pcre

* add logos on whitespace split compare

* add heatmap to the doc

---------

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Co-authored-by: Luc Georges <luc.sydney.georges@gmail.com>
* use yada (double-array trie) in wordpiece

* experiment: different data structure for wordpiec

* 1 less alloc

* wip: shared scratch

* wip: re-used scratch

* lint

* lint

* bpe scratch

* lint

* word cache

* fmt

* simpler syntax

* comment out cache (faulty)

* cleanup
The Audit and READMEs jobs went red repo-wide on feat/train_encode_split
(so on every PR against it):

- audit: two advisories published since the ignore list was last updated —
  RUSTSEC-2026-0204 (crossbeam-epoch, transitive) and RUSTSEC-2025-0057
  (fxhash, unmaintained). Both are transitive and not fixable from here, so
  ignore them alongside the existing three (rust/node/python workflows).
- README: the root crate's lib.rs wrapped a sentence but README.md wasn't
  regenerated; `cargo readme` re-syncs it.
Drop the 'Lint Benchmarks with RustFmt' step: it passes the file
straight to rustfmt without the package's edition, so rustfmt (>= Rust
1.97) formats it with default 2024 style and contradicts what
'cargo fmt --all' (edition 2018 -> 2021 style) enforces on the same
file. The step is redundant anyway: benches are workspace targets and
already covered by --all.

Regenerate tk-encode/README.md with cargo-readme 3.3.3, which strips
doctest hidden lines ('# ...') that 3.3.2 leaked into the rendered
README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* bump rust version to 2024

* fmt, clippy, fix sync in bpe trainer

* bindings fmt

* bindings lint

* syntactic sugar nice

* python fmt

* fix: clippy in scalar path

* unsafe + lock

* fixmes
* fixture_bench: per-fixture cold caches, encode_fast baseline, phase separation

- Throughput rows now measure what a plain .encode() loop over that one
  corpus reaches: the pipeline is rebuilt per fixture (fresh scratch
  pool -> fresh BPE word cache) and the baseline is cloned (released
  BPE Clone starts cold). Previously one shared insert-once cache
  accumulated across all 18 fixtures and saturated within the first
  one or two, so later rows depended on manifest order and ran against
  a frozen, mostly-foreign cache.
- The baseline is timed through encode_fast everywhere (throughput,
  thread sweep, memory child, probe): the pipeline computes no offsets,
  so timing the baseline's offset-tracking encode would flatter it.
- Phases are isolated: all warm throughput + id gates first, then the
  stage ladder + regex references (fresh caller-owned scratches), then
  the thread sweep and memory children. Stage numbers and headline
  numbers now share the same cache regime.
- Debloat: onig/fancy/pcre2 references collapse into one SplitEngine
  trait + shared composed-split chain; fixtures are read and chunked
  once into a Fixture struct instead of three times.

JSON schema is unchanged (render_pipeline_bench.py contract); verified
on the gpt2 shard: all keys present, ids_match and ids_match_baseline
green on all 18 fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* benches iter

* oter

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci: faster?

* try more tricks
* bucket vocab store in BPE

* fix
* Implement: PipelinePostProcessor

* display post-process in benches

* Apply suggestions from code review

Co-authored-by: Simon Brandeis <33657802+SBrandeis@users.noreply.github.com>

* fmt

* comments

* todos
* decode bench + oracle + rewrite encode oracle

* docs iter

* Apply suggestions from code review

Co-authored-by: Simon Brandeis <33657802+SBrandeis@users.noreply.github.com>

* decode_batch + decode_stream surface

* decode oracle: specials, pairs, stream, batch + one test per model x fixture

* oracle sweeps add_special_tokens; benches encode with specials

* simplify oracles: fixed windows, per-model tests, no macros

* fix: doctest

* charts: honest decode-memory rows, decode basis + flags spelled out

* fix stubs?

* ci: pin ruff, 0.16 breaks legacy python style
SBrandeis and others added 5 commits July 27, 2026 12:37
…-4 (#2246)

The manifest loaded gpt-oss-slim.json (3,060 of 199,998 vocab entries)
and glm-5.2-slim.json (2,951 of 154,820), so their bench numbers ran on
~1.5% of the real vocab — unrealistically small merge tables and cache
footprints. Point both at the full configs and add two archetypes:
gemma-4 (262,144-vocab byte-fallback BPE, slim-only until now) and
mistral-small-4 (tekken byte-level BPE with 1,000 added specials).

The full configs land in hf-internal-testing/tokenizers-test-data via a
pending PR (which also refreshes llama-2.json and llama-3-tokenizer.json
to byte-exact copies of the source models), so `make bench-models`
needs that merge to fetch them.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* perf: handroll tekken regex

* wire in tk-encode
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* new literal module in AtomSplit

* use Literal in Replace normalizer and Split pretok

* box the Literal finder

`memmem::Finder` carries a few hundred bytes of prefilter state on x86_64 (much
less on aarch64, which is why this only showed up in CI): a `Literal` stored
inline blew `NormalizerWrapper` and `DecoderWrapper` up to 352 bytes through
`Replace`, which `clippy::large_enum_variant` rejects.

* keep the &str / &String patterns, backed by Literal

The Python bindings search with a `&String` pattern — `NormalizedString.replace`
and `.split` take a plain `str` — so dropping these impls broke every job that
builds the bindings. Reinstating them through `Literal` keeps the public API and
still drops the regex engine from the literal path: they used to escape the
string and compile a regex on every call.

An empty pattern now covers the input by byte length rather than character
count, like every other impl (the old count sliced mid-character).
* new literal module in AtomSplit

* use Literal in Replace normalizer and Split pretok

* box the Literal finder

`memmem::Finder` carries a few hundred bytes of prefilter state on x86_64 (much
less on aarch64, which is why this only showed up in CI): a `Literal` stored
inline blew `NormalizerWrapper` and `DecoderWrapper` up to 352 bytes through
`Replace`, which `clippy::large_enum_variant` rejects.

* keep the &str / &String patterns, backed by Literal

The Python bindings search with a `&String` pattern — `NormalizedString.replace`
and `.split` take a plain `str` — so dropping these impls broke every job that
builds the bindings. Reinstating them through `Literal` keeps the public API and
still drops the regex engine from the literal path: they used to escape the
string and compile a regex on every call.

An empty pattern now covers the input by byte length rather than character
count, like every other impl (the old count sliced mid-character).

* Support Metaspace pre-tok

* format

* comments

* lint
@SBrandeis
SBrandeis force-pushed the feat/train_encode_split branch from d183afe to 0bcf291 Compare July 31, 2026 17:44
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
* adds `EncodeHandle` return type to pave the way for async
* takes `impl Into<Inputs>` as an input
    * pairs are not supported atm

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
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.

4 participants