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