server: keep the Qwen thinking checkpoint when tools are offered but unused — 14x on agentic turns (1.5s vs 23.2s)#13
Open
azamatgizzatullin wants to merge 1 commit into
Conversation
…unused should_remember_thinking_checkpoint() rejected any request carrying tool definitions, while the call site already rejects turns that produced tool calls (!parsed_calls.len). The predicate therefore also dropped the far more common "tools offered, tools not used" case, which is the normal shape of an agentic coding session: clients send the full tool list on every request and answer with text most turns. The live checkpoint was never remembered, so every turn re-prefilled the whole prompt. Scope the guard to non-Qwen formats. For Qwen the reuse path in thinking_live_visible_prefix_prompt() additionally proves an exact token prefix of the remembered visible tokens (ds4_tokens_starts_with) on top of the byte-prefix match and live_tokens == live_pos, so a tools-present prompt that renders differently fails closed into ordinary token/text/disk matching. Qwen is also the only affected protocol: /v1/responses and /v1/messages reject Qwen3.6 at request validation, so neither the Responses visible-transcript route nor the Anthropic tool-call-id route is available as an alternative. Measured on M5 Pro 24 GiB, Metal SSD streaming, Q4_K_S expert-major, identical 3-turn conversation differing only by the tools field: turns 2/3 go from 23.2s/24.8s to 1.5s/1.7s. At agent scale (14,310-token prompt, 4 tools) turns 2/3 go from ~150s to 1.6s/1.3s. Reuse-path output is byte-identical to the cold path on all three turns under greedy decode. ds4_test --server passes unchanged; test_thinking_checkpoint_remember_gate asserts has_tools against DS4_CHAT_FORMAT_DEEPSEEK_V4, which is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Hi Andrea! Small server-side fix on the Qwen3.6 fork-main path, found while
driving
ds4-serverfrom a real agentic coding client (OpenCode) on a 24 GiBM5 Pro.
One line in
should_remember_thinking_checkpoint()costs every agentic turn afull re-prefill. The predicate rejects requests that merely carry tool
definitions, while the call site already rejects turns that produced tool
calls. Agentic clients send the whole tool list on every request and answer with
text most turns, so the live checkpoint is never remembered and each turn
re-prefills from token 0.
Measured result (M5 Pro 24 GiB, Metal SSD streaming, Q4_K_S expert-major)
Identical 3-turn conversation, greedy, run twice. The only difference
between the two runs is the presence of the
toolsfield; the model emits notool call in either run.
toolstools, beforetools, afterRealistic agent scale — 14,310-token system prompt (repo listing), 4 tool
definitions,
-c 65536,--ssd-streaming-cache-experts 1800:The cold first prefill is unchanged and expected — this only stops paying it
again on every subsequent turn. Answers stayed correct across the run
(
220,team-3,1hagainst the generated fixture). Swap stayed at 0.Correctness (reuse path vs cold path, byte-identical)
Speed alone would not justify touching a cache guard, so the reuse path was
diffed against the cold path on identical inputs. Greedy (
temperature=0,reasoning_effort=none). The cold arm defeats reuse by interleaving anunrelated request, which advances the live session so
thinking_live.live_tokens != live_posand forces a full prefill.119— identicalvalidate input 5 before processing and log result 5.— identicalvalidate input 10 before processing and log result 10.— identical3/3 byte-identical.
--serveris the suite covering request parsing, chat rendering, thinkingcontrols and KV disk-cache bookkeeping — the exact surface touched here.
test_thinking_checkpoint_remember_gatepasses unchanged: itshas_toolsassertion is written against
DS4_CHAT_FORMAT_DEEPSEEK_V4, which this changedoes not alter.
--logprob-vectors,--long-contextand--tool-call-qualitywere not run:they default to the DeepSeek V4 Flash artifact and vectors, which this
environment does not have. Happy to run any of them if you point at the expected
artifact for the Qwen path.
The change
ds4_server.c:10607:The call site (
ds4_server.c:11951) already guards the sampled-tool-call case:so
r->has_toolsinside the predicate only adds the broader tools offered,tools not used case.
Why this is fail-closed rather than a widened trust boundary.
thinking_live_visible_prefix_prompt()requires, for Qwen specifically,s->thinking_live.visible_tokens.len > 0 && ds4_tokens_starts_with(&req->prompt, &s->thinking_live.visible_tokens)on top of the byte-prefix match andlive_tokens == live_pos. If a tools-present prompt renders differently on thenext turn, that token-prefix check fails and the request falls back to ordinary
token/text/disk matching. Nothing reuses mismatched state silently — which is
also what the correctness table above measures.
Why Qwen is the only affected protocol.
/v1/responsesand/v1/messagesboth reject Qwen3.6 at request validation (
"Qwen3.6 currently supports only /v1/chat/completions"), so the Responses visible-transcript route and theAnthropic tool-call-id route are unavailable here and there is no alternative
continuation path. DeepSeek deployments have both, which is likely why this has
not surfaced before.
Upstreamability classification
Model-specific research on a fork-main path. Per
FORK_NOTES.mdtheQwen3.6-35B-A3B path is FORK MAIN and currently marked not mergeable upstream,
so the patch keeps the explicit
DS4_CHAT_FORMAT_QWEN36guard and staysfork-scoped. No
FORK_NOTES.mdboundary change is needed.The observation is not Qwen-specific though: on
/v1/chat/completionsthepredicate rejects "tools offered" while the call site already rejects "tools
used", for every chat format. If you consider the broader relaxation sound, the
DeepSeek arm needs its own correctness evidence — particularly the interaction
with
canonicalize_tool_checkpoint()and exact DSML replay — which thisenvironment cannot produce. Flagging it rather than proposing it blind.
Open question
Is
has_toolsin this predicate intentional beyond the sampled-tool-call casethe call site already covers — e.g. guarding against the tool list changing
between turns? If so, a narrower condition (keying the checkpoint on the
rendered tool block, or comparing it) would preserve that intent while keeping
reuse alive for stable tool lists. Happy to redo it that way instead.
Environment
6d5572d("docs: refresh Qwen 16 GB production benchmark")metal-arm64, cleanmake, no other deltasDS4_QWEN_EXPERIMENTAL_METAL=1)Qwen3.6-35B-A3B-DS4-ExpertMajor-v1-Q4_K_S(native expert-major GGUF)321 experts prefill / 1400–1800 decode, swap 0 throughout