Skip to content

reasoning-budget: Implementing thinking-budget mechanism to control thought process - #25961

Draft
laurencehardman wants to merge 11 commits into
ggml-org:masterfrom
laurencehardman:master
Draft

reasoning-budget: Implementing thinking-budget mechanism to control thought process#25961
laurencehardman wants to merge 11 commits into
ggml-org:masterfrom
laurencehardman:master

Conversation

@laurencehardman

Copy link
Copy Markdown

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

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes - Claude Sonnet to assist with extending the boilerplate reasoning-budget module, test generation, build, and final sense-check of the implementation.

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
@laurencehardman
laurencehardman requested review from a team, ggerganov and ngxson as code owners July 21, 2026 14:59
@github-actions github-actions Bot added documentation Improvements or additions to documentation testing Everything test related server labels Jul 21, 2026
…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
@coder543

Copy link
Copy Markdown
Contributor

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.

@laurencehardman
laurencehardman marked this pull request as draft July 23, 2026 10:35
…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
@thekk1

thekk1 commented Aug 9, 2026

Copy link
Copy Markdown

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.

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.
I already use it with the Strix pentesting agent (https://github.com/usestrix/strix) and this is very helpful to "help" the model, in particular the MoE models Qwen3.6-35b and Ornith1.0-35b, to prevent overthinking and generate bullshit findings.

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.

Hundsbuah added a commit to Hundsbuah/llama.cpp that referenced this pull request Aug 16, 2026
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
```
masterjaso added a commit to masterjaso/llama.cpp that referenced this pull request Aug 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation server testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants