Skip to content

Changing reasoning_effort re-reads the whole prompt: the effort text is the first thing the template renders #458

Closed

Description

@sebastianrcnt

What happens

serve/chat_template.jinja puts the reasoning effort instructions at the very top of the first system turn (<|im_start|>system\nReasoning effort is set to xhigh. ...). The conversation cache reuses the longest common token prefix, so a request that only changes reasoning_effort (or turns thinking off) differs from the previous one a few tokens in and re-reads the entire prompt.

Agent clients do this routinely. OpenCode's variants, a "think harder" toggle, a subagent with a different effort, none for a quick tool step. On a 150K-token agent session that is a full prefill (~100 s at 1.5K tok/s on a 3090) just for switching the level.

Measured (no GPU needed)

The same 22K-token conversation (system prompt + 20 turns) rendered with this repo's template at xhigh vs. the other levels, tokenized with the Strata tokenizer; "first differing token" is the most the cache can reuse:

template prompt tokens low vs xhigh medium vs xhigh thinking off vs xhigh
current (origin/main) 22,144 9 3 3
effort in a trailing system turn (below) 22,148 22,110 22,102 22,102

What I'm running locally (works, but it needs both halves)

  1. Template: drop reasoning_instructions from the top system turn and emit it as a short system turn right before the generation prompt:
    {%- if add_generation_prompt %}
        {%- if reasoning_instructions %}
            {{- '<|im_start|>system\n' + reasoning_instructions + '<|im_end|>\n' }}
        {%- endif %}
        {{- '<|im_start|>assistant\n' }}
  2. Engine: the turn checkpoint goes at the last <|im_start|>, which is now the assistant header after that trailing block - so with the template change alone the checkpoint holds the effort text and even a normal next turn reuses nothing (I measured 0 reused on a plain follow-up with stock 0.1.34 + the new template). A small rule moves it in front of the trailing system turn (--tail-role-token <id of "system">, 8678 for this tokenizer):
    if (turn_at > 0 && o.tail_role_token >= 0)
        for (int64_t i = turn_at - 1; i > resume; --i)
            if (ids[(size_t) i] == o.turn_token) {
                if (ids[(size_t) i + 1] == o.tail_role_token) turn_at = i;   // <|im_start|>system right before
                break;
            }
    (It could just as well be the default whenever the turn before the last header is a system turn, without a flag.)

With both, on an RTX 3090 (OrcaRouter IQ3_XXS, 262K, int8 KV): switching effort on an 8,222-token prompt reused 8,155 tokens; seven requests in a row on a ~5K prompt, each with a different effort value (extreme, minimal, none, low, high, and two non-standard ones that the template maps to its default), reused 5,005-5,047 tokens on every request after the first, and normal follow-up turns keep reusing as before.

The open question

This changes what the model sees: the effort text moves from the top of the system prompt to just before the answer. I have not measured whether that changes how well the level is followed or answer quality (the official Qwen template puts it at the top). Would you take a PR along these lines (perhaps opt-in first) or do you prefer another approach (e.g. a checkpoint right after the effort line, so only the system prompt is re-read)?

Activity

  1. Niko1221 commented on Oct 2, 2026

    @Niko1221
    Owner

    Thanks, a sharp observation. You're right: the effort text sits at the very start of the first system turn, so a different effort changes the prefix and the cached prompt can't be reused. We can't simply move it, because the default effort's sentence is in the same place for every request, and moving it would change every default prompt and answer. What could work is opt-in: for a non-default effort, put the instruction in a short system turn at the end instead (a config flag), plus an engine rule for the turn checkpoint. That needs a quality check that the model still follows the effort there. Keeping this open for that.

  2. added 2 commits that reference this issue on Oct 4, 2026
  3. Niko1221 commented on Oct 4, 2026

    @Niko1221
    Owner

    Added in 0.1.39 as an opt-in, along the lines you proposed: "effort_position": "end" in strata-<model>.json. Every prompt then starts the way the default effort's does, and a low or medium effort goes in a short system turn right before the answer. Thinking off is just the empty thinking block, as before. The engine has your checkpoint rule behind a flag (--tail-role-token). The server passes it only when the engine has it, so the checkpoint sits in front of that turn, and a request that only changes the effort, or the next turn, reuses the conversation. Without the key nothing changes. We haven't measured yet how well the model follows a level placed there. Numbers from your sessions would be welcome.

  4. Niko1221 commented on Oct 4, 2026

    @Niko1221
    Owner

    Added in 0.1.39 as an opt-in: "effort_position": "end" in strata-<model>.json. A request that only changes the effort then reuses the cached conversation.

    Thanks @sebastianrcnt for the measurement and the proposal.

  5. added 2 commits that reference this issue on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions