|
15 | 15 | // specific language governing permissions and limitations |
16 | 16 | // under the License. |
17 | 17 |
|
18 | | -//! IVF with per-list, per-dimension 8-bit residual scalar quantization. |
| 18 | +//! IVF with per-dimension 8-bit residual scalar quantization. |
19 | 19 |
|
20 | 20 | use crate::coarse::CoarseAssignment; |
21 | | -use crate::distance::{fvec_madd, preprocess_vectors, MetricType}; |
| 21 | +use crate::distance::{preprocess_vectors, MetricType}; |
22 | 22 | use crate::ivfpq::RowIdFilter; |
23 | 23 | use crate::kmeans::{self, KMeansConfig}; |
24 | 24 | use crate::sq::ScalarQuantizer; |
@@ -86,9 +86,14 @@ impl IVFSQIndex { |
86 | 86 | self.quantizer_centroids = |
87 | 87 | kmeans::kmeans_train(&KMeansConfig::default(), &processed, n, self.d, self.nlist); |
88 | 88 | self.coarse_assignment.reset(); |
89 | | - let (list_ids, residuals) = self.assign_residuals(&processed, n); |
90 | | - self.sq.train(&residuals, n); |
91 | | - self.train_list_sqs(&list_ids, &residuals); |
| 89 | + let list_ids = self.coarse_assignment.assign( |
| 90 | + &processed, |
| 91 | + n, |
| 92 | + &self.quantizer_centroids, |
| 93 | + self.nlist, |
| 94 | + self.d, |
| 95 | + ); |
| 96 | + self.train_list_sqs(&processed, &list_ids); |
92 | 97 | } |
93 | 98 |
|
94 | 99 | pub fn add(&mut self, data: &[f32], ids: &[i64], n: usize) { |
@@ -247,44 +252,39 @@ impl IVFSQIndex { |
247 | 252 | } |
248 | 253 | } |
249 | 254 |
|
250 | | - fn assign_residuals(&mut self, processed: &[f32], n: usize) -> (Vec<usize>, Vec<f32>) { |
251 | | - let list_ids = self.coarse_assignment.assign( |
252 | | - processed, |
253 | | - n, |
254 | | - &self.quantizer_centroids, |
255 | | - self.nlist, |
256 | | - self.d, |
257 | | - ); |
258 | | - let mut residuals = vec![0.0f32; n * self.d]; |
259 | | - for i in 0..n { |
260 | | - let vector = &processed[i * self.d..(i + 1) * self.d]; |
261 | | - self.write_residual( |
262 | | - vector, |
263 | | - list_ids[i], |
264 | | - &mut residuals[i * self.d..(i + 1) * self.d], |
265 | | - ); |
| 255 | + fn train_list_sqs(&mut self, data: &[f32], list_ids: &[usize]) { |
| 256 | + if list_ids.is_empty() { |
| 257 | + self.sq = ScalarQuantizer::new(self.d); |
| 258 | + self.list_sqs = vec![self.sq.clone(); self.nlist]; |
| 259 | + return; |
266 | 260 | } |
267 | | - (list_ids, residuals) |
268 | | - } |
269 | | - |
270 | | - fn train_list_sqs(&mut self, list_ids: &[usize], residuals: &[f32]) { |
271 | | - let mut list_residuals = vec![Vec::new(); self.nlist]; |
272 | | - for (i, &list_id) in list_ids.iter().enumerate() { |
273 | | - let residual = &residuals[i * self.d..(i + 1) * self.d]; |
274 | | - list_residuals[list_id].extend_from_slice(residual); |
| 261 | + let mut list_rows = vec![Vec::new(); self.nlist]; |
| 262 | + for (row, &list_id) in list_ids.iter().enumerate() { |
| 263 | + list_rows[list_id].push(row); |
275 | 264 | } |
276 | | - self.list_sqs = vec![self.sq.clone(); self.nlist]; |
277 | | - for (list_id, values) in list_residuals.iter().enumerate() { |
278 | | - if !values.is_empty() { |
279 | | - let mut sq = ScalarQuantizer::new(self.d); |
280 | | - sq.train(values, values.len() / self.d); |
281 | | - self.list_sqs[list_id] = sq; |
| 265 | + let trained = list_rows |
| 266 | + .par_iter() |
| 267 | + .enumerate() |
| 268 | + .map(|(list_id, rows)| { |
| 269 | + (!rows.is_empty()).then(|| { |
| 270 | + ScalarQuantizer::train_residual_rows(data, rows, self.list_centroid(list_id)) |
| 271 | + }) |
| 272 | + }) |
| 273 | + .collect::<Vec<_>>(); |
| 274 | + let mut mins = vec![f32::INFINITY; self.d]; |
| 275 | + let mut maxs = vec![f32::NEG_INFINITY; self.d]; |
| 276 | + for sq in trained.iter().flatten() { |
| 277 | + for dim in 0..self.d { |
| 278 | + mins[dim] = mins[dim].min(sq.mins[dim]); |
| 279 | + maxs[dim] = maxs[dim].max(sq.maxs[dim]); |
282 | 280 | } |
283 | 281 | } |
284 | | - } |
285 | | - |
286 | | - fn write_residual(&self, vector: &[f32], list_id: usize, out: &mut [f32]) { |
287 | | - fvec_madd(vector, self.list_centroid(list_id), -1.0, out); |
| 282 | + self.sq = ScalarQuantizer::with_dimension_bounds(self.d, mins, maxs); |
| 283 | + // A training sample may contain only a handful of rows in a partition. |
| 284 | + // Its extrema severely clip unseen residuals (including constant sample |
| 285 | + // dimensions). Pool the observed residual bounds across partitions; |
| 286 | + // retain per-list metadata so existing v1 files keep their own bounds. |
| 287 | + self.list_sqs = vec![self.sq.clone(); self.nlist]; |
288 | 288 | } |
289 | 289 |
|
290 | 290 | pub(crate) fn list_centroid(&self, list_id: usize) -> &[f32] { |
@@ -312,23 +312,46 @@ fn append_encoded_rows( |
312 | 312 | output_codes: &mut Vec<u8>, |
313 | 313 | ) { |
314 | 314 | output_ids.reserve(rows.len()); |
315 | | - output_codes.reserve(rows.len().saturating_mul(d)); |
316 | | - let mut residual = vec![0.0f32; d]; |
317 | | - let mut code = vec![0u8; d]; |
318 | | - for &row in rows { |
319 | | - let vector = &data[row * d..(row + 1) * d]; |
320 | | - fvec_madd(vector, centroid, -1.0, &mut residual); |
321 | | - sq.encode(&residual, &mut code); |
322 | | - output_ids.push(input_ids[row]); |
323 | | - output_codes.extend_from_slice(&code); |
| 315 | + if rows.is_empty() { |
| 316 | + return; |
324 | 317 | } |
| 318 | + let start = output_codes.len(); |
| 319 | + output_codes.resize(start + rows.len() * d, 0); |
| 320 | + sq.encode_residual_rows(data, rows, centroid, &mut output_codes[start..]); |
| 321 | + output_ids.extend(rows.iter().map(|&row| input_ids[row])); |
325 | 322 | } |
326 | 323 |
|
327 | 324 | #[cfg(test)] |
328 | 325 | mod tests { |
329 | 326 | use super::*; |
| 327 | + use crate::distance::fvec_madd; |
330 | 328 | use std::collections::HashSet; |
331 | 329 |
|
| 330 | + #[test] |
| 331 | + fn sparse_partition_bounds_do_not_collapse_unseen_residuals() { |
| 332 | + let mut index = IVFSQIndex::new(2, 3, MetricType::L2); |
| 333 | + index.set_quantizer_centroids(vec![0.0, 0.0, 10.0, 10.0, 20.0, 20.0]); |
| 334 | + index.train_list_sqs(&[-2.0, -2.0, 10.0, 10.0, 12.0, 12.0], &[0, 1, 1]); |
| 335 | + // Partition zero saw only one residual and partition two was empty. |
| 336 | + // Neither should freeze future vectors at a sample's constant value. |
| 337 | + index.add(&[0.0, 0.0, 20.0, 20.0], &[42, 43], 2); |
| 338 | + let mut distances = [0.0; 2]; |
| 339 | + let mut labels = [0; 2]; |
| 340 | + index.search( |
| 341 | + &[0.0, 0.0, 20.0, 20.0], |
| 342 | + 2, |
| 343 | + 1, |
| 344 | + 3, |
| 345 | + &mut distances, |
| 346 | + &mut labels, |
| 347 | + ); |
| 348 | + assert_eq!(labels, [42, 43]); |
| 349 | + assert!( |
| 350 | + distances.iter().all(|&distance| distance < 0.001), |
| 351 | + "{distances:?}" |
| 352 | + ); |
| 353 | + } |
| 354 | + |
332 | 355 | #[test] |
333 | 356 | fn ivfsq_full_scan_recalls_added_vector() { |
334 | 357 | let d = 4; |
|
0 commit comments