Skip to content

poc(encode): target encode path — bitsplit + batched model + fused cache probe - #2279

Closed
ArthurZucker wants to merge 133 commits into
feat/train_encode_splitfrom
poc/target-encode
Closed

ArthurZucker wants to merge 133 commits into
feat/train_encode_splitfrom
poc/target-encode

Conversation

@ArthurZucker

@ArthurZucker ArthurZucker commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Supersedes #2272. This is the same intent — one tree with the optimization work in it — but
built on the current cache stack and measured end to end against gigatoken on this machine.

Not for merge as-is: bitsplit is vendored here as tokenizers/bitsplit. It belongs in its own
library; this PR exists so the end state can be measured instead of argued about, and so the
individual PRs underneath it can be reviewed knowing what they add up to.

What is in it

bitsplit pre-tokenization as a bitstream program for the gpt2 and cl100k regexes — 64 bytes decided per register op instead of one token per unpredictable branch. split 0.64 → 0.34 ns/B on English. SIMD block builders for both NEON and SSSE3; where neither is available the routing keeps the FSM, since the portable builder is slower than it (0.89 vs 0.64 ns/B). Byte-exact: the whole tk-encode suite, including the legacy oracles, passes with it routed in, and bitsplit's own suite passes on x86 under Rosetta.
#2278 the model takes a whole chunk of pre-tokens at a time; a cache hit is one load plus an unconditional store of the slot's lanes, written at the caller's cursor
#2277 home-slot probe, no second read of the slot, hardware CRC over the packed key
#2276 the word key is one page-guarded 16-byte load and a masked shift, not a memcpy

Measured

Full encode, 4 MB inputs, single thread, warm cache, aarch64, lto = "fat". gigatoken 0.10 built
and run on the same machine, same corpora, same tokenizer.json — token counts match it exactly on
all three models, so this is like for like.

model · corpus this PR gigatoken
gpt2 · english 1228 1554 0.79×
gpt2 · code 674 806 0.84×
gpt2 · chinese 970 517 1.88×
gpt2 · russian 847 632 1.34×
llama-3 · english 1265 1405 0.90×
llama-3 · code 749 802 0.93×
llama-3 · chinese 998 537 1.86×
llama-3 · russian 1022 664 1.54×

Geomean vs gigatoken: gpt2 1.14×, llama-3 1.24×. Ahead overall; within 7–10% on its two best
cells, well ahead on everything with long pre-tokens.

For scale, feat/bpe-cache-min on the same bench is english 368 / code 178, so this is 3.3× the
base
on English and 3.8× on code.

Reproduce with cargo run --release --example stage_bench (included), which prints the marginal
ns/B of each stage.

On the CI benchmark

The first push of this branch regressed it: the transpose was NEON-only, the CI runner is x86_64,
so the routing handed it bitsplit's byte-at-a-time fallback (split 0.89 ns/B) in place of the FSM
(0.64). Fixed twice over — there is now an SSSE3 builder, and bitsplit::fast_builder() gates the
routing so any target without a SIMD build keeps the FSM.

Where the remaining gap is

With the model stubbed to answer every pre-token with one token, the per-pre-token call structure
measured 0.64 ns/B before #2278 — gigatoken's entire encode — and is 0.09 now. What is left
on English is split 0.33 + model 0.47; matching gigatoken outright needs the split cheaper still,
which is bitsplit's own roadmap (o200k and the class-run family have no bitstream grammar yet, so
atomsplit::fsm cannot be retired).

Dead ends, measured, so nobody repeats them

  • Fusing the cache key into the split's emit walk: 7–22% worse on every corpus.
  • Prefetching the next word's slot, even with the hash already computed: −19%, or 1.0× when
    batched. The table is L1-resident — 512 slots performs the same as 262144.
  • gigatoken's cache pre-seed is not its advantage (unseeded is within noise); its cache is —
    disable it and English collapses 1554 → 32 MB/s. Our merge is 1.8–6.0× faster than theirs uncached.

PipelineTokenizer benchmark

9 / 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

86bd5faa7 · 2026-08-04 11:15 UTC · Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz · 32 cores

Per-model encode throughput vs latest release

vs base branch (e87a72c56) — 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 · ×8.72 vs v0.23.1 · ×1.11 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+2 (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 4.5 31.9 ×7.11 ×1.15 4% (1.2) 70% (20.4) 16% (4.6) 10% (2.8) 0% (0.0) match
arb_Arab lang 2.8 26.7 ×9.55 ×1.08 3% (1.2) 76% (28.2) 8% (3.0) 13% (4.9) 0% (0.0) match
ben_Beng lang 3.8 37.6 ×9.91 ×1.09 4% (1.2) 74% (19.6) 11% (3.0) 10% (2.7) 0% (0.0) match
cmn_Hani lang 2.2 21.2 ×9.57 ×1.14 3% (1.2) 69% (32.2) 20% (9.2) 9% (4.4) 0% (0.0) match
ell_Grek lang 2.5 26.2 ×10.55 ×1.10 3% (1.2) 78% (29.4) 8% (3.0) 11% (4.0) 0% (0.0) match
eng_Latn lang 2.7 20.3 ×7.62 ×1.11 5% (2.5) 82% (40.2) 5% (2.5) 7% (3.5) 0% (0.0) match
heb_Hebr lang 2.6 24.6 ×9.50 ×1.23 3% (1.2) 74% (28.1) 8% (3.1) 14% (5.4) 0% (0.0) match
hin_Deva lang 3.8 33.9 ×9.01 ×1.24 4% (1.2) 76% (20.5) 11% (2.9) 10% (2.6) 0% (0.0) match
jpn_Jpan lang 2.7 27.6 ×10.17 ×1.02 3% (1.2) 66% (23.7) 14% (5.0) 17% (6.0) 0% (0.0) match
kat_Geor lang 3.9 37.9 ×9.82 ×1.35 4% (1.2) 75% (19.6) 11% (2.9) 9% (2.4) 0% (0.1) match
kor_Hang lang 1.6 15.8 ×9.79 ×0.90 2% (1.2) 58% (36.1) 22% (13.5) 19% (11.6) 0% (0.0) match
rus_Cyrl lang 2.4 25.0 ×10.42 ×1.05 3% (1.2) 76% (28.0) 8% (2.9) 14% (5.0) 0% (0.0) match
tam_Taml lang 4.4 39.9 ×9.16 ×1.04 5% (1.2) 76% (18.8) 10% (2.6) 9% (2.3) 0% (0.0) match
tha_Thai lang 5.1 43.9 ×8.62 ×1.33 5% (1.2) 81% (18.6) 10% (2.2) 4% (0.9) 0% (0.0) match
added_normalized_dense modalities 3.4 20.4 ×5.99 ×1.04 3% (1.3) 88% (42.8) 8% (4.0) 1% (0.6) 0% (0.1) match
added_normalized_sparse modalities 3.1 20.0 ×6.52 ×1.09 3% (1.7) 84% (42.1) 8% (3.8) 4% (2.1) 0% (0.1) match
added_special_dense modalities 3.7 41.3 ×11.06 ×1.08 21% (5.2) 38% (9.3) 36% (8.8) 5% (1.2) 0% (0.0) match
added_special_sparse modalities 3.3 23.4 ×7.12 ×1.10 8% (3.6) 68% (29.0) 18% (7.8) 6% (2.5) 0% (0.0) match
agentic-traces modalities 2.4 20.1 ×8.44 ×1.12 5% (2.2) 81% (39.9) 6% (2.9) 9% (4.2) 0% (0.0) match
agentic_swe modalities 2.5 20.5 ×8.28 ×1.06 4% (2.0) 83% (40.3) 4% (2.1) 8% (4.0) 0% (0.0) match
code_mixed modalities 2.4 19.6 ×8.10 ×1.04 4% (2.2) 82% (40.0) 5% (2.6) 8% (3.8) 0% (0.0) match
math_latex modalities 2.5 20.0 ×8.00 ×1.11 5% (2.4) 81% (40.2) 6% (2.9) 8% (4.0) 0% (0.0) match
deepseek-v4 — deepseek 3-regex split-heavy byte-level BPE · ×25.03 vs v0.23.1 · ×3.99 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 3.2 65.6 ×20.29 ×1.98 4% (0.7) 0% (0.0) 33% (5.0) 65% (10.0) 0% (0.0) match
arb_Arab lang 3.2 61.9 ×19.36 ×3.83 4% (0.6) 0% (0.0) 22% (3.8) 73% (12.5) 1% (0.2) match
ben_Beng lang 3.9 145.4 ×37.14 ×8.40 9% (0.6) 0% (0.0) 50% (3.5) 40% (2.8) 1% (0.1) match
cmn_Hani lang 2.8 18.7 ×6.66 ×1.23 1% (0.8) 0% (0.0) 6% (3.5) 92% (51.7) 0% (0.0) match
ell_Grek lang 3.2 89.6 ×27.58 ×5.15 6% (0.7) 0% (0.0) 33% (3.8) 66% (7.7) 0% (0.0) match
eng_Latn lang 2.3 68.4 ×30.30 ×5.50 14% (2.0) 0% (0.0) 35% (5.3) 50% (7.4) 2% (0.2) match
heb_Hebr lang 2.7 55.1 ×20.73 ×4.25 3% (0.6) 0% (0.0) 21% (3.9) 77% (14.5) 0% (0.0) match
hin_Deva lang 3.4 146.9 ×42.68 ×7.04 10% (0.7) 0% (0.0) 55% (3.8) 36% (2.5) 0% (0.0) match
jpn_Jpan lang 2.6 24.4 ×9.28 ×1.41 2% (0.7) 0% (0.0) 8% (3.4) 91% (38.2) 0% (0.0) match
kat_Geor lang 3.5 111.1 ×31.36 ×6.64 7% (0.6) 0% (0.0) 37% (3.3) 56% (5.0) 0% (0.0) match
kor_Hang lang 2.5 32.3 ×13.03 ×1.68 2% (0.7) 0% (0.0) 13% (4.0) 85% (26.5) 0% (0.0) match
rus_Cyrl lang 2.8 52.1 ×18.34 ×3.66 3% (0.6) 0% (0.0) 19% (3.7) 77% (14.7) 1% (0.1) match
tam_Taml lang 3.8 77.8 ×20.63 ×4.51 5% (0.6) 0% (0.0) 27% (3.1) 68% (7.8) 0% (0.0) match
tha_Thai lang 4.2 40.4 ×9.56 ×2.89 3% (0.6) 0% (0.0) 11% (2.7) 87% (21.5) 0% (0.0) match
added_normalized_dense modalities 3.5 154.3 ×44.58 ×6.92 14% (0.8) 0% (0.0) 49% (2.8) 38% (2.2) 0% (0.0) match
added_normalized_sparse modalities 3.2 140.9 ×43.36 ×7.52 17% (1.2) 0% (0.0) 53% (3.7) 30% (2.2) 0% (0.0) match
added_special_dense modalities 2.1 66.1 ×31.86 ×1.87 45% (6.4) 3% (0.5) 39% (5.5) 12% (1.7) 0% (0.0) match
added_special_sparse modalities 2.0 76.9 ×37.78 ×3.83 31% (3.8) 1% (0.2) 52% (6.5) 17% (2.1) 0% (0.0) match
agentic-traces modalities 1.9 77.1 ×40.72 ×5.48 15% (1.8) 0% (0.0) 46% (5.3) 40% (4.7) 0% (0.0) match
agentic_swe modalities 2.0 92.3 ×45.45 ×6.40 16% (1.5) 0% (0.0) 46% (4.4) 39% (3.7) 0% (0.0) match
code_mixed modalities 2.1 81.7 ×39.26 ×5.43 15% (1.7) 0% (0.0) 45% (5.0) 39% (4.3) 1% (0.1) match
math_latex modalities 1.8 64.3 ×35.11 ×4.71 14% (1.9) 0% (0.0) 40% (5.5) 47% (6.5) 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.51 5.04 5.88 40.7 20.4 9.2 — 8.1× / 6.9× 4.0× / 3.5× 1.8× / 1.6× —
arb_Arab 1.01 3.02 3.77 5.79 47.5 23.7 10.5 — 12.6× / 8.2× 6.3× / 4.1× 2.8× / 1.8× —
ben_Beng 1.46 2.95 3.53 5.03 36.1 17.2 7.9 — 10.2× / 7.2× 4.9× / 3.4× 2.2× / 1.6× —
cmn_Hani 1.59 2.36 3.52 4.29 58.5 35.0 14.4 — 16.6× / 13.6× 9.9× / 8.1× 4.1× / 3.4× —
ell_Grek 1.02 3.04 3.79 5.80 46.4 22.1 9.9 — 12.3× / 8.0× 5.8× / 3.8× 2.6× / 1.7× —
eng_Latn 0.60 1.80 5.28 6.47 61.5 39.2 15.9 — 11.6× / 9.5× 7.4× / 6.1× 3.0× / 2.5× —
heb_Hebr 1.01 3.11 3.92 6.01 49.2 24.5 10.7 — 12.6× / 8.2× 6.3× / 4.1× 2.7× / 1.8× —
hin_Deva 1.37 3.12 3.76 5.52 38.1 19.6 8.8 — 10.1× / 6.9× 5.2× / 3.6× 2.4× / 1.6× —
jpn_Jpan 1.56 3.50 3.36 5.29 52.6 28.7 12.1 — 15.7× / 9.9× 8.6× / 5.4× 3.6× / 2.3× —
kat_Geor 1.38 2.49 3.32 4.43 32.3 15.5 7.3 — 9.7× / 7.3× 4.7× / 3.5× 2.2× / 1.7× —
kor_Hang 1.09 2.84 4.02 5.77 49.0 28.3 12.2 — 12.2× / 8.5× 7.1× / 4.9× 3.0× / 2.1× —
rus_Cyrl 1.02 3.02 3.66 5.66 46.3 22.0 10.0 — 12.6× / 8.2× 6.0× / 3.9× 2.7× / 1.8× —
tam_Taml 1.37 2.99 3.14 4.76 31.1 13.8 6.6 — 9.9× / 6.5× 4.4× / 2.9× 2.1× / 1.4× —
tha_Thai 1.37 2.58 2.73 3.95 27.4 10.4 5.3 — 10.0× / 6.9× 3.8× / 2.6× 1.9× / 1.3× —
added_normalized_dense 0.08 1.77 2.76 4.44 40.9 21.2 9.4 — 14.8× / 9.2× 7.7× / 4.8× 3.4× / 2.1× —
added_normalized_sparse 0.08 1.76 3.74 5.42 47.4 27.6 11.6 — 12.7× / 8.7× 7.4× / 5.1× 3.1× / 2.1× —
added_special_dense 0.09 1.77 5.52 7.21 168.5 106.5 42.9 — 30.5× / 23.4× 19.3× / 14.8× 7.8× / 5.9× —
added_special_sparse 0.11 1.77 6.47 8.14 98.5 60.9 24.8 — 15.2× / 12.1× 9.4× / 7.5× 3.8× / 3.0× —
agentic-traces 0.61 1.80 5.35 6.53 86.3 67.6 22.4 — 16.1× / 13.2× 12.6× / 10.3× 4.2× / 3.4× —
agentic_swe 0.57 1.77 4.38 5.58 84.1 73.8 21.1 — 19.2× / 15.1× 16.9× / 13.2× 4.8× / 3.8× —
code_mixed 0.57 1.77 5.00 6.19 72.2 65.2 18.7 — 14.5× / 11.7× 13.1× / 10.5× 3.7× / 3.0× —
math_latex 0.63 1.81 5.50 6.69 75.8 50.0 19.4 — 13.8× / 11.3× 9.1× / 7.5× 3.5× / 2.9× —
gemma-4 — byte-fallback BPE, Metaspace-style split (gemma-4) · ×1.85 vs v0.23.1 · ×0.41 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 275+0 (peak 371)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 5.4 13.1 ×2.42 ×0.48 1% (0.6) 3% (2.3) 0% (0.0) 94% (74.7) 2% (2.0) match
arb_Arab lang 2.7 4.0 ×1.46 ×0.30 0% (0.6) 1% (2.8) 0% (0.1) 96% (255.7) 3% (6.8) match
ben_Beng lang 3.8 5.5 ×1.46 ×0.30 0% (0.6) 1% (1.9) 0% (0.1) 101% (198.1) 0% (0.0) match
cmn_Hani lang 6.6 18.0 ×2.74 ×0.53 1% (0.6) 1% (0.5) 0% (0.0) 98% (57.9) 0% (0.0) match
ell_Grek lang 3.1 4.7 ×1.52 ×0.32 0% (0.6) 1% (2.8) 0% (0.1) 99% (223.6) 0% (0.0) match
eng_Latn lang 1.2 1.5 ×1.22 ×0.27 0% (2.0) 1% (5.1) 0% (0.1) 99% (693.0) 0% (0.0) match
heb_Hebr lang 3.3 5.6 ×1.69 ×0.33 0% (0.6) 2% (2.9) 0% (0.1) 99% (184.3) 0% (0.0) match
hin_Deva lang 3.7 5.7 ×1.54 ×0.32 0% (0.6) 1% (2.5) 0% (0.1) 98% (184.5) 0% (0.1) match
jpn_Jpan lang 5.8 11.9 ×2.06 ×0.41 1% (0.6) 0% (0.4) 0% (0.0) 98% (84.3) 0% (0.3) match
kat_Geor lang 4.8 8.5 ×1.77 ×0.35 1% (0.6) 1% (1.6) 0% (0.0) 99% (120.2) 0% (0.0) match
kor_Hang lang 4.8 10.9 ×2.24 ×0.43 1% (0.6) 3% (2.9) 0% (0.1) 97% (87.7) 0% (0.0) match
rus_Cyrl lang 2.7 3.7 ×1.36 ×0.34 0% (0.6) 1% (2.6) 0% (0.0) 99% (280.6) 0% (0.0) match
tam_Taml lang 4.1 6.0 ×1.49 ×0.31 0% (0.6) 1% (1.5) 0% (0.0) 100% (168.7) 0% (0.0) match
tha_Thai lang 4.9 8.0 ×1.62 ×0.34 0% (0.6) 1% (0.8) 0% (0.0) 99% (127.9) 0% (0.0) match
added_normalized_dense modalities 1.5 2.0 ×1.29 ×0.27 0% (0.8) 1% (3.2) 0% (0.1) 90% (498.2) 9% (52.4) match
added_normalized_sparse modalities 1.4 1.9 ×1.29 ×0.29 0% (1.2) 1% (4.8) 0% (0.1) 99% (689.6) 0% (3.1) match
added_special_dense modalities 3.8 37.7 ×9.80 ×1.85 35% (9.6) 31% (8.3) 28% (7.5) 6% (1.7) 0% (0.0) match
added_special_sparse modalities 6.0 50.2 ×8.34 ×5.12 25% (5.2) 40% (8.4) 27% (5.7) 8% (1.7) 0% (0.0) match
agentic-traces modalities 1.5 2.0 ×1.32 ×0.32 0% (1.7) 1% (4.6) 0% (0.1) 99% (598.9) 0% (0.0) match
agentic_swe modalities 1.6 2.2 ×1.39 ×0.33 0% (1.5) 1% (6.1) 0% (0.1) 84% (488.4) 14% (82.7) match
code_mixed modalities 1.5 2.0 ×1.34 ×0.32 0% (1.6) 1% (5.4) 0% (0.1) 100% (634.9) 0% (0.0) match
math_latex modalities 1.2 1.5 ×1.25 ×0.27 0% (1.9) 1% (4.8) 0% (0.1) 110% (851.2) 0% (0.0) match
gpt2 — gpt2 ByteLevel regex · ×35.09 vs v0.23.1 · ×3.03 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 27+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.0 83.5 ×27.68 ×2.04 6% (0.6) 0% (0.0) 30% (3.4) 62% (7.1) 2% (0.2) match
arb_Arab lang 2.9 84.2 ×28.95 ×3.29 5% (0.6) 0% (0.0) 15% (1.7) 79% (9.0) 0% (0.0) match
ben_Beng lang 2.4 129.5 ×54.38 ×2.85 8% (0.6) 0% (0.0) 32% (2.3) 60% (4.4) 0% (0.0) match
cmn_Hani lang 2.7 39.1 ×14.49 ×1.41 3% (0.6) 0% (0.0) 9% (2.3) 87% (21.5) 1% (0.2) match
ell_Grek lang 2.9 111.1 ×38.07 ×3.92 7% (0.6) 0% (0.0) 19% (1.8) 74% (6.7) 0% (0.0) match
eng_Latn lang 2.5 99.5 ×39.96 ×7.32 20% (2.0) 0% (0.0) 14% (1.4) 67% (6.7) 0% (0.0) match
heb_Hebr lang 2.7 81.5 ×29.81 ×2.82 5% (0.6) 0% (0.0) 15% (1.9) 79% (9.7) 1% (0.1) match
hin_Deva lang 2.4 104.6 ×44.20 ×2.81 9% (0.6) 0% (0.0) 32% (2.2) 59% (4.1) 0% (0.0) match
jpn_Jpan lang 2.9 40.5 ×14.06 ×1.91 3% (0.6) 0% (0.0) 9% (2.2) 88% (21.6) 0% (0.0) match
kat_Geor lang 2.7 119.7 ×44.28 ×1.65 9% (0.6) 0% (0.0) 30% (2.1) 59% (4.1) 2% (0.1) match
kor_Hang lang 2.3 46.8 ×20.07 ×1.04 4% (0.6) 0% (0.0) 10% (1.9) 85% (15.5) 1% (0.2) match
rus_Cyrl lang 2.5 66.5 ×26.93 ×2.44 5% (0.6) 0% (0.0) 14% (1.8) 82% (10.8) 0% (0.0) match
tam_Taml lang 2.0 110.7 ×55.76 ×1.48 8% (0.6) 0% (0.0) 30% (2.3) 63% (4.8) 0% (0.0) match
tha_Thai lang 2.4 64.6 ×27.21 ×1.80 6% (0.6) 0% (0.0) 19% (2.1) 76% (8.5) 0% (0.0) match
added_normalized_dense modalities 3.6 164.0 ×45.55 ×6.77 20% (0.7) 0% (0.0) 19% (0.7) 61% (2.2) 0% (0.0) match
added_normalized_sparse modalities 3.5 172.8 ×49.25 ×8.30 29% (1.2) 0% (0.0) 20% (0.8) 52% (2.1) 0% (0.0) match
added_special_dense modalities 2.3 71.2 ×31.05 ×1.55 44% (5.1) 0% (0.0) 42% (4.9) 16% (1.8) 0% (0.0) match
added_special_sparse modalities 2.4 81.8 ×33.93 ×3.63 33% (3.2) 0% (0.0) 48% (4.6) 22% (2.1) 0% (0.0) match
agentic-traces modalities 2.0 105.9 ×51.83 ×6.57 23% (1.7) 0% (0.0) 19% (1.4) 56% (4.2) 2% (0.1) match
agentic_swe modalities 2.1 109.1 ×52.96 ×4.44 20% (1.4) 0% (0.0) 21% (1.5) 59% (4.2) 0% (0.0) match
code_mixed modalities 2.0 105.5 ×53.39 ×5.16 22% (1.6) 0% (0.0) 19% (1.4) 59% (4.3) 0% (0.0) match
math_latex modalities 2.2 95.8 ×44.20 ×6.37 22% (1.9) 0% (0.0) 16% (1.4) 60% (5.3) 2% (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.66 3.52 3.39 4.25 25.7 21.6 5.6 4.8 7.6× / 6.0× 6.4× / 5.1× 1.7× / 1.3× 1.4× / 1.1×
arb_Arab 1.00 3.02 1.74 3.76 30.3 27.3 6.9 5.2 17.4× / 8.0× 15.7× / 7.3× 4.0× / 1.8× 3.0× / 1.4×
ben_Beng 1.45 2.95 2.32 3.82 63.1 59.8 14.3 4.0 27.2× / 16.5× 25.8× / 15.6× 6.2× / 3.7× 1.7× / 1.1×
cmn_Hani 1.59 2.36 2.30 3.08 25.6 22.1 6.0 2.4 11.1× / 8.3× 9.6× / 7.2× 2.6× / 1.9× 1.0× / 0.8×
ell_Grek 1.01 3.04 1.76 3.79 27.1 23.4 6.0 4.7 15.4× / 7.1× 13.3× / 6.2× 3.4× / 1.6× 2.7× / 1.2×
eng_Latn 0.60 1.79 1.38 2.58 42.3 44.9 12.4 3.8 30.6× / 16.4× 32.5× / 17.4× 8.9× / 4.8× 2.8× / 1.5×
heb_Hebr 1.00 3.09 1.86 3.95 30.0 27.7 6.9 3.0 16.1× / 7.6× 14.9× / 7.0× 3.7× / 1.8× 1.6× / 0.8×
hin_Deva 1.36 3.19 2.23 4.06 58.7 58.7 14.0 4.2 26.4× / 14.5× 26.4× / 14.5× 6.3× / 3.5× 1.9× / 1.0×
jpn_Jpan 1.55 3.52 2.24 4.21 23.1 19.0 5.1 3.9 10.3× / 5.5× 8.4× / 4.5× 2.3× / 1.2× 1.7× / 0.9×
kat_Geor 1.38 2.48 2.07 3.17 16.4 14.8 4.1 1.9 7.9× / 5.2× 7.2× / 4.7× 2.0× / 1.3× 0.9× / 0.6×
kor_Hang 1.09 2.82 1.87 3.60 30.3 29.6 7.4 3.7 16.3× / 8.4× 15.9× / 8.2× 4.0× / 2.1× 2.0× / 1.0×
rus_Cyrl 1.02 3.00 1.77 3.75 26.5 23.3 5.9 2.5 14.9× / 7.1× 13.2× / 6.2× 3.4× / 1.6× 1.4× / 0.7×
tam_Taml 1.37 2.96 2.25 3.84 66.9 62.8 14.9 3.8 29.7× / 17.4× 27.9× / 16.3× 6.6× / 3.9× 1.7× / 1.0×
tha_Thai 1.36 2.61 2.12 3.37 38.1 33.6 8.7 3.2 17.9× / 11.3× 15.8× / 10.0× 4.1× / 2.6× 1.5× / 0.9×
added_normalized_dense 0.07 1.75 0.68 2.36 22.6 25.7 6.5 2.0 33.2× / 9.6× 37.7× / 10.9× 9.6× / 2.8× 2.9× / 0.8×
added_normalized_sparse 0.07 1.73 0.84 2.50 30.2 35.0 8.8 2.7 35.8× / 12.1× 41.5× / 14.0× 10.4× / 3.5× 3.2× / 1.1×
added_special_dense 0.08 1.75 4.91 6.58 90.9 107.3 20.6 2.9 18.5× / 13.8× 21.9× / 16.3× 4.2× / 3.1× 0.6× / 0.4×
added_special_sparse 0.08 1.73 4.61 6.27 58.9 68.2 15.5 3.4 12.8× / 9.4× 14.8× / 10.9× 3.4× / 2.5× 0.7× / 0.5×
agentic-traces 0.56 1.77 1.42 2.63 58.2 73.1 16.0 4.5 41.0× / 22.1× 51.5× / 27.8× 11.3× / 6.1× 3.2× / 1.7×
agentic_swe 0.57 1.76 1.46 2.65 58.1 82.6 15.4 3.9 39.7× / 21.9× 56.5× / 31.1× 10.5× / 5.8× 2.7× / 1.5×
code_mixed 0.53 1.77 1.42 2.65 57.8 83.0 15.9 4.3 40.7× / 21.8× 58.5× / 31.3× 11.2× / 6.0× 3.0× / 1.6×
math_latex 0.61 1.81 1.44 2.63 50.2 55.7 14.3 4.2 34.9× / 19.1× 38.8× / 21.2× 10.0× / 5.4× 2.9× / 1.6×
gpt-oss — o200k-regex byte-level BPE (gpt-oss) · ×20.66 vs v0.23.1 · ×2.74 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.0 52.7 ×17.55 ×2.31 4% (0.6) 0% (0.0) 31% (5.0) 65% (10.4) 0% (0.0) match
arb_Arab lang 3.0 51.4 ×17.26 ×2.89 3% (0.6) 0% (0.0) 20% (3.6) 76% (13.7) 0% (0.0) match
ben_Beng lang 4.0 125.6 ×31.54 ×7.48 9% (0.6) 0% (0.0) 57% (4.0) 34% (2.4) 0% (0.0) match
cmn_Hani lang 3.1 14.3 ×4.57 ×1.23 1% (0.6) 0% (0.0) 6% (3.7) 94% (63.2) 0% (0.0) match
ell_Grek lang 3.3 75.5 ×23.06 ×4.79 5% (0.6) 0% (0.0) 30% (3.5) 66% (7.6) 0% (0.0) match
eng_Latn lang 2.6 70.9 ×27.33 ×1.76 16% (2.0) 0% (0.0) 38% (4.7) 46% (5.7) 0% (0.0) match
heb_Hebr lang 2.9 54.0 ×18.68 ×3.31 4% (0.6) 0% (0.0) 22% (3.7) 75% (12.5) 0% (0.0) match
hin_Deva lang 3.8 131.6 ×34.31 ×5.00 9% (0.6) 0% (0.0) 62% (4.1) 28% (1.9) 0% (0.0) match
jpn_Jpan lang 3.4 17.1 ×5.02 ×1.38 1% (0.6) 0% (0.0) 6% (3.4) 92% (51.8) 1% (0.6) match
kat_Geor lang 3.9 97.8 ×25.09 ×7.10 7% (0.6) 0% (0.0) 36% (3.2) 58% (5.3) 0% (0.0) match
kor_Hang lang 2.7 24.9 ×9.20 ×1.68 2% (0.6) 0% (0.0) 10% (4.0) 89% (34.1) 0% (0.0) match
rus_Cyrl lang 3.2 46.2 ×14.47 ×3.12 3% (0.6) 0% (0.0) 17% (3.4) 81% (16.5) 0% (0.0) match
tam_Taml lang 3.9 66.0 ×16.97 ×5.15 4% (0.6) 0% (0.0) 26% (3.5) 70% (9.6) 0% (0.0) match
tha_Thai lang 4.3 31.8 ×7.33 ×2.87 2% (0.6) 0% (0.0) 12% (3.5) 86% (25.9) 0% (0.0) match
added_normalized_dense modalities 3.6 162.7 ×44.70 ×6.79 16% (0.7) 0% (0.0) 43% (2.0) 42% (2.0) 0% (0.0) match
added_normalized_sparse modalities 3.4 135.7 ×39.51 ×3.39 19% (1.2) 0% (0.0) 49% (3.0) 32% (2.0) 0% (0.0) match
added_special_dense modalities 2.4 74.9 ×31.59 ×1.01 46% (5.1) 0% (0.0) 43% (4.8) 14% (1.6) 0% (0.0) match
added_special_sparse modalities 2.5 76.6 ×31.12 ×1.02 31% (3.2) 0% (0.0) 54% (5.7) 19% (2.0) 0% (0.0) match
agentic-traces modalities 2.5 83.1 ×32.70 ×2.25 16% (1.7) 0% (0.0) 47% (4.9) 37% (3.8) 0% (0.0) match
agentic_swe modalities 2.5 93.4 ×36.97 ×3.48 16% (1.4) 0% (0.0) 46% (4.1) 39% (3.5) 0% (0.0) match
code_mixed modalities 2.4 87.1 ×36.11 ×1.80 16% (1.6) 0% (0.0) 46% (4.6) 37% (3.7) 1% (0.1) match
math_latex modalities 2.4 72.5 ×30.86 ×1.97 16% (1.9) 0% (0.0) 41% (5.0) 43% (5.1) 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.76 3.51 5.01 5.76 28.2 13.9 6.6 4.7 5.6× / 4.9× 2.8× / 2.4× 1.3× / 1.1× 0.9× / 0.8×
arb_Arab 1.00 3.00 3.59 5.60 33.0 16.5 7.8 5.0 9.2× / 5.9× 4.6× / 3.0× 2.2× / 1.4× 1.4× / 0.9×
ben_Beng 1.45 3.01 3.98 5.54 23.4 11.3 5.4 2.8 5.9× / 4.2× 2.8× / 2.0× 1.4× / 1.0× 0.7× / 0.5×
cmn_Hani 1.58 2.37 3.70 4.49 21.2 11.2 5.4 2.5 5.7× / 4.7× 3.0× / 2.5× 1.5× / 1.2× 0.7× / 0.5×
ell_Grek 1.01 3.02 3.50 5.51 29.0 15.8 6.9 5.1 8.3× / 5.3× 4.5× / 2.9× 2.0× / 1.2× 1.4× / 0.9×
eng_Latn 0.57 1.75 4.74 5.92 41.5 29.1 13.6 4.2 8.8× / 7.0× 6.1× / 4.9× 2.9× / 2.3× 0.9× / 0.7×
heb_Hebr 1.02 3.08 3.71 5.77 32.6 17.6 8.2 2.9 8.8× / 5.6× 4.8× / 3.1× 2.2× / 1.4× 0.8× / 0.5×
hin_Deva 1.43 3.11 4.14 5.82 25.8 13.2 6.4 3.2 6.2× / 4.4× 3.2× / 2.3× 1.5× / 1.1× 0.8× / 0.5×
jpn_Jpan 1.55 3.49 3.45 5.39 20.2 9.7 4.8 3.7 5.9× / 3.8× 2.8× / 1.8× 1.4× / 0.9× 1.1× / 0.7×
kat_Geor 1.38 2.48 3.24 4.35 18.3 10.4 4.5 2.1 5.6× / 4.2× 3.2× / 2.4× 1.4× / 1.0× 0.6× / 0.5×
kor_Hang 1.09 2.80 3.99 5.70 32.5 19.3 8.9 3.6 8.1× / 5.7× 4.8× / 3.4× 2.2× / 1.6× 0.9× / 0.6×
rus_Cyrl 1.02 2.98 3.44 5.40 28.3 15.6 6.7 4.8 8.2× / 5.2× 4.5× / 2.9× 2.0× / 1.2× 1.4× / 0.9×
tam_Taml 1.37 2.95 3.54 5.12 18.7 8.9 4.2 3.0 5.3× / 3.7× 2.5× / 1.7× 1.2× / 0.8× 0.8× / 0.6×
tha_Thai 1.36 2.58 3.52 4.73 13.0 5.8 2.7 2.2 3.7× / 2.8× 1.7× / 1.2× 0.8× / 0.6× 0.6× / 0.5×
added_normalized_dense 0.08 1.72 2.04 3.68 33.0 18.2 11.6 2.1 16.2× / 9.0× 8.9× / 4.9× 5.7× / 3.2× 1.0× / 0.6×
added_normalized_sparse 0.08 1.72 3.00 4.64 35.1 21.6 11.9 2.9 11.7× / 7.6× 7.2× / 4.6× 4.0× / 2.6× 1.0× / 0.6×
added_special_dense 0.08 1.72 4.81 6.46 82.1 70.2 24.1 3.4 17.1× / 12.7× 14.6× / 10.9× 5.0× / 3.7× 0.7× / 0.5×
added_special_sparse 0.08 1.72 5.71 7.35 55.4 43.2 17.0 3.6 9.7× / 7.5× 7.6× / 5.9× 3.0× / 2.3× 0.6× / 0.5×
agentic-traces 0.55 1.75 4.89 6.09 52.1 47.2 17.3 4.7 10.7× / 8.6× 9.6× / 7.8× 3.5× / 2.8× 1.0× / 0.8×
agentic_swe 0.52 1.73 4.13 5.34 53.4 55.2 17.5 4.0 12.9× / 10.0× 13.4× / 10.3× 4.2× / 3.3× 1.0× / 0.8×
code_mixed 0.52 1.73 4.61 5.82 53.5 52.8 17.9 4.5 11.6× / 9.2× 11.4× / 9.1× 3.9× / 3.1× 1.0× / 0.8×
math_latex 0.60 1.77 4.98 6.15 48.2 37.1 16.0 4.6 9.7× / 7.8× 7.5× / 6.0× 3.2× / 2.6× 0.9× / 0.7×
glm-5.2 — cl100k-variant regex byte-level BPE (glm-5.2) · ×24.40 vs v0.23.1 · ×2.70 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 231)

Fixture Group v0.23.1 MB/s Pipeline MB/s Speedup Δ base added-token normalize pre-tokenize model post Ids
amh_Ethi lang 3.2 72.1 ×22.32 ×1.60 9% (1.2) 0% (0.0) 27% (3.5) 64% (8.3) 0% (0.0) match
arb_Arab lang 3.5 70.4 ×20.04 ×4.14 8% (1.2) 0% (0.0) 13% (1.9) 77% (11.1) 2% (0.3) match
ben_Beng lang 3.3 155.9 ×47.58 ×5.73 19% (1.2) 0% (0.0) 39% (2.4) 43% (2.6) 0% (0.0) match
cmn_Hani lang 3.6 16.3 ×4.49 ×1.16 2% (1.2) 0% (0.0) 4% (2.4) 93% (57.5) 1% (0.5) match
ell_Grek lang 3.8 96.3 ×25.21 ×4.93 11% (1.2) 0% (0.0) 18% (1.9) 69% (7.2) 1% (0.1) match
eng_Latn lang 2.8 95.0 ×33.38 ×2.19 24% (2.6) 0% (0.0) 15% (1.6) 60% (6.3) 1% (0.1) match
heb_Hebr lang 3.0 68.7 ×23.06 ×2.78 9% (1.2) 0% (0.0) 14% (1.9) 81% (10.8) 0% (0.0) match
hin_Deva lang 2.9 161.8 ×55.57 ×5.31 19% (1.2) 0% (0.0) 38% (2.3) 43% (2.7) 0% (0.0) match
jpn_Jpan lang 3.8 20.4 ×5.37 ×1.36 2% (1.2) 0% (0.0) 5% (2.4) 94% (45.8) 0% (0.0) match
kat_Geor lang 3.9 106.6 ×27.00 ×4.80 15% (1.2) 0% (0.0) 27% (2.2) 59% (4.7) 0% (0.0) match
kor_Hang lang 3.0 29.0 ×9.81 ×1.62 4% (1.2) 0% (0.0) 6% (2.0) 89% (28.5) 1% (0.3) match
rus_Cyrl lang 3.3 51.4 ×15.51 ×2.78 6% (1.2) 0% (0.0) 10% (1.9) 85% (15.7) 0% (0.0) match
tam_Taml lang 2.7 115.4 ×41.98 ×3.27 16% (1.2) 0% (0.0) 32% (2.3) 52% (3.7) 0% (0.0) match
tha_Thai lang 3.2 48.5 ×14.96 ×2.28 6% (1.2) 0% (0.0) 12% (2.2) 82% (15.2) 0% (0.0) match
added_normalized_dense modalities 3.9 186.3 ×47.42 ×7.24 31% (1.3) 0% (0.0) 20% (0.8) 48% (2.0) 0% (0.0) match
added_normalized_sparse modalities 3.7 168.2 ×44.92 ×4.13 37% (1.8) 0% (0.0) 21% (1.0) 42% (2.0) 0% (0.0) match
added_special_dense modalities 2.4 43.0 ×17.91 ×0.94 60% (12.6) 0% (0.0) 32% (6.7) 9% (1.9) 0% (0.0) match
added_special_sparse modalities 2.4 60.0 ×24.70 ×1.07 48% (6.7) 0% (0.0) 39% (5.5) 13% (1.9) 0% (0.0) match
agentic-traces modalities 2.6 104.6 ×39.52 ×2.99 30% (2.4) 0% (0.0) 20% (1.6) 49% (3.9) 1% (0.1) match
agentic_swe modalities 2.7 113.7 ×42.85 ×4.21 29% (2.1) 0% (0.0) 23% (1.7) 49% (3.6) 0% (0.0) match
code_mixed modalities 2.5 106.2 ×42.05 ×2.39 29% (2.3) 0% (0.0) 20% (1.6) 52% (4.1) 0% (0.0) match
math_latex modalities 2.5 87.1 ×34.91 ×2.27 25% (2.5) 0% (0.0) 16% (1.6) 56% (5.5) 2% (0.2) 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.66 3.57 3.52 4.43 29.5 16.2 5.8 4.7 8.4× / 6.7× 4.6× / 3.7× 1.6× / 1.3× 1.3× / 1.1×
arb_Arab 1.00 2.99 1.89 3.89 34.8 20.2 7.2 5.1 18.4× / 8.9× 10.7× / 5.2× 3.8× / 1.9× 2.7× / 1.3×
ben_Beng 1.45 2.94 2.38 3.88 50.4 29.6 10.8 3.7 21.2× / 13.0× 12.4× / 7.6× 4.5× / 2.8× 1.6× / 1.0×
cmn_Hani 1.58 2.37 2.43 3.22 21.5 12.0 4.9 2.4 8.8× / 6.7× 4.9× / 3.7× 2.0× / 1.5× 1.0× / 0.8×
ell_Grek 1.01 3.02 1.87 3.88 31.5 17.8 6.3 4.7 16.8× / 8.1× 9.5× / 4.6× 3.4× / 1.6× 2.5× / 1.2×
eng_Latn 0.59 1.76 1.56 2.73 50.2 34.0 13.0 3.7 32.1× / 18.4× 21.7× / 12.4× 8.3× / 4.8× 2.4× / 1.4×
heb_Hebr 1.00 3.10 1.89 3.99 34.8 20.9 7.3 3.0 18.5× / 8.7× 11.0× / 5.2× 3.9× / 1.8× 1.6× / 0.7×
hin_Deva 1.36 3.11 2.32 4.07 53.1 31.9 12.7 4.0 22.8× / 13.0× 13.7× / 7.8× 5.5× / 3.1× 1.7× / 1.0×
jpn_Jpan 1.55 3.50 2.38 4.33 20.2 10.7 4.3 3.7 8.5× / 4.7× 4.5× / 2.5× 1.8× / 1.0× 1.6× / 0.9×
kat_Geor 1.38 2.47 2.16 3.25 18.7 11.4 4.2 2.0 8.6× / 5.7× 5.3× / 3.5× 2.0× / 1.3× 0.9× / 0.6×
kor_Hang 1.16 2.81 2.03 3.68 35.2 22.3 7.8 3.8 17.4× / 9.6× 11.0× / 6.1× 3.8× / 2.1× 1.9× / 1.0×
rus_Cyrl 1.03 2.97 1.87 3.81 30.4 17.7 6.4 2.5 16.3× / 8.0× 9.5× / 4.6× 3.4× / 1.7× 1.3× / 0.6×
tam_Taml 1.37 2.94 2.28 3.85 49.6 29.0 10.3 3.4 21.7× / 12.9× 12.7× / 7.5× 4.5× / 2.7× 1.5× / 0.9×
tha_Thai 1.36 2.59 2.19 3.42 30.9 17.2 6.9 3.0 14.1× / 9.0× 7.8× / 5.0× 3.1× / 2.0× 1.4× / 0.9×
added_normalized_dense 0.07 1.73 0.83 2.50 27.5 18.4 7.1 1.9 33.0× / 11.0× 22.1× / 7.4× 8.5× / 2.8× 2.3× / 0.8×
added_normalized_sparse 0.07 1.73 0.97 2.62 36.5 24.1 9.5 2.6 37.7× / 13.9× 24.9× / 9.2× 9.8× / 3.6× 2.7× / 1.0×
added_special_dense 0.07 1.73 6.74 8.40 109.6 77.9 22.4 3.1 16.3× / 13.0× 11.6× / 9.3× 3.3× / 2.7× 0.5× / 0.4×
added_special_sparse 0.07 1.73 5.51 7.17 70.5 48.6 16.4 3.5 12.8× / 9.8× 8.8× / 6.8× 3.0× / 2.3× 0.6× / 0.5×
agentic-traces 0.57 1.76 1.62 2.80 63.0 53.0 16.2 4.4 39.0× / 22.5× 32.8× / 18.9× 10.1× / 5.8× 2.7× / 1.6×
agentic_swe 0.53 1.73 1.65 2.86 64.7 59.5 16.6 3.8 39.1× / 22.6× 36.0× / 20.8× 10.1× / 5.8× 2.3× / 1.3×
code_mixed 0.52 1.73 1.60 2.81 62.3 56.3 16.4 4.1 38.9× / 22.2× 35.2× / 20.0× 10.2× / 5.8× 2.6× / 1.5×
math_latex 0.60 1.78 1.61 2.79 58.4 41.8 15.3 4.1 36.2× / 20.9× 25.9× / 15.0× 9.5× / 5.5× 2.6× / 1.5×
llama-2 — model-bounded BPE, no pre-tokenizer · ×4.66 vs v0.23.1 · ×0.59 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 16+0 (peak 20) · Pipeline 24+0 (peak 24)

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 31.7 ×11.00 ×0.77 0% (0.1) 9% (2.9) 0% (0.0) 91% (28.6) 0% (0.0) match
arb_Arab lang 5.2 31.4 ×6.05 ×0.63 0% (0.1) 11% (3.5) 0% (0.0) 88% (27.7) 0% (0.1) match
ben_Beng lang 5.5 57.7 ×10.51 ×0.71 0% (0.1) 15% (2.5) 0% (0.0) 86% (14.6) 0% (0.0) match
cmn_Hani lang 5.1 46.7 ×9.19 ×0.76 0% (0.1) 3% (0.6) 0% (0.0) 98% (20.5) 0% (0.0) match
ell_Grek lang 5.2 39.0 ×7.51 ×0.68 0% (0.1) 14% (3.5) 0% (0.0) 85% (21.7) 1% (0.2) match
eng_Latn lang 1.4 1.7 ×1.26 ×0.28 0% (0.1) 1% (7.5) 0% (0.0) 98% (653.2) 0% (2.8) match
heb_Hebr lang 5.2 44.9 ×8.70 ×0.73 0% (0.1) 17% (3.7) 0% (0.0) 85% (18.6) 0% (0.0) match
hin_Deva lang 6.0 53.6 ×8.96 ×0.68 0% (0.1) 17% (3.3) 0% (0.0) 81% (15.2) 1% (0.2) match
jpn_Jpan lang 6.6 61.0 ×9.26 ×0.76 0% (0.1) 4% (0.6) 0% (0.0) 95% (15.6) 0% (0.0) match
kat_Geor lang 7.2 69.1 ×9.61 ×0.80 1% (0.1) 16% (2.2) 0% (0.0) 84% (12.1) 0% (0.0) match
kor_Hang lang 4.1 34.3 ×8.29 ×0.66 0% (0.1) 13% (3.7) 0% (0.0) 87% (24.3) 0% (0.0) match
rus_Cyrl lang 3.0 4.4 ×1.48 ×0.27 0% (0.1) 2% (3.3) 0% (0.0) 100% (215.5) 0% (0.0) match
tam_Taml lang 6.0 67.5 ×11.21 ×0.79 0% (0.1) 14% (2.1) 0% (0.0) 85% (12.5) 0% (0.0) match
tha_Thai lang 7.6 61.4 ×8.07 ×0.74 0% (0.1) 8% (1.3) 0% (0.0) 92% (15.1) 0% (0.0) match
added_normalized_dense modalities 1.9 2.6 ×1.37 ×0.30 0% (0.1) 1% (4.2) 0% (0.0) 102% (394.9) 0% (0.0) match
added_normalized_sparse modalities 1.2 1.5 ×1.24 ×0.21 0% (0.1) 1% (6.6) 0% (0.0) 100% (533.6) 0% (0.0) match
added_special_dense modalities 2.5 41.2 ×16.56 ×2.00 21% (5.0) 66% (15.6) 5% (1.3) 8% (1.9) 0% (0.0) match
added_special_sparse modalities 4.2 52.7 ×12.70 ×5.12 11% (2.1) 77% (14.5) 3% (0.6) 9% (1.7) 0% (0.0) match
agentic-traces modalities 1.5 2.0 ×1.33 ×0.28 0% (0.1) 1% (6.4) 0% (0.0) 97% (570.8) 2% (11.8) match
agentic_swe modalities 1.8 2.5 ×1.40 ×0.36 0% (0.1) 2% (8.2) 0% (0.0) 102% (452.4) 0% (0.0) match
code_mixed modalities 1.6 2.0 ×1.30 ×0.30 0% (0.1) 1% (7.3) 0% (0.0) 89% (550.3) 10% (59.2) match
math_latex modalities 1.5 1.8 ×1.26 ×0.28 0% (0.1) 1% (6.9) 0% (0.0) 110% (637.6) 0% (0.0) match
llama-3 — cl100k-regex byte-level BPE (llama-3), single regex · ×29.54 vs v0.23.1 · ×2.79 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 92+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 2.9 75.1 ×25.56 ×1.57 5% (0.6) 0% (0.0) 28% (3.5) 63% (8.0) 4% (0.5) match
arb_Arab lang 3.4 70.1 ×20.82 ×3.94 4% (0.6) 0% (0.0) 14% (1.9) 82% (11.4) 0% (0.0) match
ben_Beng lang 3.0 149.2 ×49.91 ×4.80 10% (0.6) 0% (0.0) 37% (2.4) 53% (3.4) 0% (0.0) match
cmn_Hani lang 3.6 18.4 ×5.14 ×1.11 1% (0.6) 0% (0.0) 4% (2.4) 94% (51.4) 1% (0.3) match
ell_Grek lang 3.7 99.0 ×26.90 ×5.15 6% (0.6) 0% (0.0) 19% (1.9) 74% (7.2) 0% (0.0) match
eng_Latn lang 2.8 99.0 ×35.78 ×2.17 20% (2.0) 0% (0.0) 16% (1.6) 62% (6.1) 1% (0.1) match
heb_Hebr lang 2.9 73.8 ×25.72 ×2.95 5% (0.6) 0% (0.0) 15% (1.9) 80% (10.5) 1% (0.1) match
hin_Deva lang 3.1 181.3 ×57.87 ×2.37 12% (0.6) 0% (0.0) 43% (2.3) 46% (2.5) 0% (0.0) match
jpn_Jpan lang 3.8 21.1 ×5.61 ×1.34 1% (0.6) 0% (0.0) 5% (2.4) 93% (43.3) 0% (0.1) match
kat_Geor lang 3.2 132.1 ×41.26 ×3.46 9% (0.6) 0% (0.0) 32% (2.2) 58% (3.9) 1% (0.1) match
kor_Hang lang 2.8 29.1 ×10.28 ×1.57 2% (0.6) 0% (0.0) 6% (2.0) 93% (29.6) 0% (0.0) match
rus_Cyrl lang 3.2 54.6 ×16.99 ×3.30 3% (0.6) 0% (0.0) 10% (1.9) 85% (15.1) 1% (0.2) match
tam_Taml lang 2.6 146.1 ×56.40 ×4.31 9% (0.6) 0% (0.0) 35% (2.3) 56% (3.6) 0% (0.0) match
tha_Thai lang 3.3 46.4 ×14.17 ×2.33 3% (0.6) 0% (0.0) 11% (2.2) 86% (17.4) 0% (0.0) match
added_normalized_dense modalities 3.8 262.3 ×69.89 ×10.21 21% (0.7) 0% (0.0) 23% (0.8) 56% (2.0) 0% (0.0) match
added_normalized_sparse modalities 3.5 225.9 ×65.31 ×5.43 29% (1.2) 0% (0.0) 24% (1.0) 48% (2.0) 0% (0.0) match
added_special_dense modalities 2.4 74.5 ×31.67 ×1.04 38% (5.1) 2% (0.2) 46% (6.2) 17% (2.3) 0% (0.0) match
added_special_sparse modalities 2.5 92.3 ×37.68 ×1.30 31% (3.3) 0% (0.0) 51% (5.4) 21% (2.2) 0% (0.0) match
agentic-traces modalities 2.6 137.3 ×53.51 ×3.71 23% (1.7) 0% (0.0) 23% (1.6) 54% (3.9) 0% (0.0) match
agentic_swe modalities 2.6 141.6 ×54.52 ×5.03 22% (1.4) 0% (0.0) 25% (1.7) 53% (3.5) 0% (0.0) match
code_mixed modalities 2.5 130.2 ×52.03 ×2.79 22% (1.6) 0% (0.0) 22% (1.6) 53% (3.9) 3% (0.2) match
math_latex modalities 2.4 103.9 ×42.81 ×2.57 21% (1.9) 0% (0.0) 18% (1.6) 63% (5.7) 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.51 3.53 4.37 24.6 16.6 5.8 4.7 7.0× / 5.6× 4.7× / 3.8× 1.6× / 1.3× 1.3× / 1.1×
arb_Arab 1.00 3.02 1.89 3.90 28.9 20.6 7.3 5.2 15.3× / 7.4× 10.9× / 5.3× 3.9× / 1.9× 2.7× / 1.3×
ben_Beng 1.45 2.95 2.42 3.92 41.6 29.4 10.8 3.7 17.2× / 10.6× 12.2× / 7.5× 4.5× / 2.8× 1.5× / 1.0×
cmn_Hani 1.58 2.35 2.43 3.20 18.4 12.3 4.9 2.5 7.6× / 5.8× 5.0× / 3.8× 2.0× / 1.5× 1.0× / 0.8×
ell_Grek 1.01 3.04 1.89 3.92 26.1 18.2 6.3 4.8 13.8× / 6.7× 9.6× / 4.6× 3.3× / 1.6× 2.5× / 1.2×
eng_Latn 0.57 1.75 1.57 2.75 39.3 34.5 12.9 3.7 25.1× / 14.3× 22.0× / 12.6× 8.2× / 4.7× 2.4× / 1.4×
heb_Hebr 1.00 3.08 1.93 4.01 28.5 21.1 7.2 3.0 14.8× / 7.1× 10.9× / 5.3× 3.8× / 1.8× 1.5× / 0.7×
hin_Deva 1.36 3.12 2.34 4.09 42.8 32.4 12.0 4.0 18.3× / 10.4× 13.8× / 7.9× 5.1× / 2.9× 1.7× / 1.0×
jpn_Jpan 1.55 3.50 2.41 4.36 17.7 10.7 4.3 3.7 7.3× / 4.0× 4.5× / 2.5× 1.8× / 1.0× 1.5× / 0.8×
kat_Geor 1.38 2.48 2.15 3.26 15.8 11.6 4.3 2.0 7.3× / 4.8× 5.4× / 3.6× 2.0× / 1.3× 0.9× / 0.6×
kor_Hang 1.10 2.82 2.04 3.77 28.5 22.9 7.7 3.8 13.9× / 7.6× 11.2× / 6.1× 3.8× / 2.1× 1.9× / 1.0×
rus_Cyrl 1.01 3.00 1.87 3.86 25.3 18.5 6.2 2.5 13.5× / 6.5× 9.9× / 4.8× 3.3× / 1.6× 1.3× / 0.6×
tam_Taml 1.37 2.95 2.30 3.88 40.4 28.3 10.4 3.3 17.6× / 10.4× 12.3× / 7.3× 4.5× / 2.7× 1.5× / 0.9×
tha_Thai 1.36 2.58 2.21 3.42 26.0 17.1 6.9 3.0 11.8× / 7.6× 7.7× / 5.0× 3.1× / 2.0× 1.3× / 0.9×
added_normalized_dense 0.07 1.73 0.83 2.50 21.6 18.5 6.9 1.9 26.0× / 8.7× 22.2× / 7.4× 8.3× / 2.8× 2.3× / 0.8×
added_normalized_sparse 0.07 1.72 1.00 2.66 28.5 24.6 9.2 2.6 28.4× / 10.7× 24.6× / 9.3× 9.2× / 3.5× 2.6× / 1.0×
added_special_dense 0.07 1.72 6.18 7.83 90.8 81.9 25.5 3.2 14.7× / 11.6× 13.3× / 10.5× 4.1× / 3.3× 0.5× / 0.4×
added_special_sparse 0.07 1.72 5.40 7.06 55.4 49.5 16.4 3.5 10.3× / 7.8× 9.2× / 7.0× 3.0× / 2.3× 0.6× / 0.5×
agentic-traces 0.56 1.76 1.65 2.84 51.3 52.0 15.9 4.4 31.2× / 18.1× 31.6× / 18.3× 9.7× / 5.6× 2.7× / 1.5×
agentic_swe 0.53 1.74 1.68 2.89 52.1 59.5 16.3 3.8 30.9× / 18.1× 35.4× / 20.6× 9.7× / 5.6× 2.3× / 1.3×
code_mixed 0.53 1.73 1.60 2.81 50.0 56.7 16.2 4.1 31.2× / 17.8× 35.3× / 20.2× 10.1× / 5.8× 2.6× / 1.5×
math_latex 0.62 1.77 1.64 2.79 46.1 41.7 14.7 4.1 28.1× / 16.5× 25.4× / 15.0× 8.9× / 5.3× 2.5× / 1.5×
mistral-small-4 — tekken byte-level BPE, 1k added specials (mistral-small-4) · ×9.15 vs v0.23.1 · ×1.01 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 193) · 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 2.6 35.2 ×13.56 ×0.84 5% (1.2) 0% (0.0) 64% (15.9) 31% (7.8) 0% (0.0) match
arb_Arab lang 2.9 28.9 ×10.11 ×1.37 4% (1.2) 0% (0.0) 57% (18.6) 38% (12.4) 1% (0.3) match
ben_Beng lang 3.7 52.7 ×14.10 ×2.78 7% (1.2) 0% (0.0) 77% (12.5) 16% (2.6) 0% (0.0) match
cmn_Hani lang 3.1 15.3 ×4.88 ×0.89 2% (1.2) 0% (0.0) 21% (12.8) 77% (48.0) 0% (0.1) match
ell_Grek lang 3.1 36.0 ×11.75 ×1.95 5% (1.2) 0% (0.0) 69% (17.7) 26% (6.7) 1% (0.1) match
eng_Latn lang 2.5 23.5 ×9.41 ×0.61 6% (2.5) 0% (0.0) 79% (33.3) 15% (6.1) 0% (0.0) match
heb_Hebr lang 2.8 28.7 ×10.28 ×1.70 4% (1.2) 0% (0.0) 60% (19.8) 37% (12.2) 0% (0.1) match
hin_Deva lang 3.5 49.4 ×14.16 ×1.95 6% (1.2) 0% (0.0) 82% (15.0) 12% (2.1) 0% (0.0) match
jpn_Jpan lang 3.2 16.8 ×5.24 ×1.06 2% (1.2) 0% (0.0) 20% (11.2) 77% (42.7) 0% (0.1) match
kat_Geor lang 3.5 50.4 ×14.47 ×3.40 7% (1.2) 0% (0.0) 65% (11.5) 29% (5.2) 0% (0.0) match
kor_Hang lang 2.4 17.4 ×7.16 ×0.92 2% (1.2) 0% (0.0) 41% (22.0) 57% (30.5) 0% (0.0) match
rus_Cyrl lang 2.8 27.6 ×9.79 ×1.67 3% (1.2) 0% (0.0) 52% (17.6) 45% (15.3) 0% (0.0) match
tam_Taml lang 3.6 43.8 ×12.11 ×2.91 6% (1.2) 0% (0.0) 49% (9.7) 45% (9.0) 0% (0.0) match
tha_Thai lang 4.3 33.5 ×7.86 ×2.29 4% (1.2) 0% (0.0) 23% (6.4) 72% (20.2) 0% (0.1) match
added_normalized_dense modalities 3.3 40.0 ×12.13 ×1.41 6% (1.3) 0% (0.0) 86% (19.5) 9% (2.0) 0% (0.0) match
added_normalized_sparse modalities 3.0 33.1 ×11.09 ×0.79 6% (1.7) 0% (0.0) 87% (23.3) 7% (1.8) 0% (0.1) match
added_special_dense modalities 2.1 15.8 ×7.64 ×0.23 8% (5.1) 0% (0.0) 89% (54.0) 3% (1.7) 0% (0.0) match
added_special_sparse modalities 2.2 21.8 ×9.98 ×0.34 9% (3.6) 0% (0.0) 87% (36.8) 4% (1.8) 0% (0.0) match
agentic-traces modalities 2.1 14.1 ×6.78 ×0.41 4% (2.3) 0% (0.0) 92% (57.6) 5% (2.9) 0% (0.0) match
agentic_swe modalities 2.4 13.2 ×5.61 ×0.54 3% (2.0) 0% (0.0) 92% (65.9) 4% (3.2) 1% (0.9) match
code_mixed modalities 2.3 14.1 ×6.10 ×0.33 3% (2.2) 0% (0.0) 91% (59.3) 5% (3.6) 0% (0.0) match
math_latex modalities 2.3 18.3 ×7.84 ×0.46 5% (2.5) 0% (0.0) 85% (42.0) 10% (4.9) 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.68 3.51 15.85 16.69 26.7 14.7 6.6 — 1.7× / 1.6× 0.9× / 0.9× 0.4× / 0.4× —
arb_Arab 1.00 3.03 18.56 20.59 31.3 17.4 7.8 — 1.7× / 1.5× 0.9× / 0.8× 0.4× / 0.4× —
ben_Beng 1.45 2.95 12.54 14.05 22.2 11.7 5.5 — 1.8× / 1.6× 0.9× / 0.8× 0.4× / 0.4× —
cmn_Hani 1.59 2.35 12.80 13.57 21.0 12.1 5.8 — 1.6× / 1.5× 0.9× / 0.9× 0.5× / 0.4× —
ell_Grek 1.02 3.05 17.71 19.74 27.6 16.4 7.1 — 1.6× / 1.4× 0.9× / 0.8× 0.4× / 0.4× —
eng_Latn 0.58 1.78 33.32 34.51 38.9 30.6 13.7 — 1.2× / 1.1× 0.9× / 0.9× 0.4× / 0.4× —
heb_Hebr 1.00 3.09 19.82 21.91 30.7 18.5 8.2 — 1.6× / 1.4× 0.9× / 0.8× 0.4× / 0.4× —
hin_Deva 1.36 3.12 15.04 16.80 24.2 14.0 6.7 — 1.6× / 1.4× 0.9× / 0.8× 0.4× / 0.4× —
jpn_Jpan 1.55 3.50 11.22 13.16 20.2 10.6 5.1 — 1.8× / 1.5× 0.9× / 0.8× 0.5× / 0.4× —
kat_Geor 1.38 2.50 11.51 12.63 17.4 10.8 4.6 — 1.5× / 1.4× 0.9× / 0.9× 0.4× / 0.4× —
kor_Hang 1.09 2.82 22.03 23.77 30.9 20.6 9.1 — 1.4× / 1.3× 0.9× / 0.9× 0.4× / 0.4× —
rus_Cyrl 1.02 3.00 17.58 19.56 27.0 16.1 6.7 — 1.5× / 1.4× 0.9× / 0.8× 0.4× / 0.3× —
tam_Taml 1.37 2.96 9.70 11.29 17.9 9.4 4.3 — 1.8× / 1.6× 1.0× / 0.8× 0.4× / 0.4× —
tha_Thai 1.36 2.59 6.42 7.64 13.0 6.1 2.9 — 2.0× / 1.7× 0.9× / 0.8× 0.4× / 0.4× —
added_normalized_dense 0.08 1.76 19.52 21.20 30.7 18.8 12.0 — 1.6× / 1.5× 1.0× / 0.9× 0.6× / 0.6× —
added_normalized_sparse 0.08 1.74 23.27 24.93 31.8 22.1 11.8 — 1.4× / 1.3× 0.9× / 0.9× 0.5× / 0.5× —
added_special_dense 0.08 1.76 54.02 55.70 82.3 75.9 27.5 — 1.5× / 1.5× 1.4× / 1.4× 0.5× / 0.5× —
added_special_sparse 0.08 1.73 36.81 38.47 51.5 44.0 17.0 — 1.4× / 1.3× 1.2× / 1.1× 0.5× / 0.4× —
agentic-traces 0.59 1.77 57.62 58.80 53.5 55.1 18.3 — 0.9× / 0.9× 1.0× / 0.9× 0.3× / 0.3× —
agentic_swe 0.53 1.73 65.85 67.05 54.8 63.2 18.7 — 0.8× / 0.8× 1.0× / 0.9× 0.3× / 0.3× —
code_mixed 0.53 1.73 59.27 60.47 52.3 58.3 18.1 — 0.9× / 0.9× 1.0× / 1.0× 0.3× / 0.3× —
math_latex 0.62 1.78 41.98 43.14 46.5 39.5 16.3 — 1.1× / 1.1× 0.9× / 0.9× 0.4× / 0.4× —
Not yet supported: t5-base
t5-base not supported

McPatate and others added 30 commits July 20, 2026 11:41
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
new `UnsafeCell` with atomic impl should allow us to scale linearly with core count

Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
At 10 kB a large share of each pass is per-call overhead -- scratch checkout,
added-token scan, post-process -- which rates implementations for work that is
not tokenizing. The throughput phase now encodes one ~10 MB input per fixture,
repeating a fixture that is shorter than that.

The scaling sweep parallelises across chunks, so it re-slices that same input
into ~512 kB pieces rather than encoding more of them: every thread still has
units to take, and the phase costs exactly what it did before.

The memory child keeps its 10 kB prefix -- its `rss0` is supposed to measure the
tokenizer, not a corpus sitting next to it.

Note this makes phase 1 encode 10x the bytes it used to; the released-crate
baseline is the slow side of that.
@huggingface huggingface deleted a comment from github-actions Bot Aug 4, 2026
@huggingface huggingface deleted a comment from github-actions Bot 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`.
Brings in the parallel encode runtime from #2213: the `ParallelPlan` ladder,
the claim-cursor job core, `EncodeHandle`, and the fork-safe pool.

Both branches had rewritten `tokenizer/pipeline.rs`, so #2213's runtime is
taken whole and this branch's work is grafted back onto it:

- `PipelineInner` carries `normalizers: Vec<_>` (metaspace-as-normalizer) and
  the wired `PipelinePostProcessor` instead of the single normalizer and the
  unused `_post_processor`. `stride_boundary` now asks every normalizer;
  `PipelineNormalizer::Metaspace` answers no, so t5/albert take the
  `Normalized` plan rather than a raw cut.
- The model kernels call the batched `tokenize_spans` instead of looping
  `tokenize_pipeline` per span -- the point of this branch.
- Special-token framing is applied per *input*, where an input's chunks are
  joined (`take_result` / `encode_serial`), never per chunk. `encode` frames by
  default; `encode_with` takes the flag. `encode_one` is the single-sequence
  synchronous path, and `encode_generic::<STAGE>` keeps the bench ablation ladder.
- The thread-local `EncodeState` replaces this branch's per-tokenizer
  `ScratchPool`, but the scratch holds a word cache keyed on word bytes alone --
  so it is now rebuilt when the thread's last encode used a *different*
  tokenizer, not merely a different model kind. Two BPE tokenizers sharing a
  thread traded ids without this.

Also:
- `BucketVocabStore::build` returns the empty store instead of panicking on an
  empty vocab; `BPE::default()` is one, and the plan tests build from it.
- #2213's oracle keeps its own file (`pipeline_parallel_oracle.rs`) next to the
  per-model one, and compares against `add_special_tokens = true` now that the
  post-processor runs -- which also checks the framing lands once per input.
- `bpe_pipeline_oracle` skips a model whose *legacy* reference can't encode
  without `fancy-regex` (deepseek): with the backend on, all four are exact.
The merge had four ways to silently drop parallelism (plan selection, the
scratch identity check, the framing hook, the pool), and none of them show up
as a test failure. `par_check` prints serial vs one-document-parallel vs
batch-parallel MB/s per model, so a regression is one command away.

Also clears the two clippy warnings the merged tree inherited.
…gh encode_generic

`ab_giga` mirrors gigatoken's own `encode_st` protocol (one repeated buffer, pass 0 cold /
later passes warm, best-of over the warm passes, token count printed) so the two can be
compared on the same inputs. Token counts match gigatoken exactly on all 8 model x corpus
pairs, so the numbers are apples-to-apples.

Single-thread warm, 4 MB, interleaved best-of-3 (this box swings ~20% by machine state, so
runs must be interleaved -- a single run cannot resolve 10%):

  gpt2     english 1145/1488 .77x | code 619/776 .80x | chinese 776/493 1.57x | russian 698/602 1.16x
  llama-3  english 1139/1354 .84x | code 714/780 .92x | chinese 953/503 1.89x | russian 1001/627 1.60x

Geomean vs gigatoken: gpt2 1.03x, llama-3 1.24x.

`encode_one` now goes through the fused `encode_generic` instead of the worker kernel
`encode_one_with`, which splits the pipeline across two functions so a worker can enter at
either. Worth ~3% on a whole sequence; it is not the whole story (see below).

Measured cost of the merge itself: same harness on the pre-merge tree is 1-8% faster on 7 of
8 pairs (~3% mean). That is the `Arc<PipelineInner>` pointer hop the parallel runtime needs
for its 'static workers, and it buys 4-10x on multiple threads.
One 4 MB document per thread through the parallel `encode`, matching gigatoken's `hf_mt`
(`encode_docs_ragged` over the same docs) in thread count, work per thread and MiB/s.

14 threads, best-of-3 interleaved, ours/giga:
  gpt2     english 6240/9229 .68x | code 4252/3056 1.39x | chinese 1181/1251 .94x | russian 4463/1185 3.77x
  llama-3  english 5075/8766 .58x | code 3380/4071 .83x  | chinese 1380/1926 .72x  | russian 4894/1638 2.99x
Geomean: gpt2 1.35x, llama-3 1.01x.

Scaling against each side's own 1-thread encode is the more useful read: english 5.4x/4.5x
(giga 6.2x/6.5x), code 6.8x/4.9x (3.9x/5.2x), russian 6.4x/4.9x (2.0x/2.6x), but
**chinese only 1.6x on both models** (giga 2.5x/3.8x) -- the whitespace-boundary striding
limit already noted for single-document CJK, which is why gigatoken overtakes us at 14
threads on chinese despite our 1.5-1.7x single-thread lead there.
The harness returned a fresh `Vec` per encode (`encode_one`) while gigatoken's reserves once
and `clear()`s per pass. That cost an allocation plus a first touch of the whole token array
every pass -- and it scales with TOKENS, not bytes, so it quietly penalised exactly the
token-dense corpora: chinese emits 2.9 M tokens for a 4 MB input against english's 0.8 M.

Chinese was reading 1.52x when it is 1.87x. Corrected, single thread, warm, ours/giga:

  gpt2     english .81x | code .83x | dense 1.02x | russian 1.36x | greek 1.35x
           arabic 1.33x | hindi 1.26x | thai 1.33x | korean 1.49x | chinese 1.87x  geomean 1.23x
  llama-3  english .91x | code .95x | dense .99x  | russian 1.62x | greek 1.59x
           arabic 1.72x | hindi 1.59x | thai 1.70x | korean 1.84x | chinese 1.91x  geomean 1.43x

Token counts match gigatoken exactly on all 20 pairs. Also sweeps every corpus now, not four,
and takes deepseek-v4 when present (0.55-0.92x there -- that model wants the char-fold, which
is not in this tree).
`plan()` chose `Raw` whenever the *config* exposed a stride boundary, without asking whether
the *input* contains one. Punctuation-terminated CJK contains none at all: Chinese prose has
no space anywhere, and a newline after `。` is not a legal cut because the punct rule
` ?[…]+[\r\n]*` absorbs it into the punct token (`NEWLINE_PREV` excludes punctuation for
exactly that reason). `data/corpora/chinese.txt` is 100% such newlines -- 722 of them, every
one preceded by `。`, and zero spaces.

Striding then did what `stride_range`'s own doc comment warns about: "text with no boundaries
at all degrades to stride 0 owning the whole input". Measured on a 4 MB doc: 513 strides
tiled, **1 resolved, covering all 4199874 bytes**, after scanning the document twice looking
for cuts that do not exist. 0.28x the plain serial encode, and flat across thread counts
because there is only ever one work unit.

`plan()` now probes the longest input for an actual cut (bounded at 32 KB, once per encode)
and falls through to `Pretokenized` when there is none -- which pre-tokenizes serially and
parallelises the model over span groups. That prefix runs the normal pre-tokenizer, so it is
`bitsplit` wherever bitsplit is wired (gpt2, cl100k), which is what makes it cheap enough to
prefer over a degenerate stride.

gpt2 chinese, 14 x 4 MB docs, MB/s ours/giga -- was 1193/1243 (0.94x), now 3004/1120 (2.68x);
on a cool box the same fix measured 4798/1243 (3.86x). Single thread on one doc: 260 -> 732.
Token counts unchanged (23531424), and every plan was already byte-exact, so plan choice only
ever trades throughput. Spaced text still probes as cuttable and takes `Raw` unchanged.

`cut_exists` passes `lo = 1`, not 0: `boundary_in_window` reads one byte of left context via
`block_lo - 1`, so 0 underflows to `usize::MAX` and spins forever in the char-boundary
walk-back (it hangs, it does not panic, in release). Byte 0 is never a cut anyway.
@SBrandeis
SBrandeis force-pushed the feat/bpe-cache-min branch 3 times, most recently from 8195f43 to 4b47341 Compare August 4, 2026 23:14
Base automatically changed from feat/bpe-cache-min to feat/train_encode_split August 5, 2026 12:10
ArthurZucker added a commit that referenced this pull request Aug 6, 2026
…g the flag (onto #2241) (#2302)

* perf(bpe): prove the whole-pretoken fold per entry instead of trusting the flag

`ignore_merges` lets a pretoken that is itself a vocabulary entry be emitted as
that entry, skipping the merge loop. It is declared in the config, so models
trained expecting it (llama-3) get it and gpt2 does not -- even though ~93% of
English pretokens are a single vocabulary entry that provably cannot encode to
anything else.

The property is checkable, so it need not be assumed. At load, run the merge
engine on each entry's own text and record whether it reduces back to itself.
Configs that already declare `ignore_merges` are untouched; the proof only runs
when the config did not ask, so this can only enable a case that was safe.

A bitset, not a bool. A single flag is all-or-nothing and one entry is enough
to lose it: on gpt2 exactly one of 50,257 disagrees -- `<|endoftext|>`, which
decomposes to seven tokens -- so a global flag reads "not provable" and the
other 50,256 keep paying for it. Measured with a bool first: exactly flat.
Per-entry bits cost 6.3 KB for gpt2 and a disagreeing entry costs only itself.

Encoding is one probe into the vocabulary MPHF already held, then one bit test.
No new table: the alternative for this workload is a large pretoken cache, and
gigatoken's is ~64 MB against these 6.3 KB.

Measured on the #2279 encode path (same change, ported here), gpt2, ns/byte,
10 kB documents each encoded once with the cache carried, median of 7
interleaved rounds:

    chat-mistral   4.01 -> 2.65   1.51x
    chat-chatml    3.88 -> 2.74   1.42x
    english        5.47 -> 4.01   1.36x
    math-latex     4.69 -> 3.97   1.18x
    code           3.11 -> 2.69   1.16x
    agentic-swe    3.16 -> 2.92   1.08x
    chinese       10.99 -> 10.89  1.01x
    hindi          2.61 -> 2.61   1.00x
    thai           5.48 -> 5.93   0.92x

The gain tracks how often a pretoken is a whole vocabulary entry: 94.8% on
chat-mistral and 93.1% on english against 37.6% on chinese and 11.9% on hindi.
Thai loses 8% to probes that miss; gating the probe on the content-byte gate
was tried and is a wash, because the miss path then computes the gate twice.

Load cost is +7 ms median (61 -> 68 ms), the proof being one merge per entry.

Exactness: the fold changes what the model emits, so it is the one thing that
must be checked. Full benchmark matrix on the #2279 path, 7 models x 30
corpora: 174 cells byte-exact against tokenizers 0.23.1 and 29 mismatched --
the same 29 albert (Unigram) cells that already fail before this change.

* perf(bpe): pack the foldable flag into the id, dropping the side bitset

The proof's answer was kept in a separate 6.3 KB bitset consulted after the
vocabulary probe. It does not need to exist: an id needs 31 bits at most, the
lookup already loads the entry to verify the key, and this crate already packs
flags beside an id -- a merge value is a product id in the low 30 bits with
SAFE_MASK above it. Same convention here: FOLD_BIT is bit 31 of the stored id,
VOCAB_ID_MASK is the rest.

So the fold answer arrives on a load that already happened. No side table to
allocate, no second structure to keep in step with the vocabulary, and no
second lookup on the encode path.

`get_bytes_foldable` returns both halves; `get_bytes` masks, so no caller sees
the flag. `content`/`byte_content` mask too -- the bit must never escape this
type, and an unmasked export would have leaked it into `get_vocab()`. Build
asserts every id fits under the flag.

The model keeps one `fold_by_flag` bool to tell "proven per entry" from "the
config declared ignore_merges, so fold every hit".

No behaviour change: 338 tests pass, including the test that pins ids for
strings mixing folded words, merged words and a literal `<|endoftext|>`.

* Apply suggestion from @ArthurZucker
…elimiter (#2300)

`merge_word` picks between multipass and the two-tier queue with a per-byte
gate indexed by the word's first byte. With a ByteLevel pre-tokenizer the
leading space arrives as `Ġ` (C4 A0) and with Metaspace as `▁` (E2 96 81) --
both >= 0x80, so every space-prefixed word was read as multibyte and given the
CJK gate of 8. Any English word longer than eight bytes went to the queue,
which is the wrong engine for a short word.

`build_byte_to_gate` already makes this argument for the ASCII space and maps
" \t\n\r" to the multibyte gate, precisely because a leading delimiter says
nothing about the script of the rest. The conclusion was right and the remedy
was backwards: what follows the delimiter should choose, so look past it.

Measured on gpt2, ns/byte, 10 kB documents each encoded once with the cache
carried across them, median of 3 interleaved rounds:

    english        9.02 -> 7.13   1.27x
    chat-mistral   6.28 -> 5.04   1.25x
    chat-chatml    6.07 -> 4.95   1.23x
    math-latex     7.04 -> 5.76   1.22x
    code           4.03 -> 3.54   1.14x
    agentic-swe    4.27 -> 3.92   1.09x
    thai           7.12 -> 6.65   1.07x
    chinese       13.75 -> 13.19  1.04x
    hindi          2.97 -> 2.86   1.04x

No corpus regresses. The gain is largest exactly where the vocabulary merges
hardest -- English prose, chat templates and LaTeX run 0.72-0.79 merges per
byte, against 0.28 for Chinese -- because that is where picking the wrong
merge engine is paid most often.

The gate only decides which engine runs, so this must not move a single id.
Verified across the full benchmark matrix, 7 models x 30 corpora: 174 cells
byte-exact against tokenizers 0.23.1 and 29 mismatched, the same 29 albert
(Unigram) cells that already failed before this change. The new test pins the
case that was wrong, with words straddling the 8-byte boundary in both
directions.
ArthurZucker added a commit that referenced this pull request Aug 6, 2026
* wip / ai: scratch pool

* scratch pool iteration

* implement word cache

* lint

* change defaults + max length + capacity bump

* more sensible values

* rewrite without hashmap

* iteration

* iteration

* fmt

* review comments

* draft first commit

* nits

* proper bpe only benches

* proper features

* update

* update what we balckbox

* comments

* commetn

* first drafts

* fix glue

* draft table builds

* start mphf draft

* impl get

* fix mphf

* nits

* fixes

* slowly but surely

* fixes

* table tests pass

* create top merges

* update

* up

* progress in the conversion table build

* update

* fixes

* dman

* ...

* eager flag was missing

* update

* design a manual example

* with / without thief

* remove some shit

* more simplifications

* single test move it

* draft engine

* remove complication

* remove one flag that was not worth it

* current status

* monomorphize for perfs

* fixes

* rename

* start drafting the two tier merge

* todo is getting fixed

* nit

* SparseFold for table!

* move stuff around

* multipass bpe

* scratches

* isolate pipeline bpe

* remove unused mergemap

* u16 is faster

* add convert.rs

* update

* less branches

* update

* it was not worth it

* finally

* nits, renaming and moving files here and there

* renaming

* fix more of the renaming

* pipeline vs legacy

* first fix for the gate

* update the local bench

* up

* bit better benches

* update

* revert unrelated changes

* more reverts

* more reversion!

* cleanup

* nits

* more clippy

* perf(merge bpe): improvement suggestions (#2275)

* rewrite + document multipass

* rename

* perf: save a lookup when there is no merge

* perf: compiler gotchas

* Idiomacy:
- 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

* review: address comments on merge_multipass

- reword the copy/merge cursor step, drop the redundant invariant line
- show the trailing dead slots in the pass diagrams instead of shrinking
  the boxes, with a legend for the padding
- apply the suggested SAFE-bit wording
- move MergeState above merge_once, its only user
- has_merged -> was_merged, known_pair_value -> cached_pair_value
- inline copy_last into merge_once
- reframe the step doc around the lookup it wins back

---------

Co-authored-by: Arthur <arthur.zucker@gmail.com>

* fix(bpe): the byte gate classifies the delimiter, not the word (#2294)

* fix(bpe): the byte gate classifies the delimiter, not the word

`merge_word` indexes `byte_to_mode` on `sequence.as_bytes()[0]`, but a
pre-tokenizer hands the model its delimiter attached to the front, so byte 0
describes the delimiter rather than the word. ByteLevel gives `"Ġword"`
(`Ġ` = U+0120 = `C4 A0`), Metaspace gives `"▁word"` (`▁` = U+2581 = `E2 96 81`)
— both land above 0x7F and take GATE_MULTI(8), even when the word behind them
is pure ASCII and belongs on GATE_ASCII(24).

`build_byte_to_gate` already special-cases a literal leading space down to
GATE_MULTI, with a comment noting that the first byte "says nothing about the
script of the rest" — so the hazard was known. The multibyte spellings of the
same delimiter never got the same treatment, and forcing them to MULTI is the
wrong direction anyway: the answer is to classify the content.

`content_start` skips one leading delimiter (`▁`, `Ġ`, or ASCII whitespace) and
`merge_word` indexes the gate there. A delimiter-only word keeps offset 0,
where MULTI remains the right conservative answer.

Reach, on 200 kB of the english corpus with the gpt2 pre-tokenizer: 30.6% of
words sit in the gate's decision band (9..24 bytes) AND are Ġ-prefixed with
ASCII content, so they change engine. On code.txt only 0.9% do.

Measured, gpt2/english, 10 kB chunks, best-of-3 warm, three interleaved A/B
rounds to cancel drift:

    with fix     97.2  98.3  98.7 MB/s
    without      89.4  91.0  85.3 MB/s

Non-overlapping: +10.7%. code/dense/russian/chinese are flat, consistent with
their much smaller affected fraction.

Byte-exact by construction — the gate only chooses between two merge engines
that must already agree — and confirmed: identical token counts on every
model/corpus probed. `cargo test -p tk-encode` 311/313, the one failure
(`normalizers::precompiled::pipeline_precompiled_matches_legacy`, a missing
data fixture) reproduces unchanged without this commit.

NOT a fix for llama-2. Its config has `pre_tokenizer: None`, so a 10 kB chunk
arrives as a single word whose length exceeds both gates; it always took the
two-tier queue and still does. llama-2's cost is the merge over one unbounded
span, which is a separate problem.

* Update tokenizers/tk-encode/src/models/bpe/merge_hot_cold_queue.rs

* Update tokenizers/tk-encode/src/models/bpe/merge_hot_cold_queue.rs

* Perf/bpe merge review (#2297)

* chore(node): sync Cargo.lock with tk-encode's libc dependency

* refactor(bpe): flatten the filename prefixes, group the released-crate BPE under legacy/

* refactor(bpe): split the tables file, document the packed merge value once

* refactor(bpe): move code next to its users

* refactor(bpe): one name per concept

* refactor(bpe): collapse the three copies of the char-as-bytes push

* perf(bpe): prove the whole-pretoken fold per entry instead of trusting the flag (onto #2241) (#2302)

* perf(bpe): prove the whole-pretoken fold per entry instead of trusting the flag

`ignore_merges` lets a pretoken that is itself a vocabulary entry be emitted as
that entry, skipping the merge loop. It is declared in the config, so models
trained expecting it (llama-3) get it and gpt2 does not -- even though ~93% of
English pretokens are a single vocabulary entry that provably cannot encode to
anything else.

The property is checkable, so it need not be assumed. At load, run the merge
engine on each entry's own text and record whether it reduces back to itself.
Configs that already declare `ignore_merges` are untouched; the proof only runs
when the config did not ask, so this can only enable a case that was safe.

A bitset, not a bool. A single flag is all-or-nothing and one entry is enough
to lose it: on gpt2 exactly one of 50,257 disagrees -- `<|endoftext|>`, which
decomposes to seven tokens -- so a global flag reads "not provable" and the
other 50,256 keep paying for it. Measured with a bool first: exactly flat.
Per-entry bits cost 6.3 KB for gpt2 and a disagreeing entry costs only itself.

Encoding is one probe into the vocabulary MPHF already held, then one bit test.
No new table: the alternative for this workload is a large pretoken cache, and
gigatoken's is ~64 MB against these 6.3 KB.

Measured on the #2279 encode path (same change, ported here), gpt2, ns/byte,
10 kB documents each encoded once with the cache carried, median of 7
interleaved rounds:

    chat-mistral   4.01 -> 2.65   1.51x
    chat-chatml    3.88 -> 2.74   1.42x
    english        5.47 -> 4.01   1.36x
    math-latex     4.69 -> 3.97   1.18x
    code           3.11 -> 2.69   1.16x
    agentic-swe    3.16 -> 2.92   1.08x
    chinese       10.99 -> 10.89  1.01x
    hindi          2.61 -> 2.61   1.00x
    thai           5.48 -> 5.93   0.92x

The gain tracks how often a pretoken is a whole vocabulary entry: 94.8% on
chat-mistral and 93.1% on english against 37.6% on chinese and 11.9% on hindi.
Thai loses 8% to probes that miss; gating the probe on the content-byte gate
was tried and is a wash, because the miss path then computes the gate twice.

Load cost is +7 ms median (61 -> 68 ms), the proof being one merge per entry.

Exactness: the fold changes what the model emits, so it is the one thing that
must be checked. Full benchmark matrix on the #2279 path, 7 models x 30
corpora: 174 cells byte-exact against tokenizers 0.23.1 and 29 mismatched --
the same 29 albert (Unigram) cells that already fail before this change.

* perf(bpe): pack the foldable flag into the id, dropping the side bitset

The proof's answer was kept in a separate 6.3 KB bitset consulted after the
vocabulary probe. It does not need to exist: an id needs 31 bits at most, the
lookup already loads the entry to verify the key, and this crate already packs
flags beside an id -- a merge value is a product id in the low 30 bits with
SAFE_MASK above it. Same convention here: FOLD_BIT is bit 31 of the stored id,
VOCAB_ID_MASK is the rest.

So the fold answer arrives on a load that already happened. No side table to
allocate, no second structure to keep in step with the vocabulary, and no
second lookup on the encode path.

`get_bytes_foldable` returns both halves; `get_bytes` masks, so no caller sees
the flag. `content`/`byte_content` mask too -- the bit must never escape this
type, and an unmasked export would have leaked it into `get_vocab()`. Build
asserts every id fits under the flag.

The model keeps one `fold_by_flag` bool to tell "proven per entry" from "the
config declared ignore_merges, so fold every hit".

No behaviour change: 338 tests pass, including the test that pins ids for
strings mixing folded words, merged words and a literal `<|endoftext|>`.

* Apply suggestion from @ArthurZucker

* docs(bpe): say where the hot queue's heap comes from

hot_push/hot_pop are std's BinaryHeap sift_up/sift_down_range, vendored and
specialised: min-order over a raw u64 key, so no Reverse wrapper around the
packed (rank, index). Note the "hole" form too -- shift into the hole, write
once -- since it reads like an unrolled swap otherwise.

---------

Co-authored-by: SBrandeis <33657802+SBrandeis@users.noreply.github.com>
Brings the branch up to the clean base (15 commits, including the WordCache
merged as af5a3e3 / #2262) without rewriting history, so the PRs stacked on
this branch stay valid. The PR diff now shows this branch's own changes rather
than re-adding files the base already has.

Both branches had reorganised the same modules since the fork on 31 Jul, so
the conflicts were structural rather than line-level. How each was resolved:

- `word.rs`, `serialization.rs` -> the base moved them into `legacy/`, this
  branch renamed them to `legacy_word.rs` / `legacy_serialization.rs`. Verified
  the base's move is content-identical to the fork point, so the flat names are
  kept and the `legacy/` copies dropped: a rename collision, nothing lost.
- `model.rs` -> the base rewrote it (+228/-1562) while this branch split it
  into `bpe_model.rs` and `legacy_model.rs`. This branch's split is kept.
- `word_cache.rs` -> the two copies have ZERO shared commits (the squash-merge
  of #2262 removed the common ancestry) and differ by 939/782 lines. This
  branch's version is kept: its pipeline, model and merge code are written
  against it, and it is the version every benchmark on this branch measured.
- The remaining 12 conflicted files are this branch's own work and keep its
  side.

Worth knowing: keeping this branch's `word_cache.rs` means any fix that landed
in #2262 during review and is absent here is NOT picked up by this merge. The
two implementations need reconciling properly before this branch merges down,
and this commit does not do that.

Verified after the merge: builds clean, 391 tests pass, and the full benchmark
matrix is unchanged at 203 cells byte-exact against tokenizers 0.23.1 with 29
mismatched -- the same albert (Unigram) cells that already failed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants