diff --git a/tests/unit/EvictionPolicyStrategy/test_eviction_policy.py b/tests/unit/EvictionPolicyStrategy/test_eviction_policy.py index c369291..b3a2ccf 100644 --- a/tests/unit/EvictionPolicyStrategy/test_eviction_policy.py +++ b/tests/unit/EvictionPolicyStrategy/test_eviction_policy.py @@ -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, ) @@ -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() diff --git a/vcache/vcache_core/cache/eviction_policy/README.md b/vcache/vcache_core/cache/eviction_policy/README.md index 685a81f..02e8c8d 100644 --- a/vcache/vcache_core/cache/eviction_policy/README.md +++ b/vcache/vcache_core/cache/eviction_policy/README.md @@ -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. diff --git a/vcache/vcache_core/cache/eviction_policy/strategies/__init__.py b/vcache/vcache_core/cache/eviction_policy/strategies/__init__.py index 32877cd..dcc382a 100644 --- a/vcache/vcache_core/cache/eviction_policy/strategies/__init__.py +++ b/vcache/vcache_core/cache/eviction_policy/strategies/__init__.py @@ -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 ( @@ -16,4 +17,5 @@ "NoEvictionPolicy", "SCUEvictionPolicy", "CostAwareEvictionPolicy", + "GPCAEvictionPolicy", ] diff --git a/vcache/vcache_core/cache/eviction_policy/strategies/gpca.py b/vcache/vcache_core/cache/eviction_policy/strategies/gpca.py new file mode 100644 index 0000000..af22221 --- /dev/null +++ b/vcache/vcache_core/cache/eviction_policy/strategies/gpca.py @@ -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})" + )