Repository navigation
No valid checkpoint to restore + Max length reached on multi-turn tool calls (Qwen3.6-MoE, NPU) #744
Description
Summary
On multi-turn tool-call conversations, flm serve fails with No valid checkpoint to restore followed by Max length reached, stopping prefilling.... The model never produces a final answer — the second turn (after the tool result is returned) is rejected.
Environment
| FLM version | 1.0.5 |
| Model | qwen3.6-moe:35b-a3b (NPU2) |
| NPU firmware | 1.1.2.64 |
| Platform | Linux, AMD Ryzen AI 9 HX 470 (Strix Halo) |
| Client | OpenCode 1.18.31 (OpenAI-compatible API) |
| Endpoint | POST /v1/chat/completions |
Reproduction
- Start
flm serve qwen3.6-moe:35b-a3b --ctx-len 262144 --pmode performance. - Send a request with
toolsdefined. The model correctly emits a<tool_call>andfinish_reason = tool_calls. - Send a follow-up request with the same conversation plus the
assistant(tool_calls) andtool(result) messages. - FLM fails — see log below.
Log
[FLM] Use cached prompt!
[FLM] Matched 2 out of 4 messages (2 new to prefill).
[FLM] Start prefill...
[FLM] Total images: 0
[FLM] No valid checkpoint to restore
[WARNING] Max length reached, stopping prefilling...
[FLM] Creating checkpoint at context length 0
[🔵 ] NPU Lock Released!
The server returns:
{"error":{"message":"Max length reached!","type":"model_error","code":400}}Root cause
Qwen3_6_MOE::insert() calls qwen3_6_moe_npu::restore() when meta_info.restore_allowed == true. On the second turn, restore() returns a negative value (no valid checkpoint), but the method then unconditionally assigns:
this->total_tokens = restore_idx; // becomes -1
this->token_history = checkpoint_his;checkpoint_his still holds the previous turn's token history (~16k tokens). total_tokens is now desynchronized from the actual KV-cache size. _shared_insert() then checks:
if (this->total_tokens + tokens.size() >= this->MAX_L) {
header_print("WARNING", "Max length reached, stopping prefilling...");
return false;
}Because total_tokens was never reset on restore failure, the check fires even though the actual context is well below MAX_L (32 768). clear_context() is never called, so the KV cache is left in an inconsistent state.
Proposed fix
In common/AutoModel/modeling_qwen3_6_moe.cpp, inside Qwen3_6_MOE::insert():
1. Handle restore() failure explicitly — reset state instead of assigning a negative restore_idx:
if (meta_info.restore_allowed) {
restore_idx = qwen3_6_moe_engine->restore();
- this->total_tokens = restore_idx;
- this->token_history = checkpoint_his;
+ if (restore_idx < 0) {
+ this->lm_engine->clear_context();
+ this->total_tokens = 0;
+ this->token_history.clear();
+ this->checkpoint_his.clear();
+ } else {
+ this->total_tokens = restore_idx;
+ this->token_history = checkpoint_his;
+ }
}2. Re-sync total_tokens with token_history after _shared_insert() — guarantees the counter reflects the real KV-cache size regardless of what happened in restore() / clear_context() / _shared_insert():
bool success = has_images
? this->_shared_insert(meta_info, tokens, is_cancelled, &image_payload, last_image_token_index)
: this->_shared_insert(meta_info, tokens, is_cancelled, nullptr);
+ if (this->total_tokens != (int)this->token_history.size()) {
+ this->total_tokens = this->token_history.size();
+ }
+
checkpoint_his = token_history;
int checkpoint_idx = qwen3_6_moe_engine->checkpoint();
return success;
}Verification
After applying the patch and rebuilding FLM, the same multi-turn tool call now completes successfully. Full log:
[FLM] Use cached prompt!
[FLM] Matched 2 out of 4 messages (2 new to prefill).
[FLM] Start prefill...
[FLM] Total images: 0
[FLM] No valid checkpoint to restore
[FLM] Prefill chunk 1/5 with 4096 tokens
[FLM] Prefill chunk 2/5 with 4096 tokens
[FLM] Prefill chunk 3/5 with 4096 tokens
[FLM] Prefill chunk 4/5 with 4096 tokens
[FLM] Prefill chunk 5/5 with 303 tokens
[FLM] Creating checkpoint at context length 16687
[FLM] Start generating...
[FLM] Model RAW Output:
Вот список проиндексированных проектов:
... (correct answer)
The model now returns the expected final answer instead of a 400 error.
Impact
Tool-calling agents (OpenCode, Claude Code, Cline, and any OpenAI-compatible client that performs multi-step tool calls) are unusable with flm serve on qwen3.6-moe without this fix. The bug affects all multi-turn tool interactions, not just a specific client.
Activity
winaes13-ux commentedon Sep 18, 2026
Addendum:
default_context_lengthsizing forqwen3.6-moeContext
The patch described above fixes the
restore()failure path, but it does not remove the need for a correctly sizeddefault_context_length. Without a sufficiently largeMAX_L, the same multi-turn session still fails later withMax length reachedonce the prompt has grown enough.Why
default_context_lengthmattersWhen
restore()returns a negative value, the patchedQwen3_6_MOE::insert()resetstotal_tokensto 0 and clearstoken_history/checkpoint_his._shared_insert()then performs a full prefill of the entire prompt:If
MAX_Lis smaller than the prompt size, the prefill is rejected:and the server returns HTTP 400:
{"error":{"message":"Max length reached!","type":"model_error","code":400}}MAX_Lis read frommodel_list.json→default_context_length. Forqwen3.6-moe:35b-a3bthe upstream value is 32 768, which is not enough for long agent sessions.Sizing rule
A full re-prefill happens on top of whatever was already in the KV cache before the reset, so
MAX_Lmust cover the worst-case prompt with a 2× headroom:Component sizes (OpenCode, OpenAI-compatible client)
system_promptAGENTS.md+ MCP hintstools_definitioncodebase-memory-mcp(15) +context7(2) + built-insconversation_historymax_tokensmax_tokensworst_case_promptdefault_context_length2 × 28 096 = 56 192→ rounded upRounding table
worst_case_prompt2 × worst_case_promptdefault_context_length(rounded)Workaround used locally
Until the model-side fix lands, the user-side copy of
model_list.jsonis patched viaFLM_CONFIG_PATH:FLM_CONFIG_PATHis read bycommon/utils.cpp(get_models_directory()/get_model_path()), and the user copy survives FLM upgrades, unlike the system copy under/opt/fastflowlm/share/flm/.Verification
With the patch and
default_context_length = 65 536in place, a 28-message agent session completes withoutMax length reached. The longest observed prefill in the test session was:30 850 < 32 768on the first attempt, but the failure path adds another full prefill on top of it — which is whyMAX_Lmust be sized for2 × worst_case_prompt, not1 × worst_case_prompt.Suggested fix in the repository
insert()patch from the main report.default_context_lengthforqwen3.6-moe:35b-a3binmodel_list.jsonto a value ≥2 × worst_case_promptfor the intended client configuration, or document the sizing rule so users can adjust it themselves.default_context_lengthas a runtime override (e.g.--ctx-len) that is independent of the model registry, so users don’t need to editmodel_list.jsonat all.winaes13-ux commentedon Sep 21, 2026
Addendum 2: Official
chat_template.jinjafor Qwen3.6-MoE breaks tool calling on C++ enginesSummary
The official
chat_template.jinjashipped withQwen3.6-35B-A3B-NPU2uses Python-specific Jinja constructs that are not supported by the C++ minja runtime inside FastFlowLM. As a result, tool calling is unreliable: the template either fails to render, drops tool arguments, or produces malformed XML that the parser cannot recover from. On long agent sessions this manifests asMax length reachedand HTTP 400 responses, even when the context is well belowdefault_context_length.Affected component
qwen3.6-moe:35b-a3b(NPU2)~/.config/flm/models/Qwen3.6-35B-A3B-NPU2/chat_template.jinjaRoot cause
The upstream Qwen3.6 template relies on Python-only Jinja filters, most notably:
|items— used to iterate overtool.argumentsandtool.parameters.properties|safe— used to mark JSON-serialized fragments as safe|stringon non-string values in a few branchesFastFlowLM’s minja runtime does not implement these filters. Depending on the code path, this leads to one of three failure modes:
arguments, so the agent receives an empty payload.<tool_call>block and cannot recover, which in turn triggers theNo valid checkpoint to restorepath and eventuallyMax length reached.All three failure modes are reproducible on a stock FastFlowLM 1.0.5 installation without any user modifications.
Reproduction
flm serve qwen3.6-moe:35b-a3b.toolsarray (e.g. via OpenCode, Claude Code, or a directcurl).role: "tool") frequently fails withMax length reachedor with a malformed tool call.Evidence from the log
The failure is not caused by the context size itself — the same session completes when the template is replaced.
Workaround
Replace the official
chat_template.jinjain the model directory with the community-maintained fixed template from:The fixed template (v22.5 at the time of writing) rewrites all Python-only filters into minijinja-safe equivalents and fixes several related issues, including:
|items,|safe) → universal constructs<think>blocks before tool calls{}tool arguments on multi-step turns<think></think>blocks in chat historyLocal patch applied
The fixed template is roughly 3.6× larger than the official one because it contains the full reasoning / tool-call / error-recovery logic in a form the C++ runtime can execute.
Persistence across upgrades
The fixed template lives inside the model directory, not in the system registry. This means:
model_list.jsonis never touched).flm pull --force, because the pull operation re-extracts the model directory from the Hugging Face repository.A post-pull check is therefore recommended:
If the output is
0, the official template has been restored and must be replaced again before tool calling will work reliably.Impact
Any client that relies on multi-step tool calls (OpenCode, Claude Code, Cline, Cursor, and any OpenAI-compatible harness) is affected. Single-turn chat still works with the official template, but agentic workflows do not.
Suggested fix in the repository
chat_template.jinjaforqwen3.6-moe:35b-a3b(and any other Qwen 3.5/3.6/3.8 variants that use the same Python-only filters).flm pull --forcebehaviour so users know the template will be overwritten on re-pull.