Skip to content

Make RotatingKVCache trimmable so prefix cache reuse works for sliding-window models#1437

Open
amirarsalan90 wants to merge 1 commit into
ml-explore:mainfrom
amirarsalan90:fix/rotating-kv-prefix-reuse
Open

Make RotatingKVCache trimmable so prefix cache reuse works for sliding-window models#1437
amirarsalan90 wants to merge 1 commit into
ml-explore:mainfrom
amirarsalan90:fix/rotating-kv-prefix-reuse

Conversation

@amirarsalan90

Copy link
Copy Markdown

Summary

mlx-lm's automatic prompt-cache prefix reuse works for full-attention models but silently fails for sliding-window-attention models (Gemma 2/3/3n, GPT-OSS) once the prompt exceeds the attention window — every request reprocesses the whole prompt.

Root cause: RotatingKVCache.is_trimmable() returned False once its ring buffer wrapped. Since sliding-window models interleave layer types (e.g. Gemma 3 mixes RotatingKVCache with a full KVCache every 6th layer), the all-or-nothing can_trim_prompt_cache() gate then returned False for the entire model, so the server discarded the prompt cache and recomputed from scratch.

Fix

  • Split RotatingKVCache's overloaded offset into two fields: offset (absolute chronological position, drives RoPE) and _offset (physical tokens resident in the ring buffer, capped at max_size). They diverge only after a trim; pre-wrap behavior is byte-identical. This mirrors the design BatchRotatingKVCache already uses.
  • Implement a correct trim() for the wrapped case by linearizing the ring buffer into temporal order, bounded by max_size (older tokens are physically overwritten on wrap, so reuse is bounded by the window).
  • Make the server reuse/eviction paths window-aware: fetch_nearest_cache only reuses a trimmed entry if the trim fully succeeded; insert_cache keeps prefixes older than the window as fallbacks; trim_prompt_cache returns the min trimmed across layers.

Scope: RotatingKVCache + BatchRotatingKVCache. SSM/Mamba (ArraysCache) is out of scope — recurrent state can't be split at a token boundary.

Results

gemma-3-1b-it-4bit, 2802-token prompt ([long fixed prefix] + [short changing tail]), sliding window W=512:

Before After
Warm cached_tokens 0 2788
Cold latency 1.24 s 1.24 s
Warm latency (median) ~0.84 s 0.15 s
Speedup 1.1× (none) ~8×

Greedy (temperature=0) warm output is identical to cold.

Tests

  • RotatingKVCache / BatchRotatingKVCache now report trimmable after wrap and trim correctly back within the window.
  • Added unit tests that a wrapped trim matches a fresh prefix build (state-level and through-model argmax identity), plus _offset save/load round-trip and backward-compat loading of the old 4-value meta_state.

python -m unittest tests.test_prompt_cache tests.test_server tests.test_generate passes.

Refs #980.

…g-window models

RotatingKVCache reported itself non-trimmable once its ring buffer wrapped,
which made can_trim_prompt_cache() return False for the whole interleaved
cache of sliding-window models (Gemma 3, GPT-OSS). The server then discarded
the prompt cache and recomputed the full prompt on every request.

Split the rotating cache's chronological position (offset, drives RoPE) from
its physical fill (_offset), implement a correct trim() for the wrapped case
by linearizing the ring buffer, and make the server's reuse/eviction paths
aware that a rotating cache can only be trimmed back within its window.

Refs ml-explore#980.
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