Skip to content

Fix V2 sampler gate to enforce thinking_token_budget - #357

Open
ozskywalker wants to merge 1 commit into
eugr:mainfrom
ozskywalker:fix/thinking-budget-v2-gate
Open

ozskywalker wants to merge 1 commit into
eugr:mainfrom
ozskywalker:fix/thinking-budget-v2-gate

Conversation

@ozskywalker

@ozskywalker ozskywalker commented Aug 23, 2026

Copy link
Copy Markdown

Summary

thinking_token_budget is supposed to cap how many tokens a reasoning model spends "thinking" before it has to give a real answer. On the V2 GPU model runner, it silently doesn't work: if a request sets a thinking budget but doesn't also set some other special sampling option (custom temperature, penalties, etc.), the sampler skips the code path that would enforce the cap. The model then keeps reasoning until it runs out of max_tokens and returns an empty answer.

This isn't specific to any one model or recipe — it affects any request that relies on thinking_token_budget under the V2 model runner with otherwise-default sampling settings (e.g. temperature=0, which is common for tool calls and deterministic answers).

Symptom

A deepseek_v4 request with thinking_token_budget=64, max_tokens=128, temperature=0 burns all 128 tokens on reasoning and comes back with content: null, finish_reason: "length". In production this showed up as broken tool calls and missed long-context answers — same root cause, just triggered by reasoning running long in both cases.

Fix

One missing check in Sampler._requires_logits_processing
(vllm/v1/worker/gpu/sample/sampler.py):

if self.thinking_budget_state.enabled and np.any(
    self.thinking_budget_state.use_thinking_budget[idx_mapping_np]
):
    return True

Everything else needed to make budgets work (ThinkingBudgetState.add_request / apply_staged_writes / apply, and ReasoningConfig's fallback to the parser's own <think>/</think> markers) was already correct — this one gate check was the only thing missing.

Shipped as docker/patch_vllm_thinking_budget_gate.py, matching the existing patch_vllm_*.py convention (idempotent, no-ops on builds without this V2 sampler code).

Testing

Unit tests (tests/test_vllm_thinking_budget_gate_patch.py) plus on-hardware validation: restarted a live 2x DGX Spark production server on the patched image and re-ran the same checks that had just failed on the unpatched one.

Check Unpatched Patched
thinking_token_budget=64 probe content: null, finish_reason: length content: "Dawn" (correct), finish_reason: stop
tool-call gate finish_reason: length, invalid args finish_reason: tool_calls, valid args
long-context needle @ 32K / 64K / 256K MISS / MISS / MISS HIT / HIT / HIT
decode throughput (512 tok, median of 3) 376ms TTFT / 38.3 tok/s 349ms TTFT / 39.3 tok/s (no regression)

Sampler._requires_logits_processing checks every per-request sampling
feature (logit bias, penalties, bad words, non-default temperature/
min_p/top_k/top_p) except thinking_token_budget. A request that sets
only a thinking budget, with otherwise-default sampling params (e.g.
temperature=0, no penalties), makes the gate return False, so
apply_sampling_params returns the raw logits and the thinking-budget
kernel never runs. ThinkingBudgetState is otherwise fully wired
(add_request/apply_staged_writes/apply) and ReasoningConfig correctly
resolves single-token <think>/</think> markers even when
reasoning_start_str/reasoning_end_str are left empty; only this gate
check was missing. Confirmed live against DeepSeek-V4-Flash-0731 on
the b12x image: thinking_token_budget=64 with temperature=0 and
max_tokens=128 spent all 128 tokens on reasoning and returned
content: null, finish_reason: length.

Not B12X-specific; affects any model/recipe using thinking_token_budget
under the V2 GPU model runner.
@ozskywalker
ozskywalker marked this pull request as ready for review August 23, 2026 19:11
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