Skip to main content

cortiq_engine/
bounded.rs

1//! Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1).
2//!
3//! Contract: `docs/EMBRYO_BOUNDED_ANCHOR.md`. The operator is a TRAINED
4//! bounded attention, not a masked full attention: per token `t`
5//!
6//! ```text
7//! window keys:  j ∈ (t − W, t]                (W keys INCLUDING the token)
8//! window score: s_j = ( R(t − j) q̂_t ) · k̂_j / √hd
9//! sink score:   s_s = q̂_t · k̂ˢ_s / √hd,   s ∈ [0, S)      (NoPE)
10//! one softmax over {s_s} ∪ {s_j};   out_t = Σ_s p_s v̂ˢ_s + Σ_j p_j v_j
11//! ```
12//!
13//! The ring stores RAW (unrotated) keys; the query is rotated by the
14//! distance `Δ = t − j ∈ [0, W)` through a `[W][rd/2]` cos/sin table.
15//! With absolute RoPE `q_rot(t) = R(t) q̂`, `k_rot(j) = R(j) k̂` one has
16//! `q_rot(t)·k_rot(j) = q̂ᵀ R(t)ᵀ R(j) k̂ = (R(t−j) q̂)·k̂` (every 2-D block
17//! of `R` is a plane rotation, so `R(t)ᵀ R(j) = R(j − t)`), which is why
18//! the trainer may keep rotating by absolute positions and only mask,
19//! while the served operator never sees an absolute position at all.
20//!
21//! State per layer: `ring_k, ring_v [kvh][W][hd]` + the insert counter —
22//! a record of fixed size derived from the header, identical in prefill,
23//! decode and across turns. Nothing here grows with the context.
24
25use crate::qtensor::QTensor;
26
27/// Rows of rollback history kept beside the ring so a speculative reject
28/// (`truncate_last`) can restore the slots the rejected tokens overwrote.
29/// Bounded by construction; not part of the wire state.
30pub const UNDO_DEPTH: usize = 64;
31
32/// Weights of one bounded-anchor layer.
33pub struct BoundedWeights {
34    pub wq: QTensor,
35    pub wk: QTensor,
36    pub wv: QTensor,
37    pub wo: QTensor,
38    /// Trained NoPE sink keys `[kvh][sink][hd]` (weights, not positions).
39    pub sink_k: Vec<f32>,
40    /// Trained sink values `[kvh][sink][hd]`.
41    pub sink_v: Vec<f32>,
42    pub sink: usize,
43    /// Ring capacity: the widest group window.
44    pub window: usize,
45    /// `swa_sink_v2`: the exact window of every KV group (`[kvh]`, each
46    /// `≤ window`); empty = every group attends to the whole `window` (v1).
47    pub group_windows: Vec<usize>,
48}
49
50/// Relative-rotation table: `cos/sin[Δ][i] = cos/sin(Δ · inv_freq[i])`
51/// for `Δ ∈ [0, W)`, built once per model from the layer's `inv_freq`
52/// (same convention as `attention::rope_rotate_scaled`, angles computed
53/// in f64 so the table carries only the final f32 rounding).
54#[derive(Debug, Clone)]
55pub struct BoundedRope {
56    pub window: usize,
57    /// `rotary_dim / 2`: dims `[0, half)` pair with `[half, 2·half)`;
58    /// dims past `2·half` are copied unrotated (partial rotary).
59    pub half: usize,
60    pub cos: Vec<f32>,
61    pub sin: Vec<f32>,
62}
63
64impl BoundedRope {
65    /// `rope_scale` is the YaRN attention factor the absolute path applies
66    /// to BOTH q and k; the relative path rotates only q, so the window
67    /// score carries `scale²` (`(s·R(t)q)·(s·R(j)k) = s²·(R(t−j)q)·k`).
68    pub fn new(window: usize, inv_freq: &[f32], rope_scale: f32) -> Self {
69        let half = inv_freq.len();
70        let s2 = (rope_scale as f64) * (rope_scale as f64);
71        let mut cos = Vec::with_capacity(window * half);
72        let mut sin = Vec::with_capacity(window * half);
73        for delta in 0..window {
74            for &f in inv_freq {
75                let (sn, cs) = ((delta as f64) * (f as f64)).sin_cos();
76                cos.push((cs * s2) as f32);
77                sin.push((sn * s2) as f32);
78            }
79        }
80        Self {
81            window,
82            half,
83            cos,
84            sin,
85        }
86    }
87
88    /// `out = R(Δ) x` (first `2·half` dims rotated, the rest copied).
89    #[inline]
90    pub fn rotate(&self, delta: usize, x: &[f32], out: &mut [f32]) {
91        let half = self.half;
92        let c = &self.cos[delta * half..(delta + 1) * half];
93        let s = &self.sin[delta * half..(delta + 1) * half];
94        for i in 0..half {
95            let x0 = x[i];
96            let x1 = x[i + half];
97            out[i] = x0 * c[i] - x1 * s[i];
98            out[i + half] = x0 * s[i] + x1 * c[i];
99        }
100        let r = 2 * half;
101        if x.len() > r {
102            out[r..x.len()].copy_from_slice(&x[r..]);
103        }
104    }
105}
106
107/// Bit-for-bit copy of everything the operator mutates (speculation).
108#[derive(Debug, Clone)]
109pub struct BoundedSnapshot {
110    ring_k: Vec<f32>,
111    ring_v: Vec<f32>,
112    seen: usize,
113}
114
115/// Per-layer bounded state: the ring of the last `W` raw keys/values per
116/// KV head plus the insert counter. `len = min(seen, W)`, next write slot
117/// `head = seen mod W`, and the key of position `j` lives in slot
118/// `j mod W` — so no absolute position is ever stored.
119#[derive(Debug, Clone)]
120pub struct BoundedState {
121    pub window: usize,
122    pub num_kv_heads: usize,
123    pub head_dim: usize,
124    /// `[kvh][W][hd]`, raw (unrotated) keys.
125    pub ring_k: Vec<f32>,
126    /// `[kvh][W][hd]`.
127    pub ring_v: Vec<f32>,
128    /// Tokens inserted since the last clear (= the next position).
129    pub seen: usize,
130    /// Rollback rows: the slot contents each of the last `UNDO_DEPTH`
131    /// inserts overwrote (`[UNDO_DEPTH][kvh][hd]` for k and v).
132    undo_k: Vec<f32>,
133    undo_v: Vec<f32>,
134    undo_len: usize,
135    undo_head: usize,
136}
137
138impl BoundedState {
139    pub fn new(num_kv_heads: usize, head_dim: usize, window: usize) -> Self {
140        let n = num_kv_heads * window * head_dim;
141        let u = UNDO_DEPTH * num_kv_heads * head_dim;
142        Self {
143            window,
144            num_kv_heads,
145            head_dim,
146            ring_k: vec![0.0; n],
147            ring_v: vec![0.0; n],
148            seen: 0,
149            undo_k: vec![0.0; u],
150            undo_v: vec![0.0; u],
151            undo_len: 0,
152            undo_head: 0,
153        }
154    }
155
156    /// Filled slots, `min(seen, W)`.
157    #[inline]
158    pub fn len(&self) -> usize {
159        self.seen.min(self.window)
160    }
161
162    #[inline]
163    pub fn is_empty(&self) -> bool {
164        self.seen == 0
165    }
166
167    /// Next write slot, `seen mod W`.
168    #[inline]
169    pub fn head(&self) -> usize {
170        self.seen % self.window
171    }
172
173    /// Bytes of the wire-visible state (ring + counter). The undo rows
174    /// are scratch of fixed size and are not state.
175    pub fn state_bytes(&self) -> usize {
176        (self.ring_k.len() + self.ring_v.len()) * std::mem::size_of::<f32>()
177            + std::mem::size_of::<u64>()
178    }
179
180    /// Zero the record (fresh sequence). Capacity never changes.
181    pub fn clear(&mut self) {
182        self.ring_k.fill(0.0);
183        self.ring_v.fill(0.0);
184        self.seen = 0;
185        self.undo_len = 0;
186        self.undo_head = 0;
187    }
188
189    /// Write `k, v` (`[kvh][hd]`, raw) into slot `seen mod W`, saving
190    /// the overwritten rows for rollback.
191    pub fn insert(&mut self, k: &[f32], v: &[f32]) {
192        let (kvh, hd, w) = (self.num_kv_heads, self.head_dim, self.window);
193        debug_assert_eq!(k.len(), kvh * hd);
194        debug_assert_eq!(v.len(), kvh * hd);
195        let slot = self.head();
196        let u = self.undo_head;
197        for h in 0..kvh {
198            let r = (h * w + slot) * hd;
199            let uo = (u * kvh + h) * hd;
200            self.undo_k[uo..uo + hd].copy_from_slice(&self.ring_k[r..r + hd]);
201            self.undo_v[uo..uo + hd].copy_from_slice(&self.ring_v[r..r + hd]);
202            self.ring_k[r..r + hd].copy_from_slice(&k[h * hd..(h + 1) * hd]);
203            self.ring_v[r..r + hd].copy_from_slice(&v[h * hd..(h + 1) * hd]);
204        }
205        self.undo_head = (u + 1) % UNDO_DEPTH;
206        self.undo_len = (self.undo_len + 1).min(UNDO_DEPTH);
207        self.seen += 1;
208    }
209
210    /// Undo the last `n` inserts exactly (restores the overwritten slots).
211    /// Returns how many were rolled back — fewer than `n` only when the
212    /// rollback history (`UNDO_DEPTH`) or the inserted count is shorter.
213    pub fn rollback(&mut self, n: usize) -> usize {
214        let (kvh, hd, w) = (self.num_kv_heads, self.head_dim, self.window);
215        let n = n.min(self.undo_len).min(self.seen);
216        for _ in 0..n {
217            self.seen -= 1;
218            let slot = self.seen % w;
219            let u = (self.undo_head + UNDO_DEPTH - 1) % UNDO_DEPTH;
220            for h in 0..kvh {
221                let r = (h * w + slot) * hd;
222                let uo = (u * kvh + h) * hd;
223                self.ring_k[r..r + hd].copy_from_slice(&self.undo_k[uo..uo + hd]);
224                self.ring_v[r..r + hd].copy_from_slice(&self.undo_v[uo..uo + hd]);
225            }
226            self.undo_head = u;
227            self.undo_len -= 1;
228        }
229        n
230    }
231
232    pub fn snapshot(&self) -> BoundedSnapshot {
233        BoundedSnapshot {
234            ring_k: self.ring_k.clone(),
235            ring_v: self.ring_v.clone(),
236            seen: self.seen,
237        }
238    }
239
240    /// Restore a snapshot taken on THIS state (same geometry). The undo
241    /// history is discarded: it described inserts that no longer exist.
242    pub fn restore(&mut self, s: &BoundedSnapshot) {
243        debug_assert_eq!(s.ring_k.len(), self.ring_k.len());
244        self.ring_k.copy_from_slice(&s.ring_k);
245        self.ring_v.copy_from_slice(&s.ring_v);
246        self.seen = s.seen;
247        self.undo_len = 0;
248        self.undo_head = 0;
249    }
250
251    /// Bit-for-bit equality of the wire-visible state.
252    pub fn same_state(&self, other: &BoundedState) -> bool {
253        self.window == other.window
254            && self.seen == other.seen
255            && self.ring_k == other.ring_k
256            && self.ring_v == other.ring_v
257    }
258
259    /// Attend every Q head of every KV group over `sink ∪ window` and
260    /// write `[nh][hd]` into `out`. `q` is `[nh][hd]` RAW (unrotated);
261    /// the current token must already be inserted (the window includes
262    /// it, matching the trainer's band `col == S + row`).
263    #[allow(clippy::too_many_arguments)]
264    pub fn attend(
265        &self,
266        q: &[f32],
267        num_heads: usize,
268        sink_k: &[f32],
269        sink_v: &[f32],
270        sink: usize,
271        group_windows: &[usize],
272        rope: &BoundedRope,
273        scale: f32,
274        out: &mut [f32],
275    ) {
276        let (kvh, hd, w) = (self.num_kv_heads, self.head_dim, self.window);
277        let nh = num_heads;
278        let hpk = nh / kvh.max(1);
279        debug_assert_eq!(hpk * kvh, nh);
280        debug_assert_eq!(q.len(), nh * hd);
281        debug_assert_eq!(out.len(), nh * hd);
282        debug_assert_eq!(sink_k.len(), kvh * sink * hd);
283        debug_assert_eq!(rope.window, w);
284        debug_assert!(group_windows.is_empty() || group_windows.len() == kvh);
285        let m = self.len();
286        let n = sink + m;
287        // keys group g attends to: its own window of the ring (v2), or all
288        let mg = |g: usize| group_windows.get(g).map_or(m, |&gw| gw.min(m));
289        let head = self.head();
290        thread_local! {
291            static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>)> =
292                const { std::cell::RefCell::new((Vec::new(), Vec::new())) };
293        }
294        SCRATCH.with(|s| {
295            let mut s = s.borrow_mut();
296            let (scores, qrot) = &mut *s;
297            scores.clear();
298            scores.resize(nh * n, 0.0);
299            qrot.clear();
300            qrot.resize(nh * hd, 0.0);
301            // Sink scores on the raw query (NoPE).
302            for h in 0..nh {
303                let g = h / hpk;
304                let qh = &q[h * hd..(h + 1) * hd];
305                for s in 0..sink {
306                    let kr = &sink_k[(g * sink + s) * hd..(g * sink + s + 1) * hd];
307                    scores[h * n + s] = crate::attention::dot_f32(qh, kr) * scale;
308                }
309            }
310            // Window scores: Δ = 0 is the token itself; slot(Δ) walks the
311            // ring backwards from the last written slot.
312            for d in 0..m {
313                let slot = (head + w - 1 - d) % w;
314                for h in 0..nh {
315                    if d >= mg(h / hpk) {
316                        continue;
317                    }
318                    rope.rotate(d, &q[h * hd..(h + 1) * hd], &mut qrot[h * hd..(h + 1) * hd]);
319                }
320                for h in 0..nh {
321                    let g = h / hpk;
322                    if d >= mg(g) {
323                        continue;
324                    }
325                    let kr = &self.ring_k[(g * w + slot) * hd..(g * w + slot + 1) * hd];
326                    scores[h * n + sink + d] =
327                        crate::attention::dot_f32(&qrot[h * hd..(h + 1) * hd], kr) * scale;
328                }
329            }
330            // One softmax per head over sinks ∪ its group's window
331            // (attention_head order).
332            for h in 0..nh {
333                let sc = &mut scores[h * n..h * n + sink + mg(h / hpk)];
334                let max = sc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
335                let mut sum = 0.0f32;
336                for v in sc.iter_mut() {
337                    *v = (*v - max).exp();
338                    sum += *v;
339                }
340                if sum > 0.0 {
341                    for v in sc.iter_mut() {
342                        *v /= sum;
343                    }
344                }
345            }
346            out.fill(0.0);
347            for h in 0..nh {
348                let g = h / hpk;
349                let oh = &mut out[h * hd..(h + 1) * hd];
350                let p = &scores[h * n..(h + 1) * n];
351                for s in 0..sink {
352                    let vr = &sink_v[(g * sink + s) * hd..(g * sink + s + 1) * hd];
353                    if p[s].abs() >= 1e-12 {
354                        crate::attention::axpy_f32(oh, vr, p[s]);
355                    }
356                }
357                for d in 0..mg(g) {
358                    let slot = (head + w - 1 - d) % w;
359                    let vr = &self.ring_v[(g * w + slot) * hd..(g * w + slot + 1) * hd];
360                    let pw = p[sink + d];
361                    if pw.abs() >= 1e-12 {
362                        crate::attention::axpy_f32(oh, vr, pw);
363                    }
364                }
365            }
366        });
367    }
368}
369
370/// Per-token configuration of a bounded layer (geometry + the shared
371/// rotation table). No position: the operator has none.
372pub struct BoundedAttnCfg<'a> {
373    pub num_heads: usize,
374    pub num_kv_heads: usize,
375    pub head_dim: usize,
376    pub hidden_size: usize,
377    /// Score scale (1/√hd unless the arch overrides).
378    pub scale: f32,
379    pub rope: &'a BoundedRope,
380    pub pool: Option<&'a crate::pool::Pool>,
381}
382
383/// One position: `q̂ k̂ v = W x`, insert, attend, `W_o`. The cache's
384/// `bounded` record must exist (installed from the header at load).
385pub fn bounded_attention(
386    hidden: &[f32],
387    w: &BoundedWeights,
388    cache: &mut crate::kv_cache::LayerKvCache,
389    cfg: &BoundedAttnCfg,
390) -> Vec<f32> {
391    let (nh, nkv, hd) = (cfg.num_heads, cfg.num_kv_heads, cfg.head_dim);
392    let mut q = crate::attention::take_buf(nh * hd);
393    let mut k = crate::attention::take_buf(nkv * hd);
394    let mut v = crate::attention::take_buf(nkv * hd);
395    w.wq.matvec(hidden, &mut q, cfg.pool);
396    w.wk.matvec(hidden, &mut k, cfg.pool);
397    w.wv.matvec(hidden, &mut v, cfg.pool);
398    let mut ao = crate::attention::take_buf(nh * hd);
399    cache.bounded_step(&q, &k, &v, w, cfg.rope, cfg.scale, nh, &mut ao);
400    let mut out = crate::attention::take_buf(cfg.hidden_size);
401    w.wo.matvec(&ao, &mut out, cfg.pool);
402    crate::attention::recycle_buf(&mut q);
403    crate::attention::recycle_buf(&mut k);
404    crate::attention::recycle_buf(&mut v);
405    crate::attention::recycle_buf(&mut ao);
406    out
407}
408
409/// A prefill chunk of `b` positions: the projections run as chunk
410/// GEMMs (each weight row streams once per chunk), the operator runs
411/// per position over ring + chunk — the scores of a position are
412/// against at most `S + W` keys whatever the chunk or the context, and
413/// the per-position arithmetic is the same code as `bounded_attention`.
414pub fn bounded_attention_batch(
415    normed_all: &[f32],
416    b: usize,
417    w: &BoundedWeights,
418    cache: &mut crate::kv_cache::LayerKvCache,
419    cfg: &BoundedAttnCfg,
420) -> Vec<f32> {
421    let (nh, nkv, hd, hs) = (cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.hidden_size);
422    debug_assert_eq!(normed_all.len(), b * hs);
423    let mut q_all = crate::attention::take_buf(b * nh * hd);
424    let mut k_all = crate::attention::take_buf(b * nkv * hd);
425    let mut v_all = crate::attention::take_buf(b * nkv * hd);
426    w.wq.matmat(normed_all, b, &mut q_all, cfg.pool);
427    w.wk.matmat(normed_all, b, &mut k_all, cfg.pool);
428    w.wv.matmat(normed_all, b, &mut v_all, cfg.pool);
429    let mut ao_all = crate::attention::take_buf(b * nh * hd);
430    for bi in 0..b {
431        let q = &q_all[bi * nh * hd..(bi + 1) * nh * hd];
432        let k = &k_all[bi * nkv * hd..(bi + 1) * nkv * hd];
433        let v = &v_all[bi * nkv * hd..(bi + 1) * nkv * hd];
434        let ao = &mut ao_all[bi * nh * hd..(bi + 1) * nh * hd];
435        cache.bounded_step(q, k, v, w, cfg.rope, cfg.scale, nh, ao);
436    }
437    let mut out = crate::attention::take_buf(b * hs);
438    w.wo.matmat(&ao_all, b, &mut out, cfg.pool);
439    crate::attention::recycle_buf(&mut q_all);
440    crate::attention::recycle_buf(&mut k_all);
441    crate::attention::recycle_buf(&mut v_all);
442    crate::attention::recycle_buf(&mut ao_all);
443    out
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    fn synth(n: usize, salt: u64, scale: f32) -> Vec<f32> {
451        (0..n)
452            .map(|i| {
453                let x = (i as u64)
454                    .wrapping_mul(6364136223846793005)
455                    .wrapping_add(salt.wrapping_mul(1442695040888963407) ^ 0x9E3779B97F4A7C15);
456                let x = (x ^ (x >> 31)).wrapping_mul(0xBF58476D1CE4E5B9);
457                (((x >> 11) as f64 / (1u64 << 53) as f64 - 0.5) as f32) * scale
458            })
459            .collect()
460    }
461
462    /// `(R(t−j) q)·k == (R(t) q)·(R(j) k)` — the relative table against
463    /// the absolute `rope_rotate_scaled` of the full-attention path, so
464    /// the sign convention is the runtime's own, not assumed.
465    #[test]
466    fn relative_rotation_equals_absolute_pair() {
467        let hd = 16;
468        let inv = crate::attention::rope_inv_freq(hd, 10_000.0);
469        let rope = BoundedRope::new(64, &inv, 1.0);
470        for (t, j) in [(0usize, 0usize), (5, 5), (7, 3), (63, 0), (300, 250), (1000, 990)] {
471            let q = synth(hd, t as u64 + 1, 1.0);
472            let k = synth(hd, j as u64 + 77, 1.0);
473            let mut qa = q.clone();
474            let mut ka = k.clone();
475            crate::attention::rope_rotate_scaled(&mut qa, t, &inv, 1.0);
476            crate::attention::rope_rotate_scaled(&mut ka, j, &inv, 1.0);
477            let absolute: f64 = qa.iter().zip(&ka).map(|(a, b)| (*a as f64) * (*b as f64)).sum();
478            let mut qr = vec![0.0; hd];
479            rope.rotate(t - j, &q, &mut qr);
480            let relative: f64 = qr.iter().zip(&k).map(|(a, b)| (*a as f64) * (*b as f64)).sum();
481            assert!(
482                (absolute - relative).abs() < 2e-4,
483                "t={t} j={j}: absolute {absolute} vs relative {relative}"
484            );
485        }
486    }
487
488    #[test]
489    fn rollback_restores_overwritten_slots_bit_for_bit() {
490        let (kvh, hd, w) = (2, 4, 8);
491        let mut st = BoundedState::new(kvh, hd, w);
492        for p in 0..20 {
493            st.insert(&synth(kvh * hd, p, 1.0), &synth(kvh * hd, 100 + p, 1.0));
494        }
495        let snap = st.snapshot();
496        for p in 20..25 {
497            st.insert(&synth(kvh * hd, p, 1.0), &synth(kvh * hd, 100 + p, 1.0));
498        }
499        assert_eq!(st.rollback(5), 5);
500        assert!(st.same_state(&{
501            let mut s = BoundedState::new(kvh, hd, w);
502            s.restore(&snap);
503            s
504        }));
505        assert_eq!(st.seen, 20);
506        assert_eq!(st.len(), w);
507    }
508}