Repository navigation
llm-runner: --raw-tokens is always the context; add --raw-continuation for scoring - #374
Conversation
5e5ae2a to
d1a3e0e
Compare
d1a3e0e to
843f454
Compare
stikves
left a comment
There was a problem hiding this comment.
This seems to add a new runtime path, this is not entirely tested (for example zero input, or context length mismatches)
Are we treating the symptom, instead of the root cause here?
carinapeng
left a comment
There was a problem hiding this comment.
This change makes sense and thanks for fixing for the eval correctness
Pls address the opens before merging, this PR looks fine in making raw tokens the context. +1 on the question of whether we are treating the root cause of the issue
Thanks. On testing: the new path is --raw-continuation, and every refusal it makes happens before the model loads, each with its own test in RawTokenScoringTests. That covers an empty context or continuation, an unreadable or malformed file, a negative or out-of-vocabulary id in either file, and context + continuation longer than the model's context length. I've also added the zero-input case for generation: an empty --raw-tokens used to reach the engine and is now refused up front. On root cause: the bug wasn't the split itself. llm-runner guessed "score" from --raw-tokens plus a logits flag, then hard-coded tokens[0] as the context. That made generating from raw tokens with logits impossible, and it gave scoring only a one-token context. Fixing just the split (say, a --context-length N) would have kept the guess, so --raw-tokens --save-logits still couldn't generate. This PR removes the guess. --raw-tokens is always the context, and scoring happens only when you name the ids with --raw-continuation, the same way text mode has --prompt and --continuation. The library side follows suit: evaluateRawTokens(context:continuation:) takes an explicit context, the old overload is deprecated with its behaviour pinned by a test, and both scoring modes share one feed. |
…n for scoring
`llm-runner --raw-tokens ... --save-logits` (or --print-logits) did not
generate. It split the ids into a one-token context (tokens[0]) and a
forced continuation (tokens[1...]) and scored that continuation, so
--max-tokens had no effect and the `tokens` document was never written.
The raw-token mode now mirrors --prompt and --continuation:
- --raw-tokens FILE is always the context. With --save-logits or
--print-logits it generates and writes the `tokens` document
(token_id, incremental_text, top_logits per generated position).
- --raw-continuation FILE ({"tokens": [...]}) gives the ids to score as
a forced continuation of the whole context, and writes the scoring
document.
- The run refuses, before the model loads, --raw-continuation without
--raw-tokens, without a logits output, together with --continuation,
or with an empty or unreadable context or continuation file.
The decision and every error that names a flag live in llm-runner
(RawTokenScoring, ContinuationOptionError); CoreAILanguageModels names no
command-line flag. TextGenerator.evaluateRawTokens(context:continuation:)
and evaluateContinuation share one feed, so both scoring modes feed the
model the same way.
CoreAISequentialEngine ran a scored continuation as one `main` batch of
context plus continuation. A width-1 `main` beside a prefill graph
aborts on that batch. Such an asset now takes the generation path: the
context goes through the prefill graph and each continuation id through
`main`, one at a time. Assets without a prefill graph, or with a
dynamic-width `main`, still batch.
Tests: a new LLMRunnerTests target covers argument parsing and the
refusals; ContinuationEvaluationTests covers both raw-token modes; a
gated parity test (FORCED_CONTINUATION_PARITY_BUNDLE) checks the batch
and step paths agree on a real bundle.
…under --json-schema - Drop the CoreAISequentialEngine forced-continuation batching gate and its parity test. The fix for the raw-token mode needs no engine change, and the gate never applies to an asset the exporter here produces. - Keep TextGenerator.evaluateRawTokens(_:) as a deprecated overload with its old split ([tokens[0]] as the context, tokens[1...] scored), and keep the five ContinuationEvaluationError cases llm-runner no longer throws, deprecated. ContinuationEvaluationResult gains a public init. - --raw-continuation refuses --batch-prototype, --image and --video, which take their own paths and skipped the scoring silently. - --raw-continuation refuses, before the model loads, an id outside the model's vocabulary and a context plus continuation longer than the model's context length. - With --json-schema, --raw-continuation masks each position's logits with the schema's grammar before scoring, as constrained generation masks them before sampling, and refuses a continuation id the schema does not allow or that comes after the JSON is complete.
…gTests Refusal tests now pass the expected ContinuationOptionError as a value (ContinuationOptionError is Equatable) and the call as the one trailing closure, so each test checks the whole error, not only its case.
d1e6782 to
1ce320c
Compare
…ion, refuse empty input - --raw-tokens help says it is the context, that the run generates, and that scoring needs --raw-continuation. - --raw-tokens with --save-logits or --print-logits and no --raw-continuation prints a note on stderr with the migration: [t0, t1, ..., tn] becomes --raw-tokens [t0] --raw-continuation [t1, ..., tn]. - An empty --raw-tokens is refused before the model loads when the run generates, as it already was when it scores.
1ce320c to
6b1b251
Compare
Is a strong statement. Do we check the paths for EOS handling (getting the additional EOS tokens from tokenizer)? |
stikves
left a comment
There was a problem hiding this comment.
Mostly good, please check the claim for EOS handling, and update the comments.
The xgrammar C bridge passed no stop token ids to TokenizerInfo, so ConstrainedGenerationSession dropped the stopTokenIds it was given and every schema-constrained session, generation included, fell back to the stop tokens xgrammar detects from the vocabulary. With Qwen3, a JSON continuation ending in <|im_end|>, its EOS, was refused when scored, and one ending in <|endoftext|> was scored as certain. The bridge and TokenizerInfo now take the stop token ids. Scoring under --json-schema builds its grammar with the stop tokens generation uses (the tokenizer's EOS, the additional ones from tokenizer_config.json and single-token --stop-tokens) and the same whitespace limit, through a shared StopSequences.singleTokenStopIds. Doc comments in RawTokenScoring and runModel now say what the code does, without flag names.
Good catch. The xgrammar C bridge passed no stop token ids to xgrammar's With Qwen3-0.6B, scoring
Fixed in 19b92fa:
This changes constrained generation too: once the JSON is complete, the model can now choose any of its stop tokens. Visible output is unchanged with Qwen3, because both of its end tokens were already in the stop list. |
Type of Change
Motivation and Context
Closes #373.
llm-runner --raw-tokens CTX --save-logits out.json(or--print-logits) does not generate. It splits the ids into a one-token context (tokens[0]) and a forced continuation (tokens[1...]) and scores that continuation instead. So--max-tokenshas no effect, and thetokensdocument (token_id,incremental_text,top_logits) is never written. There was no way to generate from pre-tokenized input and save the logits of what was generated.This PR makes the raw-token mode mirror the text mode, where
--promptis the context and--continuationis what gets scored:--raw-tokens CTX+--save-logitsor--print-logitsCTXtokensdocument, one entry per generated position, up to--max-tokens--raw-continuation CONTCTXcontext_tokens,continuation_tokens,positions,total_log_probability,perplexity, … with one position per id inCONTCONTuses the same{"tokens": [...]}format as--raw-tokens, and the context can be any length.The run refuses, before it loads the model, when
--raw-continuationis given:--raw-tokens;--print-logitsor--save-logits;--continuation,--batch-prototype,--imageor--video(each of those takes its own path and would skip the scoring);With
--json-schema, scoring uses the grammar constrained generation uses, with the same stop tokens and whitespace limit: each continuation position's logits are masked by the schema's grammar before they are scored. Once the JSON is complete, only a stop token is allowed: the tokenizer's EOS, the additional ones fromtokenizer_config.json, or a single-token--stop-tokens. A continuation id the schema does not allow, or one after the stop token, is refused with its position.Behaviour change. If you relied on
--raw-tokens+--save-logitsto get the scoring document, split the input:[t0, t1, …, tn]becomesCTX = [t0]andCONT = [t1, …, tn], passed as--raw-tokens CTX --raw-continuation CONT. You can now also give the scored continuation a context longer than one token.Changes
llm-runner(RawTokenScoring.swift,LLMRunnerMain.swift). New--raw-continuationoption.RawTokenScoringdecides between generating and scoring, makes every refusal above before the model loads, and masks the scores under--json-schema. Every error that names a command-line flag lives beside it asContinuationOptionError, with the existing messages unchanged.TextGenerator.swift. NewevaluateRawTokens(context:continuation:), which takes a context of any length. It and the textevaluateContinuationshare one private feed (scoreForcedContinuation), so both scoring modes feed the model the same way. The oldevaluateRawTokens(_:)stays, deprecated, with its old split ([tokens[0]]as the context,tokens[1...]scored).ContinuationEvaluation.swift. TheContinuationEvaluationErrorcases that name llm-runner flags stay, deprecated; llm-runner no longer throws them.ContinuationEvaluationResultgains a public memberwiseinit. No public API is removed.xgrammar_c_bridge.h/.cpp,TokenizerInfo.swift,ConstrainedGenerationSession.swift). The C bridge passed no stop token ids to xgrammar, soConstrainedGenerationSessiondropped thestopTokenIdsit was given. Every schema-constrained session, generation included, used the stop tokens xgrammar detects from the vocabulary. With Qwen3 that is<|endoftext|>alone, so after the JSON the model could never choose<|im_end|>, its EOS. The bridge andTokenizerInfonow take the ids, and both decoding strategies and scoring shareStopSequences.singleTokenStopIds.Package.swift. Adds aLLMRunnerTeststest target. It adds no product.No inference engine changes. Constrained generation's visible output is unchanged where xgrammar's detected stop token was also in the stop list (as with Qwen3); what changes is which stop token the model may choose.
Testing
Tested locally
Added/updated tests
Added/updated docs
swift build --build-testsand the fullswift testsuite pass on macOS.New tests:
LLMRunnerTests: argument parsing; each refusal above, including--batch-prototype,--image,--video, ids outside the vocabulary (negative and too large, in either file) and more ids than the context length; scoring under a JSON schema (an allowed continuation is scored over the allowed ids; once the JSON is complete only a stop token is allowed and is scored; a disallowed id and an id after the stop token are refused).StopTokenTests: the stop tokens a grammar is given are the ones allowed once the JSON is complete, are refused before it, and end the grammar when accepted, through bothConstrainedGenerationSessionandTokenizerInfo. Two of them fail without the bridge fix.ContinuationEvaluationTests: generation from raw tokens writes thetokensdocument; scoring feeds the whole multi-token context and scores each continuation id; empty context or continuation is refused; the deprecatedevaluateRawTokens(_:)keeps its split; text--continuationis unchanged.End to end on Qwen3-0.6B with
--inference-engine-variant coreai-sequential:--raw-tokens ctx.json --save-logits gen.json --max-tokens 4generates and writes thetokensdocument.--raw-tokens ctx.json --raw-continuation cont.json --save-logits out.jsonscores every continuation id against the whole context.--json-schemawith{"a": integer}and a continuation of{"a": 1}: every position's probability is at least its unmasked value (total log-probability −23.3 unmasked, −6.1 masked);{"b": 1}is refused at position 1.{"a": 1}followed by an end token:<|im_end|>is scored at −7.454 in total, the same as without it, and<|endoftext|>at −22.235. Before the stop-token fix,<|im_end|>was refused and<|endoftext|>scored as certain (−7.454).--image,--video,--batch-prototype, an id of 10,000,000 and a 50,000-id context are each refused before the model loads.