session: snapshot and restore a session's decoding state (#548) - #603
Merged
Merged
Conversation
geist_session_snapshot_size / geist_session_snapshot / geist_session_restore (EXPERIMENTAL) save a session's complete decoding state into a caller buffer and put it back into the same or another session of the same loaded model. The image holds the used KV rows [0, kv_len) of every attention layer (all KV representations are position-major from offset 0, so the buffer prefix is the state), every Gated-DeltaNet layer's conv and recurrent state, the pending logits (made dense first when the spec head left them sparse), a decode step whose forward is still owed, the pinned prefix and the sampler RNG. Taking one runs no forward pass and changes nothing; a restored session continues bit-identically. That is the reusable prefix pin_prefix cannot give the qwen35 hybrid, whose reset clears the recurrence: prefill the constant context once, snapshot, then restore + prefill the new tokens per request. Restore validates before it writes: magic/version, a per-model snapshot_id, the KV representation, max_seq_len, the pending token and the exact size; anything else is GEIST_E_FORMAT with the session untouched. KIVI, an enabled MTP drafter and embedding models are GEIST_E_UNSUPPORTED. Device-local buffers (Vulkan) go through buffer_download/upload. tests/test_session_snapshot_unit covers llama and a qwen35 hybrid built in memory, every CPU backend, FP32/F16/INT8/INT4 KV, greedy and sampling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb4xVBy5t3fTdSStHPPKPg
…-xm9brj # Conflicts: # CHANGELOG.md # include/geist_arch.h # src/archs/transformer/arch.c
4 tasks 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.
Requested by Plasma · project thread
Summary
Before: a Qwen3.5 (DeltaNet hybrid) session had no reusable prefix.
pin_prefixreturnsGEIST_E_UNSUPPORTEDthere, so every request re-prefilled the whole constant context.After:
geist_session_snapshotsaves a session's complete decoding state into a caller buffer.geist_session_restoreputs it back into the same session or into another session of the same model (a fork). Prefill the constant part once, snapshot, then restore + prefill only the new tokens per request. Restored sessions continue bit-identically (tokens and logits), with greedy decoding and with sampling.How: the image holds the used KV rows
[0, kv_len)of each attention layer, the conv and recurrent state of each DeltaNet layer, the pending logits, a decode step whose forward is still owed, the pinned prefix and the sampler RNG. Every KV representation is position-major from offset 0, so only the used prefix of each buffer is copied. Device-local buffers (Vulkan) go throughbuffer_download/upload. Restore validates everything before it writes: magic/version, a per-modelsnapshot_id, the KV mode,max_seq_len, the pending token and the exact size. A bad image returnsGEIST_E_FORMATand leaves the session untouched.Closes #548.
Changes
include/geist_util.h:geist_session_snapshot_size,geist_session_snapshot,geist_session_restore(EXPERIMENTAL). Out-size-first, handle last, likepeek_logits.include/geist_arch.h: three optional decoder ops, appended so existing offsets stay put.src/archs/transformer/snapshot.c: the implementation.snapshot_idon the arch state binds an image to its model.src/engine/session.c: public wrappers. They refuse while an audio stream is open.GEIST_E_UNSUPPORTED: KIVI KV cache, an enabled MTP drafter (GEIST_MTP=1), embedding models.pin_prefixdocs point to it.Testing
make test-unit: 92 passed, 31 skipped (x86-64 AVX-512, gcc 14)make MODE=asan test-unit: samemake format-checkclean (clang-format 22.1.5);check-headersandcheck-api-contractpassmake BACKENDS="vulkan cpu_x86 cpu_scalar" test-unit FILTER=snapshoton lavapipe: the Vulkan path passes tootests/test_session_snapshot_unitbuilds a llama and a qwen35 hybrid in memory and runs on every backend in the build. It covers FP32/F16/INT8/INT4 KV, greedy and sampling. Cases: restore after a prefill into the same and another session, a second turn, restoring twice, mid-decode with an owed forward, prefix reuse across interleaved sessions, an empty image, and a pinned prefix carried by the image. It also checks refusals: short buffer, truncated or corrupted image, another model, another KV mode, too small amax_seq_len, and KIVI. Mutation-checked: dropping the DeltaNet state, the RNG or the owed-forward flag on restore fails it.No perf impact: no existing path changed. Not measured on Metal (no Mac here). The test lists
metal, so the macOS leg runs it.API impact
include/geist.hEXPERIMENTALsurface onlySTABLEsymbol🤖 Generated with Claude Code
https://claude.ai/code/session_01Nb4xVBy5t3fTdSStHPPKPg
Generated by Claude Code