[kv_offload] Session Aware Eviction Policy - #50422
InbarShapira wants to merge 32 commits into
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
|
This pull request has merge conflicts that must be resolved before it can be |
900b9b7 to
7ee7570
Compare
|
Documentation preview: https://vllm--50422.org.readthedocs.build/en/50422/ |
7ee7570 to
ff7b7aa
Compare
Additional benchmark: gpt-oss-120b under GuideLLMref: #49152 Benchmark PlanGuideLLM 1. Configure the KV-offload connector. export POLICY=sae # or lru, arc
export KV_TRANSFER_CONFIG="{
\"kv_connector\": \"OffloadingConnector\",
\"kv_role\": \"kv_both\",
\"kv_connector_extra_config\": {
\"cpu_bytes_to_use\": 25769803776,
\"eviction_policy\": \"$POLICY\"
}
}"
2. Start the vLLM server. Run on: 2× A100-SXM4-80GB, 1 gpu, 8 cpu, 32GB mem, vllm serve openai/gpt-oss-120b \
--port 8000 \
--kv-transfer-config "$KV_TRANSFER_CONFIG" \
--tensor-parallel-size=2 \
--gpu-memory-utilization=0.7 \
--disable-hybrid-kv-cache-manager3. Run GuideLLM against the server. DATA='{"kind":"synthetic_text","prompt_tokens":4096,"output_tokens":512,"turns":5,"prefix_buckets":[{"bucket_weight":100,"prefix_count":256,"prefix_tokens":10000}]}'
guidellm benchmark \
--target http://localhost:8000 \
--backend "kind=openai_http,request_format=/v1/completions" \
--profile "kind=concurrent,streams=64" \
--constraint "kind=max_duration,seconds=700" \
--seed "kind=static,value=889" \
--data "$DATA" \
--output-path guidellm_${POLICY}Benchmark ResultsThroughput and TTFT
Cache effectiveness
Conclusion. vs LRU on gpt-oss-120b + GuideLLM |
Design doc for adding Session-Aware Eviction (SAE) as a third CachePolicy alongside LRU and ARC, ported from the out-of-tree sae_kv_offload plugin. Also adds four per-policy cache-effectiveness counters with a `policy` label. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
…face Drop the proposed on_lookup/on_prepare_store hooks. SAE fits the existing per-key CachePolicy surface (get/insert/remove/touch/ evict/clear/mark_evictable/mark_non_evictable) the same way LRU and ARC do, at the cost of two documented semantic differences from the v0.18 reference: sessions are reconstructed from the call sequence, and per-batch position weighting is dropped. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
…tion Task-by-task plan covering: SAECachePolicy under the existing CachePolicy interface (Tasks 1-6), registration in _CACHE_POLICIES with policy_kwargs (Task 7), four per-policy cache-effectiveness counters (Task 8), CPUOffloadingSpec validation and startup log (Task 9), doc update (Task 10), and an end-to-end smoke test (Task 11). Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Skeleton class implementing CachePolicy with construction and missing-key lookup only. Remaining methods raise NotImplementedError and will be filled in by subsequent tasks. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
First insert opens a session; consecutive inserts join it; touch/ evict/remove/clear close it. initial_hits is seeded from ghost sum incrementally per insert. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
touch bumps per-session hits and last_touch; clear resets state. Both close the currently-open session. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Every get() call adds ghost_hit_weight (resident) or ghost_miss_weight (non-resident) to _key_ghost. Every decay_interval calls, session hits and ghost scores decay by decay_factor and non-resident entries below 0.01 are pruned. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Restores the reference algorithm's is_ready check in SAECachePolicy.get(): only actually-readable resident blocks earn ghost_hit_weight; resident-but-not-ready blocks and non-resident blocks both earn ghost_miss_weight. The ghost-hit test now uses a ready block accordingly. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Tracks keys with ref_cnt == 0 in an OrderedDict for eviction candidate scans. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
evict(n, protected) runs the admission gate (returns None when the would-be new session's score is below the worst incumbent's) and otherwise walks sessions sorted by SAE's score function worst-first, yielding idle non-protected keys from each session's tail until n are collected. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
CPUOffloadingManager now accepts cache_policy="sae" and forwards policy_kwargs to the CachePolicy constructor. LRU/ARC ignore the kwargs (default empty dict). Manager also records _policy_name for downstream labelled metrics. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
CPUOffloadingManager now tallies lookups/hits/misses/evictions per call cycle and emits them via get_stats() as four labelled Prometheus counters (vllm:cpu_block_lookup_total, cpu_block_hit_total, cpu_block_miss_total, block_eviction_total), each carrying a "policy" label so all three policies (lru/arc/sae) surface uniformly on a single dashboard. HIT_PENDING counts as a hit; RETRY does not increment lookups. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
CPUOffloadingSpec now validates eviction_policy in {lru,arc,sae},
rejects sae_* keys when the active policy is not sae, extracts and
range-validates SAE tunables, and logs the active policy at INFO.
Four labelled counter definitions are added to
build_metric_definitions so the counters emitted by
CPUOffloadingManager land on /metrics with a `policy` label.
Assisted-by: Claude
Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Adds an Eviction Policy section covering "sae" as a supported kv_connector_extra_config["eviction_policy"] value alongside "lru" and "arc", its five sae_* tunables and their validation rules, and the four labelled cache-effectiveness counters emitted by all three policies (vllm:cpu_block_lookup_total, cpu_block_hit_total, cpu_block_miss_total, block_eviction_total). Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Constructs CPUOffloadingSpec with eviction_policy=sae, retrieves the manager (verifying the policy is SAECachePolicy with the configured decay_interval), issues one lookup, and confirms the four labelled counters land on the stats payload with the "sae" policy label. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Audit against the reference sae_kv_offload plugin identified three unintended divergences beyond the two documented adaptations (session-boundary reconstruction, position-weight drop). Fixed: 1. touch() bumped hits once per key rather than once per unique session. Now builds the touched-session set first and bumps each session at most once, matching manager.py:184-193. 2. Session hits accumulated as float indefinitely because decay dropped the int() cast. _run_decay now truncates hits per manager.py:161; a new _seal_open_session helper truncates hits at every session close point (touch / evict / remove). 3. Admission gate blended ghost-derived freq_bonus into the new-session score. Reference gate is bare `logical_timer + pos_bonus` and explicitly excludes ghost scores (manager.py:219-224). _admission_gate_allows now matches. Also drops the unused `protected` parameter since ghost sums are no longer needed at the gate. Tests updated: touch assertion now expects hits==1 for two keys of the same session; three new tests lock in decay truncation, session-close truncation, and gate-ignores-ghost. All 30 SAE tests + full manager regression pass. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
…hm flow Mirrors the structured docstring style used by ARCCachePolicy — Data Structures, Algorithm Flow (one section per method), Session Score formula, Tunables, and Semantic differences from the reference. Also folds in the third semantic difference documented in yesterday's design-doc update (start_pos always zero) so the in-code and out-of-code descriptions of SAE stay aligned. Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Captures the state a fresh Claude Code session needs to resume: - What's complete (18 commits, 11 plan tasks + parity fixes) - Test-run command and expected result (69 pass) - The three intentional + three fixed unintended semantic differences from the reference algorithm - The paused benchmark-harness brainstorm — all decisions locked in so far (scope, drivers, workloads, metrics, location, reporting, e2e target), remaining open questions, and the next-action list - The server-startup smoke-test command - Environment reminders from AGENTS.md - The pending fork URL and PR-open URL Assisted-by: Claude Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com>
Signed-off-by: <>
Signed-off-by: <>
Signed-off-by: <>
Reorganize the SAE class docstring: motivation first, then Data Structures / Session Score / Algorithm Flow / Tunables. Score-input fields (hits, last_touch, prefix_depth) now live with the formula they drive; Data Structures keeps only the load-bearing state. Algorithm Flow's four hooks read as a state machine, with the load-bearing invariants preserved (record_lookup is separate from get, admission gate is fresh-only, evict is atomic all-or-nothing). Also correct four small inaccuracies from the previous version: last_touch is set, not incremented; evictable_blocks is used as a set, not an OrderedDict; prefix_depth is a count of already-cached batch keys, not "preceding" the first new key; add the "one touch = one hit per session" invariant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: <>
Signed-off-by: <>
All assertions are already covered by test_spec_config_validation.py (eviction_policy, get_manager returns SAECachePolicy, policy_kwargs propagation) and test_manager_policy_metrics.py (per-policy counter labels). The smoke test also builds a full VllmConfig with a real ModelConfig, which is disproportionately expensive for the coverage it adds. Signed-off-by: <>
Commit d7bc5d727 removed the 'policy' label from the four CPU cache
effectiveness counters (both the definitions and the emit sites), but
missed the tests. Update:
- test_stats_emit_four_counters_with_policy_label ->
test_stats_emit_four_counters (and 3 other tests in that file):
index the stats dict under () instead of (policy,) since increase_counter
is now called without labelvalues.
- test_build_metric_definitions_includes_four_labelled_counters ->
test_build_metric_definitions_includes_four_counters:
assert labelnames == () instead of ('policy',).
The parametrization over lru/arc/sae is kept — it still catches
per-policy regressions in the manager's counting logic, even though the
Prometheus label is gone.
Signed-off-by: <>
…sion The parametrization over lru/arc/sae is not applied to test_already_stored_block_not_evicted_during_prepare_store because SAE's admission gate returns None from prepare_store in this scenario (the incumbent session holding [1,2] outscores the baseline of a new session for [3,4,5]). The equivalent protected-key contract is covered directly at the policy layer by tests/v1/kv_offload/cpu/policies/test_sae_policy.py:: test_evict_skips_protected_keys. Adding a comment so future maintainers don't try to extend the parametrization without accounting for the semantic difference. Signed-off-by: <>
Two defensive fixes uncovered during code review: - insert(): assert the key isn't already owned by a session. The manager filters already-stored keys via get() before its insert loop, so a resident key reaching insert() would silently overwrite key_to_session and leave a dangling entry in the old session's key list. Fail loudly instead. - remove(): nest the "session emptied" cleanup inside the keys-is-not-None branch. The previous `if not keys:` also fired when keys was None; pop(..., None) made it safe today but obscured intent. Signed-off-by: <>
…nverter Historically content_is_valid() kept only conversations containing at least one non-ASCII byte (via has_non_english_chars). Expose that as an explicit --exclude-non-english / --no-exclude-non-english flag on the converter so pure-English conversations can be kept when desired, and default it to True to preserve the prior behavior. Signed-off-by: <>
…ig ctor The CPUOffloadingSpec constructor now takes a single OffloadingConfig argument (upstream refactor); these tests were still building VllmConfig + KVCacheConfig and passing them positionally, causing every case in test_spec_config_validation.py to fail with: TypeError: CPUOffloadingSpec.__init__() takes 2 positional arguments but 3 were given Replace the two old helpers with one _make_offloading_config helper that mirrors tests/v1/kv_offload/test_factory.py, update all eight call sites, and drop the now-unused VllmConfig / KVCacheConfig imports. Behaviour under test is unchanged (unknown policy raises, sae-key-under- non-sae raises, tunable range validation, kwargs storage, get_manager returns SAE, default is LRU, metric definitions present). Signed-off-by: Inbar Shapira <inbar.shapira@ibm.com> Signed-off-by: <>
Upstream introduced CachePolicyFactory (dc1be79) which resolves cache policies by name and calls `policy_cls(cache_capacity=num_blocks)` directly, replacing the old spec->manager->policy `policy_kwargs` plumbing this branch had added. SAE now uses its defaults (decay_interval=500, decay_factor=0.9, ghost_hit_weight=12.0, ghost_miss_weight=1.0, ghost_norm=12.0) — the sae_* extra_config keys and their validation have been removed. This drops the tests that were exercising the now-removed plumbing: - test_spec_config_validation.py: keep only the "unknown policy raises", "get_manager returns SAE for eviction_policy=sae", and "default is lru" cases; drop all sae_* range-validation tests. - test_sae_policy.py: replace test_cpu_offloading_manager_accepts_sae_ policy_and_kwargs (which passed policy_kwargs=) with a simpler test_cpu_offloading_manager_accepts_sae_policy that mirrors the lru-default test. Behaviour under test for what remains is unchanged. Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com> Signed-off-by: <>
The rebase onto upstream dropped four CPU-tier counters (lookup, hit, miss, block_eviction) along with the per-policy label that upstream had never accepted. Restore the counters themselves so the multi-turn benchmark can keep reporting hit-rate/eviction pressure alongside throughput and TTFT -- but keep them unlabelled per the earlier "drop policy label" decision (bench measures one policy per run, so the label just adds cardinality without informing any real query). - common.py: re-add CPU_BLOCK_LOOKUP/HIT/MISS/BLOCK_EVICTION enum entries under the vllm:kv_offload_* namespace (aligns with the existing STORES_SKIPPED / CPU_CACHE_USAGE_PERC naming convention). - spec.py: re-add the four OffloadingCounterMetadata definitions in build_metric_definitions, with no labelnames. - manager.py: re-add the delta counters (_lookups_delta, _hits_delta, _misses_delta, _evictions_delta) and their get_stats() emit, no labelvalues. - test_manager_policy_metrics.py: restore the file (dropped in the rebase); tests already assert the unlabelled shape (()) so no edits needed. - test_spec_config_validation.py: restore the test_build_metric_definitions_includes_four_counters case. All 504 kv_offload tests pass (7 skipped, 0 failed). Signed-off-by: Inbar Shapira <inbar_shapira@il.ibm.com> Signed-off-by: <>
ff7b7aa to
d6857de
Compare
|
@InbarShapira Thanks for this contribution. As for the value presented, I see modest improvements in a shareGPT based workload and more significant improvement in a synthetic multi-turn conversation workload. Questions about this:
|
|
I agree with @dannyharnik, |
It different - This method was devised using SWE-smith multi-turn coding workload (245 requests · 12 agents) via
I think its due to the nature of the workload |
|
This pull request has merge conflicts that must be resolved before it can be |
Purpose
Current vLLM v1 CPU KV-offload eviction policies (LRU, ARC) treat blocks as independent items. But a conversation's KV blocks are only useful as a whole chain — evicting one block in the middle forces recomputation of everything after it. SAE (Session-Aware Eviction) groups blocks stored by the same request into a session and evicts session-worst-first, tail-first, so shared prefixes outlive their suffixes. This targets the multi-turn / long-context workloads where cross-turn KV reuse matters and mid-chain evictions are the dominant TTFT tail source.
Benchmark Plan
Multi-turn benchmark exercising cross-turn KV reuse.
1. Build the workload. Resample 500 conversations with ≥ 6 turns from ShareGPT V3, seed pinned for reproducibility.
Source dataset:
Resample to the benchmark workload:
2. Configure the KV-offload connector.
cpu_bytes_to_use=27917287424= 26 GiBeviction_policyselects the CPU-tier policy under test.3. Start the vLLM server.
Run on: A100-SXM4-80GB, 1 gpu, 8 cpu, 32GB mem
vllm serve NousResearch/Hermes-3-Llama-3.1-8B \ --port 8000 \ --kv-transfer-config "$KV_TRANSFER_CONFIG" \ --gpu-memory-utilization=0.5 \ --disable-hybrid-kv-cache-manager4. Run the multi-turn benchmark against the server.
python benchmarks/multi_turn/benchmark_serving_multi_turn.py \ --url http://localhost:8000 \ --model NousResearch/Hermes-3-Llama-3.1-8B \ --input-file sharegpt-full.json \ --num-clients=4 \ --max-active-conversations=128 \ --output-file multi_turn_${POLICY}.jsonBenchmark Results
Throughput and TTFT
Cache effectiveness
Conclusion. vs LRU on ShareGPT V3 multi-turn: p95 TTFT −26.5%, p99 −26.8%, hit rate 87.6% → 97.7%, evictions 6.1× lower, +4.1% output throughput — whole-session-tail eviction holds shared prefixes across turns, so tail-latency users speed up, the cache stops thrashing, and throughput edges up rather than trading off.
Test Plan
Test Result
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.