Skip to content

glm5.2: preserve input marking in prompt tokenization (fix output stalls on stray message tags) - #148

Merged
Anbeeld merged 1 commit into
Anbeeld:v0.4.7from
mp4:glm52-message-tags-port
Sep 12, 2026
Merged

glm5.2: preserve input marking in prompt tokenization (fix output stalls on stray message tags)#148
Anbeeld merged 1 commit into
Anbeeld:v0.4.7from
mp4:glm52-message-tags-port

Conversation

@mp4

@mp4 mp4 commented Sep 7, 2026

Copy link
Copy Markdown
  1. Fixed MTMD ordering.
    2&3. Assistant continuation content marked as template text, now handles all specialize parsers.(incl gpt-oss)
  2. Token-count endpoints do not use prompt_parts
  3. Independent part tokenization changes ordinary tokenization
    common_tokenize_parts() (chat.cpp) now has a fast path: common_chat_parts_have_special_input() (mirroring the tokenizer's own parse_special=false rule for control/unknown tokens) checks whether any is_input part contains special-token text; if not, the whole concatenated prompt is tokenized in a single parse_special=true pass — token IDs identical to legacy. The protection path additionally merges adjacent same-type parts (the jinja runtime merges them at render, but continuation parts appended later didn't) so normal merges across post-render template boundaries are kept.
    Verified: test_normal_prompt_token_ids_unchanged — real multi-turn Qwen3 prompt: parts tokenization == legacy whole-prompt tokenization; plus a synthetic BPE case ("Hel"|"lo" straddling a boundary) that provably fails under naive per-part tokenization and passes here, and the same for the protection path.
  4. Remove common/arg.cpp.patch

@mp4
mp4 requested a review from Anbeeld as a code owner September 7, 2026 17:06
@Anbeeld

Anbeeld commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Please retarget this PR from main to v0.4.7 and rebase or refresh the implementation against that branch.

I re-audited the resulting patch against v0.4.7. It applies without textual conflicts, but the following issues still need to be addressed before merge:

  1. MTMD bypasses the new protection. In handle_completions_impl, the mctx != nullptr branch runs before the prompt_parts branch. process_mtmd_prompt() tokenizes the flattened prompt with parse_special=true, so OAI chat requests served by a multimodal-capable model do not receive the intended protection, even when no media is attached.

  2. Assistant continuation content is marked as template text. The continuation paths construct a string containing reasoning_content and/or message content, then append the whole string as {false, generation_prompt}. This causes request-provided continuation text to be tokenized with parse_special=true. Please preserve provenance separately for template delimiters and continuation content in the automatic and specialized parser paths.

  3. GPT-OSS tokenizes different text from prompt. The GPT-OSS handler replaces the final <|return|> with <|end|> in prompt, but does not make the same replacement in prompt_parts. Since the server tokenizes prompt_parts, the existing compatibility fix is bypassed.

  4. Token-count endpoints do not use prompt_parts. handle_count_tokens still tokenizes the flattened prompt with parse_special=true. Its result can differ from actual completion tokenization, and it retains the same input-special-token problem. Please share the provenance-aware tokenization path between inference and token counting.

  5. Independent part tokenization can change ordinary tokenization. common_tokenize_parts() tokenizes each Jinja part separately, which prevents normal tokenizer merges across template/input boundaries. Please preserve the existing token IDs for prompts that do not contain injected special-token text, and add a regression test demonstrating that behavior.

  6. Remove common/arg.cpp.patch. It is unrelated to prompt tokenization, is unapplied and stale, and contains trailing whitespace that makes git diff --check fail.

Please also add focused regression tests covering:

  • special-token text inside ordinary user/tool input;
  • operation with an MTMD context;
  • assistant continuation content;
  • the GPT-OSS <|return|> replacement;
  • consistency between completion and token-count paths;
  • unchanged token IDs for a normal prompt without injected special tokens.

Once the PR targets v0.4.7 and these paths are covered, I can re-review it.

…lls on stray message tags)

Port of llama.cpp commit f72dba3ad ('glm5.2 fixed stray message tags in input'),
targeted at v0.4.7.

- Thread jinja string parts (with is_input metadata) through chat template
  application: rendered prompt, BOS/EOS stripping, and the continuation
  generation prompt in the automatic and all specialized parser paths.
  The parts mirror data.prompt exactly (position-based edits keep them in
  sync even when a token spans a part boundary).
- common_tokenize_parts(): request-provided content (is_input) is
  tokenized with parse_special=false so stray message/special tags in
  user/tool content or continuation content cannot be injected as real
  tokens; template parts keep parse_special=true. When no is_input part
  contains special-token text, the prompt is tokenized in a single pass
  over the concatenated text (merging adjacent same-type parts), so the
  token ids are identical to the legacy whole-prompt tokenization.
- GPT-OSS: the <|return|> -> <|end|> compatibility replacement is applied
  to the prompt parts at the same global position as in the prompt string,
  so server-side tokenization of prompt_parts sees the same text.
- OAI-compat path serializes prompt_parts; the inference route
  (handle_completions_impl) and the token counting route
  (handle_count_tokens) share one input-marking-aware tokenization path
  (server_tokenize_prompt_parts), including media interleaving for MTMD
  contexts. The prompt_parts branch runs before the MTMD branch, so chat
  requests served by multimodal-capable models receive the protection even
  without attached media.
- Regression tests:
  - tests/test-prompt-parts.cpp (ctest, vocab-only GGUF + chat templates):
    special-token injection in ordinary user and tool input, assistant
    continuation provenance in the automatic and specialized (gpt-oss,
    qwen3-coder, gemma4) parser paths, GPT-OSS <|return|> replacement,
    completion/token-count consistency through the OAI serialization,
    unchanged token ids for normal prompts without injected special tokens
    (incl. a BPE merge straddling a part boundary), MTMD part layout
  - tests/test-prompt-parts-mtmd.py (opt-in, live MTMD context):
    completion/token-count agreement with and without media, and
    special-token protection in user content in an MTMD context
@mp4
mp4 changed the base branch from main to v0.4.7 September 11, 2026 20:32
@mp4
mp4 force-pushed the glm52-message-tags-port branch from 9427726 to 27d15b1 Compare September 11, 2026 20:33
@mp4

mp4 commented Sep 11, 2026

Copy link
Copy Markdown
Author
  1. Fixed MTMD ordering.
    2&3. Assistant continuation content marked as template text, now handles all specialize parsers.(incl gpt-oss)
  2. Token-count endpoints do not use prompt_parts
  3. Independent part tokenization changes ordinary tokenization
    common_tokenize_parts() (chat.cpp) now has a fast path: common_chat_parts_have_special_input() (mirroring the tokenizer's own parse_special=false rule for control/unknown tokens) checks whether any is_input part contains special-token text; if not, the whole concatenated prompt is tokenized in a single parse_special=true pass — token IDs identical to legacy. The protection path additionally merges adjacent same-type parts (the jinja runtime merges them at render, but continuation parts appended later didn't) so normal merges across post-render template boundaries are kept.
    Verified: test_normal_prompt_token_ids_unchanged — real multi-turn Qwen3 prompt: parts tokenization == legacy whole-prompt tokenization; plus a synthetic BPE case ("Hel"|"lo" straddling a boundary) that provably fails under naive per-part tokenization and passes here, and the same for the protection path.
  4. Remove common/arg.cpp.patch

@Anbeeld
Anbeeld merged commit 12f1e7c into Anbeeld:v0.4.7 Sep 12, 2026
@Anbeeld

Anbeeld commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Thanks!

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