Skip to content

server : save the draft context state with the slot - #171

Open
rcfa wants to merge 1 commit into
halo-box:masterfrom
rcfa:pr/slot-draft-state
Open

rcfa wants to merge 1 commit into
halo-box:masterfrom
rcfa:pr/slot-draft-state

Conversation

@rcfa

@rcfa rcfa commented Oct 12, 2026

Copy link
Copy Markdown

Draft, with a scope question first

This change is general server code, not Strix specific. ggml-org master has the same slot-save path, the same draft
contexts and the same gap (checked against 8bb3cf0 today), and I found no open PR or issue for it in ggml-org,
halo-box/llama.cpp or here. By AGENTS.md that would make it a halo-box/llama.cpp change. I am opening it here as a
draft because it extends the slot-file appendix that came in through #98 (itself a forward-port of
ggml-org#26004), and because we hit it with this fork's target model (Qwen3.8-Flash-Next, qwen4exp, with
the shared MTP head). If you would rather not carry it here, please close it; as an agent I cannot open it in
halo-box/llama.cpp.

This PR was written by an AI agent (Claude Code), working for @rcfa; what was and was not verified is listed below.

Overview

A slot save file holds the target context state and, since #98, the context checkpoints. It does not hold the state
of the draft context. A draft that is fed from the target's batches (draft-mtp here) has therefore seen none of the
restored prompt after /slots/{id}?action=restore, and fewer drafts are accepted for the rest of the conversation.
If the slot held another conversation before the restore, the draft context still holds that one instead.

The change appends the draft context's sequence state after the checkpoint appendix (SDFT magic, version, one
blob), the way the RAM prompt cache already keeps it (llama_state_seq_get_data_ext on ctx_dft). On restore it
first drops the slot's old draft state, then loads the appendix if there is one. Older readers stop after the
checkpoints and never see it; a file without it restores with an empty draft context. The output does not change:
the target verifies every draft.

We use it to move conversations between two quants of the same model under memory pressure (save every slot, restart
the server on the smaller quant, restore), which is how we found it.

Measurements

Device:     Ryzen AI 9 HX PRO 370 / Radeon 890M (gfx1150, Strix Point - NOT Strix Halo) / Minisforum N5 PRO, BIOS 1.05
Memory:     96 GB DDR5-5600 (2 x 48 GB SO-DIMM, ECC)
Power:      BIOS defaults; amdgpu power_dpm_force_performance_level=auto; cpufreq governor powersave (amd-pstate)
BIOS:       UMA split 48 GB
Kernel:     7.1.5+kali-amd64, amdgpu params: amdgpu.gttsize=73728 ttm.pages_limit=8650752
Backend:    Vulkan RADV, Mesa 26.1.6 (libvulkan1 1.4.357)
Build:      -DGGML_VULKAN=ON -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_BUILD_RPATH_USE_ORIGIN=ON (gcc 16.2)
Baseline:   02e2c2b55e78d688509bdffe6276751ccf2e2069 (master), built and run in the same session
Change:     b7149217a12a44bc42974b10497c069d7664df35
Model:      ISTA-DASLab Qwen3.8-Flash-Next GSQ-RCO IQ3_XXS, draft: unsloth shared MTP head (Q4_K_M)
Server:     llama-server -ngl 99 -c 262144 -np 4 -kvu --cache-ram 0 --no-cache-idle-slots --mmproj (F16)
            --spec-type draft-mtp --spec-draft-n-max 1 -ngld 99 --slot-save-path DIR (GGML_VK_MMV_NO_SPLIT=1)

P = 16384 tokens of unseen text, saved after a prefill with n_predict: 0; every run then sends P plus the next 64
tokens and decodes 128 tokens greedily (ignore_eos). One run each:

run master: drafts accepted master: decode this PR: drafts accepted this PR: decode
own prefill (no restore) 56 / 71 (79%) 14.70 t/s 56 / 71 (79%) 14.81 t/s
erase, restore on the same server 45 / 82 (55%) 13.13 t/s 55 / 72 (76%) 14.68 t/s
restore on a fresh server process 42 / 85 (49%) 12.42 t/s 56 / 71 (79%) 14.98 t/s
restore into slot 1, which held 20000 other tokens 47 / 80 (59%) 12.81 t/s 55 / 72 (76%) 14.76 t/s

Every continuation processed only the 64 new tokens. The slot file grows by the draft state, 36.6 MiB at 16k tokens
for this MTP head (771 -> 807 MiB).

File compatibility, each restore on a fresh server process:

file restored by drafts accepted decode
this PR's (with the appendix) master 42 / 85 (49%) 12.67 t/s
master's (without it) this PR 42 / 85 (49%) 12.70 t/s

Both restore all 16384 tokens and the 2 checkpoints, and then draft like master after a restore: master stops reading
after the checkpoints, and this PR starts with an empty draft context when there is no appendix.

Correctness

  • All eight runs above produce the same 128 tokens as master's own prefill.
  • Master's file restores on this PR and this PR's file restores on master (the second table); both give the
    same 128 tokens as master's own prefill.
  • No ggml op, model graph or compute path changes, so test-backend-ops, the corpus comparison and llama-bench
    do not apply; the greedy outputs are identical.
  • No draft decode errors in either server log.

Not verified

  • Only draft-mtp. draft-simple, draft-eagle3, draft-dflash and draft-dspark keep their draft context the
    same way (fed from the target's batches through process()), but we have no such draft models here.
  • The tools/server pytest suite (including its slot-save tests) was not run: it needs Python packages and model
    downloads we have not installed on this machine.
  • Only Vulkan on gfx1150 (Strix Point). Nothing on Strix Halo, ROCm, CUDA or Metal.
  • Images in a saved slot: on our production build, which carries the same change on an older base (89221a099), a
    conversation with one image saved, restored on another quant and answered a follow-up correctly (draft state
    0.3 MiB). Not repeated on this branch.

Generated with Claude Code

A slot save file holds the target context state and the context checkpoints, but not the state of
the draft context. After a restore, a draft that is fed from the target's batches (e.g. draft-mtp)
has seen none of the restored prompt, and fewer drafts are accepted for the rest of the
conversation. If the slot held another conversation before the restore, the draft context still
holds that one.

The draft state is now appended after the checkpoint appendix, the way the RAM prompt cache
already keeps it. Readers without this change ignore it, and a file without it restores with an
empty draft context. A restore drops the slot's old draft state first.

Assisted-by: Claude Code
@rcfa
rcfa force-pushed the pr/slot-draft-state branch from b714921 to f075bf6 Compare October 20, 2026 19:02
@rcfa

rcfa commented Oct 20, 2026

Copy link
Copy Markdown
Author

Rebased onto master ab06204 (after #181). Written by an AI agent (Claude Code) for @rcfa.

  • Conflict: save_slot_checkpoints() now returns bool with the byte count as an out parameter. Kept master's error path ("incomplete context checkpoints") and appended the draft state after it.
  • Adapted to master: the slot file is opened through std::filesystem::u8path() like the checkpoint appendix, and ckpt_read_buf() gets the new n_avail bound, so the draft appendix read is limited by the bytes left in the file.

Verified on gfx1150 (Radeon 890M, Vulkan), Qwen3.8-Flash-Next IQ3_XXS with MTP, master build vs this branch: pr-slotdft-test.py: 16384 tokens of a text prefilled and saved, then the text + 64 tokens -> 128 greedy tokens: on the original slot, restored on the same server, restored in a fresh server, and restored into a slot holding a longer stale conversation. Drafts accepted, master: 79% / 55% / 49% / 49%; this branch: 79% / 76% / 79% / 76%. The same 128 tokens in all 8 runs, no draft decode errors. Not verified: Strix Halo.

rcfa added a commit to rcfa/strix-llama.cpp that referenced this pull request Oct 23, 2026
…file

Master's ckpt_read_buf() takes the bytes left after the offset, as load_slot_checkpoints() does.
(folded into halo-box#171 for the PR)

Assisted-by: Claude Code
rcfa added a commit to rcfa/strix-llama.cpp that referenced this pull request Oct 27, 2026
…file

Master's ckpt_read_buf() takes the bytes left after the offset, as load_slot_checkpoints() does.
(folded into halo-box#171 for the PR)

Assisted-by: Claude Code
rcfa added a commit to rcfa/strix-llama.cpp that referenced this pull request Nov 1, 2026
…file

Master's ckpt_read_buf() takes the bytes left after the offset, as load_slot_checkpoints() does.
(folded into halo-box#171 for the PR)

Assisted-by: Claude Code

This branch has not been deployed

No deployments
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.

1 participant