Tokenizer fidelity: the whitespace lookahead
A model only behaves the way it was trained if it sees the same token IDs it was trained on. 0.4.1 fixes a long-standing difference between hipfire's byte-level BPE tokenizer and the Hugging Face reference: how a run of whitespace before a word is split. It is a CPU-side correction, so it applies on every GPU and every quantization.
01 Pre-tokenization in one paragraph
the ideaByte-level BPE tokenizers (GPT-2 style, used by the Qwen family) first cut text into pieces with a regular expression, then run BPE merges inside each piece. Merges never cross a piece boundary, so where the regex cuts decides which tokens are even possible. The pattern is an ordered list of alternatives; at each position the first alternative that matches wins.
(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+
The interesting alternative is the second-to-last one,
\s+(?!\S): “a run of whitespace that is not
followed by a non-space character.” Before a word, the regex engine
backs off by one character to satisfy the lookahead, which leaves the
last space of the run for the word that follows.
02 Worked example
Four spaces of indentation in Python, with and without the alternative:
with \s+(?!\S) (Hugging Face, 0.4.1):
'def' ' f' '():\n' ' ' ' return' ' ' '1'
└ 3 ┘ └ space joins the word
without it (before 0.4.1):
'def' ' f' '():\n' ' ' 'return' ' ' '1'
└ 4 ┘ └ bare word
Same text, different pieces, so different token IDs: the model sees
" return" (the token it was trained on in this position)
instead of "return" after a different whitespace token. Code,
tool-call JSON and aligned text are full of whitespace runs, so agent
prompts hit this constantly.
Pieces shown are the output of the pattern above, and of the same pattern
with the \s+(?!\S) alternative removed, on this string.
03 What changed in 0.4.1
how hipfire does it- The lookahead alternative is honored. A whitespace run before a non-space leaves its last Unicode character for the following alternative; newline alternatives and trailing whitespace keep their own boundaries.
- The model's own split pattern is used. The runtime reads the Hugging Face Split regex embedded in the model rather than imposing one pattern (with three-digit number grouping) on every byte-BPE model.
- The declared NFC normalizer is honored for byte-BPE inputs, and the pre-tokenizer and normalizer now take part in the persistent cache identity.
The previous code applied one pattern to every GPT-2-style tokenizer: Qwen2, Qwen3, Qwen3.5 / 3.6 / 3.8 dense and MoE (including the 27B), Flash-Next, and dots.ocr. SentencePiece tokenizers are unchanged.
04 How it is tested
0.4.1 commits an offline Hugging Face corpus with exact reference IDs and
a test, hf_tokenizer_corpus, that checks every ID, not just
token counts:
| corpus part | cases |
|---|---|
| reconstructed Hermes agent prompts | 138 |
| TC requests + Strix Halo TC captures | 55 + 14 |
benchmarks/prompts/*.txt | 44 |
| synthetic whitespace / Unicode cases | 694 |
| total (868,076 reference tokens) | 945 |
scripts/gen_hf_tokenizer_corpus.py regenerates the corpus from
local captures with the pinned Hugging Face tokenizer and chat template,
and records input MD5s alongside the IDs.
A broader differential test, which compares every shipped byte-BPE family against Hugging Face with seeded fuzz input, was written after the 0.4.1 release and is not part of it. 0.4.1's guarantee is the fixed corpus above.
05 What it buys you
06 Limits
- There is no switch: prompt token IDs change for any input with whitespace runs before non-space text.
- Generation for the same text is not expected to match older releases.
- Fixed-ID references (KLD references, kernel identity pins, frozen pre-tokenized fixtures) are intentionally unchanged, because they do not re-encode text.
07 Sources
crates/hipfire-runtime/src/tokenizer.rs: the pre-tokenizer splitter and the canonical pattern.scripts/gen_hf_tokenizer_corpus.py: the corpus generator.- CHANGELOG, v0.4.1 § “Tokenizer fidelity” and the v0.4.1 release notes (defaults that changed).