Skip to main content

qualia_core_db/inference/
kv_dict.rs

1//! W5b Phase 4b — the KV-dictionary type and its sparse **codec**, in CORE.
2//!
3//! This is the "engine runs the certified artifact" half of the sparse-KV-dictionary work. A learned
4//! per-layer dictionary `D` of `n_atoms` unit atoms represents each KV vector as a **k-sparse** linear
5//! combination — `k` (atom_index, coefficient) pairs. The engine needs two operations at runtime, and
6//! neither may depend on the forge feature:
7//!   * [`KvDictionary::encode`] — Orthogonal Matching Pursuit: a vector → its k-sparse code (write path).
8//!   * [`KvDictionary::reconstruct`] — code → vector (read path, in attention).
9//!
10//! The MOD **learner** that PRODUCES a dictionary is a forge/training step and lives in
11//! `wgsl_forge::calibration::kv_dictionary` (which re-exports this type). Colocation would blur "forge
12//! produces, engine runs", so only the data + codec + the small numeric helpers they share live here.
13//! Pure CPU + `f32`; the GPU reconstruction shader (Phase 4b step 5) mirrors [`reconstruct`] in WGSL.
14
15#![cfg(any(not(target_arch = "wasm32"), feature = "wasm-llm"))]
16
17/// A learned per-layer KV dictionary: `n_atoms` atoms, each of length `dim`, row-major in `atoms`.
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct KvDictionary {
20    pub dim: usize,
21    pub n_atoms: usize,
22    /// `n_atoms × dim`, row-major; each atom is L2-normalized.
23    pub atoms: Vec<f32>,
24    /// Sparsity `k` this dictionary was trained for (atoms per coded vector).
25    pub sparsity: usize,
26}
27
28/// A K-sparse code of one vector: `indices[i]` selects an atom, `coeffs[i]` its weight.
29#[derive(Debug, Clone, PartialEq)]
30pub struct SparseCode {
31    pub indices: Vec<u32>,
32    pub coeffs: Vec<f32>,
33}
34
35impl KvDictionary {
36    #[inline]
37    pub(crate) fn atom(&self, a: usize) -> &[f32] {
38        &self.atoms[a * self.dim..(a + 1) * self.dim]
39    }
40
41    /// Normalize every dictionary atom in place after loading or training.
42    /// Returns `false` when the declared shape is inconsistent or an atom has
43    /// effectively zero magnitude.
44    pub fn normalize_atoms(&mut self) -> bool {
45        if self.dim == 0 || self.atoms.len() != self.dim.saturating_mul(self.n_atoms) {
46            return false;
47        }
48        for atom in self.atoms.chunks_exact_mut(self.dim) {
49            if l2(atom) <= 1e-12 {
50                return false;
51            }
52            normalize(atom);
53        }
54        true
55    }
56
57    /// Reconstruct a vector from its sparse code: `Σ coeffs[i] · atom(indices[i])`. The read-path
58    /// operation the attention shader mirrors.
59    pub fn reconstruct(&self, code: &SparseCode) -> Vec<f32> {
60        let mut out = vec![0f32; self.dim];
61        for (idx, &c) in code.indices.iter().zip(&code.coeffs) {
62            let atom = self.atom(*idx as usize);
63            for (o, &a) in out.iter_mut().zip(atom) {
64                *o += c * a;
65            }
66        }
67        out
68    }
69
70    /// Orthogonal Matching Pursuit: encode `v` with at most `k` atoms. Greedy selection by maximum
71    /// absolute correlation with the residual, re-solving all selected coefficients by least squares
72    /// each step (the "orthogonal" in OMP). Stops early if the residual is already ~0.
73    pub fn encode(&self, v: &[f32], k: usize) -> SparseCode {
74        debug_assert_eq!(v.len(), self.dim);
75        let k = k.min(self.n_atoms).max(1);
76        let mut residual = v.to_vec();
77        let mut selected: Vec<usize> = Vec::with_capacity(k);
78        let mut coeffs: Vec<f32> = Vec::with_capacity(k);
79
80        for _ in 0..k {
81            // Pick the atom most correlated with the current residual (excluding already-picked).
82            let mut best = usize::MAX;
83            let mut best_abs = 0f32;
84            for a in 0..self.n_atoms {
85                if selected.contains(&a) {
86                    continue;
87                }
88                let corr = dot(self.atom(a), &residual);
89                if corr.abs() > best_abs {
90                    best_abs = corr.abs();
91                    best = a;
92                }
93            }
94            if best == usize::MAX || best_abs <= 1e-12 {
95                break;
96            }
97            selected.push(best);
98            // Re-solve coefficients for ALL selected atoms by least squares (normal equations on the
99            // small |S|×|S| Gram matrix), then recompute the residual.
100            coeffs = least_squares_coeffs(&selected, self, v);
101            residual = v.to_vec();
102            for (&s, &c) in selected.iter().zip(&coeffs) {
103                let atom = self.atom(s);
104                for (r, &a) in residual.iter_mut().zip(atom) {
105                    *r -= c * a;
106                }
107            }
108            if l2(&residual) <= 1e-8 {
109                break;
110            }
111        }
112        SparseCode {
113            indices: selected.iter().map(|&s| s as u32).collect(),
114            coeffs,
115        }
116    }
117}
118
119/// Pack a `(atom_index, coefficient)` pair into one 32-bit KV code word — `u16 atom-index (high) |
120/// f16 coeff (low)` — stored as an `f32` in the dict-mode KV arena. The GPU attention shader unpacks the
121/// coeff with `unpack2x16float(word).x` and the index with `word >> 16`.
122#[inline]
123pub fn pack_code_word(index: u32, coeff: f32) -> f32 {
124    let w = (index << 16) | (half::f16::from_f32(coeff).to_bits() as u32);
125    f32::from_bits(w)
126}
127
128/// Inverse of [`pack_code_word`]: `(atom_index, coefficient)` from a code word.
129#[inline]
130pub fn unpack_code_word(word: f32) -> (usize, f32) {
131    let w = word.to_bits();
132    (
133        (w >> 16) as usize,
134        half::f16::from_bits((w & 0xFFFF) as u16).to_f32(),
135    )
136}
137
138impl KvDictionary {
139    /// Encode `vec` to `k` contiguous code words in `out` (len ≥ `k`). Pads with zero-coeff words if OMP
140    /// selected fewer than `k` atoms (a zero coeff reconstructs to nothing).
141    pub fn encode_to_words(&self, vec: &[f32], k: usize, out: &mut [f32]) {
142        let code = self.encode(vec, k);
143        for (i, slot) in out.iter_mut().enumerate().take(k) {
144            let (ai, ci) = if i < code.indices.len() {
145                (code.indices[i], code.coeffs[i])
146            } else {
147                (0, 0.0)
148            };
149            *slot = pack_code_word(ai, ci);
150        }
151    }
152
153    /// Reconstruct a vector from `k` contiguous code words (`words` len ≥ `k`) into `out` (len = `dim`).
154    /// The exact `f16`-coefficient inverse of [`encode_to_words`] — the compressed-cache read path.
155    pub fn reconstruct_from_words(&self, words: &[f32], k: usize, out: &mut [f32]) {
156        for o in out.iter_mut() {
157            *o = 0.0;
158        }
159        for &word in words.iter().take(k) {
160            let (ai, ci) = unpack_code_word(word);
161            if ci != 0.0 && ai < self.n_atoms {
162                let atom = self.atom(ai);
163                for (o, &a) in out.iter_mut().zip(atom) {
164                    *o += ci * a;
165                }
166            }
167        }
168    }
169}
170
171/// Least-squares coefficients for the selected atoms fitting `v`: solve `(DₛᵀDₛ) c = Dₛᵀv` via the
172/// normal equations (Gaussian elimination on the small `|S|×|S|` system).
173fn least_squares_coeffs(selected: &[usize], dict: &KvDictionary, v: &[f32]) -> Vec<f32> {
174    let s = selected.len();
175    // Gram matrix G = Dₛᵀ Dₛ (s×s) and rhs = Dₛᵀ v (s).
176    let mut g = vec![0f32; s * s];
177    let mut rhs = vec![0f32; s];
178    for i in 0..s {
179        let ai = dict.atom(selected[i]);
180        rhs[i] = dot(ai, v);
181        for j in i..s {
182            let aj = dict.atom(selected[j]);
183            let val = dot(ai, aj);
184            g[i * s + j] = val;
185            g[j * s + i] = val;
186        }
187    }
188    solve_spd(&mut g, &mut rhs, s);
189    rhs
190}
191
192/// Solve a small dense symmetric system `G c = b` in place via Gaussian elimination with partial
193/// pivoting (SPD in exact arithmetic; the pivoting + tiny ridge keep it robust to near-singular Gram
194/// matrices from correlated atoms). Result written back into `b`.
195fn solve_spd(g: &mut [f32], b: &mut [f32], n: usize) {
196    // Tiny ridge for numerical stability against duplicate/near-duplicate atoms.
197    for i in 0..n {
198        g[i * n + i] += 1e-6;
199    }
200    for col in 0..n {
201        // Partial pivot.
202        let mut piv = col;
203        let mut piv_abs = g[col * n + col].abs();
204        for r in (col + 1)..n {
205            let a = g[r * n + col].abs();
206            if a > piv_abs {
207                piv_abs = a;
208                piv = r;
209            }
210        }
211        if piv != col {
212            for c in 0..n {
213                g.swap(col * n + c, piv * n + c);
214            }
215            b.swap(col, piv);
216        }
217        let d = g[col * n + col];
218        if d.abs() <= 1e-12 {
219            continue;
220        }
221        for r in 0..n {
222            if r == col {
223                continue;
224            }
225            let factor = g[r * n + col] / d;
226            if factor == 0.0 {
227                continue;
228            }
229            for c in col..n {
230                g[r * n + c] -= factor * g[col * n + c];
231            }
232            b[r] -= factor * b[col];
233        }
234    }
235    for i in 0..n {
236        let d = g[i * n + i];
237        if d.abs() > 1e-12 {
238            b[i] /= d;
239        } else {
240            b[i] = 0.0;
241        }
242    }
243}
244
245#[inline]
246pub(crate) fn dot(a: &[f32], b: &[f32]) -> f32 {
247    a.iter().zip(b).map(|(x, y)| x * y).sum()
248}
249
250#[inline]
251pub(crate) fn l2(v: &[f32]) -> f32 {
252    dot(v, v).sqrt()
253}
254
255#[inline]
256pub(crate) fn normalize(v: &mut [f32]) {
257    let n = l2(v);
258    if n > 1e-12 {
259        for x in v.iter_mut() {
260            *x /= n;
261        }
262    }
263}