Repository navigation
Conversation
On DSV4 the slot operator requires a context checkpoint to reuse restored KV (seq_pos_min returns seq_pos_max). SLOT_RESTORE never populated checkpoints, so every restore forced a full re-prefill. Register a PARTIAL_ONLY checkpoint over the restored range after load_file. Verified 317tok -> 28tok tail-only on Qwen3.8-27B-AD, 7112tok long context resumes from restored state. Full-attention models unaffected. Assisted-by: Muse Spark
Author
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
dgabehar
added a commit
to dgabehar/llama.cpp
that referenced
this pull request
Sep 14, 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
dgabehar
added a commit
to dgabehar/llama.cpp
that referenced
this pull request
Sep 14, 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
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Fix
POST /slots/:id?action=restore(--slot-save-path) being useless on hybrid-recurrent (DSV4) models, like qwen 3llama_kv_cache_dsv4::seq_pos_min()intentionally returnsseq_pos_maxbecause DeltaNet recurrent state cannot be rewound, soserver-context.cppalways requires a context checkpoint to reuse cached state.SLOT_RESTORErestored KV+recurrent viallama_state_seq_load_filebut never populatedslot.prompt.checkpoints, so the next request always loggedforcing full prompt re-processing due to lack of cache dataand re-prefilled from zero. Full-attention models unaffected.Fix (17 lines,
tools/server/server-context.cpp:SLOT_RESTORE): after successful load, register aPARTIAL_ONLYcheckpoint covering[0, token_count-1]viaupdate_tgt/update_dft/common_speculative_get_state. Next request finds it, restores recurrent state, and evaluates only the tail.Additional information
cache-ramalone:--cache-reuseis disabled for DSV4 (get_can_shift()==false) andcache-ramdoes not survive restart/idle unload; disk save/restore is the only mechanism that does.registered checkpoint over restored state (430 tokens, pos [0,429])+cached n_tokens=430, 188ms/28t tail only, total 1103ms. 7112t/10-checkpoint context restores atcached n_tokens=7112.test_slot_save.pycoverage, so verification is via manual A/B logs and isolated build from9de0fcf2b.Reproduction:
cmake -B build-pr -S . && cmake --build build-pr --target llama-server)llama-server --slot-save-path /tmp/slots -np 1 -lv 4 -m <Qwen3.8-AD-Q5.gguf> --port 18082 --ctx-checkpoints 32POST /slots/0?action=save {"filename":"a.bin"}, overwrite slot with different prompt,POST /slots/0?action=restore {"filename":"a.bin"}, continue with original history+tail and check logs.Requirements