Skip to main content

nodedb_vector/vamana/
node_fetcher.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `NodeFetcher` trait — abstracts in-memory vs io_uring-backed FP32 vector
4//! retrieval for Vamana beam-search and rerank.
5//!
6//! The trait is intentionally *not* `Send + Sync` so that `IoUringNodeFetcher`
7//! (Data Plane, `!Send`) can implement it without wrapping in `Arc<Mutex<…>>`.
8//! The `InMemoryFetcher` impl is `Send + Sync` (plain memory slice), but the
9//! trait bound is left absent to keep the interface Data-Plane-compatible.
10//!
11//! ## Pre-fetch contract
12//!
13//! `prefetch_batch` submits I/O for a set of node indices *before* compute
14//! touches them.  On the in-memory path this is a no-op.  On the io_uring
15//! path it issues `IORING_OP_READ` SQEs in parallel; `fetch_fp32` then
16//! collects completions.  This satisfies the TPC Page Fault Hazard rule:
17//! pages are pre-warmed asynchronously so the reactor thread never blocks
18//! on a major fault.
19
20/// Abstraction over FP32 vector storage for a Vamana index partition.
21///
22/// Two impls exist:
23/// - [`InMemoryFetcher`] — plain `Vec<Vec<f32>>`, available on all targets.
24/// - `IoUringNodeFetcher` — Data Plane only, lives in `nodedb::data`.
25pub trait NodeFetcher {
26    /// Vector dimensionality.
27    fn dim(&self) -> usize;
28
29    /// Submit pre-fetch hints for `indices` so their data is ready before
30    /// `fetch_fp32` is called.
31    ///
32    /// On the in-memory path this is a no-op.  On the io_uring path this
33    /// issues `IORING_OP_READ` SQEs without blocking — completion is polled
34    /// lazily inside `fetch_fp32`.
35    fn prefetch_batch(&mut self, indices: &[u32]);
36
37    /// Retrieve the full-precision FP32 vector for node `idx`.
38    ///
39    /// On the in-memory path this copies from a slice.  On the io_uring path
40    /// this collects the pre-fetched completion or falls back to a synchronous
41    /// read if the pre-fetch was not issued.
42    ///
43    /// Returns `None` if `idx` is out of range.
44    fn fetch_fp32(&mut self, idx: u32) -> Option<Vec<f32>>;
45}
46
47/// In-memory `NodeFetcher` backed by a contiguous `Vec<Vec<f32>>`.
48///
49/// Used on all targets (including WASM) and in unit tests.  In production
50/// an mmap-backed variant may wrap this type with the same interface.
51pub struct InMemoryFetcher {
52    dim: usize,
53    vectors: Vec<Vec<f32>>,
54}
55
56impl InMemoryFetcher {
57    /// Create from an owned vector slice.  All entries must have length `dim`.
58    pub fn new(dim: usize, vectors: Vec<Vec<f32>>) -> Self {
59        Self { dim, vectors }
60    }
61
62    /// Create from a borrowed slice, cloning each vector.
63    pub fn from_slice(dim: usize, vectors: &[Vec<f32>]) -> Self {
64        Self {
65            dim,
66            vectors: vectors.to_vec(),
67        }
68    }
69
70    /// Number of nodes stored.
71    pub fn len(&self) -> usize {
72        self.vectors.len()
73    }
74
75    /// Returns `true` if no vectors are stored.
76    pub fn is_empty(&self) -> bool {
77        self.vectors.is_empty()
78    }
79}
80
81impl NodeFetcher for InMemoryFetcher {
82    fn dim(&self) -> usize {
83        self.dim
84    }
85
86    /// No-op: all data is already in RAM.
87    fn prefetch_batch(&mut self, _indices: &[u32]) {}
88
89    fn fetch_fp32(&mut self, idx: u32) -> Option<Vec<f32>> {
90        self.vectors.get(idx as usize).cloned()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn in_memory_fetcher_returns_correct_vector() {
100        let vecs = vec![
101            vec![1.0_f32, 2.0, 3.0],
102            vec![4.0_f32, 5.0, 6.0],
103            vec![7.0_f32, 8.0, 9.0],
104        ];
105        let mut f = InMemoryFetcher::new(3, vecs.clone());
106        assert_eq!(f.dim(), 3);
107        assert_eq!(f.fetch_fp32(0), Some(vecs[0].clone()));
108        assert_eq!(f.fetch_fp32(2), Some(vecs[2].clone()));
109    }
110
111    #[test]
112    fn in_memory_fetcher_oob_returns_none() {
113        let mut f = InMemoryFetcher::new(2, vec![vec![1.0, 2.0]]);
114        assert!(f.fetch_fp32(1).is_none());
115        assert!(f.fetch_fp32(100).is_none());
116    }
117
118    #[test]
119    fn in_memory_fetcher_empty() {
120        let mut f = InMemoryFetcher::new(4, vec![]);
121        assert!(f.is_empty());
122        assert!(f.fetch_fp32(0).is_none());
123    }
124
125    #[test]
126    fn prefetch_batch_is_noop() {
127        let mut f = InMemoryFetcher::new(2, vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
128        // Should not panic or error.
129        f.prefetch_batch(&[0, 1]);
130        assert_eq!(f.fetch_fp32(0), Some(vec![1.0, 2.0]));
131    }
132}