Skip to content

server: fix incremental DSML tracking across malformed text and split close tags - #997

Open
titan550 wants to merge 1 commit into
antirez:mainfrom
titan550:fix/dsml-tracker-upstream
Open

server: fix incremental DSML tracking across malformed text and split close tags#997
titan550 wants to merge 1 commit into
antirez:mainfrom
titan550:fix/dsml-tracker-upstream

Conversation

@titan550

@titan550 titan550 commented Sep 6, 2026

Copy link
Copy Markdown

My server decoded inside one open tool block for more than 900 seconds, at temperature 0, near 30,000 generated tokens. I used AI assistance to identify the issue. Two independent defects in dsml_decode_tracker_update() cause this class of failure. The commit message has the mechanics.

Defect 1: an unrecognized byte in the structural section stops the cursor permanently. Defect 2: a piece that ends on the lone < of a closing parameter tag loses that byte, and the close tag can never match again. Each defect wedges the state that selects the sampling policy. Defect 1 holds greedy sampling for the rest of the generation. The sampler has no repetition penalty, so a greedy repetition loop cannot exit, and decode runs until the context is exhausted.

Scope: cursor correctness only. This change does not bound a generation whose block never closes. That is the request in #48 and part of #895. An experimental token-budget backstop is stacked on this branch. I can send it separately if wanted.

The tests feed cumulative prefixes, byte by byte, because a one-shot feed cannot show a stuck cursor. A differential test compares the tracker to the test-only reference recognizer at random split points. Each fix was verified by reverting it alone against the new tests: the structural-loop advance -> 429 failures, the string-body one-byte hold -> 586, the JSON one-byte hold -> 298, the reference-recognizer fix -> 351.

Verification per CONTRIBUTING. Machine: Apple M5 Max, 128 GB, Darwin 27.0, Metal backend. Model: DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731 (via DS4_TEST_MODEL).

make clean && make      no new warnings (all 27 pre-existing, ds4_metal.m)
make cpu                builds
make test               ds4 tests: ok (15 suites incl. logprob-vectors,
                        long-context, tool-call-quality)
./ds4_test --server     ds4 tests: ok

Tracker cost is unchanged: 0.010 us per piece before and after (71 KB tool call, 4-byte pieces, best of 20). The change is server-side text scanning only. No inference backend is touched.

… close tags

dsml_decode_tracker_update() reads the accumulated response text with a
byte cursor, dt->pos. The cursor persists across calls. Two paths could
stop the cursor permanently. The tracker then could not see later bytes
and reported a stale classification for the rest of the generation. The
classification selects the sampling policy for the next token. A stuck
cursor holds the temperature at 0, also for payload text. The tracker
exists to keep payload sampled normally (22ca6ab).

1. The structural: loop returned on an unrecognized non-whitespace byte
   and did not advance the cursor. Stray prose after the start tag, an
   <invoke> tag without the sentinel, or a code fence stopped it
   permanently. The loop now advances past the byte. The partial-literal
   checks run first, so the loop still holds a tag prefix at the end of
   the buffer and does not skip it.

2. A piece can end on the lone '<' of a closing parameter tag.
   raw_partial_lit_min(..., 2) rejects a one-byte prefix, so the body
   loop consumed the '<'. The close tag then could not match again. A
   new one-byte check holds the cursor on that byte. The string-body
   loop reports STRING_BODY. The JSON loop reports JSON_STRUCTURAL.
   These are the values the reference recognizer returns, so the two
   recognizers stay in agreement.

3. dsml_decode_state_for_text() is the test-only reference recognizer.
   It had the same defect and gets the same fix. Without it, the
   differential tests would compare against the old defect.

This commit fixes cursor correctness only. It does not bound a
generation whose tool block never closes. That is a separate problem
(antirez#895, antirez#48). The tests feed cumulative prefixes because a one-shot feed
cannot show a stuck cursor. The tracker cost is unchanged: 0.010 us per
piece before and after (71 KB tool call fed in 4-byte pieces, best of
20 runs).
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.

1 participant