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