Skip to content

[rollout] fix: DeepSeek continuous token builder cannot render tool appends - #7630

Merged
wuxibin89 merged 1 commit into
verl-project:mainfrom
ruiling-smartbear:fix/deepseek-synthetic-tool-arguments
Aug 31, 2026
Merged

wuxibin89 merged 1 commit into
verl-project:mainfrom
ruiling-smartbear:fix/deepseek-synthetic-tool-arguments

Conversation

@ruiling-smartbear

@ruiling-smartbear ruiling-smartbear commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

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 splice tool['function']['arguments'] into the prompt by string concatenation, so with DeepSeekContinuousTokenBuilder every tool append raises TypeError on that concatenation. DeepSeek-V3 is the only one of the family that gets through, and only because its template renders tool_calls when content 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 dict arguments from OpenAIFunctionCallSchema, is no longer re-rendered on a tool append.

The base builder is left alone; Qwen-style templates run arguments through tojson and expect a mapping.

Checklist Before Starting

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; on main the new test fails with the same TypeError.
  • Real tokenizers (tokenizer files only, transformers 5.16.1), upstream main @3dab856 vs main with this patch, token ids from tokenize_non_assistant_incremental_messages on tool_agent_loop-shaped trajectories, 24 cases per row (system prompt on/off × 0 / 10 / 50 prior tool turns × 1 or 2 parallel calls × enable_thinking default / False):
model harness arguments main this PR
DeepSeek-V3 dict, json 24/24 render 24/24, identical to main
DeepSeek-R1 dict, json 0/24 (TypeError) 24/24 render
DeepSeek-V3.1 dict, json 0/24 24/24
DeepSeek-V3.2-Exp dict, json 0/24 24/24
Qwen2 / 2.5 / 3 / 3.5, MiniMax-M2, GLM-4.7, Gemma 4, gpt-oss, default builder dict – 24/24 identical to main

With 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_first flag 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 with arguments replaced by "{}".
  • Tests: _DeepSeekBoundaryTokenizer, a minimal string-concatenating renderer with no generation prompt after a tool message; one test for the tool append through create_continuous_token_builder(..., model_family="deepseek") (and the base builder raising on the same template), one for the synthetic message itself.

Checklist Before Submitting

  • Read the Contribute Guide.
  • ruff check / ruff format pass on the touched files.
  • Docs: internal tokenizer utility, no user-facing change.
  • Unit tests in tests/utils/test_continuous_token_on_cpu.py, covered by the CPU unit-test workflow.
  • CI request in ci-request once ready.

…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
wuxibin89 merged commit f97625b into verl-project:main Aug 31, 2026
63 of 70 checks passed
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.
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