reasoning-budget: Implementing thinking-budget mechanism to control thought process - #25961
reasoning-budget: Implementing thinking-budget mechanism to control thought process#25961laurencehardman wants to merge 11 commits into
Conversation
Extends the reasoning-budget sampler with two new stages around the
existing hard cutoff: an intro message forced when the reasoning block
starts (announcing the token budget via a {budget} placeholder), and a
soft warning forced at a configurable fraction of the budget, injected
at the next newline boundary. Both are opt-in via
--reasoning-budget-soft-ratio/-message and
--reasoning-budget-intro-message, with per-request overrides.
Assisted-by: Claude Sonnet
…e period Adds a HARD_PENDING state entered when the budget is exhausted: waits up to --reasoning-budget-grace-tokens N for a paragraph break (two adjacent newlines) before forcing the cutoff, instead of forcing immediately. Falls back to an immediate force once the grace period expires, so total length stays bounded (budget + grace_tokens). Natural </think> still wins over a pending grace wait, same as the existing soft/hard paths. Default is 0 (disabled), preserving prior behavior exactly. Assisted-by: Claude Sonnet
…ce tokens Some chat templates bake the reasoning start tag directly into the fixed prefill (e.g. "<think>\n"), so the tag match and the trailing template text both flow through the same silent prefill-replay loop in common_sampler_init. If that match activates a forcing state (INTRO_FORCING/SOFT_FORCING/FORCING), any prefill tokens still to come were being fed through llama_sampler_accept as if the model had already emitted them - silently advancing past the first position(s) of the forced sequence before real generation even starts. Symptom: the intro message's leading token(s) missing from the output. Fix: stop feeding prefill tokens into the reasoning-budget sampler once it enters a forcing state during replay, since remaining prefill tokens are already-fixed prompt text, not live model output. Reproduced and verified fixed end-to-end against Qwen3.6-27B, whose template prefills "<think>\n" (unlike Qwen3-14B, which lets the model open <think> itself, masking the bug during earlier testing). Assisted-by: Claude Sonnet
…avior. feat: unescape message strings from env vars
fix: absence of environment variables should disable statemachine beh…
sync: Update with upstream commits
|
Have you run any standard evals to see how this affects both scores and token consumption? Statistically over a large number of example problems/requests, does this actually force the model to “deterministically ration” its token budget and avoid hitting the hard reasoning budget limit? If it is deterministic, it should be measurable. This PR doesn’t have any evidence that it actually helps the model at all, and as others pointed out on Reddit, even tiny formatting errors in the chat template can significantly impede model performance. A model that isn’t trained to talk to itself about token budgets seems unlikely to benefit from that, but I’d love to see data. |
…ssage Adds --reasoning-budget-enable / LLAMA_ARG_THINK_BUDGET_ENABLE as a master switch: the budget/soft/intro/grace mechanism and the reasoning_control endpoint only activate when it's explicitly set, regardless of what the other --reasoning-budget-* values are. Does not gate grammar_lazy's own unrelated need for this sampler. Also changes reasoning_budget_message semantics: it's now tokenized as-is instead of having the auto-detected end tag appended, so the closing token can be fully controlled per model/template (some don't use </think>). If left blank, falls back to forcing just the auto-detected end tag alone, so the block still always closes even with no custom message configured. Assisted-by: Claude Sonnet
reasoning-budget: add master enable switch and self-contained hard me…
Merges 118 upstream commits (ggml-org/llama.cpp) on top of this fork's
10 reasoning-budget commits.
Upstream independently reworked common/reasoning-budget.{h,cpp}: the
single-sequence token_matcher was replaced with a multi-pattern
Aho-Corasick matcher (common/trie.h), supporting multiple valid
start/end tag sequences per model, plus end_match tracking so the
grammar sampler can replay whichever end sequence actually closed the
block. Reconciled by adopting upstream's matcher as the foundation and
re-layering our four extra states (INTRO_FORCING, SOFT_PENDING,
SOFT_FORCING, HARD_PENDING) and their config (soft/intro/grace) on top
of it, fixing every advance()-as-bool check to use the new >= 0 match-index
convention (a match at index 0 would otherwise be silently missed).
Upstream also independently changed the reasoning_budget_message handling
to auto-append the first detected end tag's tokens. Kept our version
instead (message tokenized as-is, expected to include its own closing
tag) since it implements an explicit, deliberate feature request from
this fork's owner - falling back to the auto-detected tag only when the
message is empty.
All 23 unit tests pass (20 ours + upstream's 3 new multi-pattern/end-match
tests), full server build succeeds, and live end-to-end testing against
a real model confirms intro/soft/hard/grace stages and the master enable
switch all still work correctly post-merge.
Assisted-by: Claude Sonnet
I have the same concerns. The initial message should be deleted from this PR because it could break the model quality in an unforeseeable way. The soft limit "reasoning-budget-soft-message" and the "reasoning-budget-soft-ratio" are very welcome from my side. The "reasoning-budget-grace-tokens" should be changed and integrated into the "reasoning-budget" functionality per default or should be a boolean. Otherwise you could set "reasoning-budget-grace-tokens" to an infinity value and break the entire meaning of this. Or just delete it. |
This source tree is based on the uploaded `ggml-org/llama.cpp` master snapshot corresponding to commit: - `10bf611e533d81f739128304991c5e133c6aebd8` — `llama : check LoRA tensor data is within file bounds (ggml-org#27056)` The reasoning-budget feature was ported from: - `ggml-org/llama.cpp` PR ggml-org#25961 - `laurencehardman/llama-mindcontrol` - reviewed fork sync commit `dc2845e3fbb607a2ae0cfc366c5ad647eb145d79` (2026-07-31) Merge performed: 2026-08-16. ## Integrated behavior - Explicit master switch: `--reasoning-budget-enable` / `--no-reasoning-budget-enable`. - Hard reasoning token budget. - Optional intro sequence with `{budget}` substitution. - Optional soft warning at a configurable consumed-budget ratio. - Optional bounded grace period after hard-budget exhaustion. - Runtime `reasoning_end` control when explicitly armed. - Custom hard-cutoff message semantics from PR ggml-org#25961: a non-empty hard message is tokenized exactly as supplied and therefore must contain the model/template closing tag; an empty hard message falls back to the first detected closing tag. - Prefill replay fix: fixed prompt tokens no longer consume positions in a just-activated forced intro/soft/hard sequence. - Multiple valid reasoning start/end sequences and end-match tracking from current upstream. - UTF-8-safe hard cutoff behavior from current upstream. - Re-arming for multiple reasoning blocks in one response. ## Conflict resolutions against the newer master The PR was not copied file-for-file. It was re-layered over the current master so newer upstream behavior is retained. 1. The current Aho-Corasick multi-pattern reasoning-tag matcher is retained. 2. The current sampler interface, including `backend_reset` and `copy_state`, is retained. 3. The PR's INTRO/SOFT/HARD_PENDING states are integrated on top of the newer upstream state machine. 4. The master switch remains authoritative even when `grammar_lazy` independently requires the reasoning tracker. In tracking-only mode, intro/soft/grace/hard-budget control values are neutralized. 5. `reasoning_control` without a finite budget can still arm runtime hard-stop control, but does not accidentally activate intro/soft/grace behavior with an unlimited (`-1`) budget. 6. The runtime `reasoning_end` endpoint now checks both the global master switch and the per-request arm flag, and reports whether the live sampler was actually in a forceable state. 7. The older hard-budget initializer signature remains available as a compatibility overload for current-upstream callers/tests. 8. Generated CLI/completion/server option tables were regenerated with the current `llama-gen-docs`; older generated documentation was not copied over current master documentation. 9. The upstream root `README.md` is preserved. The fork-specific root README/benchmark narrative is not allowed to overwrite current llama.cpp project documentation; this file records the port instead. ## Validation performed Environment used for validation: - Linux x86_64 - CMake 3.31.6 - GCC/G++ 14.2.0 Verified locally: - `test-reasoning-budget` builds successfully. - 22 test functions pass, covering the current upstream cases plus PR conflict/regression paths; the consolidated manual-force test checks all four extended active states. - UTF-8 boundary tests pass. - `llama-server` compiles and links successfully against the merged sources. - `llama-server --help` exposes the new reasoning-budget flags and environment-variable names. - `llama-gen-docs` builds and regenerates the CLI/completion/server reference tables. - `git diff --check` passes. The build environment had no DNS access to download the optional embedded llama-server Web UI asset. llama.cpp therefore built the server without an embedded UI; this did not affect compilation/linking of the server or reasoning-budget source paths. ## Not validated here - CUDA compilation was not available in the validation container. - Model-specific end-to-end generation quality was not tested because no target GGUF/model was supplied for this merge step. - The PR remains experimental/draft upstream; successful source integration and unit/build validation do not establish that the injected intro/soft messages improve quality for every model/template. ## Relevant options ```text --reasoning-budget-enable --no-reasoning-budget-enable --reasoning-budget N --reasoning-budget-message MESSAGE --reasoning-budget-soft-ratio N --reasoning-budget-soft-message MESSAGE --reasoning-budget-intro-message MESSAGE --reasoning-budget-grace-tokens N ``` Corresponding environment variables: ```text LLAMA_ARG_THINK_BUDGET_ENABLE LLAMA_ARG_THINK_BUDGET LLAMA_ARG_THINK_BUDGET_MESSAGE LLAMA_ARG_THINK_BUDGET_SOFT_RATIO LLAMA_ARG_THINK_BUDGET_SOFT_MESSAGE LLAMA_ARG_THINK_BUDGET_INTRO_MESSAGE LLAMA_ARG_THINK_BUDGET_GRACE_TOKENS ```
…ination Extends the reasoning-budget sampler with two opt-in stages around the existing hard cutoff: - One-time soft wrap-up hint forced near a configurable fraction of the budget (reasoning-budget-soft-ratio), injected at the next newline boundary, at most once per reasoning block, with multi-block re-arm. - Bounded grace region before the end sequence is forced (reasoning-budget-grace-tokens), so reasoning stays bounded at budget + grace tokens instead of an abrupt hard chop. Important design choice vs the originating draft (ggml-org#25961): this implementation deliberately does NOT inject an intro-budget message (name placeholder / budget announcement) at reasoning start. There is no intro message. The soft hint is the only injected text, and only after the soft threshold is crossed. Natural reasoning-end always takes precedence over forcing in every state. Absent the new options, behavior is byte-for-byte identical to upstream (reasoning-budget, reasoning-budget-message, reasoning-budget-soft-*, reasoning-budget-grace-tokens are all defaults-disabled). New CLI options / env: --reasoning-budget-soft-ratio N (LLAMA_ARG_THINK_BUDGET_SOFT_RATIO) --reasoning-budget-soft-message MESSAGE (LLAMA_ARG_THINK_BUDGET_SOFT_MESSAGE) --reasoning-budget-grace-tokens N (LLAMA_ARG_THINK_BUDGET_GRACE_TOKENS) Validation: 19/19 reasoning-budget unit tests pass (12 upstream + 7 new covering soft-fire-once-at-threshold, not-before-threshold, invalid-ratio disabled, grace-exhaustion forcing, grace-natural-close precedence, grace bounds total reasoning, and multi-block re-arm). Signed-off-by: Jason Neal <masterjaso@gmail.com>
Overview
Extends the reasoning-budget sampler with two new stages around the existing hard cutoff: an intro message forced when the reasoning block starts (announcing the token budget via a {budget} placeholder), and a soft warning forced at a configurable fraction of the budget, injected at the next newline boundary.
Both are opt-in via
--reasoning-budget-soft-ratio/-message, and--reasoning-budget-intro-message, with per-request overrides.Additional information
The primary driver for this change is to rein-in / restrict the tendency of (particularly low-temperature) models from entering a thought-loop where their thought process either starts becoming repetitive, or spirals into nonsense - as does Qwen3.6-27B at lower-temperature settings. This mechanism forces the LLM to determiniscally ration its thinking budget and structure its thoughts with the budget in mind, as well as cut itself off (with both a soft and hard limit) once that budget has been reached.
Requirements