[bugfix] render chat-template diffs without the generation prompt in SampleBuilder - #3
Merged
Merged
Conversation
…SampleBuilder SampleBuilder.append_text diffed successive apply_chat_template renders that each carried add_generation_prompt=True. Such renders are not prefixes of each other, so slicing new_ids[len(old_ids):] corrupts every multi-turn prompt: for a system+user pair the user header is replaced by a stray assistant header while the token count stays the same, so the corruption is silent. Affects dataset/gsm8k.py (system+user, the default grpo_gsm8k recipe) and dataset/s9_math.py; single-message dataset/math.py is unaffected. Fixes #2. build_sample now diffs renders without the generation prompt (those are true prefixes) and appends the generation prompt once at the end, mask 0. append_text is folded into build_sample; its only caller was build_sample and its incremental generation-prompt semantics cannot be made correct. apply_chat_template output is normalized through _render, which accepts both a BatchEncoding and a plain id list across transformers versions. scripts/bench_train_only.py mirrored the same diff logic and is fixed the same way; it now also inserts the generation prompt before each assistant turn, which is what the engine was actually prompted with. Note: prompt token streams for multi-turn samples change, so metrics from runs before this fix are not directly comparable.
MayDomine
force-pushed
the
fix/sample-builder-template-diff
branch
from
September 11, 2026 07:17
d5b5783 to
56852cd
Compare
Collaborator
|
LGTM. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2.
Problem
SampleBuilder.append_text(meshy/utils/sample.py) built the prompt token stream incrementally by diffing successiveapply_chat_templaterenders that each carriedadd_generation_prompt=True. Such renders are not prefixes of each other, so slicingnew_ids[len(old_ids):]produces a corrupted stream for every multi-turn prompt.Reproduced with the Qwen/Qwen3-1.7B tokenizer on a system+user pair: the builder emits
— the
<|im_start|>userheader is replaced by a stray<|im_start|>assistantheader, and the user text is masked/positioned as if it were assistant output. The token count is identical to the correct render, so nothing crashes; quality just degrades.Blast radius:
meshy/dataset/gsm8k.py(system+user — every prompt of the defaultgrpo_gsm8krecipe),meshy/dataset/s9_math.py(any multi-message prompt).meshy/dataset/math.pybuilds a single user message and is unaffected, which is why this stayed hidden.Fix
build_samplenow diffs renders made without the generation prompt — those are true prefixes of each other in ChatML-family templates — and appends the generation prompt once at the end (mask 0), sosample.tokensis exactly the prompt the inference engine should see.append_textis removed: its only caller wasbuild_sample, and its stateless "append one message with the generation prompt" semantics cannot be made correct for a message that is not the last one.append_tokens(used by the rollout worker for engine output) is unchanged._renderhelper normalizes theapply_chat_templatereturn value, which is aBatchEncodingon some transformers versions and a plain id list on others.scripts/bench_train_only.py's_Tokenizercopied the same diff logic and is fixed the same way; it additionally inserts the generation prompt before each assistant turn, matching what the engine was actually prompted with during the logged run.tests/test_sample_builder.py: a deterministic ChatML-style fake tokenizer (no model download) asserts the built stream equals a one-shotadd_generation_prompt=Truerender for single-, two- and four-message conversations, that masks cover exactly the assistant span, and that bothapply_chat_templatereturn types work. The equality tests fail on the old code.Verified against the real Qwen3-1.7B tokenizer that the fixed builder output for
[system, user]matches the one-shot render exactly.Behavior changes
SampleBuilder.append_textno longer exists; out-of-tree callers should usebuild_sampleorappend_tokens.<think>block from an assistant turn once a later message follows) still get a self-consistent stream, but it can differ from a one-shot render by exactly that rewritten span. No in-tree dataset puts assistant turns in the prompt, so this does not affect bundled recipes.