Skip to content

feat(atomsplit): run deepseek's pre-tokenizer with no regex backend - #2265

Draft
SBrandeis wants to merge 6 commits into
feat/train_encode_splitfrom
deepseek-fsm
Draft

feat(atomsplit): run deepseek's pre-tokenizer with no regex backend#2265
SBrandeis wants to merge 6 commits into
feat/train_encode_splitfrom
deepseek-fsm

Conversation

@SBrandeis

@SBrandeis SBrandeis commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

No description provided.

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

deepseek's `tokenizer.json` failed to LOAD without the `fancy-regex` feature.
Its pre-tokenizer is a `Sequence` of three `Split`s, and `atomsplit` only knew
the three as a *chain* (`fsm_deepseek`, the fused pass). `Split::new` accepts a
missing backend only for a pattern it recognizes on its own, and no single
deepseek pattern was recognized — so deserialization errored out before the
`Sequence` ever got the chance to spot the chain.

Each split now has its own byte-exact FSM, so each `Split` is recognized alone:

  fsm_deepseek_num  `\p{N}{1,3}`
  fsm_deepseek_cjk  `[一-龥぀-ゟ゠-ヿ]+`
  fsm_deepseek_big  the big regex

`fsm_deepseek` keeps fusing all three when the whole chain is present; it and
`fsm_deepseek_big` share one body under two compile-time flags saying which
earlier splits are folded in, so the grammar lives in one place. Measured
performance-neutral on `fsm_deepseek` (median +0.02% over 10 corpora, separate
binaries, alternating runs — the microbench's layout noise is several times
that).

Also:
- `DEEPSEEK_BIG` now carries literal CR/LF, matching the string deepseek ships,
  so `tk-encode` no longer keeps a second copy of the three patterns just to
  compare against them; `utils::is_deepseek` is gone (recognition is per-split).
- `GptFsm::split_into` replaces the duplicated fsm `match` in the legacy and
  pipeline paths.
- `canonicalized_for_pipeline` now rewrites `(invert, Removed)` →
  `(!invert, Isolated)` only for patterns that match the whole input
  (`GptFsm::covers_input`). deepseek's leave gaps, where the two forms differ.
- The three deepseek `Sequence` tests pointed at a fixture name that no longer
  exists, so they had been silently skipping; fixed, and un-gated from
  `fancy-regex` since the splits now build without it (same for the dsv4
  pipeline bench). With the feature on they still compare against the real
  regex engine; with it off they pin fused == chained.

Tests: per-split onig parity over both parity corpora + 9 Wikipedia languages,
chain-equals-fused over the same, and a `tokenizer.json` load+encode test whose
ids must match the legacy path in either build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI benchmark built `fixture_bench` with `bench-baseline`, which enabled
`fancy-regex`. So the throughput, memory and model-coverage numbers described a
build nobody ships, while the binary size next to them was measured on a
different binary (`binsize_pipeline`, built without it). The backend is not a
rounding error: on a minimal encode program it is +1.47 MB stripped, +71%.

`fancy-regex` was in that feature only to supply one of the *reference regex
engines* the report times our split against. Those move to their own binary:

  bench-baseline = the released crate, to compare against   -> fixture_bench
  bench-engines  = onig + PCRE2 + fancy-regex + logos       -> pretok_engines

`fixture_bench` now builds with no regex backend, and the shared work list —
corpus loading, the model manifest, the timing helpers, `--shard` — lives in
`examples/bench_common/` so a number from one binary is comparable to the other.
The report is unchanged: each shard runs both binaries and the `report` job folds
the engine numbers back onto the row they describe as `pretok_vs_regex`, which is
where the renderer already looks. Verified end to end locally — merge attaches
198 rows (9 models x 22 fixtures) and the renderer emits the same tables.

All 10 manifest models load with no backend, which is what makes this possible:
deepseek needed per-split FSMs (previous commit) and the SentencePiece pair
(gemma-4, llama-2) needed literal patterns, whose `Split(" ")` / `Replace(" ",
"▁")` are not regexes at all. `tests/no_regex_backend.rs` pins that, so adding a
model that needs an engine fails a test instead of quietly turning into an error
card in the report.

Ran the benchmark this way over all 22 fixtures: 9/10 models benched with
ids_match true against the released crate; t5-base is the pre-existing Metaspace
pipeline gap, unrelated to the backend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SBrandeis
SBrandeis changed the base branch from feat/train_encode_split to feat/literal-patterns July 30, 2026 08:53
Base automatically changed from feat/literal-patterns to feat/train_encode_split July 31, 2026 14:15
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.

2 participants