Add cross-request LRU prompt cache (PromptTrie + byte-bounded eviction)#436
Open
GoodOlClint wants to merge 1 commit into
Open
Add cross-request LRU prompt cache (PromptTrie + byte-bounded eviction)#436GoodOlClint wants to merge 1 commit into
GoodOlClint wants to merge 1 commit into
Conversation
Swift only reuses a KV cache within a single `ChatSession`. A new request whose prompt shares a prefix with an earlier one re-processes that prefix from scratch. Python `mlx_lm` has had a cross-request store for this (`PromptTrie` + `LRUPromptCache` in `mlx_lm/models/cache.py`, used by `server.py`); this ports it. Adds: - `KVCache.nbytes` — byte accounting, the prerequisite for any byte-bounded policy. Every Python cache implements `nbytes`; no Swift cache did. The basis is the allocated (step-rounded) buffers from `innerState()`, not the offset-sliced `state`, matching Python. Declared as an `open var` on `BaseKVCache` rather than left to the protocol-extension default, so subclasses get a dynamically-dispatched override point. - `PromptTrie` — prefix trie returning the nearest stored sequence (exact / shorter / longer, plus the common-prefix length). - `LRUPromptCache` — the store: `fetchNearestCache` returning a deep copy and the token remainder still to process, prefix trim-reuse, `maxSize`/`maxBytes` eviction, type-aware eviction ordering (assistant -> user -> system), and `statsByType()`. The port does not inherit two bugs still open in the Python implementation, reported in ml-explore/mlx-lm#1495 and fixed there by ml-explore/mlx-lm#1496: a value stored at a single-token prefix never matched (`search` used `> 0` where `>= 0` is correct), and eviction ignored fetch recency, making the store FIFO rather than LRU. Both are pinned by regression tests. No existing behavior changes: this adds new types plus one protocol requirement with a default implementation. Nothing is wired into `ChatSession`/`Evaluate`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GoodOlClint
force-pushed
the
pr/prompt-cache-lru
branch
from
July 17, 2026 18:51
0726927 to
7283688
Compare
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.
Proposed changes
Swift only reuses a KV cache within a single
ChatSession. A new request whose prompt shares a prefix with an earlier one re-processes that prefix from scratch. Pythonmlx_lmhas had a cross-request store for exactly this —PromptTrie+LRUPromptCacheinmlx_lm/models/cache.py, used byserver.py— and this ports it to Swift.What's added
KVCache.nbytes(Libraries/MLXLMCommon/KVCache.swift) — byte accounting, the prerequisite for any byte-bounded policy. Every Python cache implementsnbytes; no Swift cache did. The basis is the allocated (step-rounded) buffers frominnerState(), not the offset-slicedstate, matching Python'sKVCache.nbytes.It is declared as an
open varonBaseKVCacherather than left to the protocol-extension default, so subclasses get a dynamically-dispatched override point. (A protocol-extension default becomes the witness atBaseKVCache's conformance and a subclass property would only shadow it statically.)PromptTrie— prefix trie returning the nearest stored sequence:exact/shorter/longer, plus the common-prefix length.LRUPromptCache— the store itself:fetchNearestCache(returns a deep copy plus the token remainder still to process), prefix trim-reuse,maxSize/maxByteseviction, type-aware eviction ordering (assistant→user→system, so system prompts survive longest), andstatsByType().Two Python bugs fixed at port time
The port deliberately does not inherit two bugs still open in the Python implementation — reported in ml-explore/mlx-lm#1495 and fixed there by ml-explore/mlx-lm#1496:
searchused> 0where>= 0is correct).Both are pinned by regression tests (
PromptTrieTests,LRUPromptCacheTests).Scope / notes
ChatSession/Evaluate— that is deliberate, to keep this reviewable on its own and to stay out of the way of Reuse cached chat transcript prefixes #370, which approaches prefix reuse at theChatSessionlevel. The two are complementary rather than competing: Reuse cached chat transcript prefixes #370 addssupportsStaticPrefixReuseand this addsnbytes; both touch only theKVCacheprotocol block, so they should merge cleanly in either order.LRUPromptCacheis a non-Sendablefinal class, mirroring Python's single-server-thread assumption. Cross-task consumers wrap it inSerialAccessContainer, asChatSessionalready does elsewhere.Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes🤖 Generated with Claude Code