[Bug]: reasoning_loop_recovery: recover never fires for a low or medium request (focused_recovery_prompt only matches the template's xhigh sentence)
I maintainer di solito rispondono entro 3 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
Direzione di ricerca
Inizia da focused_recovery_prompt() e dal suo chiamante intorno a serve/server.py:196; lo script di riproduzione nell'issue mostra come esercitare entrambe le posizioni del prompt senza un engine. Aggiorna recovery per sostituire la frase di sforzo effettiva della richiesta, con un messaggio di skip chiaro quando nessuna frase corrisponde. Aggiungi o esegui test mirati che copiano il recovery con sforzo low, medium e xhigh, in modo che il ramo recover non restituisca più silenziosamente senza agire.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What happened
With "reasoning_loop_recovery": "recover" in a model config, a reply whose thinking repeats whole passages is detected and then does nothing: the server logs the detection and the thinking carries on to reasoning_budget_tokens (or to max_tokens when no budget is set). The reply is not continued with the low-effort instruction, and nothing says why.
Measured on 0.1.44.1 and again on 0.1.45, a community qwen4exp fine-tune (RVN abliterated IQ3_S), reasoning_effort: low, one prompt that makes the model re-verify the same facts, max_tokens 8192, reasoning_budget_tokens 8192 then 4096:
| run | budget | detection | what happened next |
|---|---|---|---|
| 1 | 8192 | repeated reasoning detected at 3072 tokens (coverage=0.277) |
thought on to 6144, then the budget wrapped it up |
| 2 | 8192 | repeated reasoning detected at 3072 tokens (coverage=0.289) |
the same |
| 3 | 4096 | repeated reasoning detected at 3072 tokens (coverage=0.343) |
thought on to 4096, then the budget wrapped it up |
So the detector works (it is the log line above); the recovery step is what never runs.
Why
focused_recovery_prompt() (serve/server.py:196 in 0.1.45) builds the spliced prompt by replacing the template's own xhigh sentence (HIGH_EFFORT, line 160) in the first system message with the low one, and returns None when that sentence is not there as whole tokens:
old, new = tok.encode(HIGH_EFFORT, parse_special=False), tok.encode(LOW_EFFORT, parse_special=False)
for i in range(len(head) - len(old) + 1):
if head[i:i + len(old)] == old:
return ids[:i] + new + ids[i + len(old):] + list(generated)
return None
With "effort_position": "start" (the default) a request that asks for low or medium is rendered with that level's sentence at the top of the first system message, so the xhigh sentence is absent and the function returns None. The caller only acts on a non-None result:
recover_prompt = focused_recovery_prompt(self.tok, ids, raw_ids)
if recover_prompt is not None:
break
recover is therefore a silent no-op for every low/medium request — which is the common case as soon as a config sets "reasoning_effort": "low" (as mine does, to keep a local model from burning its output budget on thinking) or a client picks Low in its own selector. "reasoning_loop_recovery": "stop" is unaffected (it does not use this function and did end the reply in my runs).
How I checked it (no engine needed)
Rendering one request both ways with the server's own functions and then calling focused_recovery_prompt on it:
--- effort_position="start" (default)
prompt tokens : 63
xhigh sentence in the prompt : False
low sentence in the prompt : True
focused_recovery_prompt() : None -> the recover branch cannot fire
--- effort_position="end"
prompt tokens : 105
xhigh sentence in the prompt : True
low sentence in the prompt : True
focused_recovery_prompt() : 93 ids -> it can fire
Script: build the tokenizer and the ChatTemplate from a pack's tokenizer/ exactly as main() does, then
low_text = tpl.render(msgs, reasoning_effort="low", add_generation_prompt=True) # start
hist = tpl.render(msgs, add_generation_prompt=False) # end, default (xhigh)
full = tpl.render(msgs, reasoning_effort="low", add_generation_prompt=True)
mine = tpl.render(msgs, reasoning_effort="low", add_generation_prompt=False)
end_text = hist + EFFORT_TURN.format(EFFORT_TEXT["low"]) + full[len(mine):]
for t in (low_text, end_text):
print(focused_recovery_prompt(tok, tok.encode(t, parse_special=True), []))
Workaround I use now
"effort_position": "end" in the model config: the history is then rendered with the template's default (xhigh) and the real level follows in a short system turn before the answer, so the xhigh sentence is in the prompt and the recovery can splice it. With it, the detection line is followed by resuming the same output with the low-effort instruction (reasoning_loop_recovery) (measured twice, coverage 0.277 and 0.343).
Two honest notes on that: the nudge is not a cure — in both runs the model went on thinking for another ~1000-3000 tokens to the budget — and effort_position: "end" is a separate opt-in with its own behaviour (the engine checkpoints in front of the tail turn).
Suggested fix
Pass the request's level into focused_recovery_prompt() and look for that sentence (the template knows low, medium, xhigh; high maps to xhigh), or fall back to the low sentence when the level's own is not found. If neither is found, print one line saying the recovery was skipped and why, instead of failing silently — the current behaviour costs a full thinking budget with no hint.
Environment
0.1.44.1 and 0.1.45, prebuilt CUDA 13.0 engine (sm_75/86/89/120) · RTX 5080 16 GB · Windows 11 26200 · Core Ultra 7 270K Plus, 95 GB RAM. Config: reasoning_effort: low, reasoning_budget_tokens: 4096/8192, reasoning_loop_recovery: recover, reasoning_close_retry: true.
Not tested
AMD HIP and SYCL (CUDA only here); a high/xhigh request under effort_position: "start" (that one renders the xhigh sentence at the top, so the recovery should fire — I did not run it); the Anthropic route's output_config.effort path; and whether the model's repetition after the nudge would end on its own with a different prompt.
- Lingua principale
- C++
- Stelle
- 11.6k
- Fork
- 1k
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di Niko1221/Strata
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
Niko1221/Strata#3785 · 2 commenti ·
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 75/100
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
I maintainer di solito rispondono entro 3 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
I maintainer di solito rispondono entro 3 giorni
Tutte le issue di Niko1221/Strata
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
[BUG] Failed archive extraction deletes the dataset cacheForse già presa @dajiaohuang l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
alibaba/GraphScope#4674 ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 93/100
webarkit/WebARKitLib#107 ·
I maintainer di solito rispondono entro 1 giorno
-
--view / --edit / --review passed without '=' abort the app with an uncaught std::out_of_rangeAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
ONLYOFFICE/DesktopEditors#2527 ·
-
bug good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
HEL3AN/econspace-mmo#529 ·
I maintainer di solito rispondono entro 1 giorno