Skip to content

server : preserve context checkpoints across slot save/restore - #26004

Merged
ggerganov merged 6 commits into
ggml-org:masterfrom
Tough-Respawn:fix/kv-restore-checkpoint
Oct 8, 2026
Merged

ggerganov merged 6 commits into
ggml-org:masterfrom
Tough-Respawn:fix/kv-restore-checkpoint

Conversation

@Tough-Respawn

@Tough-Respawn Tough-Respawn commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Fix the full re-prefill after slot save → restore for SWA and hybrid/recurrent models (e.g. Qwen3.5 / Qwen3-Next).

When a slot is restored, the server rebuilds the token cache but not the context checkpoints (slot->prompt.clear() discards them, and they are not part of the save file). SWA and hybrid/recurrent models cannot rewind their state without a checkpoint, so the next divergent prompt triggers "forcing full prompt re-processing due to lack of cache data" even when it shares a long prefix with the saved state. Since a recurrent state cannot be rewound, checkpoints are not reconstructible after the fact, they have to travel with the save file.

This PR preserves the context checkpoints inside the slot save file:

  • save_slot_checkpoints() appends a tagged payload (SCKP magic + version + count + per-checkpoint pos fields and state blobs) after the llama state payload
  • load_slot_checkpoints() reads it back at the offset returned by llama_state_seq_load_file and reattaches the checkpoints to the slot
  • Backward/forward compatible: old files restore exactly as before (no magic → silent skip), and new files load fine on older servers (they stop reading at the end of their own payload)
  • Guards against damaged files: blob sizes are checked against the remaining file size before allocating, truncation → warning and clean abort (no partial state), and a restored checkpoint that fails to load falls back to full re-processing
  • id_task is not serialized and is reset to -1 on load (task ids are not meaningful across save/restore)
  • n_written/n_read include the appended checkpoint bytes, so the reported size matches the file on disk; no appendix is written when the slot holds no checkpoints, leaving those files byte-identical to the format without this feature
  • A draft context configured differently at restore no longer aborts the server: the draft checkpoint data is dropped instead (second commit, see below)

The implementation lives in tools/server/server-context.cpp, using std::ifstream/std::ofstream with small read/write helpers.

Independently reproduced and confirmed by 10 users across CUDA, Metal, Vulkan and an ollama integration, including a cross-machine CUDA → Metal restore. Details below.

Additional information

Tests included in tools/server/tests/unit/test_slot_save.py:

test_slot_restore_preserves_context_checkpoints:

  1. Send a long prompt (reference: a divergent re-prompt on the live slot rolls back to a checkpoint → partial reuse, n_live tokens processed)
  2. save the slot
  3. Overwrite the slot with an unrelated prompt
  4. restore the slot
  5. Send the same divergent re-prompt, the test asserts prompt_n == n_live

Without the fix, step 5 re-processes the full prompt. The fixture sets cache_ram = 0 (the host prompt cache would otherwise mask the path under test) and n_ubatch = 32 (so a prompt-processing checkpoint lands before the divergence point).

test_slot_restore_damaged_checkpoint_appendix damages the appendix (oversized blob, corrupted state, 1025 entries) and checks that the server stays up and falls back or reuses as expected. The first test also runs with a non-ASCII file name.

test_slot_restore_checkpoints_draft_kv_type_change runs the same scenario with a draft model, and restarts the server between save and restore with the same draft KV cache type (control) or a different one (q8_0). The restored request must succeed and still roll back to the checkpoint.

The first test passes locally against this branch after the rebase, and the same scenario fails on master (assert 210 == 31). The file now also carries the multimodal slot tests that came in with #26640.

Validated locally on 06d9d0f with the full CPU CI (ci/run.sh): exit 0, ctest 46/46 (debug) and 49/49 (release), 6/6 with model in both. tools/server/tests/unit/test_slot_save.py: 10/10.

On 1bfe730 (Linux, CPU): tools/server/tests/unit/test_slot_save.py 12/12, and the new draft test fails without the second commit. The full ci/run.sh was not re-run on 1bfe730.

On 9f098d9 (Linux, CPU, merged with master 8345f33): test_slot_save.py 16/16, the damaged-appendix cases fail without the third commit. Also checked on Windows (peak RSS 19.7 MiB with @junmo-kim's fixture, 83.8 MiB before) and with hybrid models + draft-mtp / draft-eagle3 across a process restart (same tokens re-processed and same output as the live slot). The full ci/run.sh was not re-run.

On 9bc4fa7 (merged with master 42b021b): test_slot_save.py 17/17.

Rebase note. #26640 ("server : support slot save/restore with media inputs") reworked the same SLOT_SAVE / SLOT_RESTORE handlers: the save now writes a packed server_tokens::serialize() payload including media chunks instead of get_text_tokens(), and the restore does a two-pass llama_state_seq_load_file + deserialize() + validate(). This PR was rebased onto master and flattened to a single commit (06d9d0f); the later commits below came later. The split is unchanged: the state payload belongs to master, the checkpoint appendix belongs to this PR, it is still appended after the payload, and the load offset is still the end of the payload returned by llama_state_seq_load_file.

Second commit (1bfe730): draft KV type mismatch. Fix by @okigan, taken as-is, plus a regression test. The checkpoints also carry the draft model's state, and nothing checked it against the running draft context: restoring a slot saved with -ctkd f16 into a server started with -ctkd q8_0 aborted in load_dft() with checkpoint size mismatch. On restore, one draft checkpoint is now test-loaded; if it does not fit, the draft data is dropped with a warning instead of crashing. The target still rolls back to its checkpoint and the output is unchanged; the draft restarts without the prefix for that session.

Third commit (9f098d9): appendix hardening. Reported by @junmo-kim: a blob was allocated before checking that its bytes are in the file. Blob sizes are now checked against the remaining file size, as llama_state_seq_load_file does, and the 16 GiB cap is gone. Same commit:

  • a restored checkpoint that fails to load falls back to full re-processing instead of aborting
  • non-ASCII file names on Windows: the file is opened with std::filesystem::u8path(), as in common : remove fs_open_ifstream() by using u8path() #29841
  • the 1024 count cap is replaced by keeping the last n_ctx_checkpoints while reading

The file format does not change.

Fourth commit (2b81c0a): a slot save whose checkpoint appendix cannot be written completely now returns an error, like a failed payload write, instead of reporting success.

Fifth commit (9bc4fa7): a checkpoint with an empty target blob is rejected when loading the appendix (found by the automated review); logging carries the slot id, and the draft test model is loaded from the HF cache.

Sixth commit (2cb56aa): load_tgt / load_dft return bool; a checkpoint restored from a slot file that fails to load falls back to full re-processing, a checkpoint created in memory still aborts (requested in review).

Fixes #25913, same root cause reported there (checkpoints never persisted, cleared on restore).

Related: #22384 fixed the checkpoint lookup for recurrent models in a live slot; this PR addresses the save/restore path, which loses the checkpoints entirely.

Prior attempts: #20819 and #24028 address the same gap with a separate companion file (.checkpoints / .ckpt). I only found them after opening this PR. Both currently conflict with master (#20819 still uses the removed server_prompt_checkpoint type, #24028 predates the #26640 handler rework), neither includes a regression test, and a companion file can drift out of sync with its state file (in #20819 a later save without checkpoints leaves the old companion in place). This PR keeps everything in a single file instead. #28074 was closed by its author as a duplicate of this PR.

User-reported results:

Reporter Date Setup Result
wagi-sho Jul 23 Vulkan, Radeon 8050S (gfx1151), Qwen3.5-35B-A3B (Gated DeltaNet) With the fix: 181.9 s → 4.7 s (38.7x); 58,202 → 516 tokens
terisuke Jul 25 Cross-machine: CUDA GB10 → Metal M3 Ultra With the fix: cache_n 0 → 310; identical output verified by SHA-256
kfiramar Aug 14 Qwen3.8 Confirmed; no measurements provided
YUXUANCHENG Aug 16 Not specified Confirmed; no measurements provided
yitizi Aug 31 Qwen3.8-27B, hybrid SSM, strict-prefix extension Bug on master: 8 tokens processed without a restore in between vs 2,005 with one
dgabehar Sep 08 Vulkan, Radeon 780M (gfx1103), Qwen3.8-27B + draft-MTP With the fix: ~71 s → ~3 s (23.7x) on a 4,500-token prefix
m4xw Sep 15 Ollama integration, Anthropic-style caching With the fix: avoided the full prefill after a process restart
ReplicantBob Sep 22 Windows, CUDA 13.3, RTX 5070 Ti, Qwen3.6-35B-A3B hybrid, q8_0 KV With the fix: erase → restore 4,460 → 30 tokens (149x); a 36k-token agent session resumed with 13 tokens re-processed
ChrisC381 Sep 27 Radeon 8060S (gfx1151), Qwen3.8-Flash-Next 112 GB hybrid, shared-MTP With the fix: prefill 2,991 ms → 109 ms (27x); prompt_n 2,337 → 4; byte-identical greedy output; slot-save suite 10/10
okigan Oct 04 Apple M5 Pro, Metal, Qwen3.8-27B (hybrid) and gemma-4-12b (SWA), f16 / q8_0 / q4_0 KV With the fix: prompt reuse after save → restart → restore 0 % → 99 % (64 turns); resume 6.07 s → 0.79 s (Qwen3.8-27B, f16)

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES — the implementation and the test were primarily AI-generated (Claude Fable 5), working under my direction from a bug I found and measured on my hardware. I reviewed the code, ran the repro and the tests on real models (SWA and hybrid), and validated the fix end-to-end. This description was also drafted with AI assistance.

@Tough-Respawn
Tough-Respawn requested a review from a team as a code owner July 22, 2026 15:08
@ggml-gh-bot

ggml-gh-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

Hi @Tough-Respawn, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@wagi-sho

Copy link
Copy Markdown

Independent verification on Vulkan / gfx1151 with a Gated DeltaNet hybrid — the fix works

Built this branch and measured it against master on a third backend/architecture. Summary: 181.9s → 4.7s on the first request after a restore.

Environment

  • Nex-N2-mini (Qwen3.5-35B-A3B, Gated DeltaNet hybrid), UD-Q4_K_XL
  • Windows, Vulkan, AMD Radeon 8050S (gfx1151), 64GB unified memory
  • Server: -c 131072 --slot-save-path <dir> --jinja -np 1 --ctx-checkpoints 128 --checkpoint-min-step 128 --slot-prompt-similarity 0.1 --cache-ram 0 -lv 4
  • Controlled comparison: this PR (866543040) vs b10091 (b4d6c7d8f), which differs by only two unrelated commits. Same prompt, same flags, full server restart between every measurement.

Cold prefill (baseline, identical on both) — 58,202 tokens, ~185s, save 58,209 tokens.

First request after restore (same 58K prefix, divergent tail)

build prompt_n cache_n wall
b10091 (master) 58,202 0 181.9s
this PR, no id_slot 516 57,686 4.7s
this PR, with id_slot 516 57,686 4.7s

The 516 tokens are the expected checkpoint-granularity rollback, not a full-prefix hit — matching the caveat in #25913.

Multi-turn after restore (conversation accumulates, 10 turns)

build turn 1 turns 2-10
b10091 (master) 58,210 tok / 181.4s 15-52 tok / 2.5s
this PR 524 tok / 5.5s 31-52 tok / 3.2s

cache_n grows monotonically (57,686 → 58,548) across turns. No #22450-style hang or checkpoint eviction loop over 10 turns. The difference between the two builds is confined to the first request after restore, which is exactly the scope this PR claims.

Output sanity — the dangerous failure mode here is a fast but corrupted answer, so I checked the content, not just the timings. After restore the model still reads the 58K prompt body correctly: "does rule 5 come before rule 9?" → Yes; "is rule 2999 present?" → Yes; "which rule number appears first?" → 0; "is this policy sorted?" → Yes. No sign of a corrupted recurrent state.

One note on the test fixture — setting cache_ram = 0 is essential and I want to underline it, because I lost a day to exactly this before finding #25913. A 5K restore appeared to succeed (16 tokens processed) until I restarted the server to clear cache-ram; the same restore then reprocessed 5,428 tokens, a 340x difference. Leftover checkpoints from a previous live session silently mask the broken on-disk path. Good that the fixture already handles it.

Happy to re-run any of this on Vulkan/gfx1151 if it helps review.

@terisuke

terisuke commented Jul 25, 2026 •

Copy link
Copy Markdown

First of all, thanks to @wagi-sho for pointing out the --cache-ram 0 issue and for sharing the much larger Vulkan test. That caught a mistake in my first run and saved me from reporting a false positive.

I tried something slightly different: restoring a slot file produced on a CUDA machine (GB10) on a different machine running the Metal backend (M3 Ultra).

Both servers used --cache-ram 0, the source server was restarted before saving, and I verified that both machines were using the exact same model file and the same transferred slot file (SHA-256 checked on both sides).

On the Metal machine:

Run cache_n prompt_n Content SHA-256
Control (no restore) 0 314 50afb036...414ff8
After restoring CUDA slot 310 4 acf1bbed...af15f

The original CUDA run also produced acf1bbed...af15f.

The interesting part is that Metal does not normally generate the same completion as CUDA for this prompt. Without restoring the slot, the Metal output hashes to 50afb036...; after restoring the CUDA-generated state it matches the CUDA output instead. That makes me think the restored state is actually being reused across both the backend boundary and the machine boundary.

This is only a small portability test (~314 prompt tokens, a single restore, and a single completion), so I wouldn't draw any conclusions about long contexts or repeated restores yet.

One other thing I noticed while testing: the save response reported:

n_written = 179,512,968

but the actual slot file written to disk was:

336,407,384 bytes

The server log says one context checkpoint was appended, so the extra ~149.6 MiB appears to be that checkpoint payload. It might be worth documenting that n_written currently reflects only the base llama-state payload, or exposing the total on-disk size separately to avoid confusing callers.

Thanks again for working on this PR. If it would be useful, I can also test longer CUDA → Metal restores or the reverse Metal → CUDA direction.

@Tough-Respawn
Tough-Respawn force-pushed the fix/kv-restore-checkpoint branch from 8665430 to 1d3f583 Compare July 25, 2026 13:38
@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Thanks @wagi-sho @terisuke for digging into this, and especially for the cross-machine test — that's a case I had no way to cover locally.

On CUDA -> Metal: the result is expected rather than lucky. The checkpoint blobs come from llama_state_seq_get_data_ext, the same serializer that writes the base payload, just with the PARTIAL_ONLY flag — so the appendix is portable exactly to the degree the base payload is. Related caveat I hit while testing: save files are only
comparable byte-for-byte within one backend (a Vulkan and a CPU build of the same model, prompt and seed produce different bytes; -ngl 0 makes them match), which is an independent confirmation of what you saw.

On n_written: you're right, and it's a bug rather than something to document. Before this PR llama_state_seq_save_file was the only writer and returned file.tell() at close, so n_written was the on-disk size by construction. This PR appends the checkpoints after that file is closed, and reuses the offset returned by the loader to find them — which is what makes the format backward compatible — but I never added the appended bytes back into the response. n_read has the same blind spot, and the appendix header was written even for slots with no checkpoints, so every save file under-reported, not only SWA/hybrid ones.

Your numbers match the format exactly: 336,407,384 - 179,512,968 = 156,894,416 = 60 bytes of framing (12-byte header, then 8 + 4 + 4 and three 8-byte length prefixes) plus a 149.62 MiB blob, i.e. the single checkpoint your log reported.

I fixed the accounting rather than documenting the gap, because a caller that trusts n_written to copy or verify a file truncates the appendix — and the loader tolerates a truncated appendix by warning, dropping the checkpoints and restoring anyway. The restore returns 200 and looks fine while silently losing what this PR adds.

Follow-up commit:

  • save_slot_checkpoints and load_slot_checkpoints return the bytes
    appended/consumed and the responses add them, so n_written and n_read are the
    on-disk size again;
  • no appendix is written at all when the slot holds no checkpoints, so those files
    stay byte-identical to the format without this feature;
  • the tests assert n_written/n_read == os.path.getsize(save_file) on both paths.
    That guard was missing, which is why this slipped through.

Measured on stories260K (full attention) and tinygemma3 (SWA):

reported on disk
before, no checkpoints 23,768 23,780 the 12-byte empty appendix
before, SWA appendix 3,528,824 9,096,332 61% of the file unreported
after, no checkpoints 23,768 23,768 payload byte-identical, tail gone
after, SWA appendix 9,096,332 9,096,332 save and restore

Files already written by this branch keep working: one carrying an empty appendix now reports its real size, and an older SWA save still drives the same checkpoint rollback as a live slot (31 prompt tokens instead of 212).

On more testing: yes please, and a divergent follow-up would be worth more than a longer one. Your test restored and continued straight ahead, which works even without checkpoints; the case this PR fixes is a prompt that diverges mid-state after the restore. Comparing prompt_n between a live slot and a restored one on that path, in either direction, would tell us whether restored checkpoints are usable across the backend boundary and not just loadable

@whoreson

whoreson commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Could have simply copied over that ~10 lines of code from a certain fork (which has had this elementary feature for probably a year now) instead of slopping away.

@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Could have simply copied over that ~10 lines of code from a certain fork (which has had this elementary feature for probably a year now) instead of slopping away.

Which fork, which file/commit? The two obvious ones (ik_llama.cpp #1762, koboldcpp) either have the same bug open or use an in-RAM mechanism, not on-disk checkpoint persistence for recurrent state. Happy to compare.

@whoreson

whoreson commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Yes the

verboten

fork.

And even PicoLM has it.

@Tough-Respawn

Tough-Respawn commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Yes the

verboten

fork.

And even PicoLM has it.

Bold to say "10 lines" and "slop" in the same sentence where you point at picolm, because I actually read picolm: the on-disk checkpoint save/restore in your own server is ~217 lines (server.c 1143-1184 + 1242-1416), before the bookkeeping and the SSM (de)serialization. Off by ~20x on code you wrote yourself is a rough way to argue it's trivial.

As for the "verboten" fork you keep not naming: I can't compare against something you won't link. Name it, or it isn't really an argument.

And picolm is a separate 2.5k-line engine for a handful of models. This is llama.cpp, where the checkpoints get dropped on restore today (#25913, 181.9s on master above). #17428 was your live-slot half; this is the disk half. If it's really 10 lines, show the commit.

I'm here to contribute, not to troll or be trolled.

erlaubt

@whoreson

whoreson commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fus
Roh
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86

@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Fus Roh ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86

It's Fus Ro Dah, three words. You dropped the last one, same way you dropped ~200 lines counting picolm. Come back with a shout that's finished and a commit that exists ;)

@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

@Tough-Respawn
Tough-Respawn force-pushed the fix/kv-restore-checkpoint branch 2 times, most recently from 5735946 to 06d9d0f Compare August 15, 2026 11:51
@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Rebased onto master to resolve the conflict introduced by #26640, which reworked the same SLOT_SAVE / SLOT_RESTORE handlers this PR touches.

Master now writes a packed server_tokens::serialize() payload instead of a plain text-token list, and restores it through a two-pass llama_state_seq_load_file followed by deserialize() and validate(). That payload is master's, so I kept it untouched. This PR only adds a checkpoint appendix after it, which stays orthogonal: llama_state_seq_load_file still returns the end offset of the payload, which is exactly where the appendix starts, so media save/restore is unaffected.

The appendix is written after llama_state_seq_save_file has already closed the file, so it is counted separately in n_written / n_read. Counting it on both sides also keeps master's own n_read == n_written assertion in test_slot_save_restore_with_image valid.

The two original commits are squashed into one, and the now-dead if (nwrite > 0) guard was dropped since master already bails out on nwrite == 0. Net change against master: 195 insertions, 2 deletions.

@YUXUANCHENG

Copy link
Copy Markdown

Same issue… hope this gets merged asap.

@yitizi

yitizi commented Aug 31, 2026

Copy link
Copy Markdown

Independent confirmation of this bug, from a different model and a different angle.

I run a local proxy in front of llama-server that manages slots for multi-session agent work: on a session switch it saves the outgoing session's slot and restores the incoming one via /slots. The whole point was to avoid cold prefill on a hybrid model. It didn't work — every restored session still re-processed its full prompt, while the restore itself reported success.

Reduced to a single variable on Qwen3.8-27B (hybrid SSM), via /completions:

feed a 2001-token prompt → evict the slot with unrelated content
→ send a strict extension (same 2001-token prefix + a few tokens)

  without restore in between:  8 tokens processed
  with    restore in between:  2005 tokens processed   (full recompute)

Root cause matches your analysis exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only restores tokens, and the reuse path then takes do_reset because the checkpoint list is empty.

Two things that may be worth adding to the picture:

id_slot pinning bypasses the host prompt cache. Same 3211-token prompt after evicting the slot: pinned to a slot, 0 reused; unpinned, 3207 reused. The cache lookup happens during slot selection. Anyone using /slots save/restore is likely pinning too, so they lose both paths at once.

In my case, not using save/restore at all turned out to be faster than fixing it. With the proxy's slot management disabled, a new agent session went from 14,199 tokens re-prefilled (22.5 s) to 4 tokens (0.45 s), because the host prompt cache already handles cross-session prefix reuse correctly. That doesn't reduce the value of this fix — explicit session persistence through /slots still needs it — but the two mechanisms interact.

I had built a local fix for the same problem before finding this PR (a separate .ckpt sidecar file). Yours is the better design — single file, compatible in both directions — so I'm not opening anything competing.

AI usage disclosure: the measurements and root-cause analysis were done with Claude assisting; I reproduced and verified everything on my own hardware. This comment is my own.

@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Independent confirmation of this bug, from a different model and a different angle.

I run a local proxy in front of llama-server that manages slots for multi-session agent work: on a session switch it saves the outgoing session's slot and restores the incoming one via /slots. The whole point was to avoid cold prefill on a hybrid model. It didn't work — every restored session still re-processed its full prompt, while the restore itself reported success.

Reduced to a single variable on Qwen3.8-27B (hybrid SSM), via /completions:

feed a 2001-token prompt → evict the slot with unrelated content
→ send a strict extension (same 2001-token prefix + a few tokens)

  without restore in between:  8 tokens processed
  with    restore in between:  2005 tokens processed   (full recompute)

Root cause matches your analysis exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only restores tokens, and the reuse path then takes do_reset because the checkpoint list is empty.

Two things that may be worth adding to the picture:

id_slot pinning bypasses the host prompt cache. Same 3211-token prompt after evicting the slot: pinned to a slot, 0 reused; unpinned, 3207 reused. The cache lookup happens during slot selection. Anyone using /slots save/restore is likely pinning too, so they lose both paths at once.

In my case, not using save/restore at all turned out to be faster than fixing it. With the proxy's slot management disabled, a new agent session went from 14,199 tokens re-prefilled (22.5 s) to 4 tokens (0.45 s), because the host prompt cache already handles cross-session prefix reuse correctly. That doesn't reduce the value of this fix — explicit session persistence through /slots still needs it — but the two mechanisms interact.

I had built a local fix for the same problem before finding this PR (a separate .ckpt sidecar file). Yours is the better design — single file, compatible in both directions — so I'm not opening anything competing.

AI usage disclosure: the measurements and root-cause analysis were done with Claude assisting; I reproduced and verified everything on my own hardware. This comment is my own.

Thanks, this is a really useful data point: different model, different path, and a clean single-variable repro (8 vs 2005 tokens). Matches the root cause exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only brings back tokens, and the reuse path then hits do_reset because the checkpoint list is empty.

On id_slot pinning bypassing the host prompt cache: good catch, and I'd treat it as a separate issue from this PR.
The cache lookup happens during slot selection, so pinning skips it regardless of checkpoints worth its own report, since anyone using /slots save/restore is likely pinning and loses both paths at once.
My PR only restores the explicit-persistence path.

And agreed on the interaction: for pure cross-session prefix reuse the host prompt cache already handles it, often faster. This fix is for explicit session persistence through /slots: state that has to survive a server restart and be addressable by name which the host cache doesn't cover. Complementary, not competing.

On the .ckpt sidecar: thanks for not opening something competing. Single file + backward/forward compatible was the deliberate choice a sidecar can desync from the state file, whereas an appendix that older servers stop reading at their own payload end can't.

@yitizi

yitizi commented Sep 1, 2026

Copy link
Copy Markdown

Independent confirmation of this bug, from a different model and a different angle.
I run a local proxy in front of llama-server that manages slots for multi-session agent work: on a session switch it saves the outgoing session's slot and restores the incoming one via /slots. The whole point was to avoid cold prefill on a hybrid model. It didn't work — every restored session still re-processed its full prompt, while the restore itself reported success.
Reduced to a single variable on Qwen3.8-27B (hybrid SSM), via /completions:

feed a 2001-token prompt → evict the slot with unrelated content
→ send a strict extension (same 2001-token prefix + a few tokens)

  without restore in between:  8 tokens processed
  with    restore in between:  2005 tokens processed   (full recompute)

Root cause matches your analysis exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only restores tokens, and the reuse path then takes do_reset because the checkpoint list is empty.
Two things that may be worth adding to the picture:
id_slot pinning bypasses the host prompt cache. Same 3211-token prompt after evicting the slot: pinned to a slot, 0 reused; unpinned, 3207 reused. The cache lookup happens during slot selection. Anyone using /slots save/restore is likely pinning too, so they lose both paths at once.
In my case, not using save/restore at all turned out to be faster than fixing it. With the proxy's slot management disabled, a new agent session went from 14,199 tokens re-prefilled (22.5 s) to 4 tokens (0.45 s), because the host prompt cache already handles cross-session prefix reuse correctly. That doesn't reduce the value of this fix — explicit session persistence through /slots still needs it — but the two mechanisms interact.
I had built a local fix for the same problem before finding this PR (a separate .ckpt sidecar file). Yours is the better design — single file, compatible in both directions — so I'm not opening anything competing.
AI usage disclosure: the measurements and root-cause analysis were done with Claude assisting; I reproduced and verified everything on my own hardware. This comment is my own.

Thanks, this is a really useful data point: different model, different path, and a clean single-variable repro (8 vs 2005 tokens). Matches the root cause exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only brings back tokens, and the reuse path then hits do_reset because the checkpoint list is empty.

On id_slot pinning bypassing the host prompt cache: good catch, and I'd treat it as a separate issue from this PR. The cache lookup happens during slot selection, so pinning skips it regardless of checkpoints worth its own report, since anyone using /slots save/restore is likely pinning and loses both paths at once. My PR only restores the explicit-persistence path.

And agreed on the interaction: for pure cross-session prefix reuse the host prompt cache already handles it, often faster. This fix is for explicit session persistence through /slots: state that has to survive a server restart and be addressable by name which the host cache doesn't cover. Complementary, not competing.

On the .ckpt sidecar: thanks for not opening something competing. Single file + backward/forward compatible was the deliberate choice a sidecar can desync from the state file, whereas an appendix that older servers stop reading at their own payload end can't.

Filed it separately as #28139, as you suggested.

One correction to what I wrote above: the trigger is narrower than I described.
It only happens when the pinned slot is empty — pinning to a slot that still
holds tokens works fine. Root cause is a 0.0f/0.0f in the f_keep computation,
which makes the f_keep < 0.5f guard false and skips the cache lookup.

@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Independent confirmation of this bug, from a different model and a different angle.
I run a local proxy in front of llama-server that manages slots for multi-session agent work: on a session switch it saves the outgoing session's slot and restores the incoming one via /slots. The whole point was to avoid cold prefill on a hybrid model. It didn't work — every restored session still re-processed its full prompt, while the restore itself reported success.
Reduced to a single variable on Qwen3.8-27B (hybrid SSM), via /completions:

feed a 2001-token prompt → evict the slot with unrelated content
→ send a strict extension (same 2001-token prefix + a few tokens)

  without restore in between:  8 tokens processed
  with    restore in between:  2005 tokens processed   (full recompute)

Root cause matches your analysis exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only restores tokens, and the reuse path then takes do_reset because the checkpoint list is empty.
Two things that may be worth adding to the picture:
id_slot pinning bypasses the host prompt cache. Same 3211-token prompt after evicting the slot: pinned to a slot, 0 reused; unpinned, 3207 reused. The cache lookup happens during slot selection. Anyone using /slots save/restore is likely pinning too, so they lose both paths at once.
In my case, not using save/restore at all turned out to be faster than fixing it. With the proxy's slot management disabled, a new agent session went from 14,199 tokens re-prefilled (22.5 s) to 4 tokens (0.45 s), because the host prompt cache already handles cross-session prefix reuse correctly. That doesn't reduce the value of this fix — explicit session persistence through /slots still needs it — but the two mechanisms interact.
I had built a local fix for the same problem before finding this PR (a separate .ckpt sidecar file). Yours is the better design — single file, compatible in both directions — so I'm not opening anything competing.
AI usage disclosure: the measurements and root-cause analysis were done with Claude assisting; I reproduced and verified everything on my own hardware. This comment is my own.

Thanks, this is a really useful data point: different model, different path, and a clean single-variable repro (8 vs 2005 tokens). Matches the root cause exactly: server_prompt::clear() drops the checkpoints, SLOT_RESTORE only brings back tokens, and the reuse path then hits do_reset because the checkpoint list is empty.
On id_slot pinning bypassing the host prompt cache: good catch, and I'd treat it as a separate issue from this PR. The cache lookup happens during slot selection, so pinning skips it regardless of checkpoints worth its own report, since anyone using /slots save/restore is likely pinning and loses both paths at once. My PR only restores the explicit-persistence path.
And agreed on the interaction: for pure cross-session prefix reuse the host prompt cache already handles it, often faster. This fix is for explicit session persistence through /slots: state that has to survive a server restart and be addressable by name which the host cache doesn't cover. Complementary, not competing.
On the .ckpt sidecar: thanks for not opening something competing. Single file + backward/forward compatible was the deliberate choice a sidecar can desync from the state file, whereas an appendix that older servers stop reading at their own payload end can't.

Filed it separately as #28139, as you suggested.

One correction to what I wrote above: the trigger is narrower than I described. It only happens when the pinned slot is empty — pinning to a slot that still holds tokens works fine. Root cause is a 0.0f/0.0f in the f_keep computation, which makes the f_keep < 0.5f guard false and skips the cache lookup.

Thanks, and nice catch on the NaN. I'll follow #28139. Appreciate you keeping it separate from this one.

@dgabehar

dgabehar commented Sep 8, 2026

Copy link
Copy Markdown

Another independent confirmation, from yet another hardware/model combination, plus one adjacent finding that's worth flagging even though it's out of this PR's scope.

Setup: Vulkan/AMD APU (Radeon 780M, gfx1103), Qwen3.8-27B with --spec-type draft-mtp (speculative decoding via a draft model), --slot-save-path, single slot (--parallel 1).

Confirmed the bug and root cause independently before finding this PR: POST /slots/0?action=restore reports success and a perfect LCP match at the next request's slot-selection step (f_sim_best = 1.000), but the reuse-validity check immediately after finds slot.prompt.checkpoints empty and forces n_past = 0. Verbose (-lv 5) logging showed the post-restore request replaying the exact same checkpoint-creation sequence as a genuinely cold prompt, byte for byte.

Confirmed this PR's fix works, hand-ported against a different pinned commit than this PR's own base (our build tracks ggml-org/llama.cpp via a pinned-commit + vendored-patch workflow rather than a live branch, so the line context differs slightly — the logic is unchanged). Real production test: save a slot mid-conversation, evict it with an unrelated request, restore, then continue the original conversation — cache_n went from 0 to the full restored token count, wall clock from ~71s to ~3s for a ~4500-token prefix.

One thing this PR doesn't cover, for anyone else hitting a similar wall: on a speculative-decoding deployment (--model-draft/--spec-type), neither SLOT_SAVE nor SLOT_RESTORE persist the draft model's own KV cache (ctx_dft) directly — only whatever data_dft happens to be attached to a checkpoint that already existed at save time. For a short conversation that hasn't hit its first checkpoint yet, that means the draft model silently restarts from empty on restore. We added a small complementary fix for this (a companion <file>.dft, using llama_state_seq_get_size_ext/get_data_ext/set_data_ext directly on ctx_dft) — happy to open it as a follow-up PR against this one once it lands, or fold it in here if that's more useful. Not blocking on this PR either way, just flagging it as adjacent scope.

Thanks for tracking this down and writing a proper regression test — hope this helps get it unstuck.

@Tough-Respawn

Copy link
Copy Markdown
Contributor Author

Another independent confirmation, from yet another hardware/model combination, plus one adjacent finding that's worth flagging even though it's out of this PR's scope.

Setup: Vulkan/AMD APU (Radeon 780M, gfx1103), Qwen3.8-27B with --spec-type draft-mtp (speculative decoding via a draft model), --slot-save-path, single slot (--parallel 1).

Confirmed the bug and root cause independently before finding this PR: POST /slots/0?action=restore reports success and a perfect LCP match at the next request's slot-selection step (f_sim_best = 1.000), but the reuse-validity check immediately after finds slot.prompt.checkpoints empty and forces n_past = 0. Verbose (-lv 5) logging showed the post-restore request replaying the exact same checkpoint-creation sequence as a genuinely cold prompt, byte for byte.

Confirmed this PR's fix works, hand-ported against a different pinned commit than this PR's own base (our build tracks ggml-org/llama.cpp via a pinned-commit + vendored-patch workflow rather than a live branch, so the line context differs slightly — the logic is unchanged). Real production test: save a slot mid-conversation, evict it with an unrelated request, restore, then continue the original conversation — cache_n went from 0 to the full restored token count, wall clock from ~71s to ~3s for a ~4500-token prefix.

One thing this PR doesn't cover, for anyone else hitting a similar wall: on a speculative-decoding deployment (--model-draft/--spec-type), neither SLOT_SAVE nor SLOT_RESTORE persist the draft model's own KV cache (ctx_dft) directly — only whatever data_dft happens to be attached to a checkpoint that already existed at save time. For a short conversation that hasn't hit its first checkpoint yet, that means the draft model silently restarts from empty on restore. We added a small complementary fix for this (a companion <file>.dft, using llama_state_seq_get_size_ext/get_data_ext/set_data_ext directly on ctx_dft) — happy to open it as a follow-up PR against this one once it lands, or fold it in here if that's more useful. Not blocking on this PR either way, just flagging it as adjacent scope.

Thanks for tracking this down and writing a proper regression test — hope this helps get it unstuck.

Thanks, really useful confirmation on a fresh combo. Agreed the draft-context gap is real and belongs in a follow-up, #28619 is the right tracker. A couple of open suggestions for when you pick it up : the in-RAM prompt cache already saves both contexts with FLAGS_NONE, a good reference for the KV part; a separate tagged section (own version, written even without checkpoints) would keep SCKP v1 readers intact; and since draft-mtp holds state outside the KV, the meaningful test is draft acceptance after save/evict/restore versus the same continuation on a slot that stayed in memory.

dgabehar added a commit to dgabehar/llama.cpp that referenced this pull request Sep 10, 2026
POST /slots/{id}?action=save|restore never actually delivers the
KV-cache-reuse benefit it's documented to, found while building this
fleet's disk-persistence feature (master.slotSavePath) against a
draft-mtp (speculative decoding) deployment.

REVISION NOTE: the first version of this fix seeded one synthetic
full-range checkpoint after restore. That worked for the common case
(resuming a saved conversation by appending a new turn -- verified live:
3.0s vs 70.5s cold, cache_n=4533) but failed for a non-monotonic
continuation (resending a prompt SHORTER than what was saved, e.g. a
divergent retry) -- confirmed live: same 71.6s either way, cache_n=0.
Root cause of that gap: a single synthetic checkpoint's own pos_max
becomes a hard ceiling on how much reuse the very next request can ever
be trusted for (`cur.pos_max > pos_next` rejects it outright once the new
request's own match falls short of that ceiling), and there's no one
fixed pos_max that works for every possible future request. Superseded by
the approach below -- persisting whatever REAL checkpoints already
existed at save time, at their own natural positions, rather than
inventing one -- which searches correctly against any future request.

UPSTREAM PRIOR ART (found after building and testing the superseded
synthetic-checkpoint version above, before writing this one): this exact
bug is well-documented upstream --

  - Issue ggml-org/llama.cpp#25913 ("/slots
    save/restore silently loses all prompt reuse on hybrid/recurrent
    models") -- open, matches our own symptom and log evidence exactly
    (f_sim_best = 1.000 at slot selection, cache_n = 0 on the very next
    request, "restore reports success and delivers zero benefit").
  - PR ggml-org/llama.cpp#20819 -- an early fix
    attempt, now stale/conflicting (unrebased since 2026-06), stalled on
    the project's AI-disclosure contribution policy rather than a
    technical objection.
  - PR ggml-org/llama.cpp#28074 -- an independent
    submission of the SAME synthetic-single-checkpoint approach this
    fix's own superseded first version used. Closed by the author as a
    duplicate of PR #26004 ("My synthetic full-range fallback only helps
    empty-checkpoint case").
  - PR ggml-org/llama.cpp#26004 -- OPEN,
    MERGEABLE (no conflicts as of this vendoring), with a proper
    regression test (tools/server/tests/unit/test_slot_save.py ::
    test_slot_restore_preserves_context_checkpoints), and multiple
    independent community confirmations across different hardware/model
    combinations (Mac/Metal/Qwen3.6-35B-A3B; Vulkan/RADV/gfx1151/
    Qwen3.8-Flash-Next; ROCm/Qwen3.6-27B). Not yet merged as of this
    vendoring, apparently on maintainer review bandwidth rather than any
    open technical objection. This commit's own save_slot_checkpoints/
    load_slot_checkpoints functions below are adapted from that PR
    (hand-ported to apply against our exact pinned commit -- PR #26004's
    own diff did not apply byte-for-byte due to unrelated line drift from
    its last rebase).

TWO DISTINCT FIXES, both needed for this fleet's draft-mtp deployments:

1. PR #26004's approach (save_slot_checkpoints/load_slot_checkpoints, and
   their two call sites in SLOT_SAVE/SLOT_RESTORE): persist the
   checkpoints that already existed in slot.prompt.checkpoints at save
   time -- each with its own data_tgt/data_dft/data_spec -- as a tagged
   appendix after the main llama-state payload in the same save file.
   Old files without the appendix (no "SCKP" magic at the expected
   offset) restore exactly as before -- silent no-op, not an error.

2. This fleet's own addition, NOT covered by PR #26004: neither
   SLOT_SAVE nor SLOT_RESTORE persist ctx_dft (the draft model's own,
   currently-live KV cache) directly -- only whatever data_dft happens
   to be attached to an existing checkpoint. On a short conversation
   that hasn't hit its first checkpoint yet, the restored slot's draft
   model would silently start from an empty KV cache with no live
   fallback. Implemented as a companion `<save-file>.dft` written
   alongside the main save file, via
   llama_state_seq_get_size_ext/get_data_ext (capture) and
   llama_state_seq_set_data_ext (restore) -- the same primitives
   common_prompt_checkpoint::update_dft/load_dft already use internally,
   just driven directly here since there's no single-call file-based API
   for a second context. A missing .dft file (an older save, or a
   non-speculative deployment with ctx_dft == nullptr) is not an error.

Both verified together live: save -> evict (different conversation on the
same slot) -> restore -> resend-with-extension shows cache_n = 4533 (the
full restored prefix reused, only 17 new tokens processed), 3.0s versus
70.5s cold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3QNP4GznFWhhf11QY18j
@ggerganov ggerganov added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Oct 8, 2026
@ggerganov

Copy link
Copy Markdown
Member

@Tough-Respawn Could you rebase on latest master?

Tough-Respawn and others added 6 commits October 8, 2026 11:23
Append the checkpoints after the packed server_tokens payload added in ggml-org#26640
and count them in n_written / n_read, so a restored slot can still roll back to
a checkpoint instead of re-processing the whole prompt.
…text

Restoring a slot saved with a different draft KV cache type aborted in
load_dft(). Test-load one draft checkpoint on restore and drop the draft
data if it does not fit, instead of crashing. Adds a regression test.

Co-authored-by: Igor Okulist <okigan@gmail.com>
Bound each blob size by the bytes left in the file before allocating, open the
file with UTF-8 paths on Windows like the llama state payload, fall back to full
prompt re-processing when a checkpoint restored from a slot file fails to load,
and replace the 1024 count cap by keeping the last n_ctx_checkpoints while reading.
Return an error to the client when the appendix cannot be written, like a
failed payload write, and make the oversized-blob test declare a size that
cannot be allocated, so an unbounded allocation fails the test.
A saved checkpoint always holds a target state, an empty blob would roll back
without restoring anything. Also log with the slot id, and load the draft test
model from the HF cache instead of a second download.
A checkpoint restored from a slot file falls back to full prompt re-processing
when it fails to load, a checkpoint created in memory still aborts.
@pwilkin
pwilkin force-pushed the fix/kv-restore-checkpoint branch from 2cb56aa to 9223fb4 Compare October 8, 2026 09:26
@pwilkin

pwilkin commented Oct 8, 2026

Copy link
Copy Markdown
Member

@ggerganov did it.

@ggerganov
ggerganov merged commit 033df86 into ggml-org:master Oct 8, 2026
17 checks passed
cwriter pushed a commit to cwriter/llama.cpp that referenced this pull request Oct 8, 2026
Adapt the slot save/restore checkpoint appendix from ggml-org#26004 to the
shared_ptr checkpoint list and the common_state_data buffers.

Assisted-by: Claude Opus 5.5
feal87 added a commit to feal87/myllama.cpp that referenced this pull request Oct 8, 2026
Keep the fork MoE expert cache (src/llama-moecache.*). Drop the upstream one (src/llama-moe-cache.*, PR ggml-org#29887 and ggml-org#30112) and its moe_cache_size plumbing, see scripts/fork-post-merge.sh.

Keep the fork server checkpoint store and combine it with the upstream slot save/restore checkpoint appendix (PR ggml-org#26004): load_tgt / load_dft now return bool, an in-memory spec checkpoint aborts when a restore fails and a slot-file checkpoint falls back to full prompt re-processing.

Assisted-by: pi (deepseek-flash)
sourenaraya added a commit to sourenaraya/llama.cpp that referenced this pull request Oct 9, 2026
asf0 pushed a commit to asf0/strix-llama.cpp that referenced this pull request Oct 12, 2026
The 39 upstream commits after halo-box/llama.cpp's final sync. Same rules as
the previous merge: strix's side wins where the two collide, upstream's
non-conflicting changes stay, and fixes are ported into strix's code.

- server: upstream's final ggml-org#26004 (context checkpoints across slot
  save/restore) replaces strix's earlier port of it, keeping strix's on-device
  restore flags; test_slot_save.py follows upstream.
- cuda: strix's top-k (HIP radix), gated delta net and argsort kernels kept
  whole; upstream's CCCL major-version guard fix (ggml-org#29453) applied to strix's
  argsort. MMQ: upstream's out-of-bounds fix and host-side tile choice
  (ggml-org#29953) with strix's RDNA3.5 tile overrides on top; strix's fused MMQ paths
  pad src1 for the largest tile instead of the removed get_J_max. strix's
  128-column rms_norm fast path and two mmvf calls updated to upstream's new
  signatures (ggml-org#28175, warp_size).
- vulkan: strix's flash_attn_cm2.comp and topk_nary_search.comp kept whole
  (upstream's sparse cm2 FA ggml-org#30003 stays out with the rest of sparse FA).
- llama-context: strix's MTP draft vocabulary next to upstream's MoE cache.
- graph: upstream's single recurrent-state gather (ggml-org#29856) is restored and
  combined with strix's in-place gated delta net: in-place capable consumers
  keep the full gather and get their state view next to the consumer whether
  or not the rows are in place, so in-place and gathered ubatches build the
  same graph; this fixes the realloc aborts under GGML_SCHED_DEBUG_REALLOC in
  both upstream's shared-seq test and strix's seq cp graph reuse test.
- ggml-backend: alloc_buffer_n planning no longer flushes on zero-size
  tensors, which left views of a tensor larger than max_size uninitialized
  (caught by strix's test_view_init_after_max_size_split).
- SYNC.md: this fork now syncs with ggml-org/llama.cpp directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thelittlefireman pushed a commit to thelittlefireman/llama.cpp that referenced this pull request Oct 12, 2026
…org#26004)

* server : preserve context checkpoints across slot save/restore

Append the checkpoints after the packed server_tokens payload added in ggml-org#26640
and count them in n_written / n_read, so a restored slot can still roll back to
a checkpoint instead of re-processing the whole prompt.

* server : drop draft checkpoint data that does not match the draft context

Restoring a slot saved with a different draft KV cache type aborted in
load_dft(). Test-load one draft checkpoint on restore and drop the draft
data if it does not fit, instead of crashing. Adds a regression test.

Co-authored-by: Igor Okulist <okigan@gmail.com>

* server : harden the checkpoint appendix of slot save files

Bound each blob size by the bytes left in the file before allocating, open the
file with UTF-8 paths on Windows like the llama state payload, fall back to full
prompt re-processing when a checkpoint restored from a slot file fails to load,
and replace the 1024 count cap by keeping the last n_ctx_checkpoints while reading.

* server : report an incomplete checkpoint appendix as a failed slot save

Return an error to the client when the appendix cannot be written, like a
failed payload write, and make the oversized-blob test declare a size that
cannot be allocated, so an unbounded allocation fails the test.

* server : reject an empty target state in the checkpoint appendix

A saved checkpoint always holds a target state, an empty blob would roll back
without restoring anything. Also log with the slot id, and load the draft test
model from the HF cache instead of a second download.

* common : return bool from checkpoint load_tgt / load_dft

A checkpoint restored from a slot file falls back to full prompt re-processing
when it fails to load, a checkpoint created in memory still aborts.

---------

Co-authored-by: Igor Okulist <okigan@gmail.com>
BurnWW pushed a commit to BurnWW/mx-llama.cpp that referenced this pull request Oct 15, 2026
811 upstream commits (incl. the b11553 rebase and the RCCL probe fallback).

Local-only content kept:
- L3 disk-backed prompt cache (LLAMA_CACHE_DISK_DIR) with the RAM watermark
  guard, disk-only spill and in-flight write wait.
- MTP draft fix: reset deferred_prefill_failed per task, clear a stale draft
  sequence when the batch position moves backward (adapted to the upstream
  process_decode(const common_batch &) signature).

Dropped as superseded upstream:
- asymmetric Q8_0-Q4_0 KV flash attention: upstream now compiles the K/V
  combination matrix from GGML_CUDA_FA_QUANTS; the qwen-tp2/tiel-tp2 configs
  need -DGGML_CUDA_FA_QUANTS=...q8_0-q4_0... at configure time instead.
- context checkpoints in slot save/restore sidecars: upstream ggml-org#26004 writes
  them into the snapshot file itself (with draft data and a fallback to full
  re-processing when a restored checkpoint does not fit).
roverdrubber pushed a commit to roverdrubber/llama.cpp that referenced this pull request Oct 15, 2026
…org#26004)

* server : preserve context checkpoints across slot save/restore

Append the checkpoints after the packed server_tokens payload added in ggml-org#26640
and count them in n_written / n_read, so a restored slot can still roll back to
a checkpoint instead of re-processing the whole prompt.

* server : drop draft checkpoint data that does not match the draft context

Restoring a slot saved with a different draft KV cache type aborted in
load_dft(). Test-load one draft checkpoint on restore and drop the draft
data if it does not fit, instead of crashing. Adds a regression test.

* server : harden the checkpoint appendix of slot save files

Bound each blob size by the bytes left in the file before allocating, open the
file with UTF-8 paths on Windows like the llama state payload, fall back to full
prompt re-processing when a checkpoint restored from a slot file fails to load,
and replace the 1024 count cap by keeping the last n_ctx_checkpoints while reading.

* server : report an incomplete checkpoint appendix as a failed slot save

Return an error to the client when the appendix cannot be written, like a
failed payload write, and make the oversized-blob test declare a size that
cannot be allocated, so an unbounded allocation fails the test.

* server : reject an empty target state in the checkpoint appendix

A saved checkpoint always holds a target state, an empty blob would roll back
without restoring anything. Also log with the slot id, and load the draft test
model from the HF cache instead of a second download.

* common : return bool from checkpoint load_tgt / load_dft

A checkpoint restored from a slot file falls back to full prompt re-processing
when it fails to load, a checkpoint created in memory still aborts.

---------

Fork: the checkpoints were already saved, beside the file in <file>.ckpt
(3991a0d). They are now appended to the file as upstream does; the
.ckpt sidecar is read only for files saved before this, and a save
removes it. The draft state stays in <file>.dft, which upstream does not
save, and its bytes no longer move the offset where the appendix is read.
test_slot_save_sidecars.py covers the old sidecar and its removal.

Co-authored-by: Igor Okulist <okigan@gmail.com>
Assisted-by: Claude Opus 5.5
roverdrubber added a commit to roverdrubber/llama.cpp that referenced this pull request Oct 15, 2026
Reading the checkpoint appendix (ggml-org#26004) test-loads one draft checkpoint
and then clears the draft's sequence, since upstream restores no draft
state. The fork read <file>.dft before it, so the restored cells were
cleared again, and a restored chat continued with a draft context that
held only the new tokens.

On the 27B (base, separate MTP head, 7.3K prompt, temperature 0), turn 3
of a chat after a restore, fresh server per arm:

  live                    draft cells 0..8189     0.667 (266/399)  66.6 t/s
  restored, this commit   draft cells 0..8189     0.667 (266/399)  66.9 t/s
  restored without .dft   draft cells 8159..8189  0.568 (251/442)  58.5 t/s

The restored chat now drafts exactly as the live one; before, it did as
the run without a .dft does. A replay that rolls back to a checkpoint was
unaffected, as each checkpoint carries its own draft state. No server
test covers it: it needs a hybrid target with a draft context and a
continuation that rolls back to no checkpoint, and the test models have
neither; the check is qwen-project/bench/mtp-restore-2026-10-15.

Assisted-by: Claude Opus 5.5
roverdrubber added a commit to roverdrubber/llama.cpp that referenced this pull request Oct 15, 2026
The slot file holds the target's cells only, so a restored MTP chat drafted from an empty draft context and lost acceptance. Save the draft's cells in <file>.dft and read them back after the checkpoints, whose loading clears the draft's sequence.

Port of 3991a0d, eb45582 and 50e85f0 from perf, without the .ckpt sidecar (ggml-org#26004 appends the checkpoints to the file).

Assisted-by: Claude Opus 5.5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Misc. bug: /slots save/restore silently loses all prompt reuse on hybrid/recurrent models — checkpoints are never persisted