Skip to content

lean: save and restore the KV cache of a prompt prefix - #10

Merged
ilnmtlbnm merged 10 commits into
mainfrom
kv-prefix-image
Oct 23, 2026
Merged

ilnmtlbnm merged 10 commits into
mainfrom
kv-prefix-image

Conversation

@ilnmtlbnm

Copy link
Copy Markdown
Member

Save the KV cache of a prompt prefix to bytes and restore it later, so a long fixed prefix (a tools prompt, a system prompt) is computed once and then loaded instead of recomputed.

API (Rust and JS)

  • prefill(text): run the prompt through the model without generating; returns the token count.
  • prefix_key(text): the key of the image this engine and model would write for that prefix.
  • save_prefix(text, write) / restore_prefix(text, read): stream the image one tensor at a time; F32 only.
  • Errors: LeanError::PrefixImage(PrefixImageError); a refused image means recompute and overwrite, never load.

Invalidation

  • The key is a hash of the header: format and dtype, shape, backend, the GGUF length and header bytes, the tokenizer, an engine build fingerprint (all lean and lean-core sources and shaders, Cargo.lock, toolchain, target) and the prefix token ids. A restore checks every field before touching any state.
  • Storage and eviction stay with the application.

Also

  • One GGUF header parser; the KV snapshot and restore paths share the new prefix read and write.

Tests

  • Restore then generate equals a fresh engine's greedy reply, and the KV cache after the reply is byte-identical, for Qwen2.5-0.5B and Qwen3-1.7B: native Metal and Vulkan, CPU threads, headless Chromium on WebGPU and single-thread CPU.
  • A changed engine, a changed prefix and a truncated image are refused.
  • cargo test -p lean-core -p lean -p lean-chat --release and the CI clippy commands pass.

KvCache and CpuKvCache gain read_prefix/write_prefix: one layer's K or V
for positions [0, n) as [kv_heads, n, head_dim] f32 bytes, the same bytes
on both backends. The GPU read copies only the n positions of each head
into one staging buffer (Engine::read_ranges); the write uses
queue.write_buffer and submits per call so staged copies do not pile up.
gguf_header_len gives the offset where tensor data starts.
Lean::prefill(text) makes text resident without generating and returns
its token count. save_prefix writes the KV cache of a prefix as a 128-byte
header plus each layer's K and V in f32, one tensor per write;
restore_prefix reads it back one tensor at a time into a fresh engine, and
the next generate prefills only what follows the prefix. prefix_key hashes
the format version, dtype, engine build (a build-time hash of the lean and
lean-core sources and Cargo.lock), the GGUF header and length, the
tokenizer file, the backend and the prefix token ids. A restore checks the
same fields and refuses a mismatch with LeanError::PrefixImage, leaving the
engine usable. Exported to JS as prefill, prefixKey, savePrefix and
restorePrefix.
Native test on the tools prompt of 02_tools_single: the reply after a
restore equals a fresh engine's, the KV cache after the reply is
bit-identical to the writer's, and images for another engine build,
another prefix or cut short are refused. The bench worker's prefix phase
runs the same checks in a browser with the image stored in OPFS.
The full header parse checks the remaining file size before every string,
three seeks that empty the read buffer each time, and took about 470 ms
on a Qwen GGUF. The scan skips every value through one 1 MiB buffer and
takes about 4 ms; an ignored test checks it ends where the parse does.
A native build and a wasm build of the same sources run different CPU
kernels, so the target architecture and features are hashed too, with
rust-toolchain.toml.
…der's header length

Drop the scanning header_len and its re-export. GgufReader exposes the
data section offset it already computes, both models keep it, and the
fingerprint hashes that many bytes after the load, from one more open of
the source.
One per-head loop owns the KV layout. The snapshot now reads only the
prefix ranges instead of the whole max_ctx buffer.
README: restorePrefix and its read(n) are synchronous, so a browser
reads the image from a Web Worker through an OPFS sync access handle.
@ilnmtlbnm
ilnmtlbnm merged commit 4ce38c4 into main Oct 23, 2026
2 checks passed
@ilnmtlbnm
ilnmtlbnm deleted the kv-prefix-image branch October 23, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant