Skip to content

fix(qwen3): preserve explicit stop-token causes - #978

Open
RicardoMin wants to merge 2 commits into
pegainfer-project:mainfrom
RicardoMin:fix/qwen3-stop-contract-865
Open

fix(qwen3): preserve explicit stop-token causes#978
RicardoMin wants to merge 2 commits into
pegainfer-project:mainfrom
RicardoMin:fix/qwen3-stop-contract-865

Conversation

@RicardoMin

Copy link
Copy Markdown
Contributor

Related to #865

Qwen3: preserve explicit stop-token causes in the stepped contract

Why this is a separate PR

This is the scoped follow-up requested by the maintainers during review of
#865. They asked
that the broad stop-contract change be split so that the shared boundary,
frontend bridges, and one model can be reviewed and validated independently.
This PR therefore extracts the Qwen3 migration from that work; Qwen3.5 and the
other model schedulers remain on their existing contract for now.

Summary

The previous vLLM dependency update fixed several frontend compatibility issues,
but the stepped Qwen3 path still collapsed two independent controls into the
single legacy ignore_eos flag. That made an explicit stop_token_ids request
indistinguishable from a model-EOS request and forced the bridge to guess a
synthetic stop token after the scheduler had already discarded the real one.

This PR gives Qwen3 a typed stop contract. It preserves the sampled trigger
token and its logprob, carries the concrete StopCause through the scheduler
and stepped bridge, and keeps EOS handling independent from request-provided
stop IDs.

What was wrong

The old contract exposed only FinishReason::Stop or FinishReason::Length.
When a request stopped, the bridge could not tell whether the model emitted EOS
or an explicit request stop token. It therefore reconstructed a sentinel (EOS
first, otherwise the first configured stop ID). That reconstruction can report
the wrong token, loses the token's logprob, and is incorrect for a speculative
span where the first terminal token is followed by additional accepted tokens.

The old boolean also could not express the valid combination "ignore model EOS,
but still stop on these explicit request token IDs".

Contract change

Event Legacy behavior Qwen3 stepped behavior
Model EOS FinishReason::Stop; trigger may be suppressed or reconstructed FinishReason::Stop + StopCause::Eos(id); token is retained internally; wire stop_reason is absent
Explicit stop_token_ids match Often treated as ordinary output or replaced by a guessed sentinel FinishReason::Stop + StopCause::Token(id); actual ID is reported as wire stop_reason
ignore_eos=true Could inadvertently disable explicit stops Disables only model EOS; explicit request stops remain active
Length limit FinishReason::Length FinishReason::Length + no stop cause; final sampled token is retained
Speculative span Trigger/suffix ownership was ambiguous Commit the prefix through the first trigger and discard only the suffix after it

EOS has precedence when the same ID is both the active EOS token and an explicit
request stop. Completion-token accounting is incremented once, including the
trigger token.

Scope and compatibility

Following the maintainer's scope request on #865, this PR intentionally
migrates Qwen3 only. The shared request/step types
accept an optional typed cause, while the existing legacy event path remains
available for models that have not been audited. The legacy bridge keeps its
synthetic-sentinel fallback only when an old producer supplies no typed cause.
Therefore Qwen3.5 and other model schedulers are not changed in this PR and do
not need to adopt the new resolver contract yet. If the maintainers agree with
the semantics, the remaining model lines can be migrated one at a time with
their own lifecycle tests.

Implementation

  • Added StopPolicy, EosPolicy, and StopCause at the frontend engine
    boundary.
  • Converted wire EOS and explicit stop fields without collapsing them into one
    boolean for the stepped path.
  • Propagated the policy through Qwen3 request state, ledger updates, prefill,
    ordinary decode, and speculative verification.
  • Emitted the trigger token before terminal metadata and retained its logprob.
  • Mapped StopCause::Token(id) to the vLLM-compatible wire stop_reason.
  • Left the legacy bridge fallback and un-migrated model implementations intact.
  • Updated two existing test fixtures (K3 and simulator) only to supply the new
    shared default field; no legacy model runtime behavior is changed.

Automated verification

Check Result
cargo test --release -p pegainfer-frontend --lib 73 passed, 0 failed
cargo test --release -p pegainfer-qwen3 --lib 93 passed, 0 failed
Qwen3 request-stop focused tests 4 passed, 0 failed
Qwen3 speculative-stop focused tests 2 passed, 0 failed
cargo test --release -p pegainfer-sim --tests -- --test-threads=1 6 passed, 0 failed
cargo check --release -p pegainfer-qwen35 --features qwen35 Passed (control build only)
cargo build --release -p pegainfer-server --bin pegainfer Passed
cargo fmt --all -- --check Passed
git diff --check Passed

HTTP A/B verification

The comparison used two already-running OpenAI-compatible endpoints on the
same validation host. The explicit stop set covered the complete vocabulary,
so the first generated token was guaranteed to exercise the request-stop path.
This is a deterministic contract probe, not a generation-quality benchmark.

Results are shown as finish_reason / stop_reason / completion_tokens:

Target Control (ignore_eos=true) Explicit stop + EOS ignored Explicit stop + EOS enabled
Qwen3-0.6B (adapted) length / null / 8 stop / 12095 / 1 stop / 12095 / 1
Qwen3.5-0.8B (legacy control) length / null / 8 length / null / 8 length / null / 8
Contract check Qwen3 adapted Qwen3.5 legacy control
Baseline control pass pass
Explicit stop with EOS ignored pass fail
Explicit stop with EOS enabled pass fail
Stop-set order invariant pass not satisfied (no typed stop)
Trigger logprob present pass fail
Streaming typed stop pass fail
Mixed controls (3/3) 3/3 3/3
Mixed explicit stops (3/3) 3/3 0/3
Overall new-contract checks 8/8 2/8

The Qwen3.5 rows are an intentional legacy comparison: ordinary generation
still works, but its un-migrated scheduler does not yet satisfy the new typed
explicit-stop contract. They are not a claim that every legacy model fails in
all workloads.

Reproduction

Build and start each server independently. Qwen3.5 requires its feature-gated
Triton build environment; it does not support or require a
--gpu-memory-utilization CLI argument.

# Qwen3 stepped path
cargo run --release -p pegainfer-server -- \
  --model-path "$QWEN3_MODEL" \
  --served-model-name qwen3-adapted \
  --port 18081

# Qwen3.5 legacy control path (set PEGAINFER_TRITON_PYTHON if needed)
cargo run --release -p pegainfer-server --features qwen35 -- \
  --model-path "$QWEN35_MODEL" \
  --served-model-name qwen35-legacy \
  --port 18082

Then run the attached script (Python standard library only):

python3 pr865_qwen3_stop_contract_ab.py \
  --qwen3-url http://127.0.0.1:18081 \
  --qwen3-model qwen3-adapted \
  --qwen35-url http://127.0.0.1:18082 \
  --qwen35-model qwen35-legacy \
  --out stop-contract-ab.json

The script prints a compact comparison table and writes machine-readable JSON.
Use --stop-token-id ID to replace the full-vocabulary deterministic set with
a single known token when reproducing on a different prompt/model pair.

Follow-up

As requested during review of #865, this PR deliberately stops at the Qwen3
migration boundary. After the
maintainers confirm that the independent EOS/request-stop semantics are wanted,
the same policy propagation and resolver audit can be applied to Qwen3.5 and the
other legacy model lines in separate, model-scoped changes.

pr865_qwen3_stop_contract_ab.py

Signed-off-by: RicardoMin <17879681016@163.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a4d324ec8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pegainfer-qwen3/tests/common/harness.rs Outdated
Comment thread pegainfer-frontend/src/engine/stop.rs
Signed-off-by: RicardoMin <17879681016@163.com>
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