#[derive(Debug, Clone)]
pub struct EvictionContext<'a> {
pub positions: &'a [usize],
pub attention_scores: &'a [f32],
}
pub trait EvictionPolicy: Send + Sync {
fn score(&self, ctx: &EvictionContext<'_>) -> Vec<f32>;
fn name(&self) -> &str;
}
pub struct LruPolicy;
impl LruPolicy {
pub fn new() -> Self {
Self
}
}
impl Default for LruPolicy {
fn default() -> Self {
Self::new()
}
}
impl EvictionPolicy for LruPolicy {
fn score(&self, ctx: &EvictionContext<'_>) -> Vec<f32> {
let n = ctx.positions.len();
if n == 0 {
return Vec::new();
}
let max_pos = ctx.positions.iter().copied().max().unwrap_or(0) as f32;
let min_pos = ctx.positions.iter().copied().min().unwrap_or(0) as f32;
let range = max_pos - min_pos;
if range == 0.0 {
return vec![1.0; n];
}
ctx.positions
.iter()
.map(|&pos| (pos as f32 - min_pos) / range)
.collect()
}
fn name(&self) -> &str {
"lru"
}
}
pub struct H2oPolicy;
impl H2oPolicy {
pub fn new() -> Self {
Self
}
}
impl Default for H2oPolicy {
fn default() -> Self {
Self::new()
}
}
impl EvictionPolicy for H2oPolicy {
fn score(&self, ctx: &EvictionContext<'_>) -> Vec<f32> {
let n = ctx.attention_scores.len();
if n == 0 {
return Vec::new();
}
let max_attn = ctx
.attention_scores
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
let min_attn = ctx
.attention_scores
.iter()
.copied()
.fold(f32::INFINITY, f32::min);
let range = max_attn - min_attn;
if range == 0.0 {
return vec![1.0; n];
}
ctx.attention_scores
.iter()
.map(|&s| (s - min_attn) / range)
.collect()
}
fn name(&self) -> &str {
"h2o"
}
}
pub struct VotingAggregator {
policies: Vec<(Box<dyn EvictionPolicy>, f32)>,
}
impl VotingAggregator {
pub fn new() -> Self {
Self {
policies: Vec::new(),
}
}
pub fn add_policy(&mut self, policy: Box<dyn EvictionPolicy>, weight: f32) {
self.policies.push((policy, weight));
}
pub fn score(&self, positions: &[usize], attention_scores: &[f32]) -> Vec<f32> {
let ctx = EvictionContext {
positions,
attention_scores,
};
<Self as EvictionPolicy>::score(self, &ctx)
}
pub fn len(&self) -> usize {
self.policies.len()
}
pub fn is_empty(&self) -> bool {
self.policies.is_empty()
}
pub fn select_keep(
&self,
positions: &[usize],
attention_scores: &[f32],
budget: usize,
) -> Vec<usize> {
let scores = self.score(positions, attention_scores);
let mut indexed: Vec<(usize, f32)> = scores.into_iter().enumerate().collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
indexed
.into_iter()
.take(budget.min(positions.len()))
.map(|(idx, _)| idx)
.collect()
}
pub fn select_evict(
&self,
positions: &[usize],
attention_scores: &[f32],
count: usize,
) -> Vec<usize> {
let scores = self.score(positions, attention_scores);
let mut indexed: Vec<(usize, f32)> = scores.into_iter().enumerate().collect();
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
indexed
.into_iter()
.take(count.min(positions.len()))
.map(|(idx, _)| idx)
.collect()
}
}
impl Default for VotingAggregator {
fn default() -> Self {
Self::new()
}
}
impl EvictionPolicy for VotingAggregator {
fn score(&self, ctx: &EvictionContext<'_>) -> Vec<f32> {
let n = ctx.positions.len();
if n == 0 || self.policies.is_empty() {
return vec![0.0; n];
}
let mut combined = vec![0.0f32; n];
let total_weight: f32 = self.policies.iter().map(|(_, w)| w).sum();
if total_weight == 0.0 {
return combined;
}
for (policy, weight) in &self.policies {
let scores = policy.score(ctx);
debug_assert_eq!(
scores.len(),
n,
"policy '{}' returned wrong number of scores",
policy.name()
);
let norm_weight = weight / total_weight;
for (c, s) in combined.iter_mut().zip(scores.iter()) {
*c += s * norm_weight;
}
}
combined
}
fn name(&self) -> &str {
"voting_aggregator"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lru_scores_increase_with_position() {
let policy = LruPolicy::new();
let positions = vec![0, 5, 10, 15, 20];
let attn = vec![0.0; 5];
let ctx = EvictionContext {
positions: &positions,
attention_scores: &attn,
};
let scores = policy.score(&ctx);
assert_eq!(scores.len(), 5);
for i in 1..scores.len() {
assert!(
scores[i] > scores[i - 1],
"score[{i}] should be > score[{}]",
i - 1
);
}
assert!((scores[0] - 0.0).abs() < 1e-6);
assert!((scores[4] - 1.0).abs() < 1e-6);
}
#[test]
fn lru_equal_positions_equal_scores() {
let policy = LruPolicy::new();
let positions = vec![5, 5, 5];
let attn = vec![0.0; 3];
let ctx = EvictionContext {
positions: &positions,
attention_scores: &attn,
};
let scores = policy.score(&ctx);
assert!(scores.iter().all(|&s| (s - 1.0).abs() < 1e-6));
}
#[test]
fn h2o_scores_follow_attention() {
let policy = H2oPolicy::new();
let positions = vec![0, 1, 2, 3, 4];
let attn = vec![0.1, 0.5, 0.05, 0.3, 0.05];
let ctx = EvictionContext {
positions: &positions,
attention_scores: &attn,
};
let scores = policy.score(&ctx);
assert!(scores[1] > scores[0]);
assert!(scores[1] > scores[2]);
assert!(scores[1] > scores[3]);
assert!((scores[1] - 1.0).abs() < 1e-6); assert!((scores[2] - 0.0).abs() < 1e-6); }
#[test]
fn h2o_equal_attention_equal_scores() {
let policy = H2oPolicy::new();
let positions = vec![0, 1, 2];
let attn = vec![0.5, 0.5, 0.5];
let ctx = EvictionContext {
positions: &positions,
attention_scores: &attn,
};
let scores = policy.score(&ctx);
assert!(scores.iter().all(|&s| (s - 1.0).abs() < 1e-6));
}
#[test]
fn aggregator_combines_policies() {
let mut agg = VotingAggregator::new();
agg.add_policy(Box::new(LruPolicy::new()), 0.5);
agg.add_policy(Box::new(H2oPolicy::new()), 0.5);
let positions = vec![0, 1, 2, 3, 4];
let attn = vec![0.9, 0.1, 0.1, 0.1, 0.01];
let scores = agg.score(&positions, &attn);
assert_eq!(scores.len(), 5);
assert!((scores[0] - scores[4]).abs() < 0.15);
}
#[test]
fn aggregator_select_keep() {
let mut agg = VotingAggregator::new();
agg.add_policy(Box::new(LruPolicy::new()), 1.0);
let positions = vec![0, 1, 2, 3, 4];
let attn = vec![0.0; 5];
let keep = agg.select_keep(&positions, &attn, 3);
assert_eq!(keep.len(), 3);
assert!(keep.contains(&4));
assert!(keep.contains(&3));
assert!(keep.contains(&2));
}
#[test]
fn aggregator_select_evict() {
let mut agg = VotingAggregator::new();
agg.add_policy(Box::new(LruPolicy::new()), 1.0);
let positions = vec![0, 1, 2, 3, 4];
let attn = vec![0.0; 5];
let evict = agg.select_evict(&positions, &attn, 2);
assert_eq!(evict.len(), 2);
assert!(evict.contains(&0));
assert!(evict.contains(&1));
}
#[test]
fn empty_cache_returns_empty_scores() {
let policy = LruPolicy::new();
let ctx = EvictionContext {
positions: &[],
attention_scores: &[],
};
assert!(policy.score(&ctx).is_empty());
}
#[test]
fn aggregator_no_policies_returns_zeros() {
let agg = VotingAggregator::new();
let ctx = EvictionContext {
positions: &[0, 1, 2],
attention_scores: &[0.1, 0.2, 0.3],
};
let scores = <VotingAggregator as EvictionPolicy>::score(&agg, &ctx);
assert!(scores.iter().all(|&s| s == 0.0));
}
#[test]
fn aggregator_weighted_bias() {
let mut agg = VotingAggregator::new();
agg.add_policy(Box::new(H2oPolicy::new()), 1.0);
agg.add_policy(Box::new(LruPolicy::new()), 0.0);
let positions = vec![0, 1, 2];
let attn = vec![0.1, 0.5, 0.3];
let scores = agg.score(&positions, &attn);
let h2o = H2oPolicy::new();
let ctx = EvictionContext {
positions: &positions,
attention_scores: &attn,
};
let h2o_scores = h2o.score(&ctx);
for (a, h) in scores.iter().zip(h2o_scores.iter()) {
assert!((a - h).abs() < 1e-6, "aggregator={a}, h2o={h}");
}
}
}