1use 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 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 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 debug_assert!(
197 false,
198 "cosine distance should be converted to normalized L2 distance"
199 );
200 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 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 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 #[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 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; 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}