1use std::collections::{HashMap, HashSet};
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::sync::Arc;
29
30use parking_lot::RwLock;
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
38pub struct EmbeddingRegion {
39 pub layer: usize,
41 pub node_start: u64,
43 pub node_end: u64,
45}
46
47impl EmbeddingRegion {
48 pub fn new(layer: usize, node_start: u64, node_end: u64) -> Self {
50 Self {
51 layer,
52 node_start,
53 node_end,
54 }
55 }
56
57 #[inline]
59 pub fn contains(&self, node_id: u64) -> bool {
60 node_id >= self.node_start && node_id < self.node_end
61 }
62
63 #[inline]
65 pub fn size(&self) -> u64 {
66 self.node_end.saturating_sub(self.node_start)
67 }
68
69 pub fn overlaps(&self, other: &EmbeddingRegion) -> bool {
72 if self.layer != other.layer {
73 return false;
74 }
75 self.node_start < other.node_end && other.node_start < self.node_end
77 }
78}
79
80#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
89pub struct EmbeddingDelta {
90 pub region: EmbeddingRegion,
92 pub changed_ids: Vec<u64>,
94 pub vectors: Vec<Vec<f32>>,
96 pub generation: u64,
98 pub source_peer: String,
100 pub created_at_ms: u64,
102}
103
104impl EmbeddingDelta {
105 pub fn new(
107 region: EmbeddingRegion,
108 generation: u64,
109 source_peer: impl Into<String>,
110 now_ms: u64,
111 ) -> Self {
112 Self {
113 region,
114 changed_ids: Vec::new(),
115 vectors: Vec::new(),
116 generation,
117 source_peer: source_peer.into(),
118 created_at_ms: now_ms,
119 }
120 }
121
122 pub fn add_change(&mut self, node_id: u64, vector: Vec<f32>) {
124 self.changed_ids.push(node_id);
125 self.vectors.push(vector);
126 }
127
128 #[inline]
130 pub fn change_count(&self) -> usize {
131 self.changed_ids.len()
132 }
133
134 #[inline]
136 pub fn is_empty(&self) -> bool {
137 self.changed_ids.is_empty()
138 }
139
140 pub fn estimated_bytes(&self) -> usize {
145 let vector_bytes: usize = self.vectors.iter().map(|v| 4 * v.len()).sum();
146 let id_bytes = 8 * self.changed_ids.len();
147 vector_bytes + id_bytes
148 }
149}
150
151pub struct DirtyRegionTracker {
159 dirty_nodes: RwLock<HashMap<usize, HashSet<u64>>>,
161 generation: AtomicU64,
163 max_dirty_nodes: usize,
167}
168
169impl DirtyRegionTracker {
170 pub fn new(max_dirty_nodes: usize) -> Self {
175 Self {
176 dirty_nodes: RwLock::new(HashMap::new()),
177 generation: AtomicU64::new(0),
178 max_dirty_nodes,
179 }
180 }
181
182 pub fn mark_dirty(&self, layer: usize, node_id: u64) {
184 let mut guard = self.dirty_nodes.write();
185 guard.entry(layer).or_default().insert(node_id);
186 }
187
188 pub fn mark_dirty_batch(&self, layer: usize, node_ids: &[u64]) {
190 if node_ids.is_empty() {
191 return;
192 }
193 let mut guard = self.dirty_nodes.write();
194 let set = guard.entry(layer).or_default();
195 for &id in node_ids {
196 set.insert(id);
197 }
198 }
199
200 pub fn clear_region(&self, region: &EmbeddingRegion) -> usize {
204 let mut guard = self.dirty_nodes.write();
205 let set = match guard.get_mut(®ion.layer) {
206 Some(s) => s,
207 None => return 0,
208 };
209 let before = set.len();
210 set.retain(|&id| !region.contains(id));
211 let removed = before - set.len();
212 if set.is_empty() {
214 guard.remove(®ion.layer);
215 }
216 removed
217 }
218
219 pub fn dirty_in_region(&self, region: &EmbeddingRegion) -> Vec<u64> {
221 let guard = self.dirty_nodes.read();
222 match guard.get(®ion.layer) {
223 Some(set) => set
224 .iter()
225 .filter(|&&id| region.contains(id))
226 .copied()
227 .collect(),
228 None => Vec::new(),
229 }
230 }
231
232 pub fn total_dirty(&self) -> usize {
234 self.dirty_nodes.read().values().map(HashSet::len).sum()
235 }
236
237 pub fn dirty_ratio(&self) -> f64 {
241 if self.max_dirty_nodes == 0 {
242 return 0.0;
243 }
244 let ratio = self.total_dirty() as f64 / self.max_dirty_nodes as f64;
245 ratio.min(1.0)
246 }
247
248 pub fn has_dirty(&self) -> bool {
250 self.dirty_nodes.read().values().any(|s| !s.is_empty())
251 }
252
253 pub fn generation(&self) -> u64 {
255 self.generation.load(Ordering::Acquire)
256 }
257
258 pub fn advance_generation(&self) -> u64 {
262 self.generation.fetch_add(1, Ordering::AcqRel) + 1
263 }
264
265 pub fn dirty_layers(&self) -> Vec<usize> {
267 let guard = self.dirty_nodes.read();
268 let mut layers: Vec<usize> = guard
269 .iter()
270 .filter(|(_, s)| !s.is_empty())
271 .map(|(&l, _)| l)
272 .collect();
273 layers.sort_unstable();
274 layers
275 }
276}
277
278pub struct PartialSyncManager {
287 tracker: Arc<DirtyRegionTracker>,
288 pending_deltas: RwLock<Vec<EmbeddingDelta>>,
290 max_pending: usize,
292 local_peer_id: String,
294}
295
296impl PartialSyncManager {
297 pub fn new(
304 max_dirty_nodes: usize,
305 max_pending: usize,
306 local_peer_id: impl Into<String>,
307 ) -> Self {
308 Self {
309 tracker: Arc::new(DirtyRegionTracker::new(max_dirty_nodes)),
310 pending_deltas: RwLock::new(Vec::new()),
311 max_pending,
312 local_peer_id: local_peer_id.into(),
313 }
314 }
315
316 pub fn record_change(&self, layer: usize, node_id: u64) {
318 self.tracker.mark_dirty(layer, node_id);
319 }
320
321 pub fn build_delta<F>(
327 &self,
328 region: &EmbeddingRegion,
329 get_vector: F,
330 now_ms: u64,
331 ) -> EmbeddingDelta
332 where
333 F: Fn(u64) -> Option<Vec<f32>>,
334 {
335 let generation = self.tracker.generation();
336 let mut delta = EmbeddingDelta::new(
337 region.clone(),
338 generation,
339 self.local_peer_id.clone(),
340 now_ms,
341 );
342
343 let dirty_ids = self.tracker.dirty_in_region(region);
344 for node_id in dirty_ids {
345 if let Some(vec) = get_vector(node_id) {
346 delta.add_change(node_id, vec);
347 }
348 }
349 delta
350 }
351
352 pub fn build_all_deltas<F>(&self, get_vector: F, now_ms: u64) -> Vec<EmbeddingDelta>
359 where
360 F: Fn(usize, u64) -> Option<Vec<f32>>,
361 {
362 let generation = self.tracker.generation();
363
364 let layer_dirty: Vec<(usize, Vec<u64>)> = {
366 let guard = self.tracker.dirty_nodes.read();
367 guard
368 .iter()
369 .filter(|(_, s)| !s.is_empty())
370 .map(|(&layer, ids)| (layer, ids.iter().copied().collect()))
371 .collect()
372 };
373
374 let mut deltas = Vec::with_capacity(layer_dirty.len());
375 for (layer, ids) in layer_dirty {
376 let region = EmbeddingRegion::new(layer, 0, u64::MAX);
378 let mut delta =
379 EmbeddingDelta::new(region, generation, self.local_peer_id.clone(), now_ms);
380 for node_id in ids {
381 if let Some(vec) = get_vector(layer, node_id) {
382 delta.add_change(node_id, vec);
383 }
384 }
385 if !delta.is_empty() {
386 deltas.push(delta);
387 }
388 }
389 deltas
390 }
391
392 pub fn apply_delta(&self, delta: &EmbeddingDelta) {
397 if delta.changed_ids.is_empty() {
398 return;
399 }
400 let region = &delta.region;
401 let mut guard = self.tracker.dirty_nodes.write();
402 if let Some(set) = guard.get_mut(®ion.layer) {
403 for &node_id in &delta.changed_ids {
404 set.remove(&node_id);
405 }
406 if set.is_empty() {
407 guard.remove(®ion.layer);
408 }
409 }
410 }
411
412 pub fn push_pending(&self, delta: EmbeddingDelta) -> bool {
417 let mut guard = self.pending_deltas.write();
418 if guard.len() >= self.max_pending {
419 return false;
420 }
421 guard.push(delta);
422 true
423 }
424
425 pub fn ack_generation(&self, generation: u64) -> usize {
429 let mut guard = self.pending_deltas.write();
430 let before = guard.len();
431 guard.retain(|d| d.generation > generation);
432 before - guard.len()
433 }
434
435 pub fn pending_count(&self) -> usize {
437 self.pending_deltas.read().len()
438 }
439
440 pub fn tracker(&self) -> &DirtyRegionTracker {
442 &self.tracker
443 }
444
445 pub fn dirty_ratio(&self) -> f64 {
447 self.tracker.dirty_ratio()
448 }
449}
450
451#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
462 fn test_region_contains() {
463 let r = EmbeddingRegion::new(0, 10, 20);
464 assert!(r.contains(10));
466 assert!(r.contains(15));
468 assert!(!r.contains(20));
470 assert!(!r.contains(9));
472 assert!(!r.contains(100));
474 }
475
476 #[test]
477 fn test_region_overlaps() {
478 let a = EmbeddingRegion::new(0, 10, 20);
479 assert!(a.overlaps(&a));
481 let b = EmbeddingRegion::new(0, 15, 25);
483 assert!(a.overlaps(&b));
484 assert!(b.overlaps(&a));
485 let c = EmbeddingRegion::new(0, 20, 30);
487 assert!(!a.overlaps(&c));
488 let d = EmbeddingRegion::new(1, 10, 20);
490 assert!(!a.overlaps(&d));
491 let e = EmbeddingRegion::new(0, 12, 18);
493 assert!(a.overlaps(&e));
494 let f = EmbeddingRegion::new(0, 0, 10);
496 assert!(!a.overlaps(&f));
497 }
498
499 #[test]
500 fn test_region_size() {
501 let r = EmbeddingRegion::new(0, 5, 15);
502 assert_eq!(r.size(), 10);
503 let z = EmbeddingRegion::new(0, 7, 7);
505 assert_eq!(z.size(), 0);
506 let inv = EmbeddingRegion {
508 layer: 0,
509 node_start: 100,
510 node_end: 10,
511 };
512 assert_eq!(inv.size(), 0);
513 }
514
515 #[test]
518 fn test_embedding_delta_add_change() {
519 let region = EmbeddingRegion::new(0, 0, 100);
520 let mut delta = EmbeddingDelta::new(region, 1, "peer-a", 42_000);
521 assert!(delta.is_empty());
522 assert_eq!(delta.change_count(), 0);
523
524 delta.add_change(5, vec![1.0, 2.0, 3.0]);
525 delta.add_change(7, vec![4.0, 5.0]);
526 assert!(!delta.is_empty());
527 assert_eq!(delta.change_count(), 2);
528 assert_eq!(delta.changed_ids, vec![5, 7]);
529 assert_eq!(delta.vectors[0], vec![1.0, 2.0, 3.0]);
530 }
531
532 #[test]
533 fn test_embedding_delta_estimated_bytes() {
534 let region = EmbeddingRegion::new(0, 0, 100);
535 let mut delta = EmbeddingDelta::new(region, 1, "peer-a", 0);
536 assert_eq!(delta.estimated_bytes(), 0);
538
539 delta.add_change(1, vec![1.0; 4]);
541 assert_eq!(delta.estimated_bytes(), 24);
542
543 delta.add_change(2, vec![0.5; 8]);
545 assert_eq!(delta.estimated_bytes(), 64);
546 }
547
548 #[test]
551 fn test_dirty_tracker_mark_and_query() {
552 let t = DirtyRegionTracker::new(100);
553 assert!(!t.has_dirty());
554 assert_eq!(t.total_dirty(), 0);
555
556 t.mark_dirty(0, 42);
557 t.mark_dirty(0, 43);
558 t.mark_dirty(1, 10);
559
560 assert!(t.has_dirty());
561 assert_eq!(t.total_dirty(), 3);
562
563 let region = EmbeddingRegion::new(0, 40, 50);
564 let mut ids = t.dirty_in_region(®ion);
565 ids.sort_unstable();
566 assert_eq!(ids, vec![42, 43]);
567
568 let region1 = EmbeddingRegion::new(1, 0, 100);
570 let mut ids1 = t.dirty_in_region(®ion1);
571 ids1.sort_unstable();
572 assert_eq!(ids1, vec![10]);
573 }
574
575 #[test]
576 fn test_dirty_tracker_mark_batch() {
577 let t = DirtyRegionTracker::new(100);
578 t.mark_dirty_batch(0, &[1, 2, 3, 4, 5]);
579 assert_eq!(t.total_dirty(), 5);
580 t.mark_dirty_batch(0, &[1, 2, 3]);
582 assert_eq!(t.total_dirty(), 5);
583 t.mark_dirty_batch(0, &[]);
585 assert_eq!(t.total_dirty(), 5);
586 }
587
588 #[test]
589 fn test_dirty_tracker_clear_region() {
590 let t = DirtyRegionTracker::new(100);
591 t.mark_dirty_batch(0, &[10, 11, 12, 50]);
592 t.mark_dirty(1, 10);
593
594 let region = EmbeddingRegion::new(0, 10, 13);
595 let cleared = t.clear_region(®ion);
596 assert_eq!(cleared, 3);
597
598 let remaining = t.dirty_in_region(&EmbeddingRegion::new(0, 0, 100));
600 assert_eq!(remaining, vec![50]);
601 assert_eq!(
603 t.dirty_in_region(&EmbeddingRegion::new(1, 0, 100)),
604 vec![10]
605 );
606 }
607
608 #[test]
609 fn test_dirty_tracker_dirty_ratio() {
610 let t = DirtyRegionTracker::new(10);
611 assert_eq!(t.dirty_ratio(), 0.0);
612 t.mark_dirty_batch(0, &[0, 1, 2, 3, 4]);
613 assert!((t.dirty_ratio() - 0.5).abs() < 1e-9);
614 t.mark_dirty_batch(0, &[5, 6, 7, 8, 9, 10, 11]);
616 assert_eq!(t.dirty_ratio(), 1.0);
617 }
618
619 #[test]
620 fn test_dirty_tracker_advance_generation() {
621 let t = DirtyRegionTracker::new(100);
622 assert_eq!(t.generation(), 0);
623 let g1 = t.advance_generation();
624 assert_eq!(g1, 1);
625 assert_eq!(t.generation(), 1);
626 let g2 = t.advance_generation();
627 assert_eq!(g2, 2);
628 }
629
630 #[test]
631 fn test_dirty_tracker_dirty_layers() {
632 let t = DirtyRegionTracker::new(100);
633 assert!(t.dirty_layers().is_empty());
634 t.mark_dirty(2, 5);
635 t.mark_dirty(0, 1);
636 t.mark_dirty(1, 3);
637 assert_eq!(t.dirty_layers(), vec![0, 1, 2]);
638
639 t.clear_region(&EmbeddingRegion::new(1, 0, u64::MAX));
641 assert_eq!(t.dirty_layers(), vec![0, 2]);
642 }
643
644 #[test]
647 fn test_partial_sync_record_change() {
648 let mgr = PartialSyncManager::new(100, 16, "local");
649 mgr.record_change(0, 7);
650 mgr.record_change(0, 8);
651 mgr.record_change(1, 3);
652 assert_eq!(mgr.tracker().total_dirty(), 3);
653 assert!(mgr.tracker().has_dirty());
654 }
655
656 #[test]
657 fn test_partial_sync_build_delta() {
658 let mgr = PartialSyncManager::new(100, 16, "local");
659 mgr.record_change(0, 10);
660 mgr.record_change(0, 20);
661 mgr.record_change(0, 30);
662
663 let region = EmbeddingRegion::new(0, 0, 100);
664 let delta = mgr.build_delta(®ion, |node_id| Some(vec![node_id as f32; 4]), 1000);
665
666 assert_eq!(delta.change_count(), 3);
667 assert_eq!(delta.source_peer, "local");
668 assert_eq!(delta.created_at_ms, 1000);
669 let mut ids = delta.changed_ids.clone();
671 ids.sort_unstable();
672 assert_eq!(ids, vec![10, 20, 30]);
673 }
674
675 #[test]
676 fn test_partial_sync_build_all_deltas() {
677 let mgr = PartialSyncManager::new(100, 16, "local");
678 mgr.record_change(0, 1);
680 mgr.record_change(0, 2);
681 mgr.record_change(1, 99);
683
684 let deltas = mgr.build_all_deltas(|_layer, node_id| Some(vec![node_id as f32; 3]), 2000);
685
686 assert_eq!(deltas.len(), 2);
688
689 let delta_l0 = deltas
691 .iter()
692 .find(|d| d.region.layer == 0)
693 .expect("layer 0 delta");
694 let delta_l1 = deltas
695 .iter()
696 .find(|d| d.region.layer == 1)
697 .expect("layer 1 delta");
698 assert_eq!(delta_l0.change_count(), 2);
699 assert_eq!(delta_l1.change_count(), 1);
700 }
701
702 #[test]
703 fn test_partial_sync_apply_delta_clears_dirty() {
704 let mgr = PartialSyncManager::new(100, 16, "local");
705 mgr.record_change(0, 5);
706 mgr.record_change(0, 6);
707 mgr.record_change(0, 7);
708
709 let region = EmbeddingRegion::new(0, 0, 100);
711 let mut incoming = EmbeddingDelta::new(region, 1, "remote-peer", 999);
712 incoming.add_change(5, vec![1.0; 3]);
713 incoming.add_change(6, vec![2.0; 3]);
714
715 mgr.apply_delta(&incoming);
716
717 assert_eq!(mgr.tracker().total_dirty(), 1);
719 let remaining = mgr
720 .tracker()
721 .dirty_in_region(&EmbeddingRegion::new(0, 0, 100));
722 assert_eq!(remaining, vec![7]);
723 }
724
725 #[test]
726 fn test_partial_sync_ack_generation() {
727 let mgr = PartialSyncManager::new(100, 16, "local");
728 let region = EmbeddingRegion::new(0, 0, 100);
729
730 for gen in 1u64..=4 {
732 let delta = EmbeddingDelta::new(region.clone(), gen, "local", gen * 1000);
733 assert!(mgr.push_pending(delta));
734 }
735 assert_eq!(mgr.pending_count(), 4);
736
737 let removed = mgr.ack_generation(2);
739 assert_eq!(removed, 2);
740 assert_eq!(mgr.pending_count(), 2);
741
742 let removed2 = mgr.ack_generation(u64::MAX);
744 assert_eq!(removed2, 2);
745 assert_eq!(mgr.pending_count(), 0);
746 }
747}