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