1use std::sync::{Arc, RwLock};
2
3use dashmap::DashMap;
4use moka::sync::Cache;
5use ndarray::{Array1, Array2, Axis, s};
6use rayon::prelude::*;
7use zarrs::storage::{ReadableListableStorage, ReadableWritableListableStorage};
8
9use crate::build::assign::determine_node_assignments;
10use crate::build::builder::TRACKED_MEMORY_FRACTION;
11use crate::build::source::EmbeddingsSource;
12use crate::build::writer::{append_node_batch, build_group};
13use crate::dtype::EmbeddingDtype;
14use crate::error::{EcpError, Result};
15use crate::metric::Metric;
16use crate::search::Node;
17
18type CachedNode = Arc<(Array2<f32>, Array1<u32>)>;
20
21pub(crate) struct NodeCache {
24 cache: Cache<String, CachedNode>,
26}
27
28impl NodeCache {
29 fn entry_bytes(entry: &(Array2<f32>, Array1<u32>)) -> u32 {
31 (entry.0.len() * size_of::<f32>() + entry.1.len() * size_of::<u32>()) as u32
32 }
33
34 pub(crate) fn new(max_capacity_bytes: u64) -> Self {
36 NodeCache {
37 cache: Cache::builder()
38 .max_capacity(max_capacity_bytes)
39 .weigher(|_, v: &CachedNode| Self::entry_bytes(v))
40 .build(),
41 }
42 }
43
44 fn get_or_read(
46 &self,
47 store: &ReadableWritableListableStorage,
48 group_path: &str,
49 ) -> Result<CachedNode> {
50 self.cache
51 .try_get_with(group_path.to_string(), || {
52 let read_store: ReadableListableStorage = store.clone().readable_listable();
53 let node = Node::new(read_store, group_path.to_string(), "node_ids".to_string());
54 let missing = || {
57 EcpError::Corrupt(format!(
58 "{group_path} has no embeddings or children on disk"
59 ))
60 };
61 let representatives = node.embeddings()?.ok_or_else(missing)?.clone();
62 let child_ids = node.children()?.ok_or_else(missing)?.clone();
63 Ok::<CachedNode, EcpError>(Arc::new((representatives, child_ids)))
64 })
65 .map_err(|e: Arc<EcpError>| (*e).clone())
66 }
67}
68
69pub(crate) struct BuildConfig<'a> {
71 pub(crate) store: &'a ReadableWritableListableStorage,
72 pub(crate) node_cache: &'a NodeCache,
73 pub(crate) target_level: u32,
75 pub(crate) total_levels: u32,
76 pub(crate) metric: Metric,
77 pub(crate) is_normalized: bool,
78 pub(crate) chunk_shape: &'a [u64],
79 pub(crate) embedding_dtype: EmbeddingDtype,
80
81 pub(crate) leaf_locks: Option<&'a DashMap<u32, Arc<RwLock<()>>>>,
84}
85
86fn fan_out<F>(count: usize, on_caller_thread: bool, process: F) -> Result<Vec<(u32, u32)>>
90where
91 F: Fn(usize) -> Result<Vec<(u32, u32)>> + Sync + Send,
92{
93 let per_index: Vec<Vec<(u32, u32)>> = if on_caller_thread {
94 (0..count).map(process).collect::<Result<_>>()?
95 } else {
96 (0..count)
97 .into_par_iter()
98 .map(process)
99 .collect::<Result<_>>()?
100 };
101 Ok(per_index.into_iter().flatten().collect())
102}
103
104fn route_batch_to_node(
108 config: &BuildConfig,
109 level: u32,
110 node_idx: u32,
111 data_embeddings: &Array2<f32>,
112 data_ids: &Array1<u32>,
113) -> Result<Vec<(u32, u32)>> {
114 let group_path = format!("/lvl_{level}/node_{node_idx}");
115
116 if level == config.target_level {
118 let child_key = if level == config.total_levels {
119 "item_ids"
120 } else {
121 "node_ids"
122 };
123 let write = || {
124 append_node_batch(
125 config.store,
126 &group_path,
127 child_key,
128 data_embeddings,
129 data_ids,
130 config.chunk_shape,
131 config.embedding_dtype,
132 )
133 };
134 match config.leaf_locks {
135 Some(locks) => {
136 let lock = locks
137 .entry(node_idx)
138 .or_insert_with(|| Arc::new(RwLock::new(())))
139 .clone();
140 let _guard = lock.write().unwrap();
141 write()?;
142 }
143 None => write()?,
144 }
145 return Ok(vec![(level, node_idx)]);
146 }
147
148 let entry = config.node_cache.get_or_read(config.store, &group_path)?;
150 let (representatives, child_ids) = (&entry.0, &entry.1);
151
152 let (offsets, assignment) = determine_node_assignments(
153 representatives,
154 data_embeddings,
155 config.metric,
156 config.is_normalized,
157 );
158
159 fan_out(
160 representatives.nrows(),
161 config.leaf_locks.is_some(),
162 |child| {
163 let start = offsets[child] as usize;
164 let end = offsets[child + 1] as usize;
165 if start == end {
166 return Ok(Vec::new());
167 }
168 let vec_indices: Vec<usize> = assignment
169 .slice(s![start..end])
170 .iter()
171 .map(|&i| i as usize)
172 .collect();
173 let child_embeddings = data_embeddings.select(Axis(0), &vec_indices);
174 let child_ids_batch = Array1::from_iter(vec_indices.iter().map(|&i| data_ids[i]));
175 route_batch_to_node(
176 config,
177 level + 1,
178 child_ids[child],
179 &child_embeddings,
180 &child_ids_batch,
181 )
182 },
183 )
184}
185
186pub(crate) fn add_data(
190 config: &BuildConfig,
191 root_embeddings: &Array2<f32>,
192 data_embeddings: &Array2<f32>,
193 data_ids: &Array1<u32>,
194) -> Result<Vec<(u32, u32)>> {
195 let (offsets, assignment) = determine_node_assignments(
196 root_embeddings,
197 data_embeddings,
198 config.metric,
199 config.is_normalized,
200 );
201
202 fan_out(
203 root_embeddings.nrows(),
204 config.leaf_locks.is_some(),
205 |root_node| {
206 let start = offsets[root_node] as usize;
207 let end = offsets[root_node + 1] as usize;
208 if start == end {
209 return Ok(Vec::new());
210 }
211 let vec_indices: Vec<usize> = assignment
212 .slice(s![start..end])
213 .iter()
214 .map(|&i| i as usize)
215 .collect();
216 let node_embeddings = data_embeddings.select(Axis(0), &vec_indices);
217 let node_ids = Array1::from_iter(vec_indices.iter().map(|&i| data_ids[i]));
218 route_batch_to_node(config, 1, root_node as u32, &node_embeddings, &node_ids)
219 },
220 )
221}
222
223#[derive(Clone, Copy)]
225pub struct BuildTreeArgs<'a> {
226 pub store: &'a ReadableWritableListableStorage,
227 pub root_embeddings: &'a Array2<f32>,
228 pub representatives: &'a EmbeddingsSource,
231 pub dataset: &'a EmbeddingsSource,
233 pub total_levels: u32,
234 pub metric: Metric,
235 pub is_normalized: bool,
236 pub fallback_batch_vecs: usize,
238 pub chunk_shape: &'a [u64],
239 pub embedding_dtype: EmbeddingDtype,
240 pub memory_limit_bytes: usize,
241}
242
243pub fn build_tree(args: &BuildTreeArgs) -> Result<()> {
257 let BuildTreeArgs {
258 store,
259 root_embeddings,
260 representatives,
261 dataset,
262 total_levels,
263 metric,
264 is_normalized,
265 fallback_batch_vecs,
266 chunk_shape,
267 embedding_dtype,
268 memory_limit_bytes,
269 } = *args;
270
271 let node_size = root_embeddings.nrows() as u64;
272 let tracked_budget = (memory_limit_bytes as f64 * TRACKED_MEMORY_FRACTION) as usize;
273 let bytes_per_vec = (root_embeddings.ncols() * size_of::<f32>()).max(1);
274
275 let total_cacheable_bytes: usize = (1..total_levels)
278 .map(|l| node_size.pow(l + 1) as usize * bytes_per_vec)
279 .sum();
280 let cache_capacity = total_cacheable_bytes.min(tracked_budget * 3 / 4);
281 let batch_share = tracked_budget.saturating_sub(cache_capacity);
282 if batch_share < bytes_per_vec {
283 log::warn!(
284 "build memory budget leaves {batch_share} bytes for batching against \
285 {bytes_per_vec} bytes per vector; batches will collapse to about \
286 one vector each, which will be slow"
287 );
288 }
289 let memory_floor_vecs = (batch_share / bytes_per_vec).max(1);
290 let node_cache = NodeCache::new(cache_capacity as u64);
291
292 for target_level in 1..=total_levels {
293 build_group(store, &format!("/lvl_{target_level}"))?;
294
295 let source = if target_level == total_levels {
297 dataset
298 } else {
299 representatives
300 };
301 let (total_vec_count, _dim) = source.shape()?;
302 let vec_count = if target_level == total_levels {
303 total_vec_count
304 } else {
305 (node_size.pow(target_level + 1) as usize).min(total_vec_count)
306 };
307 let batch_vecs = source.chunk_aligned_batch_vecs(memory_floor_vecs, fallback_batch_vecs)?;
308
309 let config = BuildConfig {
310 store,
311 node_cache: &node_cache,
312 target_level,
313 total_levels,
314 metric,
315 is_normalized,
316 chunk_shape,
317 embedding_dtype,
318 leaf_locks: None,
319 };
320
321 let mut start = 0;
325 while start < vec_count {
326 let end = (start + batch_vecs).min(vec_count);
327 log::debug!("target_level={target_level}: processing vecs {start}..{end}");
328 let batch_embeddings = source.read_vecs(start, end)?;
329 let batch_ids: Array1<u32> = (start as u32..end as u32).collect();
330 add_data(&config, root_embeddings, &batch_embeddings, &batch_ids)?;
331 start = end;
332 }
333 }
334 Ok(())
335}
336
337#[cfg(test)]
338#[path = "utests/tree.rs"]
339mod tests;