Repository navigation
scheduler: tool-parameter markers from the served tokenizer (Nemotron tool calls cut at 1024 tokens) - #196
Open
AzeezIsh wants to merge 6 commits into
Open
scheduler: tool-parameter markers from the served tokenizer (Nemotron tool calls cut at 1024 tokens)#196AzeezIsh wants to merge 6 commits into
AzeezIsh wants to merge 6 commits into
Conversation
…nizer (Nemotron large tool calls were cut at 1024 tokens) update_tool_param_state tracks whether generation is inside a qwen3_coder <parameter=KEY> value, whose tokens are exempt from the 1024-token envelope-stuck cap. It compared against hard-coded Qwen3.6 ids (< 27, = 28, > 29, </ 510, parameter 15704). Nemotron 3.5 Super VL serves the same XML tool format with its own tokenizer (1060 / 1061 / 1062 and a merged >\n 1561 / 1885 / 31960; the Qwen ids are <so_embedding>, <SPECIAL_510>, ... there), so a value was never recognised: every file content token counted as envelope, any argument past 1024 tokens tripped tool_envelope_stuck, and the client got a salvaged fragment. Observed in an agent session: large create_file writes landing as the 11 bytes '</parameter', replace_in_file calls losing their replacements array. The markers are now resolved from the tokenizer when it is loaded (tokenizer_runtime, startup and model swap), including the merged >\n forms, and read by the guard. A tokenizer that does not encode a marker as one token leaves the tracking and the cap off instead of misfiring; --tool-max-tokens still bounds a runaway envelope. Qwen ids resolve to the same values as before, so Qwen behaviour is unchanged. Tests: the resolver on Qwen and Nemotron tables; the real guard on a Nemotron write with a 1500-token content (with </div> closes) finishes with the Nemotron markers, trips with the Qwen ids (the old behaviour), stays off with none, and a never-closed envelope is still cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talls its own; per-thread in tests The first version started the slot empty, which turned the parameter tracking off for any caller that never loads a tokenizer, including the existing think_commit span test that replays a Qwen tool call. The slot now starts at the Qwen ids the guard was hard-coded to (every serve path installs the served tokenizer's markers in tokenizer_runtime). Under cfg(test) the slot is thread-local, so tests installing other markers cannot race the ones that rely on the default. The emit tests also reset the fixture's finished flag; the shared fixture builds a finished sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry, not five ids
An in-stream tokenization battery (both tokenizers, real tokenizer.json)
showed exact-id matching misses openers even on Qwen: BPE merges the '='
into the key's first sub-word for common keys ('=file' '_path',
'=re' 'placements', '=new' '_str'), so those values were never recognised
and their content counted toward the 1024-token envelope cap. Nemotron
merges '>' with the newline ('>\n') and both merge punctuation into the
close ('.</').
ToolMarkers is now five token classes built once per tokenizer from the
decoded text of every vocabulary entry: '<' (leading whitespace allowed),
'parameter', starts-with '=', starts-with '>', ends-with '</'. The guard
matches classes; until a tokenizer installs its own, the slot holds the
five legacy ids, so callers that never load one are unchanged.
Tests: class coverage of the merged forms; a Nemotron 1500-token write
finishes with its classes and is killed by the legacy ids; a Qwen
new_str value (merged '=new') finishes with the classes and is killed by
the legacy ids (the pre-existing Qwen miss); unresolved -> off; a
never-closed envelope is still cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
Qwen leg (Qwen3.8-27B NVFP4, same box): inconclusive, no regression signalBoth arms were served bare-metal with the
|
This branch has not been deployed
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.
Problem
A code_puppy agent session on Nemotron 3.5 Super VL, served by Atlas, had every large tool call cut off.
create_filewrites landed as the 11 bytes</parameter, andreplace_in_filecalls arrived without theirreplacementsarray.Atlas caps the tool-call envelope at
MAX_TOOL_BODY_TOKENS(1024, log tagtool_envelope_stuck) and exempts the tokens inside a<parameter=KEY>value.scheduler/emit_step.rs::update_tool_param_statefound those values by matching five hard-coded Qwen3.6 token IDs (<27,=28,>29,</510,parameter15704). Nemotron's tokenizer uses different IDs (<1060,=1061,>1062,>\n1561,</1885,parameter31960). So on Nemotron no value was ever recognised, and all file content counted against the 1024-token cap. The model itself is fine.Fix
scheduler/tool_markers.rsbuilds the marker token classes once from the served tokenizer's vocabulary:<parameter=></tokenizer_runtime.rs, at startup and on model swap. The startup log names them.--tool-max-tokensstill bounds a runaway envelope.=into common key names (=file,=new,=re), which the exact-ID check also missed. Those values are now recognised as values.Verification on a GB10 (sparky)
Setup:
origin/maine29809d and fix dfd3a80.scripts/serve-nemotron-gb10.sh --mode atlas --spec g8(nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4+ LiLiCorr drafter).create_file+replace_in_filetools, temperature 0, a single-file HTML site written withcreate_file. Each request ran twice non-streaming and twice streaming (tool_calls deltas accumulated).Fix-arm startup log:
Tool parameter marker classes: < x2 parameter x1 =* x166 >* x79 *</ x34tool_envelope_stucktool_calls(salvaged){"file_path":"index.html","content":""}lengthtool_calls</html>tool_calls</html>Main: 4/4 cut at the 1024-token envelope cap (
tool_envelope_stuck= 4 in total, one per request). Fix: 4/4 complete, with identical output across runs.Unit tests:
cargo test -p spark-server --bins tool_markerspasses 8/8. The tests replay a 1500-token Nemotron write: it completes with the Nemotron markers, is cut with the old Qwen IDs, and stays uncut with no markers. A never-closed envelope is still cut.Golden check (fix arm,
serve-nemotron-gb10.sh --mode atlas --spec g8 --check): P1 PASS, P9 PASS, parallel_multiple_78 PASS. All byte-exact.Qwen: the live Qwen repro was not run in this pass. The Qwen behaviour change (merged
=keyforms now recognised as values) is covered by the unit tests only.Note on the 8192 ceiling: a first repro prompt asked for a site larger than 8192 tokens. On the fix arm those requests ran the full 8192 tokens without a single
tool_envelope_stuck, then stopped at--tool-max-tokens(8192) before the model closed the call, withfinish_reasonlengthand empty salvaged arguments. That is the separate envelope bound working as designed, not this bug. The table above uses a prompt that fits.🤖 Generated with Claude Code