Repository navigation
[rollout] fix: DeepSeek continuous token builder cannot render tool appends - #7630
Merged
wuxibin89 merged 1 commit intoAug 31, 2026
Conversation
…ppends
The synthetic assistant message that _tokenize_tool_group renders ahead of a
tool group carries "arguments": {} on its tool calls. The DeepSeek R1, V3.1
and V3.2 chat templates splice tool['function']['arguments'] into the prompt
by string concatenation, so every tool append through
DeepSeekContinuousTokenBuilder raised TypeError. DeepSeek-V3 only avoided it
because its template renders tool_calls when content is None, which the
synthetic message never is.
Carry the synthetic arguments as the JSON string "{}" in the DeepSeek builder.
The message only exists to be diffed away, so nothing else changes. Checked on
the real DeepSeek-V3 / R1 / V3.1 / V3.2-Exp tokenizers: tool appends render on
all four, and match the pre-verl-project#7628 full-history path wherever that path renders.
wuxibin89
approved these changes
Aug 31, 2026
This was referenced Aug 31, 2026
ruiling-smartbear
added a commit
to ruiling-smartbear/verl
that referenced
this pull request
Aug 31, 2026
… required token is missing DeepSeek-R1 distills keep Qwen model_type values (qwen2/qwen3) but ship DeepSeek's tokenizer, which renames <|im_end|> to its own end-of-sentence marker. Since verl-project#6804 the builder family is an exact model_type lookup, so QwenContinuousTokenBuilder.__init__ raises "Tokenizer does not define required token '<|im_end|>'" before any rollout starts (verl-project#7637). With auto inference, treat a builder whose required token is missing like an unregistered model_type and fall back to the default behavior with a warning. The fallback is a dedicated subclass carrying the synthetic tool-call arguments as the JSON string form of verl-project#7630, because these checkpoints' templates splice arguments in by string concatenation; the base class keeps the mapping form, which registered templates such as Qwen3.5, MiniMax-M2 and GLM-4.7 render with .items(). An explicitly requested family still raises.
This was referenced Aug 31, 2026
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.
What does this PR do?
ContinuousTokenBuilder._tokenize_tool_group()renders a tool group behind a synthetic assistant message whose tool calls carry"arguments": {}. The DeepSeek R1, V3.1 and V3.2 chat templates splicetool['function']['arguments']into the prompt by string concatenation, so withDeepSeekContinuousTokenBuilderevery tool append raisesTypeErroron that concatenation. DeepSeek-V3 is the only one of the family that gets through, and only because its template renderstool_callswhencontent is none, which the synthetic message never is.Found while checking #7628 on the DeepSeek family (#7628 (comment)). The fix is a one-method override: the DeepSeek builder carries the synthetic arguments as the JSON string
"{}". The message only exists to be diffed away, so the emitted tokens do not change. #7628 is what makes this sufficient: the history, with the harness's own dictargumentsfromOpenAIFunctionCallSchema, is no longer re-rendered on a tool append.The base builder is left alone; Qwen-style templates run
argumentsthroughtojsonand expect a mapping.Checklist Before Starting
[{modules}] {type}: {description}Test
tests/utils/test_continuous_token_on_cpu.py: 124 passed (122 existing + 2 new). The new DeepSeek-shaped fake template concatenates arguments like the real ones; onmainthe new test fails with the sameTypeError.main@3dab856 vsmainwith this patch, token ids fromtokenize_non_assistant_incremental_messagesontool_agent_loop-shaped trajectories, 24 cases per row (system prompt on/off × 0 / 10 / 50 prior tool turns × 1 or 2 parallel calls ×enable_thinkingdefault / False):argumentsmainmainTypeError)mainWith JSON-string arguments the naive full-history suffix diff is available as an independent reference: the fixed output matches it 24/24 on V3.1 and V3.2-Exp. On V3 and R1 it matches on the cases without prior tool turns; those two templates keep a conversation-global
is_output_firstflag that no bounded render can see, which is pre-existing and unchanged here.Script and raw output: https://github.com/ruiling-smartbear/verl-experiments/blob/main/generation_prompt_fold_experiment.md#the-fix-checked
API and Usage Example
No API change.
Design & Code Changes
DeepSeekContinuousTokenBuilder._synthetic_assistant_for_tools: the base message withargumentsreplaced by"{}"._DeepSeekBoundaryTokenizer, a minimal string-concatenating renderer with no generation prompt after a tool message; one test for the tool append throughcreate_continuous_token_builder(..., model_family="deepseek")(and the base builder raising on the same template), one for the synthetic message itself.Checklist Before Submitting
ruff check/ruff formatpass on the touched files.tests/utils/test_continuous_token_on_cpu.py, covered by the CPU unit-test workflow.ci-requestonce ready.