Skip to main content

geographdb_core/algorithms/
kv_frame_codec.rs

1//! Cartan Moving Frames KV Cache Codec.
2//!
3//! Instead of storing absolute key/value vectors `K_t`, `V_t`, this codec
4//! stores incremental *frame transitions*:
5//!
6//!   `F_t = K_t · pinv(K_{t-1})`  (Moore-Penrose pseudoinverse)
7//!
8//! Reconstruction: `K_t = F_t · F_{t-1} · … · F_1 · K_0`
9//!
10//! **Why this compresses:** For coherent text, consecutive key vectors lie on
11//! a smooth manifold.  The frame transition `F_t - I` has small Frobenius norm
12//! and low rank — far more compressible than absolute vectors.
13//!
14//! This is the differential-geometry analog of storing velocity instead of
15//! position.  Élie Cartan (1900s) developed the moving-frame method for
16//! exactly this purpose: encoding manifold geometry through infinitesimal
17//! frame changes rather than absolute coordinates.
18//!
19//! # Usage
20//! ```ignore
21//! let mut codec = FrameCodec::new(head_dim);
22//! for (k, v) in kv_pairs { codec.encode_step(&k, &v); }
23//! let output = codec.attend(&query);
24//! ```
25
26// ── Types ─────────────────────────────────────────────────────────────────────
27
28/// Cartan moving-frame KV cache codec.
29///
30/// Stores the anchor `(K_0, V_0)` plus a sequence of `d×d` frame transition
31/// matrices `F_t` for `t = 1..T`.  Decoding reconstructs `K_t` and `V_t`
32/// by cumulative matrix product from the anchor.
33pub struct FrameCodec {
34    /// Physical dimension of each key/value head vector.
35    head_dim: usize,
36    /// Anchor key K_0 (length `head_dim`).
37    k0: Vec<f32>,
38    /// Anchor value V_0 (length `head_dim`).
39    v0: Vec<f32>,
40    /// Frame transitions for keys: F_1, F_2, …, F_T.
41    /// Each entry is a flat `head_dim × head_dim` row-major matrix.
42    frames_k: Vec<Vec<f32>>,
43    /// Frame transitions for values.
44    frames_v: Vec<Vec<f32>>,
45}
46
47impl FrameCodec {
48    /// Create a new empty codec for vectors of length `head_dim`.
49    pub fn new(head_dim: usize) -> Self {
50        Self {
51            head_dim,
52            k0: Vec::new(),
53            v0: Vec::new(),
54            frames_k: Vec::new(),
55            frames_v: Vec::new(),
56        }
57    }
58
59    /// Encode one `(k, v)` pair. First call sets the anchor; subsequent calls
60    /// store the frame transition relative to the previous step.
61    ///
62    /// `k` and `v` must each have length `head_dim`.
63    pub fn encode_step(&mut self, k: &[f32], v: &[f32]) {
64        assert_eq!(k.len(), self.head_dim, "k length must equal head_dim");
65        assert_eq!(v.len(), self.head_dim, "v length must equal head_dim");
66
67        if self.k0.is_empty() {
68            // First token: store as anchor
69            self.k0 = k.to_vec();
70            self.v0 = v.to_vec();
71            return;
72        }
73
74        // Decode the previous key/value to compute frame transition
75        let k_prev = self.decode_key(self.token_count() - 1);
76        let v_prev = self.decode_val(self.token_count() - 1);
77
78        // F_t = K_t ⊗ pinv(K_{t-1})
79        // For vectors (d×1 outer product interpretation), the "frame" that maps
80        // k_prev → k is the rank-1 outer product update: F = k * k_prev^T / ‖k_prev‖²
81        // This is the closest rank-1 frame; for full d×d we use the identity
82        // + a rank-1 correction: F = I + (k - k_prev) ⊗ k_prev / ‖k_prev‖²
83        let fk = rank1_frame_transition(k, &k_prev, self.head_dim);
84        let fv = rank1_frame_transition(v, &v_prev, self.head_dim);
85
86        self.frames_k.push(fk);
87        self.frames_v.push(fv);
88    }
89
90    /// Number of tokens stored (including anchor).
91    pub fn token_count(&self) -> usize {
92        if self.k0.is_empty() {
93            0
94        } else {
95            1 + self.frames_k.len()
96        }
97    }
98
99    /// Decode the key vector at position `t` by applying cumulative frame
100    /// transitions from the anchor.
101    pub fn decode_key(&self, t: usize) -> Vec<f32> {
102        self.decode_vector(&self.k0, &self.frames_k, t)
103    }
104
105    /// Decode the value vector at position `t`.
106    pub fn decode_val(&self, t: usize) -> Vec<f32> {
107        self.decode_vector(&self.v0, &self.frames_v, t)
108    }
109
110    /// Compute standard softmax attention over all stored tokens.
111    ///
112    /// `query` has length `head_dim`. Returns the weighted sum of values.
113    pub fn attend(&self, query: &[f32]) -> Vec<f32> {
114        let n = self.token_count();
115        if n == 0 {
116            return vec![0.0; self.head_dim];
117        }
118
119        let scale = 1.0 / (self.head_dim as f32).sqrt();
120        let mut scores = Vec::with_capacity(n);
121
122        // Decode all keys to compute scores
123        let mut keys = Vec::with_capacity(n);
124        let mut vals = Vec::with_capacity(n);
125        keys.push(self.k0.clone());
126        vals.push(self.v0.clone());
127        for t in 1..n {
128            keys.push(self.decode_key(t));
129            vals.push(self.decode_val(t));
130        }
131
132        for k in &keys {
133            let dot: f32 = query.iter().zip(k.iter()).map(|(q, ki)| q * ki).sum();
134            scores.push(dot * scale);
135        }
136
137        // Softmax
138        let max_s = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
139        let mut exp_scores: Vec<f32> = scores.iter().map(|s| (s - max_s).exp()).collect();
140        let sum_exp: f32 = exp_scores.iter().sum();
141        for e in &mut exp_scores {
142            *e /= sum_exp;
143        }
144
145        // Weighted sum of values
146        let mut out = vec![0.0f32; self.head_dim];
147        for (weight, v) in exp_scores.iter().zip(vals.iter()) {
148            for (o, vi) in out.iter_mut().zip(v.iter()) {
149                *o += weight * vi;
150            }
151        }
152        out
153    }
154
155    /// Number of bytes used by the codec (f32 = 4 bytes each).
156    pub fn bytes_used(&self) -> usize {
157        let anchor = 2 * self.head_dim * 4;
158        let frames =
159            (self.frames_k.len() + self.frames_v.len()) * self.head_dim * self.head_dim * 4;
160        anchor + frames
161    }
162
163    /// Number of bytes a naive absolute-vector store would use (f32 K+V per token).
164    pub fn bytes_absolute(&self) -> usize {
165        self.token_count() * 2 * self.head_dim * 4
166    }
167
168    // ── Private ───────────────────────────────────────────────────────────────
169
170    fn decode_vector(&self, anchor: &[f32], frames: &[Vec<f32>], t: usize) -> Vec<f32> {
171        assert!(
172            t < self.token_count(),
173            "t={t} out of range (n={})",
174            self.token_count()
175        );
176        if t == 0 {
177            return anchor.to_vec();
178        }
179        let d = self.head_dim;
180        // Apply frames[0..t] sequentially: v = F_{t-1} · F_{t-2} · … · F_0 · anchor
181        let mut current = anchor.to_vec();
182        for frame in &frames[..t] {
183            current = matvec(frame, &current, d);
184        }
185        current
186    }
187}
188
189// ── Math helpers ──────────────────────────────────────────────────────────────
190
191/// Rank-1 frame transition that maps `prev` → `curr`:
192///   F = I + (curr - prev) ⊗ prev / ‖prev‖²
193///
194/// This is the minimum-norm matrix correction satisfying `F · prev = curr`.
195/// When `prev ≈ curr` (coherent text), `F ≈ I` → low Frobenius norm → high
196/// compressibility.
197fn rank1_frame_transition(curr: &[f32], prev: &[f32], d: usize) -> Vec<f32> {
198    let norm_sq: f32 = prev.iter().map(|x| x * x).sum();
199    let mut frame = identity_matrix(d);
200
201    if norm_sq < 1e-12 {
202        // prev is zero vector — store curr as absolute offset from I
203        for i in 0..d {
204            frame[i * d + i] += curr[i]; // diagonal: I + curr (approx)
205        }
206        return frame;
207    }
208
209    // delta = (curr - prev)
210    let delta: Vec<f32> = curr.iter().zip(prev.iter()).map(|(c, p)| c - p).collect();
211
212    // F[i,j] += delta[i] * prev[j] / norm_sq
213    for i in 0..d {
214        for j in 0..d {
215            frame[i * d + j] += delta[i] * prev[j] / norm_sq;
216        }
217    }
218    frame
219}
220
221/// `d×d` identity matrix (flat, row-major).
222fn identity_matrix(d: usize) -> Vec<f32> {
223    let mut m = vec![0.0f32; d * d];
224    for i in 0..d {
225        m[i * d + i] = 1.0;
226    }
227    m
228}
229
230/// Matrix-vector product: `y = M · x` where `M` is `d×d` row-major.
231fn matvec(m: &[f32], x: &[f32], d: usize) -> Vec<f32> {
232    let mut y = vec![0.0f32; d];
233    for i in 0..d {
234        for j in 0..d {
235            y[i] += m[i * d + j] * x[j];
236        }
237    }
238    y
239}
240
241// ── Tests ─────────────────────────────────────────────────────────────────────
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    fn approx_eq_vec(a: &[f32], b: &[f32], tol: f32) -> bool {
248        a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < tol)
249    }
250
251    // ── Encode / decode roundtrip ─────────────────────────────────────────────
252
253    #[test]
254    fn test_encode_decode_roundtrip_anchor() {
255        let mut codec = FrameCodec::new(4);
256        let k0 = vec![1.0, 0.0, 0.0, 0.0];
257        let v0 = vec![0.0, 1.0, 0.0, 0.0];
258        codec.encode_step(&k0, &v0);
259        assert!(approx_eq_vec(&codec.decode_key(0), &k0, 1e-6));
260        assert!(approx_eq_vec(&codec.decode_val(0), &v0, 1e-6));
261    }
262
263    #[test]
264    fn test_encode_decode_roundtrip_second_token() {
265        let mut codec = FrameCodec::new(4);
266        let k0 = vec![1.0, 0.0, 0.0, 0.0];
267        let v0 = vec![0.0, 1.0, 0.0, 0.0];
268        let k1 = vec![0.5, 0.5, 0.0, 0.0];
269        let v1 = vec![0.0, 0.5, 0.5, 0.0];
270        codec.encode_step(&k0, &v0);
271        codec.encode_step(&k1, &v1);
272        assert!(
273            approx_eq_vec(&codec.decode_key(1), &k1, 1e-5),
274            "decode_key(1) = {:?}, expected {:?}",
275            codec.decode_key(1),
276            k1
277        );
278        assert!(approx_eq_vec(&codec.decode_val(1), &v1, 1e-5));
279    }
280
281    #[test]
282    fn test_encode_decode_multiple_tokens() {
283        let d = 4;
284        let mut codec = FrameCodec::new(d);
285        let kvs: Vec<(Vec<f32>, Vec<f32>)> = (0..5)
286            .map(|i| {
287                let k = (0..d).map(|j| ((i + j) as f32) * 0.1).collect();
288                let v = (0..d).map(|j| ((i * 2 + j) as f32) * 0.1).collect();
289                (k, v)
290            })
291            .collect();
292        for (k, v) in &kvs {
293            codec.encode_step(k, v);
294        }
295        for (t, (k, v)) in kvs.iter().enumerate() {
296            assert!(
297                approx_eq_vec(&codec.decode_key(t), k, 1e-4),
298                "key mismatch at t={t}"
299            );
300            assert!(
301                approx_eq_vec(&codec.decode_val(t), v, 1e-4),
302                "val mismatch at t={t}"
303            );
304        }
305    }
306
307    #[test]
308    fn test_identity_frame_for_constant_sequence() {
309        let d = 3;
310        let mut codec = FrameCodec::new(d);
311        let k = vec![1.0, 0.0, 0.0];
312        let v = vec![0.0, 1.0, 0.0];
313        // Same vector repeated → F_t = I
314        for _ in 0..4 {
315            codec.encode_step(&k, &v);
316        }
317        // All decoded keys should equal k
318        for t in 0..4 {
319            assert!(approx_eq_vec(&codec.decode_key(t), &k, 1e-5), "t={t}");
320        }
321    }
322
323    // ── Attention ─────────────────────────────────────────────────────────────
324
325    #[test]
326    fn test_attend_matches_direct_single_token() {
327        let d = 4;
328        let k0 = vec![1.0, 0.0, 0.0, 0.0];
329        let v0 = vec![0.0, 0.0, 1.0, 0.0];
330        let q = vec![1.0, 0.0, 0.0, 0.0];
331
332        let mut codec = FrameCodec::new(d);
333        codec.encode_step(&k0, &v0);
334
335        // With 1 token, softmax(score)=1.0, output = v0
336        let out = codec.attend(&q);
337        assert!(
338            approx_eq_vec(&out, &v0, 1e-5),
339            "single-token attend = {out:?}"
340        );
341    }
342
343    #[test]
344    fn test_attend_matches_direct_multi_token() {
345        let d = 4;
346        let kvs = vec![
347            (vec![1.0f32, 0.0, 0.0, 0.0], vec![1.0f32, 0.0, 0.0, 0.0]),
348            (vec![0.0f32, 1.0, 0.0, 0.0], vec![0.0f32, 1.0, 0.0, 0.0]),
349            (vec![0.0f32, 0.0, 1.0, 0.0], vec![0.0f32, 0.0, 1.0, 0.0]),
350        ];
351        let q = vec![1.0f32, 0.0, 0.0, 0.0]; // aligns with k0
352
353        let mut codec = FrameCodec::new(d);
354        for (k, v) in &kvs {
355            codec.encode_step(k, v);
356        }
357
358        // Direct reference
359        let scale = 1.0 / (d as f32).sqrt();
360        let scores_raw: Vec<f32> = kvs
361            .iter()
362            .map(|(k, _)| k.iter().zip(&q).map(|(ki, qi)| ki * qi).sum::<f32>() * scale)
363            .collect();
364        let max_s = scores_raw.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
365        let exp: Vec<f32> = scores_raw.iter().map(|s| (s - max_s).exp()).collect();
366        let sum_exp: f32 = exp.iter().sum();
367        let weights: Vec<f32> = exp.iter().map(|e| e / sum_exp).collect();
368        let mut expected = vec![0.0f32; d];
369        for (w, (_, v)) in weights.iter().zip(kvs.iter()) {
370            for (o, vi) in expected.iter_mut().zip(v.iter()) {
371                *o += w * vi;
372            }
373        }
374
375        let out = codec.attend(&q);
376        assert!(
377            approx_eq_vec(&out, &expected, 1e-4),
378            "attend mismatch: got {out:?}, expected {expected:?}"
379        );
380    }
381
382    #[test]
383    fn test_attend_empty_returns_zero() {
384        let codec = FrameCodec::new(4);
385        let out = codec.attend(&[1.0, 0.0, 0.0, 0.0]);
386        assert_eq!(out, vec![0.0; 4]);
387    }
388
389    // ── Bytes / compression ───────────────────────────────────────────────────
390
391    #[test]
392    fn test_token_count() {
393        let mut codec = FrameCodec::new(4);
394        assert_eq!(codec.token_count(), 0);
395        codec.encode_step(&[1.0, 0.0, 0.0, 0.0], &[0.0, 1.0, 0.0, 0.0]);
396        assert_eq!(codec.token_count(), 1);
397        codec.encode_step(&[0.5, 0.5, 0.0, 0.0], &[0.0, 0.5, 0.5, 0.0]);
398        assert_eq!(codec.token_count(), 2);
399    }
400}