Skip to main content

diskann_providers/model/graph/provider/async_/
simple_neighbor_provider.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::sync::RwLock;
7
8use crate::storage::{StorageReadProvider, StorageWriteProvider};
9use diskann::{ANNError, ANNResult, graph::AdjacencyList, provider::HasId};
10use diskann_vector::contains::ContainsSimd;
11use tracing::trace;
12
13use super::common::{AlignedMemoryVectorStore, TestCallCount};
14use crate::storage::{
15    self, AsyncIndexMetadata, AsyncQuantLoadContext, DiskGraphOnly, LoadWith, SaveWith,
16};
17
18pub struct SimpleNeighborProviderAsync {
19    // Each adjacency list is stored in a fixed size slice of size max_degree * graph_slack_factor + 1.
20    // The length of the list is stored in the extra element at the end as a u32.
21    graph: AlignedMemoryVectorStore<u32>,
22    locks: Vec<RwLock<()>>,
23    num_start_points: usize,
24
25    pub num_get_calls: TestCallCount,
26}
27
28impl SimpleNeighborProviderAsync {
29    pub fn new(
30        max_points: usize,
31        num_start_points: usize,
32        max_degree: u32,
33        graph_slack_factor: f32,
34    ) -> Self {
35        let size = max_points + num_start_points;
36        let graph = AlignedMemoryVectorStore::with_capacity(
37            size,
38            (max_degree as f32 * graph_slack_factor) as usize + 1,
39        );
40        let locks = (0..size).map(|_| RwLock::new(())).collect::<Vec<_>>();
41
42        Self {
43            graph,
44            locks,
45            num_start_points,
46            num_get_calls: TestCallCount::default(),
47        }
48    }
49
50    /// Return the neighbor list for `index` as a slice.
51    ///
52    /// SAFETY:
53    ///
54    /// This function will never read out of bounds, but it does not synchronize access to
55    /// the data. It must be called while holding the corresponding lock at `self.locks[index]`.
56    unsafe fn get_slice(&self, index: usize) -> &[u32] {
57        // SAFETY: This function must be called while the corresponding lock for this slot
58        // is held.
59        let s = unsafe { self.graph.get_slice(index) };
60
61        let len = s[self.graph.dim() - 1] as usize;
62        &s[0..len]
63    }
64
65    pub fn set_neighbors_sync(&self, id: usize, neighbors: &[u32]) -> ANNResult<()> {
66        assert!(
67            neighbors.len() < self.graph.dim(),
68            "neighbors ({}) exceeded max adjacency list size ({})",
69            neighbors.len(),
70            self.graph.dim() - 1,
71        );
72
73        // Lint: We don't have a good way of recovering from lock poisoning anyways.
74        #[allow(clippy::unwrap_used)]
75        let _guard = self.locks[id].write().unwrap();
76
77        // SAFETY: We are holding the write lock for this id.
78        let list = unsafe { self.graph.get_mut_slice(id) };
79        list[0..neighbors.len()].copy_from_slice(neighbors);
80
81        // The assertion above guarantees `neighbors.len() < self.graph.dim()`, which
82        // means it fits in a `u32` (graph dim is sized in `u32` anyway).
83        list[self.graph.dim() - 1] = neighbors.len() as u32;
84        Ok(())
85    }
86
87    pub fn get_neighbors_sync(
88        &self,
89        id: usize,
90        neighbors: &mut AdjacencyList<u32>,
91    ) -> ANNResult<()> {
92        #[cfg(test)]
93        self.num_get_calls.increment();
94
95        // Lint: We don't have a good way of recovering from lock poisoning anyways.
96        #[allow(clippy::unwrap_used)]
97        let _guard = self.locks[id].read().unwrap();
98
99        // SAFETY: We are holding the read lock for `id`.
100        let list = unsafe { self.get_slice(id) };
101        neighbors.overwrite_trusted(list);
102        Ok(())
103    }
104
105    pub fn append_vector_sync(&self, id: usize, new_neighbor_ids: &[u32]) -> ANNResult<()> {
106        // Lint: We don't have a good way of recovering from lock poisoning anyways.
107        #[allow(clippy::unwrap_used)]
108        let _guard = self.locks[id].write().unwrap();
109
110        // SAFETY: We took the write lock for `id` above.
111        let list_raw = unsafe { self.graph.get_mut_slice(id) };
112        let len = list_raw[self.graph.dim() - 1] as usize;
113        let mut new_len = len;
114        let mut list = &mut list_raw[0..len];
115
116        for new_neighbor_id in new_neighbor_ids {
117            if u32::contains_simd(list, *new_neighbor_id) {
118                trace!("append_vector: new neighbor already exists");
119                continue;
120            }
121
122            if new_len < self.graph.dim() - 1 {
123                list_raw[new_len] = *new_neighbor_id;
124                new_len += 1;
125                list = &mut list_raw[0..new_len];
126            } else {
127                trace!("append_vector: some new neighbors discarded; adjacency list full");
128                break;
129            }
130        }
131
132        // `new_len < self.graph.dim()` is enforced by the loop above, so the cast is safe.
133        list_raw[self.graph.dim() - 1] = new_len as u32;
134        Ok(())
135    }
136}
137
138impl HasId for SimpleNeighborProviderAsync {
139    type Id = u32;
140}
141
142impl SimpleNeighborProviderAsync {
143    /// Load the graph directly from a canonical DiskANN graph storage at path `path`.
144    ///
145    /// See also: [`storage::bin::load_graph`].
146    pub fn load_direct<P>(provider: &P, path: &str) -> ANNResult<Self>
147    where
148        P: StorageReadProvider,
149    {
150        storage::bin::load_graph(
151            provider,
152            path,
153            |num_points, max_degree, num_start_points| {
154                // The value `num_points` is the total number of vectors discovered in the
155                // source file, including start points.
156                //
157                // Work backwards from this value to determine the internal `max_points`.
158                let max_points = num_points.checked_sub(num_start_points).ok_or_else(|| {
159                    ANNError::log_index_error(format_args!(
160                        "expected {} start points but the on-disk dataset only has {} total points",
161                        num_start_points, num_points,
162                    ))
163                })?;
164
165                // The provided `max_degree` here is the observed maximum degree in the input
166                // file. Therefore, we don't need to apply a slack factor to it.
167                Ok(Self::new(
168                    max_points,
169                    num_start_points,
170                    max_degree as u32,
171                    1.0,
172                ))
173            },
174        )
175    }
176
177    /// Save `self` directly to a canonical DiskANN graph storage at path `path`.
178    ///
179    /// See also: [`storage::bin::save_graph`].
180    pub fn save_direct<P>(&self, provider: &P, start_point: u32, path: &str) -> ANNResult<usize>
181    where
182        P: StorageWriteProvider,
183    {
184        storage::bin::save_graph(self, provider, start_point, path)
185    }
186}
187
188/// This is an adaptor for compatibility with the async index serialization.
189///
190/// The parameter consists of `(start_point, prefix)` because the index start point is not
191/// saved within `SimpleNeighborPRoviderAsync`.
192impl SaveWith<(u32, AsyncIndexMetadata)> for SimpleNeighborProviderAsync {
193    type Ok = usize;
194    type Error = ANNError;
195
196    async fn save_with<P>(
197        &self,
198        provider: &P,
199        (start_point, metadata): &(u32, AsyncIndexMetadata),
200    ) -> ANNResult<usize>
201    where
202        P: StorageWriteProvider,
203    {
204        self.save_direct(provider, *start_point, metadata.prefix())
205    }
206}
207
208/// This implementation handles the conversion between async index and disk index format.
209/// Parameters:
210/// - `start_point`: The vector ID used during async index building (exceed max_point bounds)
211/// - `actual_start_point`: The real vector ID with identical vector values as `start_point`
212/// - `prefix`: Path prefix for the disk index files
213///
214/// The substitution of `start_point` with `actual_start_point` ensures compatibility
215/// with the on-disk format while preserving the correct entry point information.
216impl SaveWith<(u32, u32, DiskGraphOnly)> for SimpleNeighborProviderAsync {
217    type Ok = usize;
218    type Error = ANNError;
219
220    async fn save_with<P>(
221        &self,
222        provider: &P,
223        (imem_start_point, actual_start_point, metadata): &(u32, u32, DiskGraphOnly),
224    ) -> Result<Self::Ok, Self::Error>
225    where
226        P: StorageWriteProvider,
227    {
228        let graph = DiskAdaptor {
229            provider: self,
230            inmem_start_point: *imem_start_point,
231            actual_start_point: *actual_start_point,
232        };
233
234        storage::bin::save_graph(&graph, provider, *actual_start_point, metadata.prefix())
235    }
236}
237
238/// This is an adaptor for compatibility with the async index serialization.
239impl LoadWith<AsyncIndexMetadata> for SimpleNeighborProviderAsync {
240    type Error = ANNError;
241
242    async fn load_with<P>(provider: &P, metadata: &AsyncIndexMetadata) -> ANNResult<Self>
243    where
244        P: StorageReadProvider,
245    {
246        Self::load_direct(provider, metadata.prefix())
247    }
248}
249
250/// This is an adaptor for compatibility with the async index serialization.
251impl LoadWith<AsyncQuantLoadContext> for SimpleNeighborProviderAsync {
252    type Error = ANNError;
253
254    async fn load_with<P>(provider: &P, ctx: &AsyncQuantLoadContext) -> ANNResult<Self>
255    where
256        P: StorageReadProvider,
257    {
258        Self::load_with(provider, &ctx.metadata).await
259    }
260}
261
262////////////////////////////////////////////
263// SetAdjacencyList and GetAdjacencyList //
264///////////////////////////////////////////
265
266/// Hook into [`storage::bin::load_graph`] by implementing [`storage::bin::SetAdjacencyList`].
267impl storage::bin::SetAdjacencyList for SimpleNeighborProviderAsync {
268    type Item = u32;
269    fn set_adjacency_list(&mut self, i: usize, element: &[u32]) -> ANNResult<()> {
270        self.set_neighbors_sync(i, element)?;
271        Ok(())
272    }
273}
274
275/// Hook into [`storage::bin::save_graph`] by implementing [`storage::bin::GetAdjacencyList`].
276impl storage::bin::GetAdjacencyList for SimpleNeighborProviderAsync {
277    type Element = u32;
278    type Item<'a> = AdjacencyList<u32>;
279
280    fn get_adjacency_list(&self, i: usize) -> ANNResult<Self::Item<'_>> {
281        let mut list = AdjacencyList::new();
282        self.get_neighbors_sync(i, &mut list)?;
283        Ok(list)
284    }
285
286    fn total(&self) -> usize {
287        self.locks.len()
288    }
289
290    fn additional_points(&self) -> u64 {
291        self.num_start_points as u64
292    }
293
294    fn max_degree(&self) -> Option<u32> {
295        Some((self.graph.dim() - 1) as u32)
296    }
297}
298
299/// This adaptor translates between the in-memory async index representation
300/// and the on-disk index format during serialization.
301///
302/// Key differences between the formats:
303/// 1. Disk format requires a valid vector ID as start point, while async index uses a
304///    virtual ID (max_points + 1) that exceeds the valid dataset range
305/// 2. In-memory index appends the virtual start point at the end of adjacency lists
306/// 3. Disk format expects additional_points = 0, while async index uses additional_points = 1
307///
308/// This adaptor handles these differences by:
309/// - Substituting the virtual start point ID with an actual dataset ID when found in adjacency lists
310/// - Excluding the virtual point from the total count (subtracting 1 from length)
311/// - Setting additional_points to 0 as required by the disk format specification
312///
313/// Used with [`storage::bin::save_graph`] to persist an async index in standard DiskANN format.
314struct DiskAdaptor<'a> {
315    provider: &'a SimpleNeighborProviderAsync,
316    inmem_start_point: u32,
317    actual_start_point: u32,
318}
319
320impl storage::bin::GetAdjacencyList for DiskAdaptor<'_> {
321    type Element = u32;
322    type Item<'item>
323        = Vec<u32>
324    where
325        Self: 'item;
326
327    fn get_adjacency_list(&self, i: usize) -> ANNResult<Self::Item<'_>> {
328        let mut list = AdjacencyList::new();
329        self.provider.get_neighbors_sync(i, &mut list)?;
330
331        // Need to change to a `Vec` because remapping the start point can cause duplicates,
332        // and changing the logic to not have duplicates changes the exact nature of the
333        // graph and breaks integration tests for the disk index builder.
334        let mut list: Vec<_> = list.into();
335        for i in list.iter_mut() {
336            if *i == self.inmem_start_point {
337                *i = self.actual_start_point;
338            }
339        }
340
341        Ok(list)
342    }
343
344    fn total(&self) -> usize {
345        // Don't include any start points at the end.
346        self.provider.locks.len() - self.provider.num_start_points
347    }
348
349    /// Fixed to 0 for the disk format
350    fn additional_points(&self) -> u64 {
351        0
352    }
353
354    fn max_degree(&self) -> Option<u32> {
355        None
356    }
357}
358
359///////////
360// Tests //
361///////////
362
363#[cfg(test)]
364mod tests {
365    use crate::storage::VirtualStorageProvider;
366
367    use super::*;
368
369    #[test]
370    fn test_neighbor_provider() {
371        let neighbor_provider = SimpleNeighborProviderAsync::new(10, 1, 5, 1.0);
372
373        let adj_list = vec![1, 2, 3];
374        neighbor_provider.set_neighbors_sync(1, &adj_list).unwrap();
375
376        let mut result = AdjacencyList::new();
377        neighbor_provider
378            .get_neighbors_sync(1, &mut result)
379            .unwrap();
380
381        assert_eq!(&adj_list, &*result);
382
383        let new_adj_list = AdjacencyList::from_iter_untrusted([4, 5, 6]);
384        neighbor_provider
385            .set_neighbors_sync(1, &new_adj_list)
386            .unwrap();
387
388        neighbor_provider
389            .get_neighbors_sync(1, &mut result)
390            .unwrap();
391
392        assert_eq!(new_adj_list, result);
393    }
394
395    #[tokio::test]
396    async fn test_save_load() {
397        let max_degree = 5;
398        let max_points = 8;
399        let additional_points = 2;
400
401        let provider =
402            SimpleNeighborProviderAsync::new(max_points, additional_points, max_degree, 1.0);
403
404        // Setup a virtual storage provider with memory filesystem
405        let storage = VirtualStorageProvider::new_memory();
406
407        // Fill the graph, each node i will have neighbors [i+1, i+2, i+3]
408        for i in 0..max_points + additional_points {
409            let neighbors: Vec<u32> = (1..4).map(|j| i as u32 + j).collect();
410            provider.set_neighbors_sync(i, &neighbors).unwrap();
411        }
412
413        let prefix = AsyncIndexMetadata::new("/resumable_test");
414        // Test SaveWith implementation
415        let start_point = 0;
416        let result = provider
417            .save_with(&storage, &(start_point, prefix.clone()))
418            .await;
419        assert!(result.is_ok(), "Failed to save with resumable context");
420
421        // Verify the file was created
422        let expected_path = prefix.prefix();
423        assert!(
424            storage.exists(expected_path),
425            "Resumable graph file was not created"
426        );
427
428        let receiver = SimpleNeighborProviderAsync::load_direct(&storage, prefix.prefix()).unwrap();
429
430        for i in 0..max_points + additional_points {
431            let mut result = AdjacencyList::new();
432            let mut loaded_result = AdjacencyList::new();
433            provider.get_neighbors_sync(i, &mut result).unwrap();
434            receiver.get_neighbors_sync(i, &mut loaded_result).unwrap();
435            assert_eq!(
436                result, loaded_result,
437                "Adjacency list for node {} doesn't match after loading",
438                i
439            );
440        }
441    }
442}