Skip to main content

lance_index/vector/
pq.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Product Quantization
5//!
6
7use std::sync::Arc;
8
9use arrow::datatypes::{self, ArrowPrimitiveType};
10use arrow_array::{Array, FixedSizeListArray, UInt8Array, cast::AsArray};
11use arrow_array::{ArrayRef, Float32Array, PrimitiveArray};
12use arrow_schema::{DataType, Field};
13use deepsize::DeepSizeOf;
14use distance::build_distance_table_dot;
15use lance_arrow::*;
16use lance_core::{Error, Result, assume_eq};
17use lance_linalg::distance::{DistanceType, Dot, L2};
18use lance_table::utils::LanceIteratorExtension;
19use num_traits::Float;
20use prost::Message;
21use storage::{PQ_METADATA_KEY, ProductQuantizationMetadata, ProductQuantizationStorage};
22use tracing::instrument;
23
24pub mod builder;
25pub mod distance;
26pub mod storage;
27pub mod transform;
28pub(crate) mod utils;
29
30use self::distance::{build_distance_table_l2, compute_pq_distance};
31pub use self::utils::num_centroids;
32use super::quantizer::{
33    Quantization, QuantizationMetadata, QuantizationType, Quantizer, QuantizerBuildParams,
34};
35use super::{PQ_CODE_COLUMN, pb};
36use crate::vector::kmeans::compute_partition;
37pub use builder::PQBuildParams;
38use utils::get_sub_vector_centroids;
39
40#[derive(Debug, Clone)]
41pub struct ProductQuantizer {
42    pub num_sub_vectors: usize,
43    pub num_bits: u32,
44    pub dimension: usize,
45    pub codebook: FixedSizeListArray,
46    pub distance_type: DistanceType,
47}
48
49impl DeepSizeOf for ProductQuantizer {
50    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
51        self.codebook.get_array_memory_size()
52            + self.num_sub_vectors.deep_size_of_children(_context)
53            + self.num_bits.deep_size_of_children(_context)
54            + self.dimension.deep_size_of_children(_context)
55            + self.distance_type.deep_size_of_children(_context)
56    }
57}
58
59impl ProductQuantizer {
60    pub fn new(
61        num_sub_vectors: usize,
62        num_bits: u32,
63        dimension: usize,
64        codebook: FixedSizeListArray,
65        distance_type: DistanceType,
66    ) -> Self {
67        Self {
68            num_bits,
69            num_sub_vectors,
70            dimension,
71            codebook,
72            distance_type,
73        }
74    }
75
76    pub fn from_proto(proto: &pb::Pq, distance_type: DistanceType) -> Result<Self> {
77        let distance_type = match distance_type {
78            DistanceType::Cosine => DistanceType::L2,
79            _ => distance_type,
80        };
81        let codebook = match proto.codebook_tensor.as_ref() {
82            Some(tensor) => FixedSizeListArray::try_from(tensor)?,
83            None => FixedSizeListArray::try_new_from_values(
84                Float32Array::from(proto.codebook.clone()),
85                proto.dimension as i32,
86            )?,
87        };
88        Ok(Self {
89            num_bits: proto.num_bits,
90            num_sub_vectors: proto.num_sub_vectors as usize,
91            dimension: proto.dimension as usize,
92            codebook,
93            distance_type,
94        })
95    }
96
97    #[instrument(name = "ProductQuantizer::transform", level = "debug", skip_all)]
98    fn transform<T: ArrowPrimitiveType>(&self, vectors: &dyn Array) -> Result<ArrayRef>
99    where
100        T::Native: Float + L2 + Dot,
101    {
102        match self.num_bits {
103            4 => self.transform_impl::<4, T>(vectors),
104            8 => self.transform_impl::<8, T>(vectors),
105            _ => Err(Error::index(format!(
106                "ProductQuantization: num_bits {} not supported",
107                self.num_bits
108            ))),
109        }
110    }
111
112    fn transform_impl<const NUM_BITS: u32, T: ArrowPrimitiveType>(
113        &self,
114        vectors: &dyn Array,
115    ) -> Result<ArrayRef>
116    where
117        T::Native: Float + L2 + Dot,
118    {
119        let fsl = vectors
120            .as_fixed_size_list_opt()
121            .ok_or(Error::index(format!(
122                "Expect to be a FixedSizeList<float> vector array, got: {:?} array",
123                vectors.data_type()
124            )))?;
125        let num_sub_vectors = self.num_sub_vectors;
126        let dim = self.dimension;
127        if NUM_BITS == 4 && !num_sub_vectors.is_multiple_of(2) {
128            return Err(Error::index(format!(
129                "PQ: num_sub_vectors must be divisible by 2 for num_bits=4, but got {}",
130                num_sub_vectors,
131            )));
132        }
133        let codebook = self.codebook.values().as_primitive::<T>();
134
135        let distance_type = self.distance_type;
136
137        let flatten_data = fsl.values().as_primitive::<T>();
138        let sub_dim = dim / num_sub_vectors;
139        let total_code_length = fsl.len() * num_sub_vectors / (8 / NUM_BITS as usize);
140        let values = flatten_data
141            .values()
142            .chunks_exact(dim)
143            .flat_map(|vector| {
144                let sub_vec_code = vector
145                    .chunks_exact(sub_dim)
146                    .enumerate()
147                    .map(|(sub_idx, sub_vector)| {
148                        let centroids = get_sub_vector_centroids::<NUM_BITS, _>(
149                            codebook.values(),
150                            dim,
151                            num_sub_vectors,
152                            sub_idx,
153                        );
154                        // SAFETY: The must be 2^NUM_BITS centroids, it's safe to unwrap_or(0),
155                        // this could happen if all distances are INFs in the case of vectors are large.
156                        assume_eq!(centroids.len(), 2_usize.pow(NUM_BITS) * sub_dim);
157                        compute_partition(centroids, sub_vector, distance_type).unwrap_or(0) as u8
158                    })
159                    .collect::<Vec<_>>();
160                if NUM_BITS == 4 {
161                    sub_vec_code
162                        .chunks_exact(2)
163                        .map(|v| (v[1] << 4) | v[0])
164                        .collect::<Vec<_>>()
165                } else {
166                    sub_vec_code
167                }
168            })
169            .exact_size(total_code_length)
170            .collect::<Vec<_>>();
171
172        let num_sub_vectors_in_byte = if NUM_BITS == 4 {
173            num_sub_vectors / 2
174        } else {
175            num_sub_vectors
176        };
177
178        debug_assert_eq!(values.len(), fsl.len() * num_sub_vectors_in_byte);
179        Ok(Arc::new(FixedSizeListArray::try_new_from_values(
180            UInt8Array::from(values),
181            num_sub_vectors_in_byte as i32,
182        )?))
183    }
184
185    // the code must be transposed
186    pub fn compute_distances(&self, query: &dyn Array, code: &UInt8Array) -> Result<Float32Array> {
187        if code.is_empty() {
188            return Ok(Float32Array::from(Vec::<f32>::new()));
189        }
190
191        match self.distance_type {
192            DistanceType::L2 => self.l2_distances(query, code),
193            DistanceType::Cosine => {
194                // it seems we implemented cosine distance at some version,
195                // but from now on, we should use normalized L2 distance.
196                debug_assert!(
197                    false,
198                    "cosine distance should be converted to normalized L2 distance"
199                );
200                // L2 over normalized vectors:  ||x - y|| = x^2 + y^2 - 2 * xy = 1 + 1 - 2 * xy = 2 * (1 - xy)
201                // Cosine distance: 1 - |xy| / (||x|| * ||y||) = 1 - xy / (x^2 * y^2) = 1 - xy / (1 * 1) = 1 - xy
202                // Therefore, Cosine = L2 / 2
203                let l2_dists = self.l2_distances(query, code)?;
204                Ok(l2_dists.values().iter().map(|v| *v / 2.0).collect())
205            }
206            DistanceType::Dot => self.dot_distances(query, code),
207            _ => panic!(
208                "ProductQuantization: distance type {} not supported",
209                self.distance_type
210            ),
211        }
212    }
213
214    /// Pre-compute L2 distance from the query to all code.
215    ///
216    /// It returns the squared L2 distance.
217    fn l2_distances(&self, key: &dyn Array, code: &UInt8Array) -> Result<Float32Array> {
218        let distance_table = self.build_l2_distance_table(key)?;
219
220        #[cfg(target_feature = "avx512f")]
221        {
222            Ok(self.compute_l2_distance(&distance_table, code.values()))
223        }
224        #[cfg(not(target_feature = "avx512f"))]
225        {
226            Ok(self.compute_l2_distance(&distance_table, code.values()))
227        }
228    }
229
230    /// Parameters
231    /// ----------
232    ///  - query: the query vector, with shape (dimension, )
233    ///  - code: the PQ code in one partition.
234    ///
235    fn dot_distances(&self, key: &dyn Array, code: &UInt8Array) -> Result<Float32Array> {
236        match key.data_type() {
237            DataType::Float16 => {
238                self.dot_distances_impl::<datatypes::Float16Type>(key.as_primitive(), code)
239            }
240            DataType::Float32 => {
241                self.dot_distances_impl::<datatypes::Float32Type>(key.as_primitive(), code)
242            }
243            DataType::Float64 => {
244                self.dot_distances_impl::<datatypes::Float64Type>(key.as_primitive(), code)
245            }
246            _ => Err(Error::index(format!(
247                "unsupported data type: {}",
248                key.data_type()
249            ))),
250        }
251    }
252
253    fn dot_distances_impl<T: ArrowPrimitiveType>(
254        &self,
255        key: &PrimitiveArray<T>,
256        code: &UInt8Array,
257    ) -> Result<Float32Array>
258    where
259        T::Native: Dot,
260    {
261        let distance_table = build_distance_table_dot(
262            self.codebook.values().as_primitive::<T>().values(),
263            self.num_bits,
264            self.num_sub_vectors,
265            key.values(),
266        );
267
268        let distances = compute_pq_distance(
269            &distance_table,
270            self.num_bits,
271            self.num_sub_vectors,
272            code.values(),
273            0,
274        );
275
276        let diff = self.num_sub_vectors as f32 - 1.0;
277        let distances = distances.into_iter().map(|d| d - diff).collect::<Vec<_>>();
278        Ok(distances.into())
279    }
280
281    fn build_l2_distance_table(&self, key: &dyn Array) -> Result<Vec<f32>> {
282        match key.data_type() {
283            DataType::Float16 => {
284                Ok(self.build_l2_distance_table_impl::<datatypes::Float16Type>(key.as_primitive()))
285            }
286            DataType::Float32 => {
287                Ok(self.build_l2_distance_table_impl::<datatypes::Float32Type>(key.as_primitive()))
288            }
289            DataType::Float64 => {
290                Ok(self.build_l2_distance_table_impl::<datatypes::Float64Type>(key.as_primitive()))
291            }
292            _ => Err(Error::index(format!(
293                "unsupported data type: {}",
294                key.data_type()
295            ))),
296        }
297    }
298
299    fn build_l2_distance_table_impl<T: ArrowPrimitiveType>(
300        &self,
301        key: &PrimitiveArray<T>,
302    ) -> Vec<f32>
303    where
304        T::Native: L2,
305    {
306        build_distance_table_l2(
307            self.codebook.values().as_primitive::<T>().values(),
308            self.num_bits,
309            self.num_sub_vectors,
310            key.values(),
311        )
312    }
313
314    /// Compute L2 distance from the query to all code.
315    ///
316    /// Type parameters
317    /// ---------------
318    /// - C: the tile size of code-book to run at once.
319    /// - V: the tile size of PQ code to run at once.
320    ///
321    /// Parameters
322    /// ----------
323    /// - distance_table: the pre-computed L2 distance table.
324    ///   It is a flatten array of [num_sub_vectors, num_centroids] f32.
325    /// - code: the PQ code to be used to compute the distances.
326    ///
327    /// Returns
328    /// -------
329    ///  The squared L2 distance.
330    #[inline]
331    fn compute_l2_distance(&self, distance_table: &[f32], code: &[u8]) -> Float32Array {
332        Float32Array::from(compute_pq_distance(
333            distance_table,
334            self.num_bits,
335            self.num_sub_vectors,
336            code,
337            100,
338        ))
339    }
340
341    /// Get the centroids for one sub-vector.
342    ///
343    /// Returns a flatten `num_centroids * sub_vector_width` f32 array.
344    pub fn centroids<T: ArrowPrimitiveType>(&self, sub_vector_idx: usize) -> &[T::Native] {
345        match self.num_bits {
346            4 => get_sub_vector_centroids::<4, _>(
347                self.codebook.values().as_primitive::<T>().values(),
348                self.dimension,
349                self.num_sub_vectors,
350                sub_vector_idx,
351            ),
352            8 => get_sub_vector_centroids::<8, _>(
353                self.codebook.values().as_primitive::<T>().values(),
354                self.dimension,
355                self.num_sub_vectors,
356                sub_vector_idx,
357            ),
358            _ => panic!(
359                "ProductQuantization: num_bits {} not supported",
360                self.num_bits
361            ),
362        }
363    }
364}
365
366impl Quantization for ProductQuantizer {
367    type BuildParams = PQBuildParams;
368    type Metadata = ProductQuantizationMetadata;
369    type Storage = ProductQuantizationStorage;
370
371    fn build(
372        data: &dyn Array,
373        distance_type: DistanceType,
374        params: &Self::BuildParams,
375    ) -> Result<Self> {
376        assert_eq!(data.null_count(), 0);
377        let fsl = data.as_fixed_size_list_opt().ok_or(Error::index(format!(
378            "PQ builder: input is not a FixedSizeList: {}",
379            data.data_type()
380        )))?;
381
382        if let Some(codebook) = params.codebook.as_ref() {
383            return Ok(Self::new(
384                params.num_sub_vectors,
385                params.num_bits as u32,
386                fsl.value_length() as usize,
387                FixedSizeListArray::try_new_from_values(codebook.clone(), fsl.value_length())?,
388                distance_type,
389            ));
390        }
391
392        params.build(data, distance_type)
393    }
394
395    fn retrain(&mut self, data: &dyn Array) -> Result<()> {
396        assert_eq!(data.null_count(), 0);
397        let params = PQBuildParams::with_codebook(
398            self.num_sub_vectors,
399            self.num_bits as usize,
400            Arc::new(self.codebook.clone()),
401        );
402
403        *self = params.build(data, self.distance_type)?;
404        Ok(())
405    }
406
407    fn code_dim(&self) -> usize {
408        self.num_sub_vectors
409    }
410
411    fn column(&self) -> &'static str {
412        PQ_CODE_COLUMN
413    }
414
415    fn use_residual(distance_type: DistanceType) -> bool {
416        PQBuildParams::use_residual(distance_type)
417    }
418
419    fn quantize(&self, vectors: &dyn Array) -> Result<ArrayRef> {
420        let fsl = vectors
421            .as_fixed_size_list_opt()
422            .ok_or(Error::index(format!(
423                "Expect to be a FixedSizeList<float> vector array, got: {:?} array",
424                vectors.data_type()
425            )))?;
426
427        match fsl.value_type() {
428            DataType::Float16 => self.transform::<datatypes::Float16Type>(vectors),
429            DataType::Float32 => self.transform::<datatypes::Float32Type>(vectors),
430            DataType::Float64 => self.transform::<datatypes::Float64Type>(vectors),
431            _ => Err(Error::index(format!(
432                "unsupported data type: {}",
433                fsl.value_type()
434            ))),
435        }
436    }
437
438    fn metadata_key() -> &'static str {
439        PQ_METADATA_KEY
440    }
441
442    fn quantization_type() -> QuantizationType {
443        QuantizationType::Product
444    }
445
446    fn metadata(&self, args: Option<QuantizationMetadata>) -> Self::Metadata {
447        let codebook_position = match &args {
448            Some(args) => args.codebook_position,
449            None => Some(0),
450        };
451
452        let codebook_position = codebook_position.expect("codebook position should be set");
453        ProductQuantizationMetadata {
454            codebook_position,
455            nbits: self.num_bits,
456            num_sub_vectors: self.num_sub_vectors,
457            dimension: self.dimension,
458            codebook: Some(self.codebook.clone()),
459            codebook_tensor: Vec::new(),
460            transposed: args.map(|args| args.transposed).unwrap_or_default(),
461        }
462    }
463
464    fn from_metadata(metadata: &Self::Metadata, distance_type: DistanceType) -> Result<Quantizer> {
465        let distance_type = match distance_type {
466            DistanceType::Cosine => DistanceType::L2,
467            _ => distance_type,
468        };
469        let codebook = match metadata.codebook.as_ref() {
470            Some(fsl) => fsl.clone(),
471            None => {
472                let tensor = pb::Tensor::decode(metadata.codebook_tensor.as_ref())?;
473                FixedSizeListArray::try_from(&tensor)?
474            }
475        };
476        Ok(Quantizer::Product(Self::new(
477            metadata.num_sub_vectors,
478            metadata.nbits,
479            metadata.dimension,
480            codebook,
481            distance_type,
482        )))
483    }
484
485    fn field(&self) -> Field {
486        let num_bytes_per_sub_vector = self.num_sub_vectors * self.num_bits as usize / 8;
487        Field::new(
488            PQ_CODE_COLUMN,
489            DataType::FixedSizeList(
490                Arc::new(Field::new("item", DataType::UInt8, true)),
491                num_bytes_per_sub_vector as i32,
492            ),
493            true,
494        )
495    }
496}
497
498impl TryFrom<&ProductQuantizer> for pb::Pq {
499    type Error = Error;
500
501    fn try_from(pq: &ProductQuantizer) -> Result<Self> {
502        let tensor = pb::Tensor::try_from(&pq.codebook)?;
503        Ok(Self {
504            num_bits: pq.num_bits,
505            num_sub_vectors: pq.num_sub_vectors as u32,
506            dimension: pq.dimension as u32,
507            codebook: vec![],
508            codebook_tensor: Some(tensor),
509        })
510    }
511}
512
513impl TryFrom<Quantizer> for ProductQuantizer {
514    type Error = Error;
515    fn try_from(value: Quantizer) -> Result<Self> {
516        match value {
517            Quantizer::Product(pq) => Ok(pq),
518            _ => Err(Error::index("Expect to be a ProductQuantizer".to_string())),
519        }
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    use std::iter::repeat_n;
528
529    use approx::assert_relative_eq;
530    use arrow::datatypes::UInt8Type;
531    use arrow_array::Float16Array;
532    use half::f16;
533    use lance_linalg::distance::l2_distance_batch;
534    use lance_linalg::kernels::argmin;
535    use lance_testing::datagen::generate_random_array;
536    use num_traits::Zero;
537    use storage::transpose;
538
539    #[test]
540    fn test_f16_pq_to_protobuf() {
541        let pq = ProductQuantizer::new(
542            4,
543            8,
544            16,
545            FixedSizeListArray::try_new_from_values(
546                Float16Array::from_iter_values(repeat_n(f16::zero(), 256 * 16)),
547                16,
548            )
549            .unwrap(),
550            DistanceType::L2,
551        );
552        let proto: pb::Pq = pb::Pq::try_from(&pq).unwrap();
553        assert_eq!(proto.num_bits, 8);
554        assert_eq!(proto.num_sub_vectors, 4);
555        assert_eq!(proto.dimension, 16);
556        assert!(proto.codebook.is_empty());
557        assert!(proto.codebook_tensor.is_some());
558
559        let tensor = proto.codebook_tensor.as_ref().unwrap();
560        assert_eq!(tensor.data_type, pb::tensor::DataType::Float16 as i32);
561        assert_eq!(tensor.shape, vec![256, 16]);
562    }
563
564    #[test]
565    fn test_l2_distance() {
566        const DIM: usize = 512;
567        const TOTAL: usize = 66; // 64 + 2 to make sure reminder is handled correctly.
568        let codebook = generate_random_array(256 * DIM);
569        let pq = ProductQuantizer::new(
570            16,
571            8,
572            DIM,
573            FixedSizeListArray::try_new_from_values(codebook, DIM as i32).unwrap(),
574            DistanceType::L2,
575        );
576        let pq_code = UInt8Array::from_iter_values((0..16 * TOTAL).map(|v| v as u8));
577        let query = generate_random_array(DIM);
578
579        let transposed_pq_codes = transpose(&pq_code, TOTAL, 16);
580        let dists = pq.compute_distances(&query, &transposed_pq_codes).unwrap();
581
582        let sub_vec_len = DIM / 16;
583        let expected = pq_code
584            .values()
585            .chunks(16)
586            .map(|code| {
587                code.iter()
588                    .enumerate()
589                    .flat_map(|(sub_idx, c)| {
590                        let subvec_centroids = pq.centroids::<datatypes::Float32Type>(sub_idx);
591                        let subvec =
592                            &query.values()[sub_idx * sub_vec_len..(sub_idx + 1) * sub_vec_len];
593                        l2_distance_batch(
594                            subvec,
595                            &subvec_centroids
596                                [*c as usize * sub_vec_len..(*c as usize + 1) * sub_vec_len],
597                            sub_vec_len,
598                        )
599                    })
600                    .sum::<f32>()
601            })
602            .collect::<Vec<_>>();
603        dists
604            .values()
605            .iter()
606            .zip(expected.iter())
607            .for_each(|(v, e)| {
608                assert_relative_eq!(*v, *e, epsilon = 1e-4);
609            });
610    }
611
612    #[test]
613    fn test_pq_transform() {
614        const DIM: usize = 16;
615        const TOTAL: usize = 64;
616        let codebook = generate_random_array(DIM * 256);
617        let pq = ProductQuantizer::new(
618            4,
619            8,
620            DIM,
621            FixedSizeListArray::try_new_from_values(codebook, DIM as i32).unwrap(),
622            DistanceType::L2,
623        );
624
625        let vectors = generate_random_array(DIM * TOTAL);
626        let fsl = FixedSizeListArray::try_new_from_values(vectors.clone(), DIM as i32).unwrap();
627        let pq_code = pq.quantize(&fsl).unwrap();
628
629        let mut expected = Vec::with_capacity(TOTAL * 4);
630        vectors.values().chunks_exact(DIM).for_each(|vec| {
631            vec.chunks_exact(DIM / 4)
632                .enumerate()
633                .for_each(|(sub_idx, sub_vec)| {
634                    let centroids = pq.centroids::<datatypes::Float32Type>(sub_idx);
635                    let dists = l2_distance_batch(sub_vec, centroids, DIM / 4);
636                    let code = argmin(dists).unwrap() as u8;
637                    expected.push(code);
638                });
639        });
640
641        assert_eq!(pq_code.len(), TOTAL);
642        assert_eq!(
643            &expected,
644            pq_code
645                .as_fixed_size_list()
646                .values()
647                .as_primitive::<UInt8Type>()
648                .values()
649        );
650    }
651}