Byte-level pre-tokenizers¶
gmlx builds a GGUF's tokenizer from the GGUF header. For a byte-level BPE
vocab, the header key tokenizer.ggml.pre names how text splits into words
before the merges run. gmlx follows llama.cpp for each name, so a prompt
gets the same token ids in both.
The table¶
PRE_TOKENIZERS in gmlx/load/tokenizer.py maps each name to an entry with
four columns:
| Column | Meaning |
|---|---|
patterns |
The split regexes, applied in sequence. Each regex splits every piece that the one before it made. |
ignore_merges |
A word that is a whole vocab entry becomes one id, and the merges do not run on it. |
add_bos |
The BOS default for a GGUF without tokenizer.ggml.add_bos_token. |
nfc |
Apply the NFC normalizer before the split. |
The regexes are llama.cpp's. Where llama.cpp keeps the regex of the original
tokenizer.json in a comment, the table uses that spelling. The tokenizers
library runs the regexes on its own engine, and a test checks that the
pieces match the pieces the regex module gives.
Names that share a regex set share one entry, as in llama.cpp. The table
also keeps three names that llama.cpp does not list, for GGUFs that already
carry them: qwen3, qwen35moe and hyv4.
The BOS default¶
llama.cpp turns BOS on for the Llama 3 and Tekken groups. The GGUF key
tokenizer.ggml.add_bos_token overrides that whenever the GGUF has it. gmlx
does the same: a missing key takes the entry's add_bos, and a present key
wins. Every other entry leaves a missing key as off.
The table does not model llama.cpp's null_bos flag, which chatglm-bpe
sets.
Unknown names¶
A name that the table does not list, or an empty name, falls back to the Qwen 2 split with no NFC. The load prints one warning that names the pre-tokenizer, because token ids can then differ from llama.cpp. An SPM vocab, such as Gemma's, has no word split and gets no warning.
Unicode normalization¶
llama.cpp applies no normalization to a BPE vocab. The nfc column is off
by default, so an entry matches llama.cpp unless it sets the column. An entry
sets it only where the reference tokenizer.json declares an NFC
normalizer: qwen2, qwen3, qwen35, qwen35moe, deepseek-r1-qwen and
k2-horizon. These entries match Hugging Face, and differ from llama.cpp on
text that is not in NFC, such as an e followed by a combining accent.
Every other entry matches llama.cpp. Each of those whose reference file was
read declares no normalizer, so it matches Hugging Face too.
The normalizer field was read from the reference file of these entries:
| Entry | Reference repository | Normalizer |
|---|---|---|
qwen2 |
Qwen/Qwen2.5-0.5B-Instruct |
NFC |
qwen3 |
Qwen/Qwen3-0.6B |
NFC |
qwen35 |
Qwen/Qwen3.5-9B |
NFC |
qwen35moe |
Qwen/Qwen3.5-35B-A3B |
NFC |
deepseek-r1-qwen |
deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B |
NFC |
k2-horizon |
IFM/K2-Horizon-7B |
NFC |
llama-bpe |
nvidia/Llama-3.1-Nemotron-Nano-8B-v1 |
none |
falcon3 |
tiiuae/Falcon3-1B-Instruct |
none |
falcon-h1 |
tiiuae/Falcon-H1-0.5B-Instruct |
none |
lfm2 |
LiquidAI/LFM2-350M |
none |
glm4, glm5 |
zai-org/GLM-5 |
none |
gpt-2 |
openai-community/gpt2 |
none |
phi-2 |
microsoft/phi-2 |
none |
dbrx |
allenai/OLMo-2-0425-1B |
none |
smaug-bpe |
HuggingFaceTB/SmolLM3-3B |
none |
starcoder |
bigcode/starcoder2-3b |
none |
tekken |
mistralai/Mistral-Nemo-Base-2407 |
none |
tekken-num3 |
mistralai/Mistral-Large-4-1T-A52B |
none |
flint has a tokenizer.json with no normalizer. kimi-k2 uses a
tiktoken vocab with no normalizer. The other entries take the default.
The snapshot test¶
tests/load/data/llama_cpp_pre.json is a snapshot of llama.cpp's table: the
pre type and flags of each name, the regex list of each pre type, and the
llama.cpp commit. scripts/llama_pre_snapshot.py writes it from a llama.cpp
checkout:
tests/load/test_tokenizer_pre_table.py checks that every name in the
snapshot is in the table or in the test's UNLISTED_PRES, with a reason.
It checks that each entry has the snapshot's flags, and the snapshot's
regexes once both spellings are put in one form. When llama.cpp adds a
name, refresh the snapshot and the test shows the new name.
Parity is measured against the snapshot's llama.cpp commit, e8df20d23.
Older llama.cpp builds split Indic, Thai and Arabic text differently.
Raw prompts and BOS¶
/v1/completions sends a raw prompt, with no chat template to write a BOS.
The route encodes it inside raw_prompt_special_tokens from
gmlx/upstream/single_bos_fix.py. Inside that block, every prepare_inputs
binding encodes with the tokenizer's special tokens and drops nothing. A
Gemma prompt then gets its BOS, as llama-server and Hugging Face give it.
When the text itself starts with BOS, the prompt holds two, as it does in
llama-server.
The chat routes do not use the block. Their templates write BOS, and the single BOS rule in the same module keeps one.