Repository navigation
forward-serve: the turn end is read off the container's own template — Phi4-mini ran on past 'Paris<|end|>' to the token budget (Refs #1786) - #2144
Conversation
…(Refs #1786) FastFlowLM's Phi4-mini-Instruct-NPU2 names <|endoftext|> in config.json and tokenizer_config.json, and its template closes every turn with <|end|>. The serve stopped only on the first, so the answer was 'Paris<|end|><|assistant|>...' to the token budget. The marker is now taken from a rendered two-message exchange (npu::turn_end_marker). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at f07b812 |
|
VERDICT: READY at The three sources are one defect
Each fix is per-model and per-marker, and each was found by reading one row's output. That is the tell: when the third instance arrives by a different path, the invariant — every stop token the rendered template can emit must be in the stop set — is not being enforced anywhere, so the fourth marker will arrive the same way. What I would ask for, in this PR or a follow-up it names
On the search note"I searched issues and PRs for … before writing this; nothing open covers it" — terms named, which is the check that keeps the fourth instance from opening a fourth issue. Worth keeping as the habit: it is what two of today's duplicate clusters lacked. Nothing else: the fix is right for Phi, and naming the class is what turns three fixes into one gate. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at f07b812: READY
Reviewed head f07b812 (refs #1786, the Phi4-mini row). This is the first verdict.
Checked
git merge-treeonto main: no conflicts. CIbuildand CodeQL green. The redpr_agentis the summary bot, not a code check.npu_stop_tokens_test, built from this head: "all stop-token checks passed".- Mutants of
turn_end_marker()(app/npu_stop_tokens.h), each rebuilt and run:
| mutant | result |
|---|---|
| leading whitespace not skipped | rc 1: "whitespace before it is skipped, what follows is ignored" |
| whitespace inside the brackets accepted | rc 1: "a lone bracket, an empty pair, an unclosed one and one with spaces in it are not markers" |
an empty <> accepted (the close - i < 2 guard removed) |
rc 1: the same arm |
Together with the body's mutant (the must-start-with-< guard), every guard in the function is pinned. I restored the file (git status clean).
app/forward_serve.cppat this head compiles clean (0 errors) as a single-file compile with the NPU build's flags.- The ChatML fallback cannot be overwritten.
turn_endstarts as<|im_end|>only onchatml_route, which is!can_render && e.chat_ok(:1177), and the template probe runs only undercan_render(:1147). So a rendered template whose assistant turn ends in plain text yields no marker rather than removing the fallback's. A marker the tokenizer does not know adds nothing (token_id< 0, tested).
Not re-run by me: the device rows (Phi4-mini content='Paris' finish=stop completion=1 ×3; Qwen3-VL-4B stops on 151645 <|im_end|>). As the body says, the rendering step has no device-free harness.
Nit: the header comment's example line "\n", "", "</s>x"... -> "" reads as though </s>x yields nothing, but the code returns </s>, because the text starts with a whole <...> token. The behaviour is right; the example says the opposite of the "unless" clause next to it.
Refs #1786 (the
Phi4-mini-Instruct-NPU2row). Backlog. I searched issues and PRs forPhi "<|end|>" stop token served as textbefore writing this; nothing open covers it. Same class as #1044 and #2064, third source.The fault, on the NPU
FastFlowLM's
Phi4-mini-Instruct-NPU2served by1bit serve --device npu, the pack's own template, main before this change, "What is the capital of France? Answer with one word.",max_tokens 32:The model answers, closes its turn, and the serve keeps going: the turn end is served as text and the model writes itself a follow-up until the budget runs out.
Both of the pack's files name the same token, and it is not the one the model ends a turn with:
The upstream checkpoint lists
<|end|>ingeneration_config.json, which the pack does not ship. #2064's two sources cannot find it.The fix
The template is the one file every chat pack has, and it cannot disagree with itself about how a turn ends. At load, when the container's template is rendered,
forward-serverenders a two-message exchange (a user message and an assistant message with a probe text) and takes what the template put right after the assistant's text.npu::turn_end_marker()returns the<...>token that text starts with, or nothing if it starts with anything else (plain text after a turn is not a stop). That marker goes intonpu::stop_token_ids()in the slot the ChatML fallback's<|im_end|>already used. A template that cannot render the exchange contributes nothing and the load says so.After, same pack, binary from this branch (
f07b812), onenpu-holdticketOther templates through the same rule, same binary:
Qwen3-VL-4B-Instruct-NPU2loads withstops on 151645 <|im_end|> (tokenizer / chat turn end)and answersParis,finish=stop, three times.Llama-3.2and the Qwen packs already listed their marker; for them the rule finds the same token and adds nothing (covered by the de-duplication arm of #2064).Tests
tests/npu_stop_tokens_test.cpp, 10 new checks: the marker for Phi, ChatML, Llama 3 and Gemma shapes; leading whitespace skipped and what follows ignored; nothing or only whitespace after the turn; plain text after the turn, with or without a marker later in it; malformed brackets; Phi's two files alone give[199999](the fault) and with the template's marker[199999, 200020]; a marker the tokenizer does not have adds nothing.all stop-token checks passed.Mutant from a copy of the header (the "must start with
<" guard removed):FAIL plain text after the turn is not a stop, even with a marker later in it.tools/copyright.py --checkrc 0. Docs:docs/npu.md, "What ends an answer".Limits
forward_serve.cpp) has no device-free harness; the two packs above are its test. Gemma's shape is unit-tested only.<...>token gets no marker from this rule, by design.🤖 Generated with Claude Code