server : preserve context checkpoints across slot save/restore - #26004
server : preserve context checkpoints across slot save/restore#26004Tough-Respawn wants to merge 1 commit into
Conversation
|
Hi @Tough-Respawn, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
Independent verification on Vulkan / gfx1151 with a Gated DeltaNet hybrid — the fix worksBuilt 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
Cold prefill (baseline, identical on both) — 58,202 tokens, ~185s, save 58,209 tokens. First request after restore (same 58K prefix, divergent tail)
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)
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 Happy to re-run any of this on Vulkan/gfx1151 if it helps review. |
|
First of all, thanks to @wagi-sho for pointing out the 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 On the Metal machine:
The original CUDA run also produced 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 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: but the actual slot file written to disk was: 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 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. |
8665430 to
1d3f583
Compare
|
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 On 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 Follow-up commit:
Measured on
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 |
|
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. |
|
Yes the
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.
|
|
Fus |
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 ;) |
|
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 |
1d3f583 to
5735946
Compare
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.
5735946 to
06d9d0f
Compare
|
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. |
|
Same issue… hope this gets merged asap. |
## Overview
Fix the full re-prefill after
slot save → restorefor SWA and hybrid/recurrent models (e.g. 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 (SCKPmagic + version + count + per-checkpoint pos fields and state blobs) after the llama state payload-
load\_slot\_checkpoints()reads it back at the offset returned byllama\_state\_seq\_load\_fileand 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 corrupted files: count cap (1024), per-blob size cap, truncation → warning and clean abort (no partial state)
-
id\_taskis not serialized and is reset to-1on load (task ids are not meaningful across save/restore)-
n\_saved/n\_restoredstill report the llama payload onlyThe change is confined to
tools/server/server-context.cpp(+110 lines), usingstd::ifstream/std::ofstreamwith small read/write helpers.## Additional information
Test included:
test\_slot\_restore\_preserves\_context\_checkpointsintools/server/tests/unit/test\_slot\_save.py:1. Send a long prompt (reference: a divergent re-prompt on the live slot rolls back to a checkpoint → partial reuse,
n\_livetokens processed)2.
savethe slot3. Overwrite the slot with an unrelated prompt
4.
restorethe slot5. Send the same divergent re-prompt — the test asserts
prompt\_n == n\_liveWithout 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) andn\_ubatch = 32(so a prompt-processing checkpoint lands before the divergence point).The 3 tests in the file pass locally against this branch; the same scenario fails on master (
assert 210 == 31). Full server suite left to CI (local build has no curl andload\_allfetches all presets).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.
## Requirements
- [x] I have read and agree with the [contributing guidelines](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md)
- AI usage disclosure: YES — the implementation and the test were primarily AI-generated (Claude), 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.