Skip to content

server: fix checkpoint handling for hybrid/recurrent models (#24055) - #25592

Open
krim404 wants to merge 2 commits into
ggml-org:masterfrom
krim404:hybrid-checkpoint-fix
Open

server: fix checkpoint handling for hybrid/recurrent models (#24055)#25592
krim404 wants to merge 2 commits into
ggml-org:masterfrom
krim404:hybrid-checkpoint-fix

Conversation

@krim404

@krim404 krim404 commented Jul 12, 2026

Copy link
Copy Markdown

Overview

this is a reimplementation of #24797

That PR was rejected because it could reuse recurrent state at positions it
was never valid for (trimming checkpoint metadata in place, resetting rs_idx
when a rollback exceeds the available snapshots). This takes the opposite
approach and keeps the invariant intact: recurrent state is only ever
restored at the exact position it was saved at.

  • checkpoint metadata records the actual validity at save time
    (pos_min = pos_max for hybrid/recurrent) instead of relying on what
    seq_pos_min happens to report - resolves [TAG_CHECKPOINTS_FIX_POS_MIN]
    for these model types
  • a checkpoint is only restored when its exact position lies inside the
    common prefix of the new prompt
  • checkpoints covering diverged content are erased instead of trimmed
  • no changes to llama_memory_hybrid semantics; seq_rm still fails
    cleanly on impossible rollbacks (now with a debug log)

On top of that, the cache is made useful for agentic clients, which strip
the reasoning of the previous reply so the next request diverges right at
the end of the previous prompt:

  • end-of-prompt checkpoints are exempt from --checkpoint-min-step
  • eviction drops the checkpoint closest to its neighbor instead of the
    oldest one, so early anchors survive edited/compacted histories
  • bounded n_rs_seq rollback is tried before the checkpoint search
  • checkpoint create/restore/erase log at INFO

Additional information

Related: #24055

Tested with Qwen3.6-35B-A3B Q4_K_M on Vulkan (RDNA3.5 APU). Resending
identical 22.8k-token request went from a ~96s full prefill to a 4-tok
cache hit. In a 10-turn agentic session normal turns only process the new
content, a mid-history edit correctly falls back to the last valid
checkpoint, and a compacted history restores an early checkpoint instead of
reprocessing everything. Correctness: at temperature 0 a checkpoint-restored
run produces byte-identical output to a fresh full prefill, and two
independent cold runs are byte-identical to each other.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - Using Fable for rewriting my (ugly) implementation and creating the test cases based on my example workload. Also used it to translate my german pre written pull request to english and add the test results and more details in.

@krim404
krim404 requested review from a team and ggerganov as code owners July 12, 2026 16:21
krim added 2 commits July 14, 2026 18:21
…g#24055)

The recurrent state is only valid at the exact position it was saved at,
but checkpoints claimed the whole [pos_min, pos_max] range reported by the
memory module, and restoring relied on that by accident. Record the actual
position instead and only restore a checkpoint when that position is still
inside the common prefix of the new prompt. Also erase checkpoints that
overlap edited history - once the new tokens are decoded they would look
valid again while holding state from content that no longer exists.

While at it, make the cache actually useful for agentic clients that strip
reasoning from previous turns: always checkpoint near the end of the prompt
(that is where the next request diverges), evict the checkpoint closest to
its neighbor instead of the oldest one so early anchors survive compaction,
try the bounded n_rs_seq rollback before searching checkpoints, and log
checkpoint activity at INFO so you can see what the cache is doing without
-lv.
…ckpoints

The n_rs_seq rollback snapshots are only guaranteed valid for tokens
decoded in the last ubatch, so rolling back across decode boundaries
could silently restore a stale state - always go through the checkpoint
path instead. Adopt a restored checkpoint into the current task so the
min-step eviction does not erase it, compare the stale bound in position
space (mtmd positions differ from token counts), and keep range (SWA)
checkpoints at the exact divergence position since resuming overwrites
that entry anyway.
@krim404
krim404 force-pushed the hybrid-checkpoint-fix branch from 5eb68b9 to e75a77a Compare July 14, 2026 16:56
@krim404

krim404 commented Jul 14, 2026

Copy link
Copy Markdown
Author

Rebased on current master to resolve the conflict with #25472 and #25649, which landed the near-prompt-end checkpointing and the min-step eviction from this same work package upstream. This PR now carries only the remaining parts: exact-position checkpoints for hybrid/recurrent memory, prefix-based restore, and stale-checkpoint invalidation.

Two adjustments on top of the rebase (second commit):

  • A restored checkpoint is now adopted by the current task, so the min-step eviction from server : evict checkpoints within min-step of each other #25472 does not erase it within the same task.
  • The n_rs_seq rollback fast-path was dropped: the rollback snapshots are only guaranteed valid for tokens decoded in the last ubatch (see the snapshot writes in delta-net-base.cpp), so skipping the checkpoint restore based on positions alone could roll back into a stale snapshot plane. The checkpoint path covers this case correctly, just without the shortcut.

@leinad-sch

Copy link
Copy Markdown

I am using this + #26004 and the checkpoints and slots are finally usable with Qwen3.6.
I can finally switch between models or continue the work I paused, without having to wait minutes (60k/800tps=75s).
I am sure it already saved me hours.

[...]
[Qwen3.6-27B-711-Bedrock-Final:5:170000] Slot 4 restore request: action=restore, slot_id=4, filename=4.bin
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.08.556.639 I srv  load_slot_ch: restored 3 context checkpoint(s) from '/llama-server-slots/enginetown--Qwen3.6-27B-711-Bedrock-Final/4.bin'
[Qwen3.6-27B-711-Bedrock-Final:5:170000] Slot 4 restored successfully:
[Qwen3.6-27B-711-Bedrock-Final:5:170000] Slot 4 restored successfully
[Qwen3.6-27B-711-Bedrock-Final:5:170000] Opened service connection 127.0.0.1:42976->127.0.0.1:20042
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.08.627.865 I slot get_availabl: id  4 | task -1 | selected slot by LCP similarity, f_sim_best = 0.980 (> 0.100 thold), f_keep = 0.975
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.08.628.328 I slot launch_slot_: id  4 | task 5 | processing task, is_child = 0
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.08.692.958 W slot   operator(): id  4 | task 5 | restored context checkpoint (pos_min = 18346, pos_max = 18346, n_tokens = 18347, n_past = 18347, size = 149.626 MiB)
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.09.294.335 I slot create_check: id  4 | task 5 | created context checkpoint 4 of 128 (pos_min = 18522, pos_max = 18522, n_tokens = 18523, size = 149.626 MiB)
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.09.921.035 I slot create_check: id  4 | task 5 | created context checkpoint 5 of 128 (pos_min = 18728, pos_max = 18728, n_tokens = 18729, size = 149.626 MiB)
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.13.600.809 I slot print_timing: id  4 | task 5 | n_decoded =    100, tg =  27.58 t/s, tg_3s =  27.58 t/s
[Qwen3.6-27B-711-Bedrock-Final:5:170000/stderr] 0.16.626.831 I slot print_timing: id  4 | task 5 | n_decoded =    183, tg =  27.51 t/s, tg_3s =  27.43 t/s
[...]

Previously this was full re-processing.

@47Hunter47

Copy link
Copy Markdown

Verified working on RTX 3090 + Qwen3.8-27B-IQ4_NL (16GB, qwen35 hybrid)

I cherry-picked both commits from this PR onto current master (7e4c0a968, build 10175) and tested with --cache-ram 2048 against a real conversation corpus (Hermes agent request dumps), single 130K slot, KV q8_0, MTP draft n-max=4.

Small prompt (~4.8K tokens)

  • Before (upstream master, no patch): checkpoint never restored — every turn re-processed the full prompt
  • After (this PR): restored context checkpoint (n_tokens=4803, 159.7 MiB) logged; turn latency 0.8s → 0.3s (~2.4x)

Large prompt (~38.5K tokens)

  • Full prompt processing (warmup): 40.8s
  • After checkpoint restore: 0.1-0.3s (~150-400x) — confirmed via:
    I slot create_check: created context checkpoint 2 of 32 (pos_min = 38583, n_tokens = 38584, size = 230.431 MiB)
    I slot operator(): restored context checkpoint (pos_min = 38583, pos_max = 38583, n_tokens = 38584, n_past = 38584, size = 230.431 MiB)
    

Observations

  • Checkpoint creation itself is fast (~0.5-0.7s for 38.5K tokens at 230 MiB) — overhead is negligible on multi-turn workloads.
  • The exact-position restore invariant works as described: checkpoints are only restored at pos_min = pos_max for hybrid/recurrent, and diverged-content checkpoints are properly erased.
  • This fixes the "full prompt re-processing every turn" problem (Misc. bug: Context checkpoints always invalidated on hybrid/recurrent models  #24055) for hybrid models in real agentic traffic. Huge win for long-context agent sessions.

Environment

  • RTX 3090 24GB, CUDA 13.0, llama.cpp build 10175 + this PR's 2 commits
  • Qwen3.8-27B-IQ4_NL (unsloth), qwen35 arch, MTP embedded, n-max=4

Thanks for the fix — this makes hybrid models actually usable for long agentic sessions.

@kfiramar

kfiramar commented Aug 14, 2026

Copy link
Copy Markdown

This is a real and important fix, I've encountered it just now - trying to use qwen 3.8 and not being able to restore anything...

Really hope it is merged ASAP

tomny-dev added a commit to tultr/llama.cpp that referenced this pull request Aug 16, 2026
Apply upstream llama.cpp PR ggml-org#25592 to the fork for Qwen hybrid/recurrent prompt-cache correctness.
@tomny-dev

Copy link
Copy Markdown

llama.cpp PR #25592 Test Report

Tested this on Qwen3.6-35B-A3B with llama-server on an RTX 5090 in an agentic coding workload with long multi-turn conversations.

This PR appears to fix the checkpoint reuse problem for me.

Before applying it, I was frequently seeing:

forcing full prompt re-processing due to lack of cache data
erased invalidated context checkpoint

and large portions of the conversation would be prefetched again on later agent turns.

After applying this PR, I still see an occasional full reprocess when a slot has no compatible state or the prompt diverges significantly, which seems expected. But once valid checkpoints are established, subsequent turns are consistently reusing the existing context.

Some examples from one session:

LCP similarity = 0.995 → prompt eval: 48 tokens
LCP similarity = 0.995 → prompt eval: 52 tokens
LCP similarity = 0.988 → prompt eval: 214 tokens
LCP similarity = 0.977 → prompt eval: 424 tokens

while the slot itself had grown past 18k tokens.

So instead of repeatedly processing the entire conversation, normal agent turns are now processing mostly just the new/changed portion. I was seeing generation around 225–230 tok/s and prompt prefill in the several-thousand tok/s range, so this made a very noticeable difference in end-to-end agent latency.

I also separately ported the prompt-cache RAM fix from #24649, so I don't want to conflate that with this PR, but the checkpoint restore/reuse behavior specifically looks fixed by #25592.

Thanks for working on this — this makes Qwen3.6 much more practical for long-running agentic workloads.

I also built a prebuilt CUDA image from my fork with these fixes applied, in case anyone wants to test this without rebuilding llama.cpp themselves:

docker pull ghcr.io/tultr/llama.cpp:master

Source: https://github.com/tultr/llama.cpp

The image is built directly from the patched master branch. Use at your own discretion, but hopefully it saves some build time for anyone wanting to reproduce/test the behavior.

@47Hunter47

Copy link
Copy Markdown

Adding a data point from our side: diagnosing this one was genuinely painful — the symptom was just "full prompt re-processing every turn" with no error, no warning, nothing in the logs pointing at checkpoint invalidation. We spent a lot of time chasing it before landing on the hybrid/recurrent exact-position restore path.

We've been running this patch (cherry-picked onto current master) in a production agentic workload since mid-August — long multi-turn sessions, 130K context, RTX 3090 — and it has held up without a single regression. The ~150-400x turn-latency improvement on restored checkpoints is the difference between usable and unusable for agent workloads.

Happy to run anything else the maintainers need before merge — longer soak, different model archs, whatever helps.

@YUXUANCHENG

Copy link
Copy Markdown

This is critical for hybrid models like qwen, hope this gets merged asap. Thank you for the work.

@tobias-weiss-ai-xr

Copy link
Copy Markdown

Today I built and deployed a local patch of llama.cpp PR #25592 to fix context checkpoint
restoration on hybrid/recurrent models (e.g., Qwen3.5, Qwopus3.6).

On hybrid SSM+attention models, saved context checkpoints were invalidated when tuned aggressively due to
incorrect pos_min metadata. This forced full prompt reprocessing instead of fast restoration — a performance regression for certain agentic use cases.

PR #25592 (open/unmerged) sets pos_min = pos_max for hybrid/recurrent models, reflecting that their state is valid only at the exact final position. It also adds checkpoint adoption, restore, and erase at exact positions with INFO logging.

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants