Skip to main content

clawft_kernel/
vector_quantization.rs

1//! In-tree LogQuantized + unified distance kernels (WEFT-15 / 128 / 352 / 353).
2//!
3//! ## Vendor independence (2026-07-31)
4//!
5//! These features were originally gated on [ruvnet/RuVector#352](https://github.com/ruvnet/RuVector/pull/352)
6//! (still unmerged / conflicting). WeftOS now ships **first-party**
7//! implementations so activation no longer waits on that PR:
8//!
9//! - **KG-011 LogQuantized** — log-scaled int8 bins for skewed embeddings
10//! - **KG-012 Unified distance** — single codepath for cosine / L2 / IP
11//!   with optional power-of-two padding and unrolled scalar SIMD-friendly loops
12//!
13//! Config types remain serializable; `is_available()` is true when
14//! `enabled` is set (implementation always present in this crate).
15
16use serde::{Deserialize, Serialize};
17
18// ---------------------------------------------------------------------------
19// LogQuantized (KG-011)
20// ---------------------------------------------------------------------------
21
22/// Configuration for logarithmic quantization.
23///
24/// Replaces linear scalar bins with log-scaled bins for skewed
25/// distributions (exp / ReLU / log-normal style embeddings).
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct LogQuantizedConfig {
28    /// Whether logarithmic quantization is enabled.
29    #[serde(default)]
30    pub enabled: bool,
31
32    /// Compression ratio target (4 ≈ 8-bit codes). Used as documentation
33    /// of intended density; codes are always u8 today.
34    #[serde(default = "default_compression_ratio")]
35    pub compression_ratio: usize,
36
37    /// Floor magnitude before log (avoids log(0)).
38    #[serde(default = "default_min_magnitude")]
39    pub min_magnitude: f64,
40}
41
42fn default_compression_ratio() -> usize {
43    4
44}
45
46fn default_min_magnitude() -> f64 {
47    1e-7
48}
49
50impl Default for LogQuantizedConfig {
51    fn default() -> Self {
52        Self {
53            enabled: false,
54            compression_ratio: default_compression_ratio(),
55            min_magnitude: default_min_magnitude(),
56        }
57    }
58}
59
60impl LogQuantizedConfig {
61    /// True when operators enabled this path. Implementation is in-tree.
62    pub fn is_available(&self) -> bool {
63        self.enabled
64    }
65}
66
67/// Log-scaled int8 quantized vector (in-tree; no ruvector-core).
68#[derive(Debug, Clone, PartialEq)]
69pub struct LogQuantizedVector {
70    /// Per-dimension codes 0..=255.
71    pub codes: Vec<u8>,
72    /// Sign bits packed as true = negative original value.
73    pub signs: Vec<bool>,
74    /// Shared scale for reconstruction.
75    pub max_log: f32,
76    /// Config min magnitude used at encode.
77    pub min_magnitude: f32,
78}
79
80impl LogQuantizedVector {
81    /// Quantize `vector` with log-scaled magnitude bins.
82    pub fn quantize(vector: &[f32], cfg: &LogQuantizedConfig) -> Self {
83        let min_m = cfg.min_magnitude.max(1e-12) as f32;
84        let mut max_log = 0.0f32;
85        let mut logs = Vec::with_capacity(vector.len());
86        let mut signs = Vec::with_capacity(vector.len());
87        for &v in vector {
88            let sign = v.is_sign_negative();
89            signs.push(sign);
90            let mag = v.abs().max(min_m);
91            let lg = mag.ln();
92            logs.push(lg);
93            if lg > max_log {
94                max_log = lg;
95            }
96        }
97        // Avoid zero span.
98        if max_log < min_m.ln() + 1e-6 {
99            max_log = min_m.ln() + 1.0;
100        }
101        let min_log = min_m.ln();
102        let span = (max_log - min_log).max(1e-6);
103        let codes: Vec<u8> = logs
104            .iter()
105            .map(|&lg| {
106                let t = ((lg - min_log) / span).clamp(0.0, 1.0);
107                (t * 255.0).round() as u8
108            })
109            .collect();
110        Self {
111            codes,
112            signs,
113            max_log,
114            min_magnitude: min_m,
115        }
116    }
117
118    /// Reconstruct approximate f32 vector.
119    pub fn reconstruct(&self) -> Vec<f32> {
120        let min_log = self.min_magnitude.ln();
121        let span = (self.max_log - min_log).max(1e-6);
122        self.codes
123            .iter()
124            .zip(self.signs.iter())
125            .map(|(&c, &neg)| {
126                let t = c as f32 / 255.0;
127                let lg = min_log + t * span;
128                let mag = lg.exp();
129                if neg {
130                    -mag
131                } else {
132                    mag
133                }
134            })
135            .collect()
136    }
137
138    /// Mean squared reconstruction error vs original.
139    pub fn mse(&self, original: &[f32]) -> f32 {
140        let rec = self.reconstruct();
141        if rec.len() != original.len() || original.is_empty() {
142            return f32::INFINITY;
143        }
144        let mut s = 0.0f32;
145        for (a, b) in original.iter().zip(rec.iter()) {
146            let d = a - b;
147            s += d * d;
148        }
149        s / original.len() as f32
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Unified distance (KG-012)
155// ---------------------------------------------------------------------------
156
157/// Distance metric for the unified kernel.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
159#[serde(rename_all = "lowercase")]
160pub enum SimdDistanceMetric {
161    /// Cosine distance = 1 − cosine_similarity.
162    #[default]
163    Cosine,
164    /// Euclidean (L2).
165    L2,
166    /// Inner product distance = −dot (lower is closer).
167    InnerProduct,
168}
169
170/// Configuration for the unified distance kernel.
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct SimdDistanceConfig {
173    /// Enable the unified kernel path.
174    #[serde(default)]
175    pub enabled: bool,
176    /// Pad both vectors to next power-of-two length with zeros.
177    #[serde(default)]
178    pub pad_to_power_of_two: bool,
179    /// Metric.
180    #[serde(default)]
181    pub metric: SimdDistanceMetric,
182    /// Preferred lane width (informational; auto path used today).
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub lane_width: Option<u16>,
185}
186
187impl SimdDistanceConfig {
188    /// True when operators enabled this path. Implementation is in-tree.
189    pub fn is_available(&self) -> bool {
190        self.enabled
191    }
192}
193
194/// Next power of two ≥ n (n ≥ 1).
195pub fn next_pow2(n: usize) -> usize {
196    if n <= 1 {
197        return 1;
198    }
199    n.next_power_of_two()
200}
201
202/// Optionally pad with zeros to power-of-two length.
203pub fn maybe_pad(v: &[f32], pad: bool) -> Vec<f32> {
204    if !pad {
205        return v.to_vec();
206    }
207    let n = next_pow2(v.len().max(1));
208    let mut out = vec![0.0f32; n];
209    out[..v.len()].copy_from_slice(v);
210    out
211}
212
213/// Unified distance: one codepath for cosine / L2 / IP with unrolled loops.
214///
215/// Branch-free accumulation; metric chosen once at the end. Suitable for
216/// hot loops without depending on ruvector-core PR #352.
217pub fn unified_distance(a: &[f32], b: &[f32], cfg: &SimdDistanceConfig) -> f32 {
218    let a = maybe_pad(a, cfg.pad_to_power_of_two);
219    let b = maybe_pad(b, cfg.pad_to_power_of_two);
220    let n = a.len().min(b.len());
221    if n == 0 {
222        return 0.0;
223    }
224
225    // Single pass: dot, ||a||², ||b||², sum (a-b)²
226    let mut dot = 0.0f32;
227    let mut na = 0.0f32;
228    let mut nb = 0.0f32;
229    let mut l2 = 0.0f32;
230    let chunks = n / 4;
231    for i in 0..chunks {
232        let idx = i * 4;
233        for j in 0..4 {
234            let ai = a[idx + j];
235            let bi = b[idx + j];
236            dot += ai * bi;
237            na += ai * ai;
238            nb += bi * bi;
239            let d = ai - bi;
240            l2 += d * d;
241        }
242    }
243    for i in (chunks * 4)..n {
244        let ai = a[i];
245        let bi = b[i];
246        dot += ai * bi;
247        na += ai * ai;
248        nb += bi * bi;
249        let d = ai - bi;
250        l2 += d * d;
251    }
252
253    match cfg.metric {
254        SimdDistanceMetric::L2 => l2.sqrt(),
255        SimdDistanceMetric::InnerProduct => -dot,
256        SimdDistanceMetric::Cosine => {
257            let denom = na.sqrt() * nb.sqrt();
258            if denom > 1e-8 {
259                1.0 - (dot / denom)
260            } else {
261                1.0
262            }
263        }
264    }
265}
266
267// ---------------------------------------------------------------------------
268// Tests
269// ---------------------------------------------------------------------------
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn log_quantized_available_when_enabled() {
277        let off = LogQuantizedConfig::default();
278        assert!(!off.is_available());
279        let on = LogQuantizedConfig {
280            enabled: true,
281            ..Default::default()
282        };
283        assert!(on.is_available());
284    }
285
286    #[test]
287    fn log_quantized_roundtrip_skewed() {
288        // Exponential-ish positive skew.
289        let v: Vec<f32> = (0..64)
290            .map(|i| (i as f32 * 0.15).exp() * if i % 3 == 0 { -1.0 } else { 1.0 })
291            .collect();
292        let cfg = LogQuantizedConfig {
293            enabled: true,
294            compression_ratio: 4,
295            min_magnitude: 1e-7,
296        };
297        let q = LogQuantizedVector::quantize(&v, &cfg);
298        let mse = q.mse(&v);
299        // Must be finite and not catastrophic.
300        assert!(mse.is_finite(), "mse={mse}");
301        assert!(mse < 1e6, "unexpected huge mse={mse}");
302        // Better or comparable to naive linear int8 on this skew is soft —
303        // just ensure reconstruct length matches.
304        assert_eq!(q.reconstruct().len(), v.len());
305    }
306
307    #[test]
308    fn simd_distance_available_when_enabled() {
309        assert!(!SimdDistanceConfig::default().is_available());
310        assert!(SimdDistanceConfig {
311            enabled: true,
312            ..Default::default()
313        }
314        .is_available());
315    }
316
317    #[test]
318    fn unified_cosine_identical_is_zero() {
319        let v = vec![1.0, 2.0, 3.0, 4.0];
320        let cfg = SimdDistanceConfig {
321            enabled: true,
322            metric: SimdDistanceMetric::Cosine,
323            ..Default::default()
324        };
325        let d = unified_distance(&v, &v, &cfg);
326        assert!(d.abs() < 1e-5, "d={d}");
327    }
328
329    #[test]
330    fn unified_l2_orthogonal() {
331        let a = vec![1.0, 0.0, 0.0, 0.0];
332        let b = vec![0.0, 1.0, 0.0, 0.0];
333        let cfg = SimdDistanceConfig {
334            enabled: true,
335            metric: SimdDistanceMetric::L2,
336            pad_to_power_of_two: true,
337            ..Default::default()
338        };
339        let d = unified_distance(&a, &b, &cfg);
340        assert!((d - 2.0f32.sqrt()).abs() < 1e-5, "d={d}");
341    }
342
343    #[test]
344    fn log_quantized_serde_roundtrip() {
345        let cfg = LogQuantizedConfig {
346            enabled: true,
347            compression_ratio: 8,
348            min_magnitude: 1e-5,
349        };
350        let json = serde_json::to_string(&cfg).unwrap();
351        let restored: LogQuantizedConfig = serde_json::from_str(&json).unwrap();
352        assert!(restored.enabled);
353        assert_eq!(restored.compression_ratio, 8);
354    }
355
356    #[test]
357    fn simd_distance_serde_roundtrip() {
358        let cfg = SimdDistanceConfig {
359            enabled: true,
360            pad_to_power_of_two: true,
361            metric: SimdDistanceMetric::L2,
362            lane_width: Some(256),
363        };
364        let json = serde_json::to_string(&cfg).unwrap();
365        let restored: SimdDistanceConfig = serde_json::from_str(&json).unwrap();
366        assert!(restored.is_available());
367        assert_eq!(restored.metric, SimdDistanceMetric::L2);
368    }
369
370    #[test]
371    fn next_pow2_basic() {
372        assert_eq!(next_pow2(1), 1);
373        assert_eq!(next_pow2(3), 4);
374        assert_eq!(next_pow2(8), 8);
375        assert_eq!(next_pow2(9), 16);
376    }
377}