You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
serve: GGUF prompts tokenized greedily, <|im_end|> after text splits into 6 ids (+3 per turn vs llama.cpp) #4804
Defect.apr serve run <model.gguf> tokenizes prompts with a greedy longest-match BPETokenizer that has no merges and no special-token split, so <|im_end|> straight after text is not one token. .<|im_end|>\n encodes as .<|im_end|>\n (6 ids) where llama.cpp gives .<|im_end|>\n (3 ids). The model sees a turn end it never saw in training, which feeds the degenerate tail in #4662.
Where. Both GGUF serve states build the tokenizer as BPETokenizer::new(vocab, vec![], unk):
AppState::with_quantized_model_and_vocab (crates/aprender-serve/src/api/mod_app_state_gpu.rs, CPU serve via crates/apr-cli/src/commands/serve/server.rs)
AppState::with_cuda_model_and_vocab (same file, CUDA serve when the GGUF has no merges handed over)
With empty merges, BPETokenizer::encode (crates/aprender-serve/src/tokenizer.rs) takes the greedy loop, which never consults special tokens. GGUFModel::encode (crates/aprender-serve/src/gguf/token.rs, canonical byte-level BPE with tokenizer_st_partition, #3726/#3993) is not used by these routes.
Measured (main build 6192aa9, CPU serve vs llama.cpp server, same GGUF, llama.cpp's /apply-template text fed to both /tokenize):
Model
Prompt
llama.cpp
apr
Diff
Qwen2.5-0.5B-Instruct f16
4 prompts
35, 35, 38, 32
41, 41, 44, 38
+6 each: two `<
Qwen3-Coder-30B-A3B Q4_K_M
4 prompts
14, 14, 17, 11
17, 17, 20, 14
+3 each: one `<
/v1/chat/completionsusage.prompt_tokens moves by the same amount (0.5B: 42/36, 42/36, 45/39, 39/33). <|im_start|> after a newline happens to survive (greedy longest match from a line start). Prompt 2 on the 30B also shows ordinary text off the merges (2 ids differ in "haiku about rust."), the #3726 class.
Done when. The GGUF serve routes encode with the same tokenizer as GGUFModel::encode, and a test asserts the ids of a ChatML prompt with <|im_end|> right after punctuation equal llama.cpp's (e.g. ?<|im_end|>\n → 30 151645 198 on Qwen2.5).
park proposal: parked, sub-issue of #4662; no commit on main @7f9a15a52b since 2026-10-04 touches the special-token split (git log --grep on im_end / special token: none). It waits with its parent.
Activity
fix(serve): GGUF servers tokenize with the canonical byte-level BPE (…
noahgift commentedon Oct 13, 2026
park proposal: parked, sub-issue of #4662; no commit on main @7f9a15a52b since 2026-10-04 touches the special-token split (git log --grep on im_end / special token: none). It waits with its parent.