uqa_storage/diskann_index/
pq.rs1use std::ops::Range;
8
9use uqa_core::memory::{BudgetedVec, MemoryError};
10
11use super::metric::{checkpoint, squared_distance};
12use super::NavigationVector;
13use crate::{read_control::StorageReadControl, StorageBackendError, StorageBackendResult};
14
15mod lloyd;
16mod training;
17
18pub use training::{PQTrainer, PQTrainingOptions, PQTrainingSummary};
19
20#[derive(Debug)]
22pub struct PQCodebook {
23 dimensions: usize,
24 pq_bytes: usize,
25 centroid_count: u16,
26 centroids: BudgetedVec<f64>,
27 training: PQTrainingSummary,
28}
29
30#[derive(Debug)]
31pub struct PQLookupTable {
32 pq_bytes: usize,
33 centroid_count: u16,
34 distances: BudgetedVec<f64>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
39pub struct PQDistance(f64);
40
41impl PQDistance {
42 pub fn get(self) -> f64 {
43 self.0
44 }
45}
46
47impl PQCodebook {
48 pub const CODEC_REVISION: u32 = 1;
49 pub const TRAINING_REVISION: u32 = 1;
50
51 pub(in crate::diskann_index) fn restore(
52 dimensions: u32,
53 pq_bytes: usize,
54 centroid_count: u16,
55 training: PQTrainingSummary,
56 centroids: BudgetedVec<f64>,
57 control: &StorageReadControl,
58 ) -> StorageBackendResult<Self> {
59 control.check()?;
60 if centroids.len() != training.validate(dimensions, pq_bytes, centroid_count)? {
61 return Err(invalid("centroid scalar count differs from dimensions"));
62 }
63 for (offset, &value) in centroids.iter().enumerate() {
64 checkpoint(offset, control)?;
65 if !value.is_finite() || value.abs() > 2.0 {
67 return Err(invalid(
68 "centroid exceeds finite navigation coordinate bounds",
69 ));
70 }
71 }
72 control.check()?;
73 Ok(Self {
74 dimensions: dimensions as usize,
75 pq_bytes,
76 centroid_count,
77 centroids,
78 training,
79 })
80 }
81
82 pub fn dimensions(&self) -> usize {
83 self.dimensions
84 }
85
86 pub fn pq_bytes(&self) -> usize {
87 self.pq_bytes
88 }
89
90 pub fn centroid_count(&self) -> u16 {
91 self.centroid_count
92 }
93
94 pub fn training(&self) -> PQTrainingSummary {
95 self.training
96 }
97
98 pub fn chunk_range(&self, chunk: usize) -> Option<Range<usize>> {
99 (chunk < self.pq_bytes).then(|| chunk_range(self.dimensions, self.pq_bytes, chunk))
100 }
101
102 pub fn chunk_centroids(&self, chunk: usize) -> Option<&[f64]> {
104 let range = self.chunk_range(chunk)?;
105 let count = usize::from(self.centroid_count);
106 Some(&self.centroids[range.start * count..range.end * count])
107 }
108
109 pub fn encode(
110 &self,
111 vector: &NavigationVector,
112 control: &StorageReadControl,
113 ) -> StorageBackendResult<BudgetedVec<u8>> {
114 self.encode_coordinates(vector.coordinates(), control)
115 }
116
117 fn encode_coordinates(
118 &self,
119 coordinates: &[f64],
120 control: &StorageReadControl,
121 ) -> StorageBackendResult<BudgetedVec<u8>> {
122 self.check_dimensions(coordinates, control)?;
123 let mut codes = BudgetedVec::new(control.memory());
124 codes.reserve(self.pq_bytes)?;
125 for chunk in 0..self.pq_bytes {
126 let range = chunk_range(self.dimensions, self.pq_bytes, chunk);
127 let count = usize::from(self.centroid_count);
128 let centroids = &self.centroids[range.start * count..range.end * count];
129 codes.push(nearest(&coordinates[range], centroids, control)?)?;
130 }
131 control.check()?;
132 Ok(codes)
133 }
134
135 pub fn lookup(
136 &self,
137 query: &NavigationVector,
138 control: &StorageReadControl,
139 ) -> StorageBackendResult<PQLookupTable> {
140 self.lookup_coordinates(query.coordinates(), control)
141 }
142
143 fn lookup_coordinates(
144 &self,
145 coordinates: &[f64],
146 control: &StorageReadControl,
147 ) -> StorageBackendResult<PQLookupTable> {
148 self.check_dimensions(coordinates, control)?;
149 let mut distances = BudgetedVec::new(control.memory());
150 let count = usize::from(self.centroid_count);
151 distances.reserve(product(self.pq_bytes, count)?)?;
152 for chunk in 0..self.pq_bytes {
153 let range = chunk_range(self.dimensions, self.pq_bytes, chunk);
154 let centroids = &self.centroids[range.start * count..range.end * count];
155 for centroid in centroids.chunks_exact(range.len()) {
156 distances.push(squared_distance(
157 &coordinates[range.clone()],
158 centroid,
159 control,
160 )?)?;
161 }
162 }
163 control.check()?;
164 Ok(PQLookupTable {
165 pq_bytes: self.pq_bytes,
166 centroid_count: self.centroid_count,
167 distances,
168 })
169 }
170
171 fn check_dimensions(
172 &self,
173 coordinates: &[f64],
174 control: &StorageReadControl,
175 ) -> StorageBackendResult<()> {
176 control.check()?;
177 if coordinates.len() != self.dimensions {
178 return Err(invalid("vector dimensions do not match the codebook"));
179 }
180 Ok(())
181 }
182}
183
184impl PQLookupTable {
185 pub fn estimate(
187 &self,
188 codes: &[u8],
189 control: &StorageReadControl,
190 ) -> StorageBackendResult<PQDistance> {
191 control.check()?;
192 if codes.len() != self.pq_bytes {
193 return Err(invalid("code length does not match the codebook"));
194 }
195 let mut distance = 0.0;
196 for (chunk, &label) in codes.iter().enumerate() {
197 checkpoint(chunk, control)?;
198 if u16::from(label) >= self.centroid_count {
199 return Err(invalid("code label exceeds the actual centroid count"));
200 }
201 distance +=
202 self.distances[chunk * usize::from(self.centroid_count) + usize::from(label)];
203 }
204 Ok(PQDistance(distance))
205 }
206}
207
208pub(in crate::diskann_index) fn chunk_range(
209 dimensions: usize,
210 chunks: usize,
211 chunk: usize,
212) -> Range<usize> {
213 let width = dimensions / chunks;
214 let extra = dimensions % chunks;
215 let start = chunk * width + chunk.min(extra);
216 start..start + width + usize::from(chunk < extra)
217}
218
219fn nearest(
220 coordinates: &[f64],
221 centroids: &[f64],
222 control: &StorageReadControl,
223) -> StorageBackendResult<u8> {
224 let mut label = 0;
225 let mut minimum = f64::INFINITY;
226 for (index, centroid) in centroids.chunks_exact(coordinates.len()).enumerate() {
227 let distance = squared_distance(coordinates, centroid, control)?;
228 if distance < minimum {
229 minimum = distance;
230 label = index as u8;
231 }
232 }
233 Ok(label)
234}
235
236fn product(left: usize, right: usize) -> StorageBackendResult<usize> {
237 left.checked_mul(right)
238 .ok_or_else(|| MemoryError::SizeOverflow.into())
239}
240
241fn filled<T: Copy>(
242 len: usize,
243 value: T,
244 control: &StorageReadControl,
245) -> StorageBackendResult<BudgetedVec<T>> {
246 let mut values = BudgetedVec::new(control.memory());
247 values.reserve(len)?;
248 for offset in 0..len {
249 checkpoint(offset, control)?;
250 values.push(value)?;
251 }
252 Ok(values)
253}
254
255fn invalid(message: &str) -> StorageBackendError {
256 StorageBackendError::Other(format!("invalid DiskANN PQ: {message}"))
257}
258
259#[cfg(test)]
260mod tests;