Repository navigation
server : preserve context checkpoints across slot save/restore - #26004
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 |
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. |
|
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: 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. 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. |
Thanks, and nice catch on the NaN. I'll follow #28139. Appreciate you keeping it separate from this one. |
|
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 Confirmed the bug and root cause independently before finding this PR: Confirmed this PR's fix works, hand-ported against a different pinned commit than this PR's own base (our build tracks One thing this PR doesn't cover, for anyone else hitting a similar wall: on a speculative-decoding deployment ( 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. |
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
|
@Tough-Respawn Could you rebase on latest |
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.
2cb56aa to
9223fb4
Compare
|
@ggerganov did it. |
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
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)
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>
…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>
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).
…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
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
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
Overview
Fix the full re-prefill after
slot save → restorefor 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 (SCKPmagic + version + count + per-checkpoint pos fields and state blobs) after the llama state payloadload_slot_checkpoints()reads it back at the offset returned byllama_state_seq_load_fileand reattaches the checkpoints to the slotid_taskis not serialized and is reset to-1on load (task ids are not meaningful across save/restore)n_written/n_readinclude 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 featureThe implementation lives in
tools/server/server-context.cpp, usingstd::ifstream/std::ofstreamwith 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:n_livetokens processed)savethe slotrestorethe slotprompt_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).test_slot_restore_damaged_checkpoint_appendixdamages 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_changeruns 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-eagle3across 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_RESTOREhandlers: the save now writes a packedserver_tokens::serialize()payload including media chunks instead ofget_text_tokens(), and the restore does a two-passllama_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 byllama_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 f16into a server started with-ctkd q8_0aborted inload_dft()withcheckpoint 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_filedoes, and the 16 GiB cap is gone. Same commit:std::filesystem::u8path(), as in common : remove fs_open_ifstream() by using u8path() #29841n_ctx_checkpointswhile readingThe 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_dftreturn 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:
gfx1151), Qwen3.5-35B-A3B (Gated DeltaNet)cache_n0 → 310; identical output verified by SHA-256gfx1103), Qwen3.8-27B + draft-MTPgfx1151), Qwen3.8-Flash-Next 112 GB hybrid, shared-MTPprompt_n2,337 → 4; byte-identical greedy output; slot-save suite 10/10Requirements