Skip to content

server : evict checkpoints within min-step of each other - #25472

Merged
aldehir merged 4 commits into
ggml-org:masterfrom
aldehir:server-checkpoint-eviction-min-step
Jul 12, 2026
Merged

server : evict checkpoints within min-step of each other#25472
aldehir merged 4 commits into
ggml-org:masterfrom
aldehir:server-checkpoint-eviction-min-step

Conversation

@aldehir

@aldehir aldehir commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Overview

Continuation of #24176 (review)

When creating a new checkpoint, evict any checkpoints that are within min-step of an earlier one.

Since we create two checkpoints towards the end of the prompt, it is possible that the last checkpoint evicts the penultimate checkpoint. To avoid this, I keep track of the task id in the checkpoint and only evict checkpoints created from prior tasks. Let me know if there is a better approach.

fixes #25023

Requirements

@aldehir
aldehir requested review from a team as code owners July 9, 2026 04:37
@github-actions github-actions Bot added the server label Jul 9, 2026
@ggerganov ggerganov self-assigned this Jul 9, 2026
@aldehir

aldehir commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Hmm, turns out the near end checkpoint isn't being created if within min-step, which I don't believe was intended.

I also opened #25420 to address prefill issues.

I can roll these fixes into a single PR if desired.

@ggerganov

Copy link
Copy Markdown
Member

I also opened #25420 to address prefill issues.

I think it's safe to merge #25420.

@ggerganov

Copy link
Copy Markdown
Member

Hmm, turns out the near end checkpoint isn't being created if within min-step, which I don't believe was intended.

Think we just have to allow near_prompt_end and it should be good:

diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
index 17941d9e9..f477d7a3a 100644
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -3506,7 +3506,10 @@ private:
                     do_checkpoint = do_checkpoint && !has_mtmd;
 
                     // no need to create checkpoints that are too close together, unless it's the last user message
-                    do_checkpoint = do_checkpoint && (slot.prompt.checkpoints.empty() || is_last_user_message || n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
+                    do_checkpoint = do_checkpoint && (
+                            slot.prompt.checkpoints.empty() ||
+                            is_last_user_message || near_prompt_end ||
+                            n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
                     SLT_DBG(slot, "main/do_checkpoint = %s, pos_min = %d, pos_max = %d\n", do_checkpoint ? "yes" : "no", pos_min, pos_max);
 
                     // note: we create the checkpoint before calling llama_decode(), so the current batch is not

Doing some testing.

Comment thread tools/server/server-context.cpp Outdated
@aldehir
aldehir force-pushed the server-checkpoint-eviction-min-step branch from 5add0ff to 33ac581 Compare July 10, 2026 03:59
@ali0une

ali0une commented Jul 10, 2026

Copy link
Copy Markdown

@aldehir did you see my comment with the clean diff?

isn't prevention at checkpoint creation time better than the id_task field + eviction loop in create_checkpoint()?

@ali0une

ali0une commented Jul 10, 2026

Copy link
Copy Markdown

@ggerganov @aldehir thanks for the work on this. I wanted to share some benchmark data that might help decide between the eviction approach and prevention at creation time.

I tested a prevention-based approach on a 29-request agent workload (branching conversations, sleep/wake, MTP). The numbers vs pre-bug baseline:

Metric Pre-bug Prevention
Old checkpoint evictions 157 43 (-72%)
"Looking for better prompt" events 15 5
Catastrophic resets multiple 0

The approach combines the near_prompt_end bypass you suggested with a relaxed floor (checkpoint_min_step / 2) for is_last_user_message instead of a full bypass. This prevents clustered checkpoints at the source, so no id_task field or post-creation eviction loop is needed:

diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -3522,12 +3522,15 @@ private:
                     // do not checkpoint after mtmd chunks
                     do_checkpoint = do_checkpoint && !has_mtmd;

-                    // no need to create checkpoints that are too close together, unless it's the last user message
-                    // apply a relaxed floor for is_last_user_message to prevent pathological clustering (#25023)
+                    // no need to create checkpoints that are too close together, unless it's the last user
+                    // message or we are near the end of the prompt (#25023)
+                    // apply a relaxed floor for is_last_user_message to prevent pathological clustering;
+                    // near_prompt_end fires unconditionally to ensure the critical end-of-prompt checkpoint exists
                     const int32_t checkpoint_floor = slot.prompt.checkpoints.empty()
                         ? 0
                         : slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step / 2;
                     do_checkpoint = do_checkpoint && (slot.prompt.checkpoints.empty()
+                        || near_prompt_end
                         || (is_last_user_message && n_tokens_start > checkpoint_floor)
                         || n_tokens_start > slot.prompt.checkpoints.back().n_tokens + params_base.checkpoint_min_step);
                     SLT_DBG(slot, "main/do_checkpoint = %s, pos_min = %d, pos_max = %d\n", do_checkpoint ? "yes" : "no", pos_min, pos_max);

Full analysis : post-enhanced-fix-analyse.md
router-post-enhanced-fix.log

@aldehir
aldehir merged commit 0c4fa7a into ggml-org:master Jul 12, 2026
23 of 25 checks passed
@aldehir

aldehir commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@ali0une I'm merging this in now and will continue to look for improvements. To do that, however, I need to find a way to benchmark this.

Regarding your min-step / 2 approach, I don't believe this really addresses the issue of the min-step invariant being violated for all user message derived checkpoints except for the last. On consecutive requests in an ongoing conversation, it would instead collapse the distance to min-step / 2, which is not what we want.

@ali0une

ali0une commented Jul 13, 2026

Copy link
Copy Markdown

@aldehir many thanks for your answer from an architectural purity standpoint and for the time spent to analyze my relaxed floor for is_last_user_message approach.

i'll keep on analysing logs from the upstream vanilla llama.cpp and my local repository with the relaxed floor for is_last_user_message and let you know if i find anything usefull.

fewtarius added a commit to fewtarius/CachyLLama that referenced this pull request Jul 19, 2026
…, OpenCL Q6_K/Adreno, CORS, checkpoint min-step, prompt cache refactor, MoE expert API stays)

Upstream highlights since 6be7459:
- model: DFlash speculative with KV rotation (ggml-org#25823)
- model: Hy3 (hy_v3) with MTP speculative decoding (ggml-org#25395)
- model: DeepseekV4 with fused hyper-connection ops (ggml-org#25585)
- ggml: 0.17.0, LIGHTNING_INDEXER, out_prod, f16 set_rows
- vulkan: Q2_0 support, native e2m1/e4m3 conversions, transfer-queue race fix
- CUDA: MMQ kernel config refactor (ggml-org#24127), tighter MMQ src1 buffer for fp4 (ggml-org#25613), CUDA graphs on Volta/Turing, MoE gate/up dedup, CUDA Virtual Devices
- ROCm: hexagon L2 cache rework, native fp4, FP16/INT8 coopmat on AMD
- SYCL: Battlemage flash attention via oneDNN XMX, XIELU op, fp16 conv2d_dw
- OpenCL: Q6_K GEMM/GEMV fix, ragged-tile MoE prefill FP16, Adreno vectorized LD/ST, A7x optimizations, ABS op
- kleidiai: SME2 f32 kernel, SME vs SME2 dispatch
- server: refactor prompt cache state ownership (ggml-org#25649) - new server_prompt_cache_state separates prompt metadata from KV data
- server: evict checkpoints within min-step (ggml-org#25472)
- server: text-only slot save/restore with mtmd (ggml-org#25076)
- server: --cors-* options (ggml-org#25655)
- server: refactored server_stream (ggml-org#25541)
- server: respect min-step when splitting prompt batches (ggml-org#25420)
- server: move chat-template thinking probe inside init try/catch (ggml-org#24093)
- common: auto-download dflash/eagle3 HF sidecars (ggml-org#25811), drop --stdin mutual-exclusion, align tokenize usage
- conversion: BitNetForCausalLM, dflash tokenizer fix, split MTP export for HY V3
- llama-quant: exclude i32 ffn_gate_tid2eid routing table, allow manual tensor types with --pure
- llama-batch: fix allowed decreasing pos in a seq (ggml-org#25449), n_keep_tail in split_equal for recurrent
- llama: refactor fused ops (ggml-org#24646), TP fix for Phi3/Bert/Plamo2/3/ChatGLM
- ui: agentic content UX, reasoning effort on mobile add sheet, MCP panel fixes, thinking menu fix
- vendor: BoringSSL 0.20250713.0
- tests: actually exercise test-recurrent-state-rollback, ds_v4_hc sentinel init, export-graph-ops graceful exit

CachyLLama preservation work (conflict resolution):

1. tools/server/server-task.h: Accept upstream's server_prompt refactor (no data member, clear() method).
   Move our t_last_used field from server_prompt to server_prompt_cache_state (where it now lives
   after the refactor). server_prompt_cache_state already has the size() method, so our old
   size() on server_prompt is no longer needed.

2. tools/server/server-context.cpp (create_checkpoint): Take upstream's min-step eviction
   pre-filter as the FIRST pass, then keep our existing highest-pos_min eviction as the
   capacity overflow fallback. These are complementary: min-step removes redundant checkpoints
   from the same task; highest-pos_min keeps the rec-window-friendly checkpoints when at cap.

3. tools/server/server-context.cpp (handle_completions_impl): Keep our std::vector<server_task>
   tasks batching for multi-prompt requests and per-user concurrency check, AND take upstream's
   res->set_req(&req) for spipe ownership transfer.

4. tools/server/server-task.cpp: Fix references to entry.tokens -> entry.prompt.tokens,
   entry.checkpoints -> entry.prompt.checkpoints, entry.n_tokens() -> entry.prompt.n_tokens().
   Update find_eviction_candidate return type from list<server_prompt>::iterator to
   list<server_prompt_cache_state>::iterator.

5. ggml/src/ggml-cuda/mmq.cuh + new mmq-config-rdna3_5.cuh: Upstream's massive MMQ refactor
   moved per-architecture config into separate files but did NOT add RDNA3.5 (gfx1150/1/2/3,
   Strix Halo). Create mmq-config-rdna3_5.cuh (231 CASE entries) derived from rdna2 with
   nthreads=128 (4 warps) and I=48 (smaller X tile) matching our original Strix Halo tuning.
   Wire into both host and device dispatch paths before the RDNA4 / RDNA2 fallback.

6. README.md and AGENTS.md: Keep CachyLLama-specific links and project context where upstream
   added parallel content.

Verified:
- cmake --build builds clean (Release, CPU-only)
- llama-server starts, --help shows all CachyLLama flags preserved:
  --cache-ssd-hot-ram, --cache-ssd-warm-ram, --cache-ssd-system-prompts,
  --cache-ssd-system-max-days, --cache-ssd-no-fsync, --cache-ssd-max-conversations,
  --max-concurrent-per-user
- /expert-stats and /expert-tracking endpoints preserved
- 55/58 tests pass; 3 failures unrelated to merge:
  - test-tokenizers-ggml-vocabs: missing model downloads
  - test-jinja-py: missing jinja2 Python module
  - test-quant-type-selection: snapshot mismatch on upstream's new MXFP4_MOE heuristic

Custom CachyLLama files untouched (no upstream conflicts):
- common/kv-ssd-cache.{cpp,h}, common/kv-ssd-posix.h, common/kv-ssd-system-cache.{cpp,h}
- common/kv_page_manager.{cpp,h}
- tools/server/server-context-page-manager.{cpp,h}
- tools/server/server-context-ssd-cache.{cpp,h}
- test_kv_page_manager.cpp, tests/test-ssd-cache-caps.cpp
- STRIX_HALO_NOTES.md, docs/development/user-isolation-design.md
- .github/workflows/build-cpu.yml, build-cuda-windows.yml, build-vulkan.yml
fewtarius added a commit to fewtarius/CachyLLama that referenced this pull request Jul 19, 2026
Upstream PR ggml-org#25472 always fires a checkpoint within the last ubatch of every
prompt regardless of --checkpoint-min-step. On dense + sliding-window models
(gemma-4) and dense MoE (Qwen3.6-35B) this doubles per-request SSD write
volume and warm restore I/O - warm TTFT for gemma-4 large went 0.65s -> 2.12s
after the merge.

Default stays off (matching pre-ggml-org#25472 behavior). Add explicit CLI flag so
profiles that want the upstream behavior can opt in.
CowboyTim pushed a commit to aardbeiplantje/llama.cpp that referenced this pull request Jul 21, 2026
smpurkis pushed a commit to smpurkis/halo-llama.cpp that referenced this pull request Jul 24, 2026
Upstream PR ggml-org#25472 always fires a checkpoint within the last ubatch of every
prompt regardless of --checkpoint-min-step. On dense + sliding-window models
(gemma-4) and dense MoE (Qwen3.6-35B) this doubles per-request SSD write
volume and warm restore I/O - warm TTFT for gemma-4 large went 0.65s -> 2.12s
after the merge.

Default stays off (matching pre-ggml-org#25472 behavior). Add explicit CLI flag so
profiles that want the upstream behavior can opt in.
smpurkis pushed a commit to smpurkis/halo-llama.cpp that referenced this pull request Jul 30, 2026
Upstream PR ggml-org#25472 always fires a checkpoint within the last ubatch of every
prompt regardless of --checkpoint-min-step. On dense + sliding-window models
(gemma-4) and dense MoE (Qwen3.6-35B) this doubles per-request SSD write
volume and warm restore I/O - warm TTFT for gemma-4 large went 0.65s -> 2.12s
after the merge.

Default stays off (matching pre-ggml-org#25472 behavior). Add explicit CLI flag so
profiles that want the upstream behavior can opt in.
smpurkis pushed a commit to smpurkis/halo-llama.cpp that referenced this pull request Jul 30, 2026
Upstream PR ggml-org#25472 always fires a checkpoint within the last ubatch of every
prompt regardless of --checkpoint-min-step. On dense + sliding-window models
(gemma-4) and dense MoE (Qwen3.6-35B) this doubles per-request SSD write
volume and warm restore I/O - warm TTFT for gemma-4 large went 0.65s -> 2.12s
after the merge.

Default stays off (matching pre-ggml-org#25472 behavior). Add explicit CLI flag so
profiles that want the upstream behavior can opt in.
smalinin pushed a commit to smalinin/llama.cpp that referenced this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkpoint erasure during normal conversation flow due to is_last_user_message bypass of checkpoint_min_step

6 participants