1use crate::store::{BlockKey, BlockMeta, Tier};
20use crate::tiering::TierConfig;
21use std::collections::HashMap;
22
23#[derive(Clone, Debug)]
29pub struct PatternVector {
30 pub key: BlockKey,
32 pub embedding: Vec<f32>,
34 pub score: f32,
36}
37
38pub trait PatternIndex {
47 fn insert(&mut self, vec: &PatternVector);
49
50 fn search_nearest(&self, query: &[f32], k: usize) -> Vec<(BlockKey, f32)>;
53
54 fn remove(&mut self, key: BlockKey);
56
57 fn len(&self) -> usize;
59
60 fn is_empty(&self) -> bool {
62 self.len() == 0
63 }
64}
65
66fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
74 if a.len() != b.len() || a.is_empty() {
75 return 0.0;
76 }
77
78 let mut dot = 0.0f32;
79 let mut norm_a_sq = 0.0f32;
80 let mut norm_b_sq = 0.0f32;
81
82 for (&x, &y) in a.iter().zip(b.iter()) {
83 dot += x * y;
84 norm_a_sq += x * x;
85 norm_b_sq += y * y;
86 }
87
88 let denom = norm_a_sq.sqrt() * norm_b_sq.sqrt();
89 if denom == 0.0 {
90 0.0
91 } else {
92 dot / denom
93 }
94}
95
96pub struct InMemoryPatternIndex {
106 vectors: Vec<PatternVector>,
107}
108
109impl InMemoryPatternIndex {
110 pub fn new() -> Self {
112 Self {
113 vectors: Vec::new(),
114 }
115 }
116}
117
118impl Default for InMemoryPatternIndex {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl PatternIndex for InMemoryPatternIndex {
125 fn insert(&mut self, vec: &PatternVector) {
126 self.vectors.retain(|v| v.key != vec.key);
128 self.vectors.push(vec.clone());
129 }
130
131 fn search_nearest(&self, query: &[f32], k: usize) -> Vec<(BlockKey, f32)> {
132 if k == 0 || self.vectors.is_empty() {
133 return Vec::new();
134 }
135
136 let mut scored: Vec<(BlockKey, f32)> = self
137 .vectors
138 .iter()
139 .map(|v| (v.key, cosine_similarity(query, &v.embedding)))
140 .collect();
141
142 scored.sort_by(|a, b| {
144 b.1.partial_cmp(&a.1)
145 .unwrap_or(core::cmp::Ordering::Equal)
146 });
147 scored.truncate(k);
148 scored
149 }
150
151 fn remove(&mut self, key: BlockKey) {
152 self.vectors.retain(|v| v.key != key);
153 }
154
155 fn len(&self) -> usize {
156 self.vectors.len()
157 }
158}
159
160pub fn pattern_from_meta(meta: &BlockMeta) -> Vec<f32> {
176 let ema = meta.ema_rate.clamp(0.0, 1.0);
177 let pop = meta.window.count_ones() as f32 / 64.0;
178 let recency = 1.0 / (1.0 + meta.tier_age as f32);
179 let count_log = ((1.0 + meta.access_count as f32).log2() / 32.0).clamp(0.0, 1.0);
180
181 vec![ema, pop, recency, count_log]
182}
183
184pub struct AdaptiveTiering<I: PatternIndex> {
201 pub index: I,
203 pub config: TierConfig,
205 block_tiers: HashMap<BlockKey, Tier>,
207}
208
209impl<I: PatternIndex> AdaptiveTiering<I> {
210 pub fn new(index: I, config: TierConfig) -> Self {
212 Self {
213 index,
214 config,
215 block_tiers: HashMap::new(),
216 }
217 }
218
219 pub fn register_block(&mut self, key: BlockKey, tier: Tier) {
225 self.block_tiers.insert(key, tier);
226 }
227
228 pub fn remove_block(&mut self, key: BlockKey) {
230 self.block_tiers.remove(&key);
231 self.index.remove(key);
232 }
233
234 pub fn registered_count(&self) -> usize {
236 self.block_tiers.len()
237 }
238
239 pub fn suggest_tier(
252 &self,
253 meta: &BlockMeta,
254 neighbors: &[(BlockKey, f32)],
255 ) -> Option<Tier> {
256 if neighbors.is_empty() {
257 return None;
258 }
259
260 let mut votes = [0.0f32; 4];
263 let mut total_weight = 0.0f32;
264
265 for &(key, similarity) in neighbors {
266 if let Some(&tier) = self.block_tiers.get(&key) {
267 let weight = similarity.max(0.0);
268 votes[tier as u8 as usize] += weight;
269 total_weight += weight;
270 }
271 }
272
273 if total_weight == 0.0 {
274 return None;
275 }
276
277 let mut best_idx = 0usize;
280 let mut best_vote = votes[0];
281 for i in 1..4 {
282 if votes[i] > best_vote {
283 best_vote = votes[i];
284 best_idx = i;
285 }
286 }
287
288 let suggested = match best_idx {
289 0 => Tier::Tier0,
290 1 => Tier::Tier1,
291 2 => Tier::Tier2,
292 3 => Tier::Tier3,
293 _ => unreachable!(),
294 };
295
296 if suggested == meta.tier {
297 None
298 } else {
299 Some(suggested)
300 }
301 }
302}
303
304#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::store::{DType, ReconstructPolicy};
312
313 fn make_key(tid: u128, idx: u32) -> BlockKey {
314 BlockKey {
315 tensor_id: tid,
316 block_index: idx,
317 }
318 }
319
320 fn make_store_meta(
321 key: BlockKey,
322 tier: Tier,
323 ema_rate: f32,
324 window: u64,
325 access_count: u32,
326 tier_age: u32,
327 ) -> BlockMeta {
328 BlockMeta {
329 key,
330 dtype: DType::F32,
331 tier,
332 bits: 8,
333 scale: 1.0,
334 zero_point: 0,
335 created_at: 0,
336 last_access_at: 100,
337 access_count,
338 ema_rate,
339 window,
340 checksum: 0,
341 reconstruct: ReconstructPolicy::None,
342 tier_age,
343 lineage_parent: None,
344 block_bytes: 1024,
345 }
346 }
347
348 #[test]
351 fn cosine_identical_vectors() {
352 let v = vec![1.0, 2.0, 3.0, 4.0];
353 let sim = cosine_similarity(&v, &v);
354 assert!((sim - 1.0).abs() < 1e-6, "sim={sim}");
355 }
356
357 #[test]
358 fn cosine_orthogonal_vectors() {
359 let a = vec![1.0, 0.0];
360 let b = vec![0.0, 1.0];
361 let sim = cosine_similarity(&a, &b);
362 assert!(sim.abs() < 1e-6, "sim={sim}");
363 }
364
365 #[test]
366 fn cosine_opposite_vectors() {
367 let a = vec![1.0, 0.0, 0.0];
368 let b = vec![-1.0, 0.0, 0.0];
369 let sim = cosine_similarity(&a, &b);
370 assert!((sim - (-1.0)).abs() < 1e-6, "sim={sim}");
371 }
372
373 #[test]
374 fn cosine_zero_vector() {
375 let a = vec![1.0, 2.0];
376 let b = vec![0.0, 0.0];
377 assert_eq!(cosine_similarity(&a, &b), 0.0);
378 }
379
380 #[test]
381 fn cosine_different_lengths() {
382 let a = vec![1.0, 2.0];
383 let b = vec![1.0, 2.0, 3.0];
384 assert_eq!(cosine_similarity(&a, &b), 0.0);
385 }
386
387 #[test]
388 fn cosine_empty() {
389 let a: Vec<f32> = vec![];
390 let b: Vec<f32> = vec![];
391 assert_eq!(cosine_similarity(&a, &b), 0.0);
392 }
393
394 #[test]
395 fn cosine_known_value() {
396 let a = vec![1.0, 1.0];
398 let b = vec![1.0, 0.0];
399 let sim = cosine_similarity(&a, &b);
400 let expected = 1.0 / 2.0f32.sqrt();
401 assert!((sim - expected).abs() < 1e-6, "sim={sim}, expected={expected}");
402 }
403
404 #[test]
407 fn index_insert_and_len() {
408 let mut idx = InMemoryPatternIndex::new();
409 assert!(idx.is_empty());
410
411 idx.insert(&PatternVector {
412 key: make_key(1, 0),
413 embedding: vec![1.0, 0.0, 0.0, 0.0],
414 score: 0.5,
415 });
416 assert_eq!(idx.len(), 1);
417 assert!(!idx.is_empty());
418 }
419
420 #[test]
421 fn index_insert_replaces_duplicate_key() {
422 let mut idx = InMemoryPatternIndex::new();
423 let key = make_key(1, 0);
424
425 idx.insert(&PatternVector {
426 key,
427 embedding: vec![1.0, 0.0, 0.0, 0.0],
428 score: 0.5,
429 });
430 idx.insert(&PatternVector {
431 key,
432 embedding: vec![0.0, 1.0, 0.0, 0.0],
433 score: 0.8,
434 });
435
436 assert_eq!(idx.len(), 1);
437
438 let results = idx.search_nearest(&[0.0, 1.0, 0.0, 0.0], 1);
440 assert_eq!(results.len(), 1);
441 assert_eq!(results[0].0, key);
442 assert!((results[0].1 - 1.0).abs() < 1e-6);
444 }
445
446 #[test]
447 fn index_remove() {
448 let mut idx = InMemoryPatternIndex::new();
449 let key = make_key(1, 0);
450
451 idx.insert(&PatternVector {
452 key,
453 embedding: vec![1.0, 0.0, 0.0, 0.0],
454 score: 0.5,
455 });
456 assert_eq!(idx.len(), 1);
457
458 idx.remove(key);
459 assert_eq!(idx.len(), 0);
460 }
461
462 #[test]
463 fn index_remove_nonexistent() {
464 let mut idx = InMemoryPatternIndex::new();
465 idx.remove(make_key(99, 0)); assert_eq!(idx.len(), 0);
467 }
468
469 #[test]
470 fn index_search_nearest_ordering() {
471 let mut idx = InMemoryPatternIndex::new();
472
473 idx.insert(&PatternVector {
475 key: make_key(1, 0),
476 embedding: vec![1.0, 0.0, 0.0, 0.0],
477 score: 0.0,
478 });
479 idx.insert(&PatternVector {
480 key: make_key(2, 0),
481 embedding: vec![0.7, 0.7, 0.0, 0.0],
482 score: 0.0,
483 });
484 idx.insert(&PatternVector {
485 key: make_key(3, 0),
486 embedding: vec![0.0, 1.0, 0.0, 0.0],
487 score: 0.0,
488 });
489
490 let results = idx.search_nearest(&[1.0, 0.1, 0.0, 0.0], 3);
492 assert_eq!(results.len(), 3);
493
494 assert_eq!(results[0].0, make_key(1, 0));
496 assert_eq!(results[1].0, make_key(2, 0));
498 assert_eq!(results[2].0, make_key(3, 0));
500
501 assert!(results[0].1 >= results[1].1);
503 assert!(results[1].1 >= results[2].1);
504 }
505
506 #[test]
507 fn index_search_nearest_k_larger_than_size() {
508 let mut idx = InMemoryPatternIndex::new();
509 idx.insert(&PatternVector {
510 key: make_key(1, 0),
511 embedding: vec![1.0, 0.0],
512 score: 0.0,
513 });
514
515 let results = idx.search_nearest(&[1.0, 0.0], 10);
516 assert_eq!(results.len(), 1);
517 }
518
519 #[test]
520 fn index_search_nearest_k_zero() {
521 let mut idx = InMemoryPatternIndex::new();
522 idx.insert(&PatternVector {
523 key: make_key(1, 0),
524 embedding: vec![1.0],
525 score: 0.0,
526 });
527
528 let results = idx.search_nearest(&[1.0], 0);
529 assert!(results.is_empty());
530 }
531
532 #[test]
533 fn index_search_nearest_empty() {
534 let idx = InMemoryPatternIndex::new();
535 let results = idx.search_nearest(&[1.0, 0.0], 5);
536 assert!(results.is_empty());
537 }
538
539 #[test]
542 fn pattern_from_meta_dimensions() {
543 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0xFFFF, 100, 10);
544 let pat = pattern_from_meta(&meta);
545 assert_eq!(pat.len(), 4);
546 }
547
548 #[test]
549 fn pattern_from_meta_ema_component() {
550 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.8, 0, 0, 0);
551 let pat = pattern_from_meta(&meta);
552 assert!((pat[0] - 0.8).abs() < 1e-6, "ema={}", pat[0]);
553 }
554
555 #[test]
556 fn pattern_from_meta_popcount_component() {
557 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, u64::MAX, 0, 0);
559 let pat = pattern_from_meta(&meta);
560 assert!((pat[1] - 1.0).abs() < 1e-6, "pop={}", pat[1]);
561
562 let meta2 = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0, 0, 0);
564 let pat2 = pattern_from_meta(&meta2);
565 assert!((pat2[1]).abs() < 1e-6, "pop={}", pat2[1]);
566
567 let meta3 = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0xFFFF_FFFF, 0, 0);
569 let pat3 = pattern_from_meta(&meta3);
570 assert!((pat3[1] - 0.5).abs() < 1e-6, "pop={}", pat3[1]);
571 }
572
573 #[test]
574 fn pattern_from_meta_recency_component() {
575 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0, 0, 0);
577 let pat = pattern_from_meta(&meta);
578 assert!((pat[2] - 1.0).abs() < 1e-6, "recency={}", pat[2]);
579
580 let meta2 = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0, 0, 9);
582 let pat2 = pattern_from_meta(&meta2);
583 assert!((pat2[2] - 0.1).abs() < 1e-6, "recency={}", pat2[2]);
584 }
585
586 #[test]
587 fn pattern_from_meta_access_count_log_component() {
588 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0, 0, 0);
590 let pat = pattern_from_meta(&meta);
591 assert!(pat[3].abs() < 1e-6, "count_log={}", pat[3]);
592
593 let meta2 = make_store_meta(make_key(1, 0), Tier::Tier1, 0.0, 0, 1, 0);
595 let pat2 = pattern_from_meta(&meta2);
596 assert!((pat2[3] - 1.0 / 32.0).abs() < 1e-4, "count_log={}", pat2[3]);
597 }
598
599 #[test]
600 fn pattern_from_meta_values_in_unit_range() {
601 let meta = make_store_meta(
603 make_key(1, 0),
604 Tier::Tier1,
605 2.0, u64::MAX, u32::MAX, u32::MAX, );
610 let pat = pattern_from_meta(&meta);
611 for (i, &v) in pat.iter().enumerate() {
612 assert!(
613 v >= 0.0 && v <= 1.0,
614 "dim {i} out of [0,1]: {v}"
615 );
616 }
617 }
618
619 #[test]
622 fn adaptive_new_and_register() {
623 let idx = InMemoryPatternIndex::new();
624 let config = TierConfig::default();
625 let mut at = AdaptiveTiering::new(idx, config);
626
627 assert_eq!(at.registered_count(), 0);
628
629 at.register_block(make_key(1, 0), Tier::Tier1);
630 assert_eq!(at.registered_count(), 1);
631
632 at.register_block(make_key(1, 0), Tier::Tier2);
633 assert_eq!(at.registered_count(), 1); }
635
636 #[test]
637 fn adaptive_remove_block() {
638 let mut idx = InMemoryPatternIndex::new();
639 let key = make_key(1, 0);
640 idx.insert(&PatternVector {
641 key,
642 embedding: vec![1.0, 0.0, 0.0, 0.0],
643 score: 0.5,
644 });
645
646 let config = TierConfig::default();
647 let mut at = AdaptiveTiering::new(idx, config);
648 at.register_block(key, Tier::Tier1);
649 assert_eq!(at.registered_count(), 1);
650 assert_eq!(at.index.len(), 1);
651
652 at.remove_block(key);
653 assert_eq!(at.registered_count(), 0);
654 assert_eq!(at.index.len(), 0);
655 }
656
657 #[test]
658 fn suggest_tier_empty_neighbors() {
659 let idx = InMemoryPatternIndex::new();
660 let config = TierConfig::default();
661 let at = AdaptiveTiering::new(idx, config);
662
663 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0, 10, 5);
664 let result = at.suggest_tier(&meta, &[]);
665 assert_eq!(result, None);
666 }
667
668 #[test]
669 fn suggest_tier_no_known_neighbors() {
670 let idx = InMemoryPatternIndex::new();
671 let config = TierConfig::default();
672 let at = AdaptiveTiering::new(idx, config);
673
674 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0, 10, 5);
675 let neighbors = vec![(make_key(2, 0), 0.9), (make_key(3, 0), 0.8)];
677 let result = at.suggest_tier(&meta, &neighbors);
678 assert_eq!(result, None);
679 }
680
681 #[test]
682 fn suggest_tier_unanimous_vote() {
683 let idx = InMemoryPatternIndex::new();
684 let config = TierConfig::default();
685 let mut at = AdaptiveTiering::new(idx, config);
686
687 at.register_block(make_key(2, 0), Tier::Tier3);
689 at.register_block(make_key(3, 0), Tier::Tier3);
690 at.register_block(make_key(4, 0), Tier::Tier3);
691
692 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0, 10, 5);
693 let neighbors = vec![
694 (make_key(2, 0), 0.9),
695 (make_key(3, 0), 0.8),
696 (make_key(4, 0), 0.7),
697 ];
698
699 let result = at.suggest_tier(&meta, &neighbors);
700 assert_eq!(result, Some(Tier::Tier3));
701 }
702
703 #[test]
704 fn suggest_tier_same_as_current_returns_none() {
705 let idx = InMemoryPatternIndex::new();
706 let config = TierConfig::default();
707 let mut at = AdaptiveTiering::new(idx, config);
708
709 at.register_block(make_key(2, 0), Tier::Tier1);
711 at.register_block(make_key(3, 0), Tier::Tier1);
712
713 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0, 10, 5);
714 let neighbors = vec![(make_key(2, 0), 0.9), (make_key(3, 0), 0.8)];
715
716 let result = at.suggest_tier(&meta, &neighbors);
717 assert_eq!(result, None);
718 }
719
720 #[test]
721 fn suggest_tier_weighted_majority() {
722 let idx = InMemoryPatternIndex::new();
723 let config = TierConfig::default();
724 let mut at = AdaptiveTiering::new(idx, config);
725
726 at.register_block(make_key(2, 0), Tier::Tier1);
728 at.register_block(make_key(3, 0), Tier::Tier1);
729 at.register_block(make_key(4, 0), Tier::Tier3);
731
732 let meta = make_store_meta(make_key(1, 0), Tier::Tier2, 0.5, 0, 10, 5);
733 let neighbors = vec![
734 (make_key(2, 0), 0.3), (make_key(3, 0), 0.3), (make_key(4, 0), 0.9), ];
738 let result = at.suggest_tier(&meta, &neighbors);
740 assert_eq!(result, Some(Tier::Tier3));
741 }
742
743 #[test]
744 fn suggest_tier_negative_similarity_ignored() {
745 let idx = InMemoryPatternIndex::new();
746 let config = TierConfig::default();
747 let mut at = AdaptiveTiering::new(idx, config);
748
749 at.register_block(make_key(2, 0), Tier::Tier3);
750 at.register_block(make_key(3, 0), Tier::Tier1);
751
752 let meta = make_store_meta(make_key(1, 0), Tier::Tier2, 0.5, 0, 10, 5);
753 let neighbors = vec![
754 (make_key(2, 0), -0.5), (make_key(3, 0), 0.5), ];
757 let result = at.suggest_tier(&meta, &neighbors);
759 assert_eq!(result, Some(Tier::Tier1));
760 }
761
762 #[test]
763 fn suggest_tier_zero_similarity_all() {
764 let idx = InMemoryPatternIndex::new();
765 let config = TierConfig::default();
766 let mut at = AdaptiveTiering::new(idx, config);
767
768 at.register_block(make_key(2, 0), Tier::Tier3);
769
770 let meta = make_store_meta(make_key(1, 0), Tier::Tier1, 0.5, 0, 10, 5);
771 let neighbors = vec![(make_key(2, 0), 0.0)];
772
773 let result = at.suggest_tier(&meta, &neighbors);
775 assert_eq!(result, None);
776 }
777
778 #[test]
781 fn integration_end_to_end() {
782 let mut idx = InMemoryPatternIndex::new();
783 let config = TierConfig::default();
784
785 let hot_key = make_key(1, 0);
787 let warm_key = make_key(2, 0);
788 let cold_key = make_key(3, 0);
789
790 let hot_meta =
791 make_store_meta(hot_key, Tier::Tier1, 0.9, u64::MAX, 1000, 2);
792 let warm_meta =
793 make_store_meta(warm_key, Tier::Tier2, 0.5, 0xFFFF_FFFF, 100, 10);
794 let cold_meta =
795 make_store_meta(cold_key, Tier::Tier3, 0.05, 0x0F, 5, 100);
796
797 let hot_emb = pattern_from_meta(&hot_meta);
799 let warm_emb = pattern_from_meta(&warm_meta);
800 let cold_emb = pattern_from_meta(&cold_meta);
801
802 idx.insert(&PatternVector {
803 key: hot_key,
804 embedding: hot_emb.clone(),
805 score: 0.9,
806 });
807 idx.insert(&PatternVector {
808 key: warm_key,
809 embedding: warm_emb.clone(),
810 score: 0.5,
811 });
812 idx.insert(&PatternVector {
813 key: cold_key,
814 embedding: cold_emb.clone(),
815 score: 0.1,
816 });
817
818 let mut at = AdaptiveTiering::new(idx, config);
819 at.register_block(hot_key, Tier::Tier1);
820 at.register_block(warm_key, Tier::Tier2);
821 at.register_block(cold_key, Tier::Tier3);
822
823 let new_key = make_key(4, 0);
825 let new_meta =
826 make_store_meta(new_key, Tier::Tier3, 0.85, u64::MAX, 800, 3);
827 let new_emb = pattern_from_meta(&new_meta);
828
829 let neighbors = at.index.search_nearest(&new_emb, 3);
830 assert!(!neighbors.is_empty());
831
832 let suggestion = at.suggest_tier(&new_meta, &neighbors);
833 assert!(
836 suggestion.is_some(),
837 "expected a tier suggestion for a hot-like pattern in Tier3"
838 );
839 let suggested = suggestion.unwrap();
840 assert_ne!(suggested, Tier::Tier3, "should not stay cold");
841 }
842}