Skip to content

fix(tokenizer): resolve eos/bos ids from tokenizer_config.json (MODEL-QWEN35-EXL3) - #3363

Open
ghazni101 wants to merge 2 commits into
mudler:mainfrom
ghazni101:row/MODEL-QWEN35-EXL3-eos
Open

ghazni101 wants to merge 2 commits into
mudler:mainfrom
ghazni101:row/MODEL-QWEN35-EXL3-eos

Conversation

@ghazni101

@ghazni101 ghazni101 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Checkpoint tokenizer.json files that ship a bare ByteLevel post_processor (every Qwen-family checkpoint) leave eos_id_/bos_id_ unset: ExtractBosEos reads only the TemplateProcessing template. When config.json and generation_config.json also decline to name eos, requests carry no eos id at all — the model emits <|im_end|> and the engine ignores it, generating fake turns to max_tokens. Measured live on Qwen3.5-9B-EXL3-4.00bpw (ROCm gfx1101): <|im_end|> (id 248046) emitted at token 10, request ran to finish_reason=length. Issue: #3364 (local ISSUE-LOCAL-01M3RY20H90NMK37V74EH34Z77, mirrored and synced).

HF resolves tokenizer.eos_token_id from the eos_token/bos_token NAMES in tokenizer_config.json (string or AddedToken object), optionally supplied by special_tokens_map.json — AutoTokenizer on this checkpoint answers 248046 (measured, transformers 5.17.0). FromHfJson now reads both sibling files and fills any id the post_processor left at -1. Template ids keep precedence; add_bos_token/add_eos_token stay unhonoured (HF ignores them on load — pinned); tokenizer_config.json wins over special_tokens_map.json via contains()-gated overwrite, matching from_pretrained kwarg ordering.

Verify: test_bpe 30/30, 1020 assertions (run in the ROCm toolchain image; the host lacks cmake so the checker is not host-reproducible). Mutation check: reverting the precedence fix fails the new override subcase with EosId -1 vs 19. Live verification: serving container rebuilt on this change returns finish_reason=stop with clean reasoning/content split, where the unfixed build ran to length.

Out of scope: add_bos/add_eos encode-side application (HF does not apply them either); GGUF path untouched (own kv EOS).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: OMP:devin/swe-2 [omp]

tokenizer.json's ByteLevel post_processor carries no TemplateProcessing
template, so ExtractBosEos left eos_id_/-bos_id_ unset on every Qwen-family
checkpoint. When the model's config.json and generation_config.json also
decline to name an eos_token_id, the request goes out with no eos at all
and the model's <|im_end|> is generated then ignored: a Qwen3.5-9B-EXL3
chat reply emitted id 248046 at token 10 and ran to finish_reason=length,
hallucinating further turns (measured live, ISSUE-LOCAL-01M3RY20H90NMK37V74EH34Z77).

HF resolves the same id from tokenizer_config.json's eos_token name
(string or AddedToken object; AutoTokenizer on the shipped config answers
248046, transformers 5.17.0). FromHfJson now reads the sibling
tokenizer_config.json and special_tokens_map.json and fills bos_id_/eos_id_
from the declared names -- only where the post_processor left them unset,
so a TemplateProcessing id keeps precedence (the pinned OPT case). The
add_bos_token/add_eos_token keys stay unhonoured: HF's loaded class ignores
them (measured) and EncodeWithSpecialTokens keeps reading only
template_bos_/template_eos_.

The DeepSeek-V2 pin's BosId()==-1 assertion was updated: HF resolves
bos_token_id from the config name even when add_bos_token resolves False
(measured, same transformers), so the pinned behaviour is the encode, not
the getter.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: OMP:devin/swe-2 [omp]
…p.json

Adversarial review of the eos/bos name fallback found the merge precedence
inverted: the first-nonempty guard let special_tokens_map.json lock in a
name before tokenizer_config.json was read, so the older file won. HF's
from_pretrained applies the config's init kwargs after consuming the map
file, so a declared key in tokenizer_config.json must overwrite the map's
value. Unconditional overwrite regressed differently -- a config that does
not declare the key would erase the map's name -- so contains() gates it:
value() cannot distinguish absent from an explicit null.

Verified empirically: the new 'tokenizer_config.json overrides
special_tokens_map.json' subcase fails on the committed code (EosId -1 vs
expected 19) and passes here; test_bpe is 30/30 green, 1020 assertions.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: OMP:devin/swe-2 [omp]
@ghazni101
ghazni101 force-pushed the row/MODEL-QWEN35-EXL3-eos branch from 86a6d22 to d5e6535 Compare September 30, 2026 16:49

@localai-org-maint-bot localai-org-maint-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new sibling-file lookup is skipped for a valid bare relative filename. In FromHfJson, std::filesystem::path("tokenizer.json").parent_path() is empty, so if (!dir.empty()) bypasses the entire EOS/BOS fix even when tokenizer_config.json is in the current directory. Loading the same bytes as "./tokenizer.json" takes the new path instead. For the ByteLevel fixture with eos_token: "<|end|>", the first spelling therefore leaves EosId() at -1 while the second resolves 19.

Please resolve an empty parent as . (or allow the empty directory to participate in the sibling join) and add a regression that loads both path spellings from the fixture directory. This is a public Tokenizer::FromHfJson path, and callers such as the tokenizer example pass the supplied filename directly.

I read the complete diff at d5e6535 against current main fce3673 and checked the callers. This is a static finding: the C++ regression, oracle comparison, and repository gate could not run here because Python/CMake/the C++ compiler are absent. I cannot push a verified repair under the repository's gate-before-push requirement from this environment. @mudler @richiejp

This branch has not been deployed

No deployments
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.

3 participants