Skip to main content

lance_index/vector/bq/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::sync::Arc;
5
6use arrow::array::AsArray;
7use arrow::datatypes::{Float16Type, Float32Type, Float64Type};
8use arrow_array::{Array, ArrayRef, FixedSizeListArray, UInt8Array};
9use arrow_schema::{DataType, Field};
10use bitvec::prelude::{BitVec, Lsb0};
11use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray, FloatType};
12use lance_core::deepsize::DeepSizeOf;
13use lance_core::{Error, Result};
14use ndarray::{Axis, ShapeBuilder, s};
15use num_traits::{AsPrimitive, FromPrimitive};
16use rand_distr::Distribution;
17use rayon::prelude::*;
18
19use crate::vector::bq::storage::{
20    RABIT_CODE_COLUMN, RABIT_METADATA_KEY, RabitQuantizationMetadata, RabitQuantizationStorage,
21    RabitQueryEstimator, rabit_binary_code_field, rabit_ex_code_field,
22};
23use crate::vector::bq::transform::{
24    ADD_FACTORS_FIELD, ERROR_FACTORS_FIELD, EX_ADD_FACTORS_FIELD, EX_SCALE_FACTORS_FIELD,
25    SCALE_FACTORS_FIELD,
26};
27use crate::vector::bq::{
28    RQBuildParams, RQRotationType, rabit_binary_code_bytes, rabit_ex_bits,
29    rotation::{apply_fast_rotation, fast_rotation_signs_len, random_fast_rotation_signs},
30    validate_rq_num_bits,
31};
32use crate::vector::quantizer::{Quantization, Quantizer, QuantizerBuildParams};
33
34/// Build parameters for RabitQuantizer.
35///
36/// num_bits: the number of bits per dimension.
37pub struct RabitBuildParams {
38    pub num_bits: u8,
39    pub rotation_type: RQRotationType,
40}
41
42impl Default for RabitBuildParams {
43    fn default() -> Self {
44        Self {
45            num_bits: 1,
46            rotation_type: RQRotationType::default(),
47        }
48    }
49}
50
51impl QuantizerBuildParams for RabitBuildParams {
52    fn sample_size(&self) -> usize {
53        // RabitQ doesn't need to sample any data
54        0
55    }
56}
57
58#[derive(Debug, Clone, DeepSizeOf)]
59pub struct RabitQuantizer {
60    metadata: RabitQuantizationMetadata,
61}
62
63pub(crate) struct RabitQuantizedBatch {
64    pub binary_codes: ArrayRef,
65    pub ex_codes: Option<ArrayRef>,
66    pub ex_res_dot_dists: Option<Vec<f32>>,
67    pub rotated_residuals: Option<Vec<f32>>,
68    pub ex_code_values: Option<Vec<u8>>,
69}
70
71#[inline]
72fn pack_sign_bits(codes: &mut [u8], rotated: &[f32]) {
73    codes.fill(0);
74    for (bit_idx, value) in rotated.iter().enumerate() {
75        if value.is_sign_positive() {
76            codes[bit_idx / u8::BITS as usize] |= 1u8 << (bit_idx % u8::BITS as usize);
77        }
78    }
79}
80
81const EX_QUANTIZATION_EPSILON: f32 = 1.0e-5;
82const EX_TIGHT_START: [f32; 9] = [0.0, 0.15, 0.20, 0.52, 0.59, 0.71, 0.75, 0.77, 0.81];
83
84fn best_ex_rescale_factor(abs_normalized: &[f32], ex_bits: u8) -> f32 {
85    let max_value = abs_normalized
86        .iter()
87        .copied()
88        .filter(|value| value.is_finite())
89        .fold(0.0f32, f32::max);
90    if max_value <= 0.0 {
91        return 0.0;
92    }
93
94    let max_code = (1usize << ex_bits) - 1;
95    let t_end = ((max_code + 10) as f32) / max_value;
96    let t_start = t_end * EX_TIGHT_START[ex_bits as usize];
97
98    let mut current_codes = Vec::with_capacity(abs_normalized.len());
99    let mut squared_denominator = abs_normalized.len() as f32 * 0.25;
100    let mut numerator = 0.0f32;
101    let mut thresholds = Vec::with_capacity(abs_normalized.len() * max_code);
102
103    for (idx, &value) in abs_normalized.iter().enumerate() {
104        if value <= 0.0 || !value.is_finite() {
105            current_codes.push(0usize);
106            continue;
107        }
108
109        let current = ((t_start * value) + EX_QUANTIZATION_EPSILON)
110            .floor()
111            .clamp(0.0, max_code as f32) as usize;
112        current_codes.push(current);
113        squared_denominator += (current * current + current) as f32;
114        numerator += (current as f32 + 0.5) * value;
115
116        let mut next = current + 1;
117        while next <= max_code {
118            let threshold = next as f32 / value;
119            if threshold < t_end {
120                thresholds.push((threshold, idx));
121            }
122            next += 1;
123        }
124    }
125
126    thresholds.sort_unstable_by(|(left, _), (right, _)| left.total_cmp(right));
127
128    let mut best_inner_product = numerator / squared_denominator.sqrt();
129    let mut best_t = t_start;
130    for (threshold, idx) in thresholds {
131        current_codes[idx] += 1;
132        let updated = current_codes[idx];
133        squared_denominator += (2 * updated) as f32;
134        numerator += abs_normalized[idx];
135
136        let current_inner_product = numerator / squared_denominator.sqrt();
137        if current_inner_product > best_inner_product {
138            best_inner_product = current_inner_product;
139            best_t = threshold;
140        }
141    }
142
143    best_t
144}
145
146fn quantize_ex_code(
147    rotated: &[f32],
148    ex_bits: u8,
149    ex_code_dst: &mut [u8],
150    ex_code_values_dst: &mut [u8],
151) -> f32 {
152    debug_assert_eq!(rotated.len(), ex_code_values_dst.len());
153    let norm_squared = rotated.iter().map(|value| value * value).sum::<f32>();
154    if norm_squared <= f32::EPSILON || !norm_squared.is_finite() {
155        ex_code_dst.fill(0);
156        ex_code_values_dst.fill(0);
157        return 0.0;
158    }
159
160    let norm = norm_squared.sqrt();
161    let abs_normalized = rotated
162        .iter()
163        .map(|value| value.abs() / norm)
164        .collect::<Vec<_>>();
165    let t = best_ex_rescale_factor(&abs_normalized, ex_bits);
166    let max_code = ((1u16 << ex_bits) - 1) as u8;
167    let mask = max_code;
168    let code_bias = -((1u32 << ex_bits) as f32 - 0.5);
169    let mut residual_dot_code = 0.0f32;
170
171    for ((&value, &abs_value), ex_code_value) in rotated
172        .iter()
173        .zip(abs_normalized.iter())
174        .zip(ex_code_values_dst.iter_mut())
175    {
176        let mut ex_code = ((t * abs_value) + EX_QUANTIZATION_EPSILON)
177            .floor()
178            .clamp(0.0, max_code as f32) as u8;
179        if value.is_sign_negative() {
180            ex_code = (!ex_code) & mask;
181        }
182        let sign_code = u8::from(value.is_sign_positive());
183        let full_code = ((sign_code as u32) << ex_bits) + ex_code as u32;
184        residual_dot_code += value * (full_code as f32 + code_bias);
185        *ex_code_value = ex_code;
186    }
187
188    crate::vector::bq::ex_dot::pack_blocked_row(ex_code_values_dst, ex_bits, ex_code_dst);
189    residual_dot_code
190}
191
192impl RabitQuantizer {
193    pub fn new<T: ArrowFloatType>(num_bits: u8, dim: i32) -> Self {
194        Self::new_with_rotation::<T>(num_bits, dim, RQRotationType::default())
195    }
196
197    pub fn new_with_rotation<T: ArrowFloatType>(
198        num_bits: u8,
199        dim: i32,
200        rotation_type: RQRotationType,
201    ) -> Self {
202        debug_assert!(dim >= 0, "RabitQ dimension should be non-negative");
203        let code_dim = dim as usize;
204        let metadata = match rotation_type {
205            RQRotationType::Matrix => {
206                // we don't need to calculate the inverse of P, just take generated Q as P^{-1}
207                let rotate_mat = random_orthogonal::<T>(code_dim);
208                let (rotate_mat, _) = rotate_mat.into_raw_vec_and_offset();
209                let rotate_mat = match T::FLOAT_TYPE {
210                    FloatType::Float16 | FloatType::Float32 | FloatType::Float64 => {
211                        let rotate_mat = <T::ArrayType as FloatArray<T>>::from_values(rotate_mat);
212                        FixedSizeListArray::try_new_from_values(rotate_mat, code_dim as i32)
213                            .unwrap()
214                    }
215                    _ => unimplemented!("RabitQ does not support data type: {:?}", T::FLOAT_TYPE),
216                };
217                RabitQuantizationMetadata {
218                    rotate_mat: Some(rotate_mat),
219                    rotate_mat_position: None,
220                    fast_rotation_signs: None,
221                    rotation_type,
222                    code_dim: code_dim as u32,
223                    num_bits,
224                    packed: false,
225                    query_estimator: RabitQueryEstimator::RawQuery,
226                }
227            }
228            RQRotationType::Fast => RabitQuantizationMetadata {
229                rotate_mat: None,
230                rotate_mat_position: None,
231                fast_rotation_signs: Some(random_fast_rotation_signs(code_dim)),
232                rotation_type,
233                code_dim: code_dim as u32,
234                num_bits,
235                packed: false,
236                query_estimator: RabitQueryEstimator::RawQuery,
237            },
238        };
239        Self { metadata }
240    }
241
242    pub fn num_bits(&self) -> u8 {
243        self.metadata.num_bits
244    }
245
246    pub fn rotation_type(&self) -> RQRotationType {
247        self.metadata.rotation_type
248    }
249
250    pub fn metadata_ref(&self) -> &RabitQuantizationMetadata {
251        &self.metadata
252    }
253
254    fn from_supplied_rotation(params: &RQBuildParams, dim: usize) -> Result<Option<Self>> {
255        let Some(metadata) = params.rotation.as_ref() else {
256            return Ok(None);
257        };
258
259        if metadata.num_bits != params.num_bits {
260            return Err(Error::invalid_input(format!(
261                "rabitq_model num_bits={} does not match requested num_bits={}",
262                metadata.num_bits, params.num_bits
263            )));
264        }
265
266        let rotated_dim = metadata.rotated_dim();
267        if rotated_dim != dim {
268            return Err(Error::invalid_input(format!(
269                "rabitq_model dimension={} does not match vector dimension={}",
270                rotated_dim, dim
271            )));
272        }
273
274        match metadata.rotation_type {
275            RQRotationType::Fast => {
276                let signs = metadata.fast_rotation_signs.as_ref().ok_or_else(|| {
277                    Error::invalid_input(
278                        "rabitq_model fast rotation is missing fast_rotation_signs".to_string(),
279                    )
280                })?;
281                let expected_len = fast_rotation_signs_len(dim);
282                if signs.len() != expected_len {
283                    return Err(Error::invalid_input(format!(
284                        "rabitq_model fast_rotation_signs length={} does not match expected length={} for dimension={}",
285                        signs.len(),
286                        expected_len,
287                        dim
288                    )));
289                }
290            }
291            RQRotationType::Matrix => {
292                let rotate_mat = metadata.rotate_mat.as_ref().ok_or_else(|| {
293                    Error::invalid_input(
294                        "rabitq_model matrix rotation is missing rotate_mat".to_string(),
295                    )
296                })?;
297                if rotate_mat.len() != dim || rotate_mat.value_length() != dim as i32 {
298                    return Err(Error::invalid_input(format!(
299                        "rabitq_model matrix rotation shape=({}, {}) does not match vector dimension={}",
300                        rotate_mat.len(),
301                        rotate_mat.value_length(),
302                        dim
303                    )));
304                }
305            }
306        }
307
308        Ok(Some(Self {
309            metadata: metadata.clone(),
310        }))
311    }
312
313    #[inline]
314    fn fast_rotation_signs(&self) -> &[u8] {
315        self.metadata
316            .fast_rotation_signs
317            .as_ref()
318            .expect("RabitQ fast rotation signs missing")
319            .as_slice()
320    }
321
322    #[inline]
323    fn rotate_mat_flat<T: ArrowFloatType>(&self) -> &[T::Native] {
324        let rotate_mat = self.metadata.rotate_mat.as_ref().unwrap();
325        rotate_mat
326            .values()
327            .as_any()
328            .downcast_ref::<T::ArrayType>()
329            .unwrap()
330            .as_slice()
331    }
332
333    #[inline]
334    fn rotate_mat<T: ArrowFloatType>(&'_ self) -> ndarray::ArrayView2<'_, T::Native> {
335        let code_dim = self.code_dim();
336        ndarray::ArrayView2::from_shape((code_dim, code_dim), self.rotate_mat_flat::<T>()).unwrap()
337    }
338
339    fn rotate_vectors<T: ArrowFloatType>(
340        &self,
341        vectors: ndarray::ArrayView2<'_, T::Native>,
342    ) -> ndarray::Array2<f32>
343    where
344        T::Native: AsPrimitive<f32>,
345    {
346        let dim = vectors.nrows();
347        let code_dim = self.code_dim();
348        match self.rotation_type() {
349            RQRotationType::Matrix => {
350                let rotate_mat = self.rotate_mat::<T>();
351                let rotate_mat = rotate_mat.slice(s![.., 0..dim]);
352                rotate_mat.dot(&vectors).mapv(|v| v.as_())
353            }
354            RQRotationType::Fast => {
355                let signs = self.fast_rotation_signs();
356                let ncols = vectors.ncols();
357                let mut rotated_data = vec![0.0f32; code_dim * ncols];
358                rotated_data
359                    .par_chunks_mut(code_dim)
360                    .enumerate()
361                    .for_each_init(
362                        || vec![0.0f32; code_dim],
363                        |scratch, (col_idx, dst)| {
364                            let column = vectors.column(col_idx);
365                            let input = column
366                                .as_slice()
367                                .expect("RabitQ input vectors should be contiguous");
368                            apply_fast_rotation(input, scratch, signs);
369                            dst.copy_from_slice(scratch);
370                        },
371                    );
372
373                ndarray::Array2::from_shape_vec((code_dim, ncols).f(), rotated_data).unwrap()
374            }
375        }
376    }
377
378    pub fn rotate_fsl_to_f32(&self, vectors: &FixedSizeListArray) -> Result<Vec<f32>> {
379        match vectors.value_type() {
380            DataType::Float16 => self.rotate_fsl_to_f32_typed::<Float16Type>(vectors),
381            DataType::Float32 => self.rotate_fsl_to_f32_typed::<Float32Type>(vectors),
382            DataType::Float64 => self.rotate_fsl_to_f32_typed::<Float64Type>(vectors),
383            value_type => Err(Error::invalid_input(format!(
384                "Unsupported data type: {:?}",
385                value_type
386            ))),
387        }
388    }
389
390    fn rotate_fsl_to_f32_typed<T: ArrowFloatType>(
391        &self,
392        vectors: &FixedSizeListArray,
393    ) -> Result<Vec<f32>>
394    where
395        T::Native: AsPrimitive<f32> + Sync,
396    {
397        let dim = self.dim();
398        if vectors.value_length() as usize != dim {
399            return Err(Error::invalid_input(format!(
400                "Vector dimension mismatch: {} != {}",
401                vectors.value_length(),
402                dim
403            )));
404        }
405        let values = vectors
406            .values()
407            .as_any()
408            .downcast_ref::<T::ArrayType>()
409            .ok_or_else(|| {
410                Error::invalid_input(format!(
411                    "Vector values have unexpected data type: {}",
412                    vectors.value_type()
413                ))
414            })?
415            .as_slice();
416        let vec_mat = ndarray::ArrayView2::from_shape((vectors.len(), dim), values)
417            .map_err(|e| Error::invalid_input(e.to_string()))?;
418        let rotated = self.rotate_vectors::<T>(vec_mat.t());
419        let code_dim = self.code_dim();
420        let mut row_major = vec![0.0f32; vectors.len() * code_dim];
421        for row_idx in 0..vectors.len() {
422            for (dst, value) in row_major[row_idx * code_dim..(row_idx + 1) * code_dim]
423                .iter_mut()
424                .zip(rotated.column(row_idx).iter())
425            {
426                *dst = *value;
427            }
428        }
429        Ok(row_major)
430    }
431
432    pub fn dim(&self) -> usize {
433        self.code_dim()
434    }
435
436    // compute the dot product of v_q * v_r
437    pub fn codes_res_dot_dists<T: ArrowFloatType>(
438        &self,
439        residual_vectors: &FixedSizeListArray,
440    ) -> Result<Vec<f32>>
441    where
442        T::Native: AsPrimitive<f32> + Sync,
443    {
444        let dim = self.dim();
445        if residual_vectors.value_length() as usize != dim {
446            return Err(Error::invalid_input(format!(
447                "Vector dimension mismatch: {} != {}",
448                residual_vectors.value_length(),
449                dim
450            )));
451        }
452
453        let sqrt_dim = (dim as f32).sqrt();
454        let values = residual_vectors
455            .values()
456            .as_any()
457            .downcast_ref::<T::ArrayType>()
458            .unwrap()
459            .as_slice();
460
461        match self.rotation_type() {
462            RQRotationType::Matrix => {
463                // convert the vector to a dxN matrix
464                let vec_mat =
465                    ndarray::ArrayView2::from_shape((residual_vectors.len(), dim), values)
466                        .map_err(|e| Error::invalid_input(e.to_string()))?;
467                let vec_mat = vec_mat.t();
468                let rotated_vectors = self.rotate_vectors::<T>(vec_mat);
469                let norm_dists = rotated_vectors.mapv(f32::abs).sum_axis(Axis(0)) / sqrt_dim;
470                debug_assert_eq!(norm_dists.len(), residual_vectors.len());
471                Ok(norm_dists.to_vec())
472            }
473            RQRotationType::Fast => {
474                let code_dim = self.code_dim();
475                let signs = self.fast_rotation_signs();
476                let mut norm_dists = vec![0.0f32; residual_vectors.len()];
477                norm_dists
478                    .par_iter_mut()
479                    .zip(values.par_chunks_exact(dim))
480                    .for_each_init(
481                        || vec![0.0f32; code_dim],
482                        |scratch, (dst, input)| {
483                            apply_fast_rotation(input, scratch, signs);
484                            *dst = scratch.iter().map(|v| v.abs()).sum::<f32>() / sqrt_dim;
485                        },
486                    );
487                Ok(norm_dists)
488            }
489        }
490    }
491
492    fn transform<T: ArrowFloatType>(
493        &self,
494        residual_vectors: &FixedSizeListArray,
495    ) -> Result<ArrayRef>
496    where
497        T::Native: AsPrimitive<f32> + Sync,
498    {
499        // we don't need to normalize the residual vectors,
500        // because the sign of P^{-1} * v_r is the same as P^{-1} * v_r / ||v_r||
501        let n = residual_vectors.len();
502        let dim = self.dim();
503        debug_assert_eq!(residual_vectors.values().len(), n * dim);
504        let values = residual_vectors
505            .values()
506            .as_any()
507            .downcast_ref::<T::ArrayType>()
508            .unwrap()
509            .as_slice();
510        let code_dim = self.code_dim();
511        let code_bytes = rabit_binary_code_bytes(code_dim);
512
513        match self.rotation_type() {
514            RQRotationType::Matrix => {
515                let vectors = ndarray::ArrayView2::from_shape((n, dim), values)
516                    .map_err(|e| Error::invalid_input(e.to_string()))?;
517                let vectors = vectors.t();
518                let rotated_vectors = self.rotate_vectors::<T>(vectors);
519
520                let quantized_vectors = rotated_vectors.t().mapv(|v| v.is_sign_positive());
521                let bv: BitVec<u8, Lsb0> = BitVec::from_iter(quantized_vectors);
522
523                let codes = UInt8Array::from(bv.into_vec());
524                debug_assert_eq!(codes.len(), n * code_bytes);
525                Ok(Arc::new(FixedSizeListArray::try_new_from_values(
526                    codes,
527                    code_bytes as i32,
528                )?))
529            }
530            RQRotationType::Fast => {
531                let signs = self.fast_rotation_signs();
532                let mut encoded_codes = vec![0u8; n * code_bytes];
533                encoded_codes
534                    .par_chunks_mut(code_bytes)
535                    .zip(values.par_chunks_exact(dim))
536                    .for_each_init(
537                        || vec![0.0f32; code_dim],
538                        |scratch, (code_dst, input)| {
539                            apply_fast_rotation(input, scratch, signs);
540                            pack_sign_bits(code_dst, scratch);
541                        },
542                    );
543                let codes = UInt8Array::from(encoded_codes);
544                debug_assert_eq!(codes.len(), n * code_bytes);
545                Ok(Arc::new(FixedSizeListArray::try_new_from_values(
546                    codes,
547                    code_bytes as i32,
548                )?))
549            }
550        }
551    }
552
553    pub(crate) fn quantize_split(
554        &self,
555        vectors: &FixedSizeListArray,
556    ) -> Result<RabitQuantizedBatch> {
557        match vectors.value_type() {
558            DataType::Float16 => self.transform_split::<Float16Type>(vectors),
559            DataType::Float32 => self.transform_split::<Float32Type>(vectors),
560            DataType::Float64 => self.transform_split::<Float64Type>(vectors),
561            value_type => Err(Error::invalid_input(format!(
562                "Unsupported data type: {:?}",
563                value_type
564            ))),
565        }
566    }
567
568    fn transform_split<T: ArrowFloatType>(
569        &self,
570        residual_vectors: &FixedSizeListArray,
571    ) -> Result<RabitQuantizedBatch>
572    where
573        T::Native: AsPrimitive<f32> + Sync,
574    {
575        let ex_bits = rabit_ex_bits(self.metadata.num_bits)?;
576        let n = residual_vectors.len();
577        let dim = self.dim();
578        debug_assert_eq!(residual_vectors.values().len(), n * dim);
579        let values = residual_vectors
580            .values()
581            .as_any()
582            .downcast_ref::<T::ArrayType>()
583            .unwrap()
584            .as_slice();
585        let code_dim = self.code_dim();
586        let code_bytes = rabit_binary_code_bytes(code_dim);
587        let ex_code_bytes = if ex_bits == 0 {
588            0
589        } else {
590            crate::vector::bq::ex_dot::blocked_ex_code_bytes(code_dim, ex_bits)
591        };
592
593        let mut encoded_codes = vec![0u8; n * code_bytes];
594        let mut encoded_ex_codes = (ex_bits != 0).then(|| vec![0u8; n * ex_code_bytes]);
595        let mut ex_res_dot_dists = (ex_bits != 0).then(|| vec![0.0f32; n]);
596        let mut rotated_residuals = vec![0.0f32; n * code_dim];
597        let mut ex_code_values = (ex_bits != 0).then(|| vec![0u8; n * code_dim]);
598
599        match self.rotation_type() {
600            RQRotationType::Matrix => {
601                let vectors = ndarray::ArrayView2::from_shape((n, dim), values)
602                    .map_err(|e| Error::invalid_input(e.to_string()))?;
603                let vectors = vectors.t();
604                let rotated_vectors = self.rotate_vectors::<T>(vectors);
605
606                encoded_codes
607                    .chunks_mut(code_bytes)
608                    .zip(rotated_residuals.chunks_mut(code_dim))
609                    .enumerate()
610                    .for_each(|(row_idx, (code_dst, rotated_dst))| {
611                        for (dst, value) in rotated_dst
612                            .iter_mut()
613                            .zip(rotated_vectors.column(row_idx).iter())
614                        {
615                            *dst = *value;
616                        }
617                        pack_sign_bits(code_dst, rotated_dst);
618                    });
619            }
620            RQRotationType::Fast => {
621                let signs = self.fast_rotation_signs();
622                encoded_codes
623                    .par_chunks_mut(code_bytes)
624                    .zip(rotated_residuals.par_chunks_mut(code_dim))
625                    .zip(values.par_chunks_exact(dim))
626                    .for_each(|((code_dst, rotated_dst), input)| {
627                        apply_fast_rotation(input, rotated_dst, signs);
628                        pack_sign_bits(code_dst, rotated_dst);
629                    });
630            }
631        }
632
633        if ex_bits != 0 {
634            let encoded_ex_codes = encoded_ex_codes
635                .as_mut()
636                .expect("ex-code buffer should exist for multi-bit RQ");
637            let ex_res_dot_dists = ex_res_dot_dists
638                .as_mut()
639                .expect("ex dot buffer should exist for multi-bit RQ");
640            let ex_code_values = ex_code_values
641                .as_mut()
642                .expect("ex-code value buffer should exist for multi-bit RQ");
643            encoded_ex_codes
644                .par_chunks_mut(ex_code_bytes)
645                .zip(ex_code_values.par_chunks_mut(code_dim))
646                .zip(ex_res_dot_dists.par_iter_mut())
647                .zip(rotated_residuals.par_chunks(code_dim))
648                .for_each(|(((ex_dst, ex_values_dst), ex_dot_dst), rotated)| {
649                    *ex_dot_dst = quantize_ex_code(rotated, ex_bits, ex_dst, ex_values_dst);
650                });
651        }
652
653        let binary_codes = UInt8Array::from(encoded_codes);
654        let ex_codes = encoded_ex_codes.map(UInt8Array::from);
655        Ok(RabitQuantizedBatch {
656            binary_codes: Arc::new(FixedSizeListArray::try_new_from_values(
657                binary_codes,
658                code_bytes as i32,
659            )?),
660            ex_codes: ex_codes
661                .map(|ex_codes| {
662                    FixedSizeListArray::try_new_from_values(ex_codes, ex_code_bytes as i32)
663                        .map(|array| Arc::new(array) as ArrayRef)
664                })
665                .transpose()?,
666            ex_res_dot_dists,
667            rotated_residuals: Some(rotated_residuals),
668            ex_code_values,
669        })
670    }
671}
672
673impl Quantization for RabitQuantizer {
674    type BuildParams = RQBuildParams;
675    type Metadata = RabitQuantizationMetadata;
676    type Storage = RabitQuantizationStorage;
677
678    fn build(
679        data: &dyn Array,
680        _: lance_linalg::distance::DistanceType,
681        params: &Self::BuildParams,
682    ) -> Result<Self> {
683        validate_rq_num_bits(params.num_bits)?;
684
685        let dim = data.as_fixed_size_list().value_length() as usize;
686        if !dim.is_multiple_of(u8::BITS as usize) {
687            return Err(Error::invalid_input(
688                "vector dimension must be divisible by 8 for IVF_RQ",
689            ));
690        }
691        if let Some(q) = Self::from_supplied_rotation(params, dim)? {
692            return Ok(q);
693        }
694
695        let q = match data.as_fixed_size_list().value_type() {
696            DataType::Float16 => Self::new_with_rotation::<Float16Type>(
697                params.num_bits,
698                data.as_fixed_size_list().value_length(),
699                params.rotation_type,
700            ),
701            DataType::Float32 => Self::new_with_rotation::<Float32Type>(
702                params.num_bits,
703                data.as_fixed_size_list().value_length(),
704                params.rotation_type,
705            ),
706            DataType::Float64 => Self::new_with_rotation::<Float64Type>(
707                params.num_bits,
708                data.as_fixed_size_list().value_length(),
709                params.rotation_type,
710            ),
711            dt => {
712                return Err(Error::invalid_input(format!(
713                    "Unsupported data type: {:?}",
714                    dt
715                )));
716            }
717        };
718        Ok(q)
719    }
720
721    fn retrain(&mut self, _data: &dyn Array) -> Result<()> {
722        Ok(())
723    }
724
725    fn code_dim(&self) -> usize {
726        if self.metadata.code_dim > 0 {
727            self.metadata.code_dim as usize
728        } else {
729            self.metadata
730                .rotate_mat
731                .as_ref()
732                .map(|rotate_mat| rotate_mat.len())
733                .unwrap_or(0)
734        }
735    }
736
737    fn column(&self) -> &'static str {
738        RABIT_CODE_COLUMN
739    }
740
741    fn use_residual(_: lance_linalg::distance::DistanceType) -> bool {
742        true
743    }
744
745    fn quantize(&self, vectors: &dyn Array) -> Result<arrow_array::ArrayRef> {
746        let vectors = vectors.as_fixed_size_list();
747        match vectors.value_type() {
748            DataType::Float16 => self.transform::<Float16Type>(vectors),
749            DataType::Float32 => self.transform::<Float32Type>(vectors),
750            DataType::Float64 => self.transform::<Float64Type>(vectors),
751            value_type => Err(Error::invalid_input(format!(
752                "Unsupported data type: {:?}",
753                value_type
754            ))),
755        }
756    }
757
758    fn metadata_key() -> &'static str {
759        RABIT_METADATA_KEY
760    }
761
762    fn quantization_type() -> crate::vector::quantizer::QuantizationType {
763        crate::vector::quantizer::QuantizationType::Rabit
764    }
765
766    fn metadata(
767        &self,
768        args: Option<crate::vector::quantizer::QuantizationMetadata>,
769    ) -> Self::Metadata {
770        let mut metadata = self.metadata.clone();
771        metadata.packed = args.map(|args| args.transposed).unwrap_or_default();
772        metadata
773    }
774
775    fn from_metadata(
776        metadata: &Self::Metadata,
777        _: lance_linalg::distance::DistanceType,
778    ) -> Result<Quantizer> {
779        validate_rq_num_bits(metadata.num_bits)?;
780        Ok(Quantizer::Rabit(Self {
781            metadata: metadata.clone(),
782        }))
783    }
784
785    fn field(&self) -> Field {
786        rabit_binary_code_field(self.code_dim())
787    }
788
789    fn extra_fields(&self) -> Vec<Field> {
790        let mut fields = vec![ADD_FACTORS_FIELD.clone(), SCALE_FACTORS_FIELD.clone()];
791        if self.metadata.query_estimator == RabitQueryEstimator::RawQuery {
792            fields.push(ERROR_FACTORS_FIELD.clone());
793        }
794        if let Some(ex_code_field) = rabit_ex_code_field(self.code_dim(), self.metadata.num_bits)
795            .expect("RabitQ num_bits should be validated")
796        {
797            fields.push(ex_code_field);
798            fields.push(EX_ADD_FACTORS_FIELD.clone());
799            fields.push(EX_SCALE_FACTORS_FIELD.clone());
800        }
801        fields
802    }
803}
804
805impl TryFrom<Quantizer> for RabitQuantizer {
806    type Error = Error;
807
808    fn try_from(quantizer: Quantizer) -> Result<Self> {
809        match quantizer {
810            Quantizer::Rabit(quantizer) => Ok(quantizer),
811            _ => Err(Error::invalid_input(
812                "Cannot convert non-RabitQuantizer to RabitQuantizer",
813            )),
814        }
815    }
816}
817
818impl From<RabitQuantizer> for Quantizer {
819    fn from(quantizer: RabitQuantizer) -> Self {
820        Self::Rabit(quantizer)
821    }
822}
823
824fn random_normal_matrix(n: usize) -> ndarray::Array2<f64> {
825    let mut rng = rand::rng();
826    let normal = rand_distr::Normal::new(0.0, 1.0).unwrap();
827    ndarray::Array2::from_shape_simple_fn((n, n), || normal.sample(&mut rng))
828}
829
830// implement the householder qr decomposition referenced from https://en.wikipedia.org/wiki/Householder_transformation#QR_decomposition
831fn householder_qr(a: ndarray::Array2<f64>) -> (ndarray::Array2<f64>, ndarray::Array2<f64>) {
832    let (m, n) = a.dim();
833    let mut q = ndarray::Array2::eye(m);
834    let mut r = a;
835
836    for k in 0..n.min(m - 1) {
837        let mut x = r.slice(s![k.., k]).to_owned();
838        let x_norm = x.dot(&x).sqrt();
839
840        if x_norm < f64::EPSILON {
841            continue;
842        }
843
844        // Create Householder vector
845        let sign = if x[0] >= 0.0 { 1.0 } else { -1.0 };
846        x[0] += sign * x_norm;
847        let u = &x / x.dot(&x).sqrt();
848
849        // Apply Householder transformation to R
850        // Compute outer product manually
851        let mut u_outer = ndarray::Array2::zeros((m - k, m - k));
852        for i in 0..(m - k) {
853            for j in 0..(m - k) {
854                u_outer[[i, j]] = u[i] * u[j];
855            }
856        }
857        let h = ndarray::Array2::eye(m - k) - 2.0 * u_outer;
858
859        // Apply transformation to R
860        let r_block = r.slice(s![k.., k..]).to_owned();
861        let h_r = h.dot(&r_block);
862        r.slice_mut(s![k.., k..]).assign(&h_r);
863
864        // Apply transformation to Q
865        let q_block = q.slice(s![.., k..]).to_owned();
866        let q_h = q_block.dot(&h);
867        q.slice_mut(s![.., k..]).assign(&q_h);
868    }
869
870    (q, r)
871}
872
873fn random_orthogonal<T: ArrowFloatType>(n: usize) -> ndarray::Array2<T::Native>
874where
875    T::Native: FromPrimitive,
876{
877    let a = random_normal_matrix(n);
878    let (q, _) = householder_qr(a);
879
880    // cast f64 matrix to T::Native matrix
881    q.mapv(|v| T::Native::from_f64(v).unwrap())
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use approx::assert_relative_eq;
888    use arrow::datatypes::Float32Type;
889    use arrow_array::{FixedSizeListArray, Float32Array};
890    use lance_linalg::distance::DistanceType;
891    use rstest::rstest;
892
893    use crate::vector::bq::storage::RABIT_BLOCKED_EX_CODE_COLUMN;
894
895    #[rstest]
896    #[case(8)]
897    #[case(16)]
898    #[case(32)]
899    fn test_householder_qr(#[case] n: usize) {
900        let a = random_normal_matrix(n);
901        let (m, n) = a.dim();
902
903        let (q, r) = householder_qr(a.clone());
904
905        // Check Q is orthogonal: Q^T * Q should be identity
906        let q_t_q = q.t().dot(&q);
907        for i in 0..m {
908            for j in 0..m {
909                let expected = if i == j { 1.0 } else { 0.0 };
910                assert_relative_eq!(q_t_q[[i, j]], expected, epsilon = 1e-5);
911            }
912        }
913
914        // Check QR decomposition: Q * R should equal original matrix
915        let qr = q.dot(&r);
916        for i in 0..m {
917            for j in 0..n {
918                assert_relative_eq!(qr[[i, j]], a[[i, j]], epsilon = 1e-5);
919            }
920        }
921
922        // Check R is upper triangular
923        for i in 1..n.min(m) {
924            for j in 0..i {
925                assert_relative_eq!(r[[i, j]], 0.0, epsilon = 1e-5);
926            }
927        }
928
929        // Additional check: Q should have shape (m, m) and R should have shape (m, n)
930        assert_eq!(q.dim(), (m, m));
931        assert_eq!(r.dim(), (m, n));
932    }
933
934    #[test]
935    fn test_rabit_quantizer_rotation_modes() {
936        let fast_q = RabitQuantizer::new_with_rotation::<Float32Type>(1, 128, RQRotationType::Fast);
937        assert_eq!(fast_q.rotation_type(), RQRotationType::Fast);
938        assert_eq!(fast_q.dim(), 128);
939        assert_eq!(fast_q.code_dim(), 128);
940
941        let matrix_q =
942            RabitQuantizer::new_with_rotation::<Float32Type>(1, 128, RQRotationType::Matrix);
943        assert_eq!(matrix_q.rotation_type(), RQRotationType::Matrix);
944        assert_eq!(matrix_q.dim(), 128);
945        assert_eq!(matrix_q.code_dim(), 128);
946    }
947
948    #[test]
949    fn test_rabit_quantizer_field_uses_binary_code_size() {
950        let q = RabitQuantizer::new_with_rotation::<Float32Type>(1, 128, RQRotationType::Fast);
951        let field = q.field();
952        let DataType::FixedSizeList(_, code_bytes) = field.data_type() else {
953            panic!("RabitQ code field should be FixedSizeList");
954        };
955        assert_eq!(*code_bytes, 16);
956    }
957
958    #[test]
959    fn test_rabit_quantizer_extra_fields_include_raw_query_error_factor() {
960        let q = RabitQuantizer::new_with_rotation::<Float32Type>(1, 128, RQRotationType::Fast);
961        let fields = q.extra_fields();
962        assert!(
963            fields
964                .iter()
965                .any(|field| field.name() == ERROR_FACTORS_FIELD.name())
966        );
967        assert!(
968            !fields
969                .iter()
970                .any(|field| field.name() == RABIT_BLOCKED_EX_CODE_COLUMN)
971        );
972
973        let q = RabitQuantizer::new_with_rotation::<Float32Type>(3, 128, RQRotationType::Fast);
974        let fields = q.extra_fields();
975        for expected in [
976            ERROR_FACTORS_FIELD.name().as_str(),
977            RABIT_BLOCKED_EX_CODE_COLUMN,
978            EX_ADD_FACTORS_FIELD.name().as_str(),
979            EX_SCALE_FACTORS_FIELD.name().as_str(),
980        ] {
981            assert!(
982                fields.iter().any(|field| field.name().as_str() == expected),
983                "missing {expected}"
984            );
985        }
986    }
987
988    #[test]
989    fn test_rabit_quantizer_requires_dim_divisible_by_8() {
990        let vectors = Float32Array::from(vec![0.0f32; 4 * 30]);
991        let fsl = FixedSizeListArray::try_new_from_values(vectors, 30).unwrap();
992        let params = RQBuildParams::new(1);
993
994        let err = RabitQuantizer::build(&fsl, DistanceType::L2, &params).unwrap_err();
995        assert!(
996            err.to_string()
997                .contains("vector dimension must be divisible by 8 for IVF_RQ"),
998            "{}",
999            err
1000        );
1001    }
1002
1003    #[test]
1004    fn test_rabit_quantizer_reuses_supplied_rotation() {
1005        let vectors = Float32Array::from(vec![0.0f32; 4 * 32]);
1006        let fsl = FixedSizeListArray::try_new_from_values(vectors, 32).unwrap();
1007        let supplied =
1008            RabitQuantizer::new_with_rotation::<Float32Type>(3, 32, RQRotationType::Fast)
1009                .metadata(None);
1010        let supplied_signs = supplied.fast_rotation_signs.clone();
1011
1012        let mut params = RQBuildParams::with_rotation_type(3, RQRotationType::Fast);
1013        params.rotation = Some(supplied);
1014
1015        let quantizer = RabitQuantizer::build(&fsl, DistanceType::L2, &params).unwrap();
1016        let metadata = quantizer.metadata_ref();
1017        assert_eq!(metadata.num_bits, 3);
1018        assert_eq!(metadata.rotation_type, RQRotationType::Fast);
1019        assert_eq!(metadata.fast_rotation_signs, supplied_signs);
1020    }
1021
1022    #[test]
1023    fn test_rabit_quantizer_validates_supplied_rotation() {
1024        let vectors = Float32Array::from(vec![0.0f32; 4 * 32]);
1025        let fsl = FixedSizeListArray::try_new_from_values(vectors, 32).unwrap();
1026        let supplied =
1027            RabitQuantizer::new_with_rotation::<Float32Type>(3, 32, RQRotationType::Fast)
1028                .metadata(None);
1029
1030        let mut wrong_num_bits = supplied.clone();
1031        wrong_num_bits.num_bits = 1;
1032        let mut params = RQBuildParams::with_rotation_type(3, RQRotationType::Fast);
1033        params.rotation = Some(wrong_num_bits);
1034        let err = RabitQuantizer::build(&fsl, DistanceType::L2, &params).unwrap_err();
1035        assert!(
1036            err.to_string()
1037                .contains("does not match requested num_bits")
1038        );
1039
1040        let mut wrong_dim = supplied.clone();
1041        wrong_dim.code_dim = 64;
1042        let mut params = RQBuildParams::with_rotation_type(3, RQRotationType::Fast);
1043        params.rotation = Some(wrong_dim);
1044        let err = RabitQuantizer::build(&fsl, DistanceType::L2, &params).unwrap_err();
1045        assert!(err.to_string().contains("does not match vector dimension"));
1046
1047        let mut wrong_sign_len = supplied;
1048        wrong_sign_len.fast_rotation_signs.as_mut().unwrap().pop();
1049        let mut params = RQBuildParams::with_rotation_type(3, RQRotationType::Fast);
1050        params.rotation = Some(wrong_sign_len);
1051        let err = RabitQuantizer::build(&fsl, DistanceType::L2, &params).unwrap_err();
1052        assert!(err.to_string().contains("fast_rotation_signs length"));
1053    }
1054
1055    #[test]
1056    fn test_rabit_quantizer_accepts_multi_bit_range() {
1057        let vectors = Float32Array::from(vec![0.0f32; 4 * 32]);
1058        let fsl = FixedSizeListArray::try_new_from_values(vectors, 32).unwrap();
1059
1060        let err =
1061            RabitQuantizer::build(&fsl, DistanceType::L2, &RQBuildParams::new(0)).unwrap_err();
1062        assert!(
1063            err.to_string().contains("IVF_RQ num_bits must be in"),
1064            "{}",
1065            err
1066        );
1067
1068        for rotation_type in [RQRotationType::Fast, RQRotationType::Matrix] {
1069            let quantizer = RabitQuantizer::build(
1070                &fsl,
1071                DistanceType::L2,
1072                &RQBuildParams::with_rotation_type(9, rotation_type),
1073            )
1074            .unwrap();
1075            let quantized = quantizer.quantize_split(&fsl).unwrap();
1076            assert!(quantized.ex_codes.is_some());
1077            assert_eq!(
1078                quantized.binary_codes.as_fixed_size_list().value_length(),
1079                4
1080            );
1081            assert_eq!(
1082                quantized
1083                    .ex_codes
1084                    .unwrap()
1085                    .as_fixed_size_list()
1086                    .value_length(),
1087                // dim=32 is padded to one 64-dim block at ex_bits=8.
1088                64
1089            );
1090        }
1091
1092        let err =
1093            RabitQuantizer::build(&fsl, DistanceType::L2, &RQBuildParams::new(10)).unwrap_err();
1094        assert!(
1095            err.to_string().contains("IVF_RQ num_bits must be in"),
1096            "{}",
1097            err
1098        );
1099    }
1100}