Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions tests/unit/EvictionPolicyStrategy/test_eviction_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from vcache.vcache_core.cache.eviction_policy.strategies.fifo import (
FIFOEvictionPolicy,
)
from vcache.vcache_core.cache.eviction_policy.strategies.gpca import (
GPCAEvictionPolicy,
)
from vcache.vcache_core.cache.eviction_policy.strategies.lru import (
LRUEvictionPolicy,
)
Expand Down Expand Up @@ -182,6 +185,93 @@ def test_cost_aware_protects_expensive_items(self):
self.assertNotIn(2, victims)
self.assertEqual(sorted(victims), [3, 4])

def test_gpca_cold_start_protects_expensive_items(self):
"""During the cold-start grace period (no t_prime yet), GPCA should
protect an expensive item even though it has no other distinguishing
signal, instead of treating it as equally evictable as cheap items."""
policy = GPCAEvictionPolicy(self.max_size, 0.9, self.eviction_percentage)

for meta in self.metadata:
meta.t_prime = None
meta.observations = [0] * 2
meta.cost = 0.0
# Item 2 was expensive to generate, so it should be protected even
# though it has never been evaluated by the decision policy yet.
self.metadata[2].cost = 100.0

victims = policy.select_victims(self.metadata)

self.assertEqual(len(victims), self.num_to_evict)
self.assertNotIn(2, victims)
self.assertEqual(sorted(victims), [0, 1])

def test_gpca_cold_start_protects_frequently_accessed_items(self):
"""During the cold-start grace period, an item with more accumulated
observations (higher frequency so far) should be protected relative
to equally cheap, less-frequently-seen items."""
policy = GPCAEvictionPolicy(self.max_size, 0.9, self.eviction_percentage)

for meta in self.metadata:
meta.t_prime = None
meta.cost = None
meta.observations = [0] * 2
# Item 3 has been observed far more than the others.
self.metadata[3].observations = [0] * 8

victims = policy.select_victims(self.metadata)

self.assertEqual(len(victims), self.num_to_evict)
self.assertNotIn(3, victims)
self.assertEqual(sorted(victims), [0, 1])

def test_gpca_confirmed_items_blend_confidence_and_frequency(self):
"""Once t_prime is confirmed, protection should favor low t_prime
(high confidence) and high observation count (high frequency),
similar in spirit to SCU but blended rather than all-or-nothing."""
policy = GPCAEvictionPolicy(self.max_size, 0.9, self.eviction_percentage)

for meta in self.metadata:
meta.cost = 0.0
self.metadata[0].t_prime, self.metadata[0].observations = 0.9, [0] * 2
self.metadata[1].t_prime, self.metadata[1].observations = 0.9, [0] * 2
self.metadata[2].t_prime, self.metadata[2].observations = 0.1, [0] * 10
self.metadata[3].t_prime, self.metadata[3].observations = 0.9, [0] * 10
self.metadata[4].t_prime, self.metadata[4].observations = 0.5, [0] * 2

victims = policy.select_victims(self.metadata)

# Item 2 (most confident and most frequently observed) must survive;
# items 0 and 1 (least confident, least frequently observed) are the
# weakest and are evicted first.
self.assertEqual(len(victims), self.num_to_evict)
self.assertNotIn(2, victims)
self.assertEqual(sorted(victims), [0, 1])

def test_gpca_does_not_penalize_unconfirmed_expensive_item_like_scu(self):
"""The specific bug GPCA fixes: SCUEvictionPolicy always evicts a
t_prime=None item first (infinite distance), regardless of its cost.
GPCA should protect an unconfirmed item instead if it is expensive,
rather than treating "not yet evaluated" as "worst in the cache"."""
policy = GPCAEvictionPolicy(self.max_size, 0.9, self.eviction_percentage)

for meta in self.metadata:
meta.cost = 0.0
self.metadata[0].t_prime, self.metadata[0].observations = 0.9, [0] * 10
self.metadata[1].t_prime, self.metadata[1].observations = 0.9, [0] * 2
self.metadata[2].t_prime, self.metadata[2].observations = 0.1, [0] * 10
self.metadata[3].t_prime, self.metadata[3].observations = 0.1, [0] * 2
# Item 4 has never been evaluated (t_prime is None, like in
# test_scu_eviction, where this made it a guaranteed victim) but is
# expensive to regenerate.
self.metadata[4].t_prime, self.metadata[4].observations = None, [0] * 2
self.metadata[4].cost = 100.0

victims = policy.select_victims(self.metadata)

self.assertEqual(len(victims), self.num_to_evict)
self.assertNotIn(4, victims)
self.assertEqual(sorted(victims), [0, 1])


if __name__ == "__main__":
unittest.main()
28 changes: 28 additions & 0 deletions vcache/vcache_core/cache/eviction_policy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,31 @@ This distance-based utility model provides a robust, data-driven foundation for
- **Suspected Losers** (high $t'_{prime}$, low $n_{obs}$), which are treated with less prejudice, allowing them an opportunity to accumulate more data.

By aligning the eviction criteria with the statistical outputs of the caching policy, the `SkyEvictionPolicy` ensures that the most valuable and reliable information is preserved.

## Grace-Period Confidence-Cost Aware (GPCA)

SCU's ideal-point distance treats any item without a confirmed $t'_{prime}$ (fewer than 6 observations, the minimum `VerifiedDecisionPolicy` needs before trusting a logistic-regression threshold estimate) as an infinite, worst-case distance — indistinguishable from a proven bad match. This punishes expensive, infrequently-accessed items the hardest, since they are the least likely to accumulate 6 observations before eviction pressure reaches them.

`GPCAEvictionPolicy` replaces that blind spot with a grace period: while an item is unconfirmed, it is protected in proportion to its generation cost and access frequency so far, instead of being maximally evictable. Once confirmed, protection is driven by SCU-style confidence blended with frequency.

### Protection Score

For every item $i$, define protection $\text{prot}_i$ (higher = more protected, evicted last):

**Cold-start** ($t'_{prime,i}$ is unset):

$\text{prot}_i = \max(0.2,\ \text{cost}'_i,\ 0.6 \cdot \text{freq}'_i)$

**Confirmed** ($t'_{prime,i}$ is set):

$\text{prot}_i = \text{clip}\left(0.3 + 0.7 \cdot (1 - t'_{prime,i}) + 0.3 \cdot \text{freq}'_i,\ 0,\ 1\right)$

where $\text{freq}'_i$ is the item's observation count, min-max normalized across the current cache contents, and $\text{cost}'_i$ is *log-scaled* generation cost, min-max normalized. Log-scaling matters here: real generation costs are typically heavy-tailed (a handful of extreme outliers next to many ordinary items), and normalizing raw cost directly would squash nearly every ordinary item's $\text{cost}'_i$ toward 0 next to a rare outlier, making the cost term meaningless for most items.

Eviction priority is $1 - \text{prot}_i$; items with the highest priority are evicted first.

With no cost data and no confidence signal at all, protection floors to 0.2 for every item — eviction degenerates toward frequency-based behavior rather than silently becoming LRU, since GPCA has no staleness term at all by design.

### Empirical Results

Benchmarked against LRU, FIFO, SCU, and CostAware on real conversational data (LmArena): GPCA shows a substantial hit-rate and hit-precision improvement under tight-to-medium cache pressure on workloads with genuine repeated queries (up to ~6x higher hit rate than the next-best policy at 20-40MB cache budgets). This advantage is specific to that regime, not universal — it disappears once the cache is generous relative to the working set, and GPCA shows a reproducible weakness protecting infrequently-reused expensive items across long reuse gaps compared to LRU, FIFO, and CostAware. It is a targeted improvement for repeated-query-heavy, memory-constrained workloads, not a drop-in replacement for every scenario.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
CostAwareEvictionPolicy,
)
from vcache.vcache_core.cache.eviction_policy.strategies.fifo import FIFOEvictionPolicy
from vcache.vcache_core.cache.eviction_policy.strategies.gpca import GPCAEvictionPolicy
from vcache.vcache_core.cache.eviction_policy.strategies.lru import LRUEvictionPolicy
from vcache.vcache_core.cache.eviction_policy.strategies.mru import MRUEvictionPolicy
from vcache.vcache_core.cache.eviction_policy.strategies.no_eviction import (
Expand All @@ -16,4 +17,5 @@
"NoEvictionPolicy",
"SCUEvictionPolicy",
"CostAwareEvictionPolicy",
"GPCAEvictionPolicy",
]
190 changes: 190 additions & 0 deletions vcache/vcache_core/cache/eviction_policy/strategies/gpca.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import math
from typing import List, Optional, Tuple

from vcache.vcache_core.cache.embedding_store.embedding_metadata_storage.embedding_metadata_obj import (
EmbeddingMetadataObj,
)
from vcache.vcache_core.cache.eviction_policy.eviction_policy import EvictionPolicy


class GPCAEvictionPolicy(EvictionPolicy):
"""
Grace-Period Confidence-Cost Aware (GPCA) eviction policy.

Motivation: SCUEvictionPolicy needs >=6 observations on an item before
VerifiedDecisionPolicy's logistic-regression estimate is trustworthy
enough to set `t_prime`; until then `t_prime` is None. SCUEvictionPolicy
treats a None `t_prime` as an infinite (worst-case) distance -- i.e. an
item that simply hasn't had a chance to prove itself yet is evicted as
aggressively as the least useful item in the cache, indistinguishable
from a genuinely bad match. This punishes expensive, rarely-accessed
items the hardest, since they're the ones least likely to reach 6 hits
before eviction pressure hits them.

GPCA replaces that blind spot with a *grace period*: while an item is
unconfirmed (`t_prime is None`), it is protected in proportion to its
generation cost and, secondarily, its access frequency so far -- instead
of being maximally evictable. Once an item is confirmed (`t_prime` is
set), protection is driven by how reliable it is (low t_prime = high
confidence the cached response is being reused correctly) blended with
frequency, similar in spirit to SCU but no longer discarding cost or
frequency information the way pure SCU does.

For every item i, define protection_i (higher = more protected, evicted
last) as:

Cold-start (t_prime_i is None):
protection_i = max(0.2, cost'_i, 0.6 * freq'_i)

Confirmed (t_prime_i is not None):
protection_i = clip(0.3 + 0.7 * (1 - t_prime_i) + 0.3 * freq'_i, 0, 1)

where freq'_i is min-max normalized access frequency (observation count)
across the current cache contents, t_prime_i is already bounded in
[0, 1] by VerifiedDecisionPolicy, and cost'_i is min-max normalized
*log-scaled* generation cost. Log-scaling matters here: real generation
costs are typically heavy-tailed (a handful of extreme outliers next to
many ordinary items), and normalizing raw cost directly would squash
nearly every ordinary item's cost'_i down to ~0 next to a rare extreme
outlier, making the cost term meaningless for most items -- the same
issue observed empirically in CostAwareEvictionPolicy's raw min-max.

Eviction priority is priority_i = 1 - protection_i; items with the
highest priority (least protected) are evicted first.

With no cost data and no SCU signal at all, protection floors to 0.2
for every item, i.e. eviction degenerates towards recency-agnostic
frequency-based behavior rather than silently becoming LRU -- unlike
CostAwareEvictionPolicy, GPCA does not fall back to pure LRU, since it
has no staleness term at all by design (frequency + cost + confidence
only).
"""

def __init__(
self,
max_size: int,
watermark: float = 0.95,
eviction_percentage: float = 0.1,
cold_start_floor: float = 0.2,
cold_start_freq_weight: float = 0.6,
confirmed_base: float = 0.3,
confirmed_confidence_weight: float = 0.7,
confirmed_freq_weight: float = 0.3,
):
"""
Args:
max_size: The absolute maximum number of items the cache can hold.
watermark: The percentage of `max_size` that triggers eviction.
eviction_percentage: The percentage of `max_size` to evict.
cold_start_floor: Minimum protection given to every unconfirmed
item regardless of cost/frequency, in [0, 1].
cold_start_freq_weight: How much access frequency (before
confirmation) contributes to protection during the grace
period, in [0, 1].
confirmed_base: Baseline protection given to every confirmed
item regardless of confidence/frequency, in [0, 1].
confirmed_confidence_weight: How strongly SCU confidence
(1 - t_prime) contributes to protection once confirmed.
confirmed_freq_weight: How strongly access frequency contributes
to protection once confirmed.
"""
super().__init__(
max_size=max_size,
watermark=watermark,
eviction_percentage=eviction_percentage,
)
self.cold_start_floor = cold_start_floor
self.cold_start_freq_weight = cold_start_freq_weight
self.confirmed_base = confirmed_base
self.confirmed_confidence_weight = confirmed_confidence_weight
self.confirmed_freq_weight = confirmed_freq_weight

def update_eviction_metadata(self, metadata: EmbeddingMetadataObj) -> None:
"""No per-access bookkeeping needed beyond what VerifiedDecisionPolicy
and the cache already maintain (`observations`, `t_prime`, `cost`).

Args:
metadata (EmbeddingMetadataObj): The metadata object to update.
"""
pass

def select_victims(self, all_metadata: List[EmbeddingMetadataObj]) -> List[int]:
"""Selects victims using the grace-period cost/confidence/frequency blend.

Args:
all_metadata (List[EmbeddingMetadataObj]): A list of all metadata
objects in the cache.

Returns:
List[int]: A list of embedding IDs for the items to be evicted.
"""
num_to_evict: int = int(self.max_size * self.eviction_percentage)
if num_to_evict == 0 or not all_metadata:
return []

costs: List[float] = [
max(m.cost, 0.0) if m.cost is not None else 0.0 for m in all_metadata
]
log_costs: List[float] = [math.log1p(c) for c in costs]
freqs: List[float] = [float(len(m.observations)) for m in all_metadata]
norm_cost: List[float] = self._min_max_normalize(log_costs)
norm_freq: List[float] = self._min_max_normalize(freqs)

priorities: List[Tuple[int, float]] = []
for meta, cost_n, freq_n in zip(all_metadata, norm_cost, norm_freq):
t_prime: Optional[float] = meta.t_prime
if t_prime is None:
protection = max(
self.cold_start_floor,
cost_n,
self.cold_start_freq_weight * freq_n,
)
else:
protection = (
self.confirmed_base
+ self.confirmed_confidence_weight * (1 - t_prime)
+ self.confirmed_freq_weight * freq_n
)
protection = min(1.0, max(0.0, protection))

priority = 1 - protection
priorities.append((meta.embedding_id, priority))

priorities.sort(key=lambda x: x[1], reverse=True)
return [embedding_id for embedding_id, _ in priorities[:num_to_evict]]

@staticmethod
def _min_max_normalize(values: List[float]) -> List[float]:
"""Min-max normalizes a list of values to the [0, 1] range.

Args:
values (List[float]): The values to normalize.

Returns:
List[float]: The normalized values, in the same order. If all
values are equal (zero range), every value normalizes to 0.0,
since there is no variation to distinguish them by.
"""
min_value: float = min(values)
max_value: float = max(values)
value_range: float = max_value - min_value
if value_range == 0:
return [0.0 for _ in values]
return [(value - min_value) / value_range for value in values]

def __str__(self) -> str:
"""Returns a string representation of the GPCAEvictionPolicy.

Returns:
str: A string representation of the instance.
"""
return (
f"GPCAEvictionPolicy(max_size={self.max_size}, "
f"watermark={self.watermark}, "
f"eviction_percentage={self.eviction_percentage}, "
f"cold_start_floor={self.cold_start_floor}, "
f"cold_start_freq_weight={self.cold_start_freq_weight}, "
f"confirmed_base={self.confirmed_base}, "
f"confirmed_confidence_weight={self.confirmed_confidence_weight}, "
f"confirmed_freq_weight={self.confirmed_freq_weight})"
)
Loading