Skip to content

Add --kv-eviction snapkv server flag - #1519

Closed
pierre427 wants to merge 2 commits into
ml-explore:mainfrom
pierre427:pr/snapkv-d-server
Closed

pierre427 wants to merge 2 commits into
ml-explore:mainfrom
pierre427:pr/snapkv-d-server

Conversation

@pierre427

@pierre427 pierre427 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

Exposes SnapKV-D (#1518) as an opt-in server policy: --kv-eviction snapkv compacts each full-attention layer to a KV budget after a fresh prefill, cutting long-context decode KV reads.

Stacked on the SnapKV-D cache PR (#1518). Until it merges the diff here also shows that commit; the server change is the last commit (server.py + one test).

How

  • New flags: --kv-eviction {none,snapkv} (default none), --kv-budget (default 512), --kv-window (24), --kv-sink-tokens (4), --kv-min-tokens (128).
  • On a fresh full prefill longer than --kv-min-tokens, the single-request path prefills the prompt under a SnapKVAttentionCapture, compacts every full-attention layer to the budget via evict_prompt_cache, and decodes from the last prompt token. Partial prefix-cache hits and short prompts are left uncompacted so the scored/retained positions stay exact.
  • Routing: SnapKV scores a prefill with a process-global attention hook that cannot separate per-request scores in a shared batch, so enabling it routes the model to the single-request generation path (batched continuous-batching is bypassed while it is on).

Scope

The eviction runs on the single-request path by design, not as a temporary limitation. SnapKV-D's benefit is reading physically fewer KV rows per decode step, and it compacts each request to a different retained-row set. mlx-lm's batched decode reads one shared (B, H, T, d) KV tensor with a uniform row count across the batch, so there is no way to give one request 512 rows and another 2000 in the same batch — masking evicted rows would not reduce the (memory-bound) read. So SnapKV is inherently a per-request-decode optimization and routes to the single-request path. Default-off, so no behavior changes unless the flag is set.

Test

tests/test_server_kv_eviction.py drives ResponseGenerator._maybe_snapkv_compact over a tiny attention model with a stubbed args namespace: eviction off is a passthrough; a fresh long prompt is compacted to the budget with the true offset preserved and then decodes; short prompts (≤ min_tokens) and partial prefix-cache hits are skipped. black / isort --profile black clean.

Pierre Lamy and others added 2 commits July 10, 2026 10:36
Long-context decode reads the whole KV cache every step, but after a prompt
is prefilled most middle rows contribute little to future attention. SnapKV-D
keeps attention sinks + a recent window + the top observation-window-scored
middle rows within a budget (SnapKV, arXiv:2404.14469) and evicts the rest,
cutting the per-token KV read proportionally. Opt-in; nothing runs unless a
caller compacts a cache.

Retained rows are a sparse subset of the prompt, so RoPE position and physical
row count must diverge:

- PositionPreservingKVCache tracks the true sequence position in `offset` for
  future rotations while storing only retained rows, and records each row's
  true position so a prefix trim (prompt-cache reuse) stays exact and
  speculative rollback trims only the generated suffix.
- snapkv_keep_indices computes the retained positions (sinks + recent + top
  scored) for a budget; a prompt at/under min_tokens or within budget is kept
  whole.
- evict_prompt_cache replaces full-attention KVCache layers with compact
  position-preserving caches, leaving other layer types untouched.
- SnapKVAttentionCapture scores a prefill by wrapping mx.fast SDPA and reducing
  only the observation-window query rows, so it never holds a prompt-sized
  attention matrix. compact_prompt_cache ties prefill + scoring + eviction into
  one call.

Tests cover the selection policy, the position-preserving cache (offset/stored
divergence, state/meta_state roundtrip, logical-prefix and speculative-suffix
trims, growth), eviction, and end-to-end capture + compaction + decode on a
tiny attention model. black/isort clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire SnapKV-D post-prefill eviction into the server as an opt-in policy.

- New flags: --kv-eviction {none,snapkv} (default none), --kv-budget,
  --kv-window, --kv-sink-tokens, --kv-min-tokens.
- On a fresh full prefill longer than the floor, the single-request path
  prefills the prompt under a SnapKVAttentionCapture, compacts every
  full-attention layer to the budget via evict_prompt_cache, and decodes
  from the last prompt token. Partial prefix-cache hits and short prompts
  are left uncompacted so retained positions stay exact.
- SnapKV scores a prefill with a process-global attention hook that cannot
  separate per-request scores in a shared batch, so enabling it routes the
  model to the single-request path.

Tested with a stubbed generator over a tiny attention model: off is a
passthrough, a fresh long prompt is compacted to the budget with the true
offset preserved and decodes, and short / partial-hit prompts are skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants