Skip to content

[Feat] Support thinking_token_budget in Model Runner V2 - #46727

Merged
njhill merged 11 commits into
vllm-project:mainfrom
chaunceyjiang:thinking-budget-runner-v2
Aug 7, 2026
Merged

njhill merged 11 commits into
vllm-project:mainfrom
chaunceyjiang:thinking-budget-runner-v2

Conversation

@chaunceyjiang

@chaunceyjiang chaunceyjiang commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

In some quantized models, such as GLM-5.2 or Qwen quantized models, the model may generate long reasoning traces.

Purpose

Support thinking_token_budget in Model Runner V2

Test Plan

see e2e

Test Result

vllm serve /mnt/data4/models/Qwen/Qwen3.5-27B-FP8 --enable-auto-tool-choice --tool-call-parser qwen3_coder --reasoning-parser qwen3
curl -sS http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "",
    "messages": [
      { "role": "user", "content": "9.11 and 9.8, which is greater?" }
    ],
    "thinking_token_budget": 10
  }' |jq
{
  "id": "chatcmpl-bc955ba3f0743a51",
  "object": "chat.completion",
  "created": 1782394356,
  "model": "/mnt/data4/models/Qwen/Qwen3.5-27B-FP8",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "\n\n**9.11 is greater than 9.8.**\n\nHere is the breakdown:\n\n*   **9.8** can be written as **9.80**.\n*   **9.11** is **9.11**.\n\nWhen comparing the decimal places:\n*   The whole number part is the same (9).\n*   In the tenths place, **8** is greater than **1**.\n\nTherefore, **9.8 > 9.11**.",
        "refusal": null,
        "annotations": null,
        "audio": null,
        "function_call": null,
        "reasoning": "Thinking Process:\n\n1.  **An"
      },
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null,
      "token_ids": null,
      "routed_experts": null
    }
  ],
  "service_tier": null,
  "system_fingerprint": "vllm-0.23.1rc1.dev418+g6e3a983cf-e4992f95",
  "usage": {
    "prompt_tokens": 24,
    "total_tokens": 141,
    "completion_tokens": 117,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "prompt_token_ids": null,
  "prompt_text": null,
  "kv_transfer_params": null
}


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@njhill

njhill commented Jun 26, 2026

Copy link
Copy Markdown
Member

Thanks @chaunceyjiang, at first glance this looks pretty good, I'll try to take a closer look soon!

@njhill njhill mentioned this pull request Jun 30, 2026
26 tasks

@NickLucche NickLucche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thanks @chaunceyjiang I think this is looking clean

@njhill njhill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @chaunceyjiang!

I think the other thing we should aim to improve is the scan of whole context each time where we only need to consider new tokens.

Comment thread vllm/v1/worker/gpu/sample/sampler.py Outdated
Comment thread vllm/v1/worker/gpu/model_runner.py Outdated
Comment thread vllm/v1/worker/gpu/sample/thinking_budget.py Outdated
Comment thread vllm/v1/worker/gpu/sample/thinking_budget.py Outdated
Comment thread vllm/v1/worker/gpu/sample/thinking_budget.py Outdated
Comment thread tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py Outdated
@njhill

njhill commented Jul 2, 2026

Copy link
Copy Markdown
Member

I think the other thing we should aim to improve is the scan of whole context each time where we only need to consider new tokens.

I pushed a draft change for this here, not sure about it yet though: njhill@f2d3627

@chaunceyjiang
chaunceyjiang force-pushed the thinking-budget-runner-v2 branch 2 times, most recently from 444bc29 to 3f90e27 Compare July 6, 2026 10:39
@chaunceyjiang
chaunceyjiang requested a review from njhill July 6, 2026 15:35
last_start = -1
last_end = -1

for i in tl.range(scan_pos, total_len):

@rishitdholakia13 rishitdholakia13 Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From what I understand, during prefill, if we're processing a long context (e.g., ~128k tokens), we currently perform a forward scan over the entire prompt to locate the <START_THINKING> and <END_THINKING> tokens. In the common case where the <START_THINKING> token appears near the end of the prompt template, this results in an unnecessary O(128k) forward scan.

One potential optimization would be to search for the thinking boundary tokens in reverse instead. Most reasoning prompt templates place the <START_THINKING> and <END_THINKING> instructions towards the end of the prompt, so a reverse scan would likely find them much sooner and reduce the amount of work performed during prefill. This would probably help lower the TTFT overhead at large context.
cc: @njhill

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Comment thread vllm/v1/worker/gpu/sample/thinking_budget.py Outdated
@dafeliton

Copy link
Copy Markdown

Hi all,

This PR is causing issues with the thinking token phrase leaking into the chat. V1 previously worked around this but it looks like after X tokens even if we aren't in a reasoning block the phrase will leak out into the main response.

@chaunceyjiang

Copy link
Copy Markdown
Collaborator Author

@dafeliton, could you provide the steps to reproduce this issue?

@dafeliton

dafeliton commented Jul 13, 2026

Copy link
Copy Markdown

@chaunceyjiang Sure. With gemma 31b, add these args:

- --enable-auto-tool-choice
  - --reasoning-parser
  - gemma4
  # Enables per-request reasoning caps via extra_body.thinking_token_budget.
  - --reasoning-config
  - '{"reasoning_start_str":"<|channel>","reasoning_end_str":"I have to give the solution based on the reasoning directly now.<channel|>"}'
  - --tool-call-parser
  - gemma4

Then set the reasoning budget to 1500 and tell the model to output something long such as a 50 paragraph essay. The reasoning end str appears in the main response. There were previous PRs for V1 that alleviated this issue.

Sample:

Prompt::
Calculate the enthalpy of neutralization when 500 mL 0.2 M HCl, 300 mL 0.3 M H2SO4 and 200 mL 0.5 M Ba(OH)2 are mixed.
A. -2.72 kcal
B. -11.42 kcal
C. -3.80 kcal
D. -16.0 kJ

Response::
<Reasoning>
The user previously asked to calculate the enthalpy of neutralization for a mixture of $\text{HCl}$, $\text{H}_2\text{SO}_4$, and $\text{Ba(OH)}_2$.
$\text{HCl}$: $500 \text{ mL} \times 0.2 \text{ M} = 0.5 \text{ L} \times 0.2 \text{ mol/L} = 0.1 \text{ mol } \text{H}^+$
$\text{H}_2\text{SO}_4$: $300 \text{ mL} \times 0.3 \text{ M} = 0.3 \text{ L} \times 0.3 \text{ mol/L} = 0.09 \text{ mol } \text{H}_2\text{SO}_4$
Since $\text{H}_2\text{SO}_4$ is diprotic: $0.09 \times 2 = 0.18 \text{ mol } \text{H}^+$
Total $\text{H}^+ = 0.1 + 0.18 = 0.28 \text{ mol } \text{H}^+$
$\text{Ba(OH)}_2$: $200 \text{ mL} \times 0.5 \text{ M} = 0.2 \text{ L} \times 0.5 \text{ mol/L} = 0.1 \text{ mol } \text{Ba(OH)}_2$
Since $\text{Ba(OH)}_2$ is dibasic: $0.1 \times 2 = 0.2 \text{ mol } \text{OH}^-$
$\text{H}^+ + \text{OH}^- \rightarrow \text{H}_2\text{O}$
$\text{H}^+$ available: $0.28 \text{ mol}$
$\text{OH}^-$ available: $0.20 \text{ mol}$
Limiting reactant: $\text{OH}^-$ ($0.20 \text{ mol}$)
Amount of water formed: $0.20 \text{ mol}$
The standard enthalpy of neutralization for a strong acid and strong base is approximately $\Delta H_{neut} \approx -57.3 \text{ kJ/mol}$ (or $\approx -13.7 \text{ kcal/mol}$).
Calculation in kcal: $0.20 \text{ mol} \times (-13.7 \text{ kcal/mol}) = -2.74 \text{ kcal}$.
Calculation in kJ: $0.20 \text{ mol} \times (-57.3 \text{ kJ/mol}) = -11.46 \text{ kJ}$.
A. $-2.72 \text{ kcal}$
B. $-11.42 \text{ kcal}$
C. $-3.80 \text{ kcal}$
D. $-16.0 \text{ kJ}$
Comparison: My calculation $-2.74 \text{ kcal}$ is very close to Option A ($-2.72 \text{ kcal}$). Small differences often arise from the exact value of $\Delta H_{neut}$ used (some texts use $-55.8 \text{ kJ/mol}$ or $-13.3 \text{ kcal/mol}$). Using $-13.6 \text{ kcal/mol}$: $0.2 \times -13.6 = -2.72 \text{ kcal}$.
Calculate moles of $\text{H}^+$ and $\text{OH}^-$.
Identify the limiting reactant.
Multiply moles of reaction by the standard enthalpy of neutralization.
Concise.
No em dashes.
Proper LaTeX.
Professional/Friendly.
<End Reasoning>

To find the enthalpy of neutralization, we first determine the total amount of $\text{H}^+$ and $\text{OH}^-$ ions present in the mixture.

1. Calculate moles of $\text{H}^+$ ions

  • From $\text{HCl}$: ( 0.500\text{ L} \times 0.2\text{ M} = 0.1\text{ mol } \text{H}^+ )
  • From $\text{H}_2\text{SO}_4$: ( 0.300\text{ L} \times 0.3\text{ M} \times 2\text{ (since it is diprotic)} = 0.18\text{ mol } \text{H}^+ )
  • Total $\text{H}^+$ = ( 0.1 + 0.18 = 0.28\text{ mol} )

2. Calculate moles of $\text{OH}^-$ ions

  • From $\text{Ba(OH)}_2$: ( 0.200\text{ L} \times 0.5\text{ M} \times 2\text{ (since it is dibasic)} = 0.20\text{ mol } \text{OH}^- )

3. Identify the limiting reactant
The neutralization reaction is:
$$\text{H}^+ + \text{OH}^- \rightarrow \text{H}_2\text{O}$$
Since we have $0.28\text{ mol}$ of $\text{H}^+$ and only $0.20\text{ mol}$ of $\text{OH}^-$, the $\text{OH}^-$ is the limiting reactant. Therefore, $0.20\text{ mol}$ of water will be formed.

4. Calculate the total enthalpy
The standard enthalpy of neutralization for a strong acid and strong base is approximately (-13.6\text{ kcal/mol}) (or (-57.3\text{ kJ/mol})).

$$\text{Total Enthalpy} = \text{moles of reaction} \times \Delta H_{neut}$$ $$\text{Total Enthalpy} = 0.20\text{ mol} \times (-13.6\text{ kcal/mol}) = -2.72\text{ kcal}$$

Correct Option:
**A.I have to give the solution based on the reasoning directly now.A. -2.72 kcal

- Clamp budgets to int32 range in add_request; oversized request values
  previously raised numpy OverflowError, crashing the engine
- Match the forced-end prefix purely on content so a resumed prompt ending
  with a partial end marker continues it instead of duplicating its first
  token (prompt_len plumbing removed from the kernel as no longer needed)
- Vectorize the cold marker scan in blocks of 1024 positions (~34x faster
  worst-case cold resume at 64k history)
- Only copy budgets to UVA when a value actually changed, instead of every
  scheduling step

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: Nick Hill <nickhill123@gmail.com>
@njhill
njhill force-pushed the thinking-budget-runner-v2 branch from 9aa4d54 to 1752c78 Compare August 7, 2026 00:38
justtestingthingsx pushed a commit to meandmyboiclaude/vllm that referenced this pull request Aug 7, 2026

@njhill njhill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @chaunceyjiang! I pushed one more commit with some additional fixes and optimizations.

@njhill njhill added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 7, 2026
@njhill
njhill enabled auto-merge (squash) August 7, 2026 00:49
@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hi @chaunceyjiang, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

1 similar comment
@mergify

mergify Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hi @chaunceyjiang, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

@chaunceyjiang

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #82766 for commit b813d3c6760b.

@njhill
njhill merged commit 72c0d67 into vllm-project:main Aug 7, 2026
119 checks passed
@chaunceyjiang
chaunceyjiang deleted the thinking-budget-runner-v2 branch August 7, 2026 03:49
randomvariable added a commit to randomvariable/vllm that referenced this pull request Aug 7, 2026
This fork and upstream independently wrote vllm/v1/worker/gpu/sample/
thinking_budget.py in the same week: 9fedae272b created it here on
2026-08-06, and upstream PR vllm-project#46727 (72c0d67) created the same path on
2026-08-07, arriving in the rebase. Neither derives from the other, and
the rebase kept ours while taking upstream's test file, so upstream's 12
tests ran against an API they were never written for.

Comparing the two on the behaviour they share, upstream's is the better
implementation and this commit takes it wholesale.

Pattern matching. Ours advanced a KMP cursor with no failure table, so a
marker with a border is missed outright:

    pattern [1,1,2] in [7,1,1,1,2,9]   -> ours []    upstream [2]
    pattern [1,2,1] in [7,1,2,1,2,1,9] -> ours [1]   upstream [1,3]

Every marker shipped today (<think>, <|START_THINKING|>, <mm:think>) is
border-free, so this was latent rather than live, but upstream matches
the full pattern at each candidate position and cannot express the bug.

Natural vs forced end markers. Upstream tracks natural_reasoning_end_
token_ids separately from reasoning_end_token_ids, so a parser whose own
end marker differs from the configured forced one still terminates
detection correctly. Ours conflated the two.

Resumed prompts. Upstream derives how much of the end sequence a resumed
prompt already emitted and continues from the next marker token, rather
than relying on incremental device state surviving preemption.

Host synchronisation. Upstream forces inside the kernel. Ours copied
three state arrays to host and rebuilt the forced rows in Python, one
.item() per row, on every step that forced.

Ours was also carrying a self-inflicted defect that upstream's design
cannot have: eleven kernel-written state tensors, whose host arrays are
not aliased to device memory, needing a bespoke StagedWriteTensor to
avoid being clobbered on flush. Upstream keeps three cached scalars.

The fork's own extensions -- the logits-processing gate fix, the
hesitation-marker penalty, and the answer reserve -- are re-applied on
top of this base in the following commit, where they read as a small
reviewable delta against upstream instead of as a parallel universe.

tests/v1/worker/gpu/sample/test_thinking_budget.py asserted the removed
internals throughout and is deleted here; it is rewritten against the new
base in the next commit. Upstream's tests/v1/worker/test_gpu_thinking_
budget.py passes 12/12 at this commit.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk>
zyp2014 pushed a commit to zyp2014/vllm that referenced this pull request Aug 21, 2026
…#46727)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
sajadn pushed a commit to linnanwang/vllm that referenced this pull request Aug 26, 2026
…#46727)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mrv2 Model Runner V2 specific performance Performance-related issues ready ONLY add when PR is ready to merge/full CI is needed v1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants