Skip to content

Commit 27af94d

Browse files
authored
perf: improve IVF-SQ training, encoding, and reader reuse (#91)
1 parent 8dcabf2 commit 27af94d

12 files changed

Lines changed: 987 additions & 200 deletions

File tree

‎core/src/index.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,8 +1464,8 @@ impl<R: SeekRead> VectorIndexReader<R> {
14641464
IVFFLAT_MAGIC => Ok(Self::IvfFlat(IVFFlatIndexReader::open_with_header(
14651465
reader, header,
14661466
)?)),
1467-
IVF_SQ_MAGIC => Ok(Self::IvfSq(IVFSQIndexReader::open_with_header(
1468-
reader, header,
1467+
IVF_SQ_MAGIC => Ok(Self::IvfSq(IVFSQIndexReader::open_with_header_and_options(
1468+
reader, header, options,
14691469
)?)),
14701470
MAGIC => Ok(Self::IvfPq(IVFPQIndexReader::open_with_header(
14711471
reader, header,

‎core/src/ivfsq.rs‎

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
//! IVF with per-list, per-dimension 8-bit residual scalar quantization.
18+
//! IVF with per-dimension 8-bit residual scalar quantization.
1919
2020
use crate::coarse::CoarseAssignment;
21-
use crate::distance::{fvec_madd, preprocess_vectors, MetricType};
21+
use crate::distance::{preprocess_vectors, MetricType};
2222
use crate::ivfpq::RowIdFilter;
2323
use crate::kmeans::{self, KMeansConfig};
2424
use crate::sq::ScalarQuantizer;
@@ -86,9 +86,14 @@ impl IVFSQIndex {
8686
self.quantizer_centroids =
8787
kmeans::kmeans_train(&KMeansConfig::default(), &processed, n, self.d, self.nlist);
8888
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);
9297
}
9398

9499
pub fn add(&mut self, data: &[f32], ids: &[i64], n: usize) {
@@ -247,44 +252,39 @@ impl IVFSQIndex {
247252
}
248253
}
249254

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;
266260
}
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);
275264
}
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]);
282280
}
283281
}
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];
288288
}
289289

290290
pub(crate) fn list_centroid(&self, list_id: usize) -> &[f32] {
@@ -312,23 +312,46 @@ fn append_encoded_rows(
312312
output_codes: &mut Vec<u8>,
313313
) {
314314
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;
324317
}
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]));
325322
}
326323

327324
#[cfg(test)]
328325
mod tests {
329326
use super::*;
327+
use crate::distance::fvec_madd;
330328
use std::collections::HashSet;
331329

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+
332355
#[test]
333356
fn ivfsq_full_scan_recalls_added_vector() {
334357
let d = 4;

0 commit comments

Comments
 (0)