Skip to main content

zeppelin_embed/
quant.rs

1//! Training-free vector quantization.
2
3mod bits4;
4mod int8;
5mod rescore;
6
7/// Actual query quantizer calls on an explicitly observed caller thread.
8#[cfg(any(test, feature = "test-seams"))]
9#[derive(Default, Debug)]
10#[doc(hidden)]
11pub struct QueryPreparationTestObservations {
12    /// Logical dimensions and rounding seed at each Bit4 preparation.
13    pub bit4: Vec<(usize, u64)>,
14    /// Logical dimensions at each affine Int8 preparation.
15    pub int8: Vec<usize>,
16}
17
18#[cfg(any(test, feature = "test-seams"))]
19thread_local! {
20    static QUERY_PREPARATIONS: std::cell::RefCell<Option<QueryPreparationTestObservations>> =
21        const { std::cell::RefCell::new(None) };
22}
23
24/// Starts observing actual quantizer calls; ordinary execution retains no trace.
25#[cfg(any(test, feature = "test-seams"))]
26#[doc(hidden)]
27pub fn begin_query_preparation_test_observations() {
28    QUERY_PREPARATIONS
29        .with(|calls| *calls.borrow_mut() = Some(QueryPreparationTestObservations::default()));
30}
31
32/// Returns recorded calls and disables collection.
33#[cfg(any(test, feature = "test-seams"))]
34#[doc(hidden)]
35pub fn take_query_preparation_test_observations() -> QueryPreparationTestObservations {
36    QUERY_PREPARATIONS.with(|calls| calls.take().unwrap_or_default())
37}
38
39pub(crate) use bits4::{Bit4ControlError, est_dot_bit4_controlled, prepare_bit4_query_controlled};
40pub use bits4::{
41    Bit4Factors, Bit4Query, dequantize_bit4, est_dot_bit4, est_dot_bit4_batch, prepare_bit4_query,
42    quantize_bit4,
43};
44pub use int8::{
45    Int8Query, Int8Vec, dequantize_int8, dot_int8_query, prepare_int8_query, quantize_int8,
46};
47pub(crate) use rescore::{
48    ExactScoreReuse, RescoreCheckError, exact_squared_l2_with_sink, rescore_top_k_reusing,
49    squared_l2_f64,
50};
51pub use rescore::{
52    RescoreError, RescoreHit, RescoreMetric, RescorePool, RescoreResult, SearchByteCounts,
53    rescore_top_k,
54};
55
56/// Typed failure from a training-free quantizer or estimator.
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub enum QuantError {
59    /// Scratch or output backing could not be allocated.
60    Allocation,
61    /// The input dimension is zero, for which a code direction is undefined.
62    EmptyVector,
63    /// The vector exceeds the dimension supported by the reused i8 kernel.
64    DimensionTooLarge {
65        /// Supplied coordinate count.
66        actual: usize,
67        /// Largest accepted coordinate count.
68        maximum: usize,
69    },
70    /// A coordinate was NaN or infinite.
71    NonFinite {
72        /// Zero-based coordinate of the first rejected value.
73        index: usize,
74    },
75    /// A caller-declared L2-normalized vector was outside the unit-norm band.
76    NonUnitNorm {
77        /// Computed squared L2 norm as IEEE binary64 bits.
78        squared_norm_bits: u64,
79        /// Absolute permitted deviation from one as IEEE binary64 bits.
80        tolerance_bits: u64,
81    },
82    /// A caller-owned output buffer had the wrong byte length.
83    OutputLength {
84        /// Required byte count.
85        expected: usize,
86        /// Supplied byte count.
87        actual: usize,
88    },
89    /// A packed row had the wrong byte length for the query dimension.
90    CodeLength {
91        /// Required byte count.
92        expected: usize,
93        /// Supplied byte count.
94        actual: usize,
95    },
96    /// Unused low-order fields in a final partial byte were not zero.
97    NonZeroPadding {
98        /// Final byte containing non-canonical padding.
99        byte: u8,
100        /// Mask selecting the unused low-order bits.
101        mask: u8,
102    },
103}
104
105impl std::fmt::Display for QuantError {
106    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::Allocation => formatter.write_str("quantization allocation failed"),
109            Self::EmptyVector => formatter.write_str("quantization vector must not be empty"),
110            Self::DimensionTooLarge { actual, maximum } => write!(
111                formatter,
112                "quantization dimension {actual} exceeds supported maximum {maximum}"
113            ),
114            Self::NonFinite { index } => {
115                write!(
116                    formatter,
117                    "quantization input is non-finite at coordinate {index}"
118                )
119            }
120            Self::NonUnitNorm {
121                squared_norm_bits,
122                tolerance_bits,
123            } => write!(
124                formatter,
125                "declared L2-normalized vector has squared norm {}; expected 1 within {}",
126                f64::from_bits(*squared_norm_bits),
127                f64::from_bits(*tolerance_bits)
128            ),
129            Self::OutputLength { expected, actual } => write!(
130                formatter,
131                "quantization output length mismatch: expected {expected}, got {actual}"
132            ),
133            Self::CodeLength { expected, actual } => write!(
134                formatter,
135                "packed quantization code length mismatch: expected {expected}, got {actual}"
136            ),
137            Self::NonZeroPadding { byte, mask } => write!(
138                formatter,
139                "packed quantization code has non-zero padding: byte={byte:#04x}, mask={mask:#04x}"
140            ),
141        }
142    }
143}
144
145impl std::error::Error for QuantError {}
146
147/// Persisted quantization scheme identifier.
148///
149/// Discriminants are an append-only storage contract. They must never be
150/// reordered, renumbered, or reused. Ids 3 and 5 are retired and permanently
151/// reserved by `docs/adr/ADR-002-retire-bit1-bit2.md`.
152#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
153#[repr(u8)]
154pub enum QuantScheme {
155    /// Uncompressed IEEE binary32 coordinates.
156    F32 = 0,
157    /// IEEE binary16 coordinates.
158    F16 = 1,
159    /// Signed eight-bit scalar quantization.
160    Int8 = 2,
161    /// Four-bit Extended-RaBitQ codes.
162    #[default]
163    Bit4 = 4,
164}
165
166impl QuantScheme {
167    /// Returns the permanent persisted identifier.
168    #[must_use]
169    pub const fn id(self) -> u8 {
170        self as u8
171    }
172
173    /// Decodes a permanent persisted identifier.
174    #[must_use]
175    pub const fn from_id(id: u8) -> Option<Self> {
176        match id {
177            0 => Some(Self::F32),
178            1 => Some(Self::F16),
179            2 => Some(Self::Int8),
180            4 => Some(Self::Bit4),
181            _ => None,
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests;
188
189#[cfg(test)]
190pub(crate) use rescore::rescore_top_k_with_check;