ecp_core/build/
representatives.rs1use ndarray::{Array1, Array2, Axis};
2use rand::seq::index::sample;
3use zarrs::storage::ReadableWritableListableStorage;
4
5use crate::build::builder::TRACKED_MEMORY_FRACTION;
6use crate::build::source::EmbeddingsSource;
7use crate::build::writer::zarrs_append;
8use crate::dtype::EmbeddingDtype;
9use crate::error::Result;
10
11#[derive(Debug, Clone, Copy)]
14pub enum RepresentativeStrategy {
15 Offset,
17 Random,
19}
20
21pub fn select_representative_ids(
24 total_items: usize,
25 target_cluster_items: usize,
26 strategy: RepresentativeStrategy,
27) -> Array1<u32> {
28 match strategy {
29 RepresentativeStrategy::Offset => (0..total_items as u32)
30 .step_by(target_cluster_items)
31 .collect(),
32 RepresentativeStrategy::Random => {
33 let total_clusters = total_items.div_ceil(target_cluster_items);
34 let mut ids: Vec<u32> = sample(&mut rand::rng(), total_items, total_clusters)
35 .into_iter()
36 .map(|i| i as u32)
37 .collect();
38 ids.sort_unstable();
39 Array1::from_vec(ids)
40 }
41 }
42}
43
44pub fn fits_in_memory(count: usize, dim: usize, memory_limit_bytes: usize) -> bool {
46 count.saturating_mul(dim).saturating_mul(size_of::<f32>()) <= memory_limit_bytes
47}
48
49pub enum Representatives {
52 InMemory {
53 embeddings: Array2<f32>,
54 ids: Array1<u32>,
55 },
56 PersistedOnly,
57}
58
59pub fn collect_representatives(
63 store: &ReadableWritableListableStorage,
64 source: &EmbeddingsSource,
65 selected_ids: &Array1<u32>,
66 fallback_batch_vecs: usize,
67 memory_limit_bytes: usize,
68 chunk_shape: &[u64],
69 dtype: EmbeddingDtype,
70) -> Result<()> {
71 let (total_items, dim) = source.shape()?;
72 let tracked_budget = (memory_limit_bytes as f64 * TRACKED_MEMORY_FRACTION) as usize;
73 let bytes_per_vec = (dim * size_of::<f32>()).max(1);
74 let memory_floor_vecs = (tracked_budget / bytes_per_vec).max(1);
75 let batch_vecs = source.chunk_aligned_batch_vecs(memory_floor_vecs, fallback_batch_vecs)?;
76 let selected: Vec<u32> = selected_ids.to_vec();
77
78 let mut start = 0;
79 while start < total_items {
80 let end = (start + batch_vecs).min(total_items);
81
82 let first = selected.partition_point(|&id| (id as usize) < start);
84 let in_range = &selected[first..];
85 let matched_ids: Vec<u32> = in_range
86 .iter()
87 .take_while(|&&id| (id as usize) < end)
88 .copied()
89 .collect();
90 if matched_ids.is_empty() {
91 start = end;
92 continue;
93 }
94 log::debug!(
95 "processing batch vecs {start}..{end} ({} matched representatives)",
96 matched_ids.len()
97 );
98
99 let batch = source.read_vecs(start, end)?;
101 let matched_vec_indices: Vec<usize> =
102 matched_ids.iter().map(|&id| id as usize - start).collect();
103 let matched_embeddings = batch.select(Axis(0), &matched_vec_indices);
104 let matched_ids_array = Array1::from_vec(matched_ids);
105 zarrs_append(
106 store,
107 "/rep_embeddings",
108 "/rep_item_ids",
109 &matched_embeddings,
110 &matched_ids_array,
111 chunk_shape,
112 dtype,
113 )?;
114
115 start = end;
116 }
117 Ok(())
118}
119
120#[cfg(test)]
121#[path = "utests/representatives.rs"]
122mod tests;