Skip to main content

cortiq_engine/
nystrom.rs

1//! Nyström (landmark) attention kernel — streaming per-GQA-group runtime
2//! for long-context `attn_type: nystrom` layers.
3//!
4//! Attention splits into an EXACT sliding window (last `w` keys) and a
5//! landmark-skeleton far field sharing ONE joint denominator:
6//!
7//! ```text
8//! out(q_t) = (Σ_{j>t-w} e_j·v_j + F·M·T_far) / (Σ_{j>t-w} e_j + F·M·Z_far)
9//! e_j   = exp(q_t·k_j/√d)                      exact near weights
10//! F_i   = exp(q_t·k̃_i/√d)                      scores vs landmark keys
11//! M     = pinv_reg(exp(Q̃·K̃ᵀ/√d))               fixed after prefill
12//! T_far = Σ_{j≤t-w} exp(Q̃·k_j/√d)·v_jᵀ         [m × dv]
13//! Z_far = Σ_{j≤t-w} exp(Q̃·k_j/√d)              [m]
14//! ```
15//!
16//! exp(q·k) is a PSD kernel, so the UNNORMALIZED skeleton (classic
17//! Nyström/CUR) is legal.  Do NOT row-softmax the factors and do NOT
18//! normalize the key scores over landmarks — both "simplifications"
19//! measurably collapse quality (validated in the torch matrix probes).
20//!
21//! Boundary discipline: key j enters T/Z at the exact step it LEAVES
22//! the window (t = j+w) — delayed insertion, no overlap, no hole; the
23//! near mass stays exact rather than Nyström-estimated.
24//!
25//! Sink tokens (spec §5b, StreamingLLM discipline): the first `sink`
26//! keys of the sequence are PERMANENT exact keys — the near mask is
27//! (t-j < w) OR (j < sink) — and must never enter the far accumulators.
28//! Here they never enter the ring window in the first place (they live
29//! in a dedicated buffer), so delayed insertion cannot see them: no
30//! double count, no gap.  Measured: sinks make the full 28/28-layer
31//! O(1) conversion viable — the default mode.
32//!
33//! Quality of THIS kernel, measured through it (`cortiq ppl --o1 all`,
34//! Qwen3-0.6B, all 28 layers, m=32 W=128 sink=4, wikitext-2 val, 12×512
35//! windows, landmarks frozen at a 256-token prefill): ×1.296 vs exact
36//! attention over the same scored tokens (28.04 vs 21.63).
37//!
38//! The older ×1.177 figure is NOT this operator: it comes from the torch
39//! matrix probe, which (a) rectifies every per-(t,j) weight — impossible
40//! to stream, the weights are never materialized — (b) builds landmarks
41//! from the FULL sequence rather than the prefill, and (c) averages in
42//! the first W positions, which are pure-exact and cost nothing.  Quote
43//! ×1.296 for the runtime; ×1.177 is an upper bound the runtime cannot
44//! reach by construction.
45//!
46//! fp32 numerics: raw exp overflows on real logits, so shifts are
47//! absorbed into diagonals.  T̂[i]/Ẑ[i] live at scale e^{-m_i} with a
48//! per-landmark running max m_i (flash-style rescale on growth); each
49//! token's landmark row uses its own shift f; near and far are brought
50//! to one common scale before the single joint division.
51
52/// Which rectifier keeps the skeleton's estimated far mass non-negative.
53///
54/// pinv(exp(Q̃K̃ᵀ/√d)) is violently ill-conditioned, so M is indefinite
55/// and the raw skeleton estimates negative weights for a large minority
56/// of keys (measured on Qwen3-0.6B: 23.5% of far weights negative,
57/// carrying 24.5% of the absolute far mass).  Unrectified, the joint
58/// denominator goes near-zero/negative and the model collapses (×510).
59///
60/// The matrix probe rectifies every estimated weight — `west =
61/// ((Fu@Mu)@E).clamp_min(0)`.  A STREAMING kernel cannot do that: the
62/// per-(t, j) weights are never materialized, they exist only already
63/// contracted against the accumulators.  Two streaming-legal stand-ins:
64///
65/// MEASURED (Qwen3-0.6B, all 28 layers, W=128, sink=4, wikitext-2 val,
66/// 12×512 windows, landmarks frozen at a 256-token prefill — i.e. the
67/// runtime's real discipline, `cortiq ppl --o1`):
68///
69/// ```text
70///        m=8              m=16             m=32
71/// agg    28.51 (×1.318)   28.82 (×1.332)   28.04 (×1.296)  ← default
72/// fm     28.97 (×1.340)   29.69 (×1.373)   30.58 (×1.414)
73/// ```
74///
75/// `Aggregate` wins at every m, so it stays the default.  `Fm` is kept
76/// selectable because its per-key guarantee is the intuitively "correct"
77/// fix and someone will re-derive it: this table is the evidence that it
78/// costs quality HERE, and the reason is that the guarantee is bought by
79/// destroying signal — clamping a landmark's coefficient zeroes its
80/// contribution to EVERY far key, including the majority where the
81/// weighted sum was already positive and accurate.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum O1Rect {
84    /// Clamp only the AGGREGATE far denominator: a row whose skeleton
85    /// denominator comes out negative drops its far field entirely.
86    /// Coarse — negative per-key mass survives untouched whenever the
87    /// row sum happens to stay positive — but measured BEST (see above):
88    /// the surviving negatives are apparently error-cancelling, not
89    /// error-causing.
90    Aggregate,
91    /// Clamp FM = F_u·M_u (an m-vector, per query row) at zero.
92    /// ŵ(t,j) = Σ_b FM[b]·E[b,j] and E = exp(·) ≥ 0 ELEMENTWISE, so
93    /// FM ≥ 0 is SUFFICIENT for every far weight to be non-negative —
94    /// a per-key guarantee bought with O(m) work on a vector the row
95    /// already materializes, state untouched.  It is strictly stronger
96    /// than the probe's clamp (a negative landmark is dropped for every
97    /// key, not only where the sum would go negative), so this is a
98    /// DIFFERENT operator, not an emulation of the matrix reference —
99    /// and, measured, a worse one.  Opt in with `--o1-rect fm`.
100    Fm,
101}
102
103/// Ridge factor for the regularized pseudo-inverse of the landmark
104/// kernel: λ = RIDGE_REL · mean(diag(AᵀA)).
105const RIDGE_REL: f64 = 1e-6;
106/// Floor for the joint denominator (mirrors the reference probe).
107const DEN_EPS: f32 = 1e-30;
108/// Prompts of length ≤ w + EXACT_SLACK skip the skeleton entirely:
109/// tiny prefills duplicate segment-mean landmarks (singular Au).
110const EXACT_SLACK: usize = 8;
111
112/// First prompt length that is safe to convert to the streaming skeleton.
113/// Keep this arithmetic checked: the value is also the deferred boundary
114/// stored by the exact KV collector, and a wrapped boundary would turn a
115/// malformed configuration into an immediate or never-ending transition.
116pub(crate) fn o1_deferred_boundary(w: usize, sink: usize) -> Option<usize> {
117    w.checked_add(sink)?
118        .checked_add(EXACT_SLACK)?
119        .checked_add(1)
120}
121
122/// Patent-17 claim 1 probe (`CMF_O1_FARONLY=1`): drop the window from
123/// the READOUT — sinks + far field only — while the ring keeps its
124/// staging role (delayed insertion is untouched). In a GDN hybrid the
125/// near field is carried by the linear-core neighbours; the window is
126/// ~70% of the operator's state and most of its per-token work. Only
127/// engages once the far field holds mass (far_len > 0) — before the
128/// first eviction the window is the only history there is.
129/// Patent-17 claim 9 (`CMF_O1_RESEAL=R`, 0/absent = off): landmarks and
130/// the mixing matrix are FROZEN at prefill, and the deep-layer stream
131/// drifts away from them — measured x3.5-3.9 ppl on the 0.8B hybrid
132/// where the matrix probe reads x1.075. Every R evictions the operator
133/// rebuilds K-landmarks from a ring of recent evicted keys, Q-landmarks
134/// from recent queries, re-inverts M, and re-warms the far accumulators
135/// by re-inserting the ring — sinks and the window stay exact
136/// throughout. Far mass older than the ring is dropped: under drifted
137/// landmarks it was mis-binned anyway, and the ring covers the depth
138/// the ppl gate scores.
139fn reseal_every() -> usize {
140    static R: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
141    *R.get_or_init(|| {
142        std::env::var("CMF_O1_RESEAL")
143            .ok()
144            .and_then(|v| v.parse().ok())
145            .unwrap_or(0)
146    })
147}
148
149/// Sample-ring capacity for reseal (evicted keys/values per group,
150/// recent queries per head).
151const RESEAL_CAP: usize = 256;
152
153fn far_only() -> bool {
154    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
155    *ON.get_or_init(|| std::env::var("CMF_O1_FARONLY").as_deref() == Ok("1"))
156}
157
158/// Streaming Nyström attention state for ONE GQA group.
159///
160/// State splits along the GQA grain, because the operator does:
161///
162/// * SHARED per KV group (`NystromGroup`) — the exact window ring, the
163///   sink buffer and the key landmarks K̃.  Under GQA every Q head of a
164///   group reads the SAME k/v rows, so all three are bit-identical
165///   across the group; storing them once per group instead of once per
166///   Q head is the point of this split (identical arithmetic,
167///   ×heads_per_kv less window memory).  K̃ = seg_means(ks, t, d, m_eff)
168///   is a pure function of the group's keys and of `t` (which fixes
169///   m_eff), so it is shareable for the same reason the keys are.
170/// * PRIVATE per Q head (`NystromHead`) — the far accumulators T̂/Ẑ and
171///   their per-landmark running maxima, the QUERY landmarks Q̃, and the
172///   mixing matrix M = pinv(exp(Q̃K̃ᵀ/√d)).  Q̃ is built from that head's
173///   own queries, so M and the far field it drives are per-Q-head and
174///   cannot be shared: the far mass a head accumulates is contracted
175///   against its own query landmarks.
176///
177/// Lifecycle: `new(m, w, sink)` → `prefill(prompt)` once → `step()` per
178/// decode token (single-head façade), or `new_group`/`prefill_group`/
179/// `step_group` for a whole GQA group at once.  All buffers are flat
180/// `Vec<f32>`, row-major; the skeleton path performs no allocations
181/// inside `step()`.
182#[derive(Clone, Debug)]
183pub struct NystromState {
184    group: NystromGroup,
185    heads: Vec<NystromHead>,
186}
187
188/// The part of the state a GQA group shares: everything derived from
189/// the group's KEYS and VALUES alone (see `NystromState`).
190#[derive(Clone, Debug)]
191struct NystromGroup {
192    /// Landmark budget (m) — effective count may be lower (`m_eff`).
193    m: usize,
194    /// Exact-window width in keys.
195    w: usize,
196    /// Permanent exact sink keys at positions 0..sink (spec §5b).
197    sink: usize,
198    d: usize,
199    dv: usize,
200    /// Effective landmark count: clamp(t/8, 4, m) at prefill.  Derived
201    /// from the prompt length, hence equal for every head of the group.
202    m_eff: usize,
203    /// Short-prompt mode: window holds ALL keys, no skeleton.  The
204    /// buffer grows on decode, so this mode may allocate in `step()` —
205    /// acceptable for the ≤ w+8-token degenerate case.
206    exact_only: bool,
207    scale: f32,
208    /// Window keys `[cap][d]` — ring buffer in skeleton mode (cap = w),
209    /// append-only in exact-only mode.
210    win_k: Vec<f32>,
211    /// Window values `[cap][dv]`.
212    win_v: Vec<f32>,
213    win_len: usize,
214    /// Ring slot of the OLDEST window entry (0 while not yet full).
215    win_head: usize,
216    /// Sink keys `[sink_len][d]` — filled once at prefill, immutable.
217    sink_k: Vec<f32>,
218    /// Sink values `[sink_len][dv]`.
219    sink_v: Vec<f32>,
220    /// Number of stored sink tokens (0 in exact-only mode, where every
221    /// key is permanent-exact anyway).
222    sink_len: usize,
223    /// Key landmarks `[m_eff][d]` — segment means of the group's keys.
224    k_tilde: Vec<f32>,
225    /// Reseal sample ring: recent EVICTED keys/values (chronological
226    /// via `samp_head`), empty unless CMF_O1_RESEAL is set.
227    samp_k: Vec<f32>,
228    samp_v: Vec<f32>,
229    samp_len: usize,
230    samp_head: usize,
231    /// Evictions since the last reseal.
232    since_reseal: usize,
233}
234
235/// The part of the state that is private to one Q head: everything that
236/// touches that head's QUERIES (see `NystromState`).
237#[derive(Clone, Debug)]
238struct NystromHead {
239    /// How the indefinite skeleton is rectified (see `O1Rect`).
240    rect: O1Rect,
241    /// Far numerator `[m_eff][dv]`, stored at scale e^{-m_max[i]}.
242    t_hat: Vec<f32>,
243    /// Far denominator `[m_eff]`, same scale.
244    z_hat: Vec<f32>,
245    /// Per-landmark running max of far logits q̃_i·k_j/√d.
246    m_max: Vec<f32>,
247    /// Number of keys absorbed into the far field.
248    far_len: usize,
249    /// Query landmarks `[m_eff][d]` (segment means of the prefill).
250    q_tilde: Vec<f32>,
251    /// Regularized pseudo-inverse of Au = exp(Q̃·K̃ᵀ/√d), `[m_eff][m_eff]`.
252    mu: Vec<f32>,
253    // Scratch preallocated at prefill so skeleton-mode step() is
254    // allocation-free.  Per head rather than per group: the heads of a
255    // group write it independently, and it is ~0.5 KB.
256    scr_s: Vec<f32>,
257    scr_fh: Vec<f32>,
258    scr_u: Vec<f32>,
259    scr_l: Vec<f32>,
260    /// Reseal: ring of this head's recent queries.
261    samp_q: Vec<f32>,
262    samp_q_len: usize,
263    samp_q_head: usize,
264}
265
266/// Everything `step()`/`advance()` mutate, captured before a
267/// speculative burst and restored bit-for-bit on rejection. The whole
268/// point of the O(1) operator is that this is SMALL — the window ring
269/// plus the far accumulators, ~150 KB a head-group — so speculation,
270/// which Patent 16 disclaims as impossible over the irreversible far
271/// insertion, becomes a memcpy. Immutable-after-seal parts (sinks,
272/// landmarks, mu) are not captured.
273pub struct O1Snapshot {
274    win_k: Vec<f32>,
275    win_v: Vec<f32>,
276    win_len: usize,
277    win_head: usize,
278    /// Per head: (t_hat, z_hat, m_max, far_len).
279    heads: Vec<(Vec<f32>, Vec<f32>, Vec<f32>, usize)>,
280}
281
282impl NystromState {
283    pub fn snapshot(&self) -> O1Snapshot {
284        O1Snapshot {
285            win_k: self.group.win_k.clone(),
286            win_v: self.group.win_v.clone(),
287            win_len: self.group.win_len,
288            win_head: self.group.win_head,
289            heads: self
290                .heads
291                .iter()
292                .map(|h| (h.t_hat.clone(), h.z_hat.clone(), h.m_max.clone(), h.far_len))
293                .collect(),
294        }
295    }
296
297    /// Restore a snapshot taken on THIS state (same geometry). The
298    /// exact-only window grows on decode, so the vectors are assigned,
299    /// not copied into.
300    pub fn restore(&mut self, s: &O1Snapshot) {
301        self.group.win_k = s.win_k.clone();
302        self.group.win_v = s.win_v.clone();
303        self.group.win_len = s.win_len;
304        self.group.win_head = s.win_head;
305        debug_assert_eq!(self.heads.len(), s.heads.len());
306        for (h, (t, z, m, fl)) in self.heads.iter_mut().zip(&s.heads) {
307            h.t_hat = t.clone();
308            h.z_hat = z.clone();
309            h.m_max = m.clone();
310            h.far_len = *fl;
311        }
312    }
313}
314
315/// Borrowed view of a sealed group's state for the GPU upload — every
316/// slice the device mirror needs, in the layout the kernels index.
317/// `exact_only` groups (degenerate short prompts) are not portable and
318/// make the caller refuse the GPU path for the layer.
319pub struct O1DeviceView<'a> {
320    pub m_eff: usize,
321    pub w: usize,
322    pub sink_len: usize,
323    pub d: usize,
324    pub dv: usize,
325    pub exact_only: bool,
326    pub scale: f32,
327    pub win_len: usize,
328    pub win_head: usize,
329    pub far_len: usize,
330    pub win_k: &'a [f32],
331    pub win_v: &'a [f32],
332    pub sink_k: &'a [f32],
333    pub sink_v: &'a [f32],
334    pub k_tilde: &'a [f32],
335    pub heads: Vec<O1HeadView<'a>>,
336}
337
338pub struct O1HeadView<'a> {
339    pub rect_fm: bool,
340    pub t_hat: &'a [f32],
341    pub z_hat: &'a [f32],
342    pub m_max: &'a [f32],
343    pub q_tilde: &'a [f32],
344    pub mu: &'a [f32],
345}
346
347impl NystromState {
348    pub fn device_view(&self) -> O1DeviceView<'_> {
349        let g = &self.group;
350        O1DeviceView {
351            m_eff: g.m_eff,
352            w: g.w,
353            sink_len: g.sink_len,
354            d: g.d,
355            dv: g.dv,
356            exact_only: g.exact_only,
357            scale: g.scale,
358            win_len: g.win_len,
359            win_head: g.win_head,
360            far_len: self.heads.first().map_or(0, |h| h.far_len),
361            win_k: &g.win_k,
362            win_v: &g.win_v,
363            sink_k: &g.sink_k,
364            sink_v: &g.sink_v,
365            k_tilde: &g.k_tilde,
366            heads: self
367                .heads
368                .iter()
369                .map(|h| O1HeadView {
370                    rect_fm: h.rect == O1Rect::Fm,
371                    t_hat: &h.t_hat,
372                    z_hat: &h.z_hat,
373                    m_max: &h.m_max,
374                    q_tilde: &h.q_tilde,
375                    mu: &h.mu,
376                })
377                .collect(),
378        }
379    }
380}
381
382impl NystromState {
383    /// Single-head state (`heads_per_kv == 1`, and the shape the kernel
384    /// unit tests use).
385    ///
386    /// `m` — landmark budget (≥ 4; see `O1_DEFAULT_M`),
387    /// `w` — exact window width (validated setting is 128),
388    /// `sink` — permanent exact sink keys (validated default is 4;
389    /// 0 reproduces the sink-free kernel bit-for-bit).
390    /// Rectifier defaults to `O1_DEFAULT_RECT`; override with
391    /// `with_rect` (the golden-parity test pins it explicitly).
392    pub fn new(m: usize, w: usize, sink: usize) -> Self {
393        Self::new_group(m, w, sink, 1)
394    }
395
396    /// State for one GQA group of `q_heads` query heads sharing a KV
397    /// head.  The window/sink/K̃ are stored ONCE for the group; each Q
398    /// head keeps its own far field, Q̃ and M.
399    pub fn new_group(m: usize, w: usize, sink: usize, q_heads: usize) -> Self {
400        assert!(m >= 4, "landmark budget must be at least 4");
401        assert!(w >= 1, "window must hold at least one key");
402        assert!(q_heads >= 1, "a GQA group needs at least one query head");
403        NystromState {
404            group: NystromGroup {
405                m,
406                w,
407                sink,
408                d: 0,
409                dv: 0,
410                m_eff: 0,
411                exact_only: true,
412                scale: 0.0,
413                win_k: Vec::new(),
414                win_v: Vec::new(),
415                win_len: 0,
416                win_head: 0,
417                sink_k: Vec::new(),
418                sink_v: Vec::new(),
419                sink_len: 0,
420                k_tilde: Vec::new(),
421                samp_k: Vec::new(),
422                samp_v: Vec::new(),
423                samp_len: 0,
424                samp_head: 0,
425                since_reseal: 0,
426            },
427            heads: (0..q_heads).map(|_| NystromHead::new()).collect(),
428        }
429    }
430
431    /// Select the skeleton rectifier for every head of the group
432    /// (builder; see `O1Rect`).
433    pub fn with_rect(mut self, rect: O1Rect) -> Self {
434        for h in &mut self.heads {
435            h.rect = rect;
436        }
437        self
438    }
439
440    /// Query heads in this group.
441    pub fn num_q_heads(&self) -> usize {
442        self.heads.len()
443    }
444
445    /// Keys absorbed into head `head`'s far field.  Exposed for the
446    /// delayed-insertion invariant test: eviction is a GROUP event, but
447    /// each head must absorb the evicted key EXACTLY once, so this must
448    /// equal the number of evictions — never a multiple of it.
449    pub fn far_len(&self, head: usize) -> usize {
450        self.heads[head].far_len
451    }
452
453    /// Absorb the whole prompt for a single-head state — see
454    /// `prefill_group`.
455    pub fn prefill(&mut self, qs: &[f32], ks: &[f32], vs: &[f32], t: usize, d: usize, dv: usize) {
456        assert_eq!(self.heads.len(), 1, "use prefill_group for a GQA group");
457        self.prefill_group(&[qs], ks, vs, t, d, dv);
458    }
459
460    /// Absorb the whole prompt for a GQA group: freeze each head's
461    /// landmarks and M, then replay the prompt through the step() state
462    /// semantics (window fill + delayed far insertion).  `qs[h]` is that
463    /// head's `[t][d]` query block; `ks` is `[t][d]` and `vs` is
464    /// `[t][dv]` — the group's shared keys/values, row-major.
465    pub fn prefill_group(
466        &mut self,
467        qs: &[&[f32]],
468        ks: &[f32],
469        vs: &[f32],
470        t: usize,
471        d: usize,
472        dv: usize,
473    ) {
474        assert_eq!(qs.len(), self.heads.len(), "one query block per head");
475        for q in qs {
476            assert_eq!(q.len(), t * d);
477        }
478        assert_eq!(ks.len(), t * d);
479        assert_eq!(vs.len(), t * dv);
480
481        let Some(k_tilde64) = self.group.prefill_shared(ks, vs, t, d, dv) else {
482            // exact-only: no skeleton, no far field — nothing per head
483            // beyond the score scratch.
484            for h in &mut self.heads {
485                h.seal_exact(t);
486            }
487            return;
488        };
489        for (h, q) in self.heads.iter_mut().zip(qs) {
490            h.seal(&self.group, q, t, &k_tilde64);
491        }
492        // Replay the post-sink prompt ONCE for the group: each key
493        // enters the shared window, evicting the (j-w)-th into every
494        // head's far field.
495        for j in self.group.sink..t {
496            Self::advance(
497                &mut self.group,
498                &mut self.heads,
499                &ks[j * d..(j + 1) * d],
500                &vs[j * dv..(j + 1) * dv],
501            );
502        }
503    }
504
505    /// One decode step for a single-head state — see `step_group`.
506    pub fn step(&mut self, q: &[f32], k: &[f32], v: &[f32], out: &mut [f32]) {
507        assert_eq!(self.heads.len(), 1, "use step_group for a GQA group");
508        self.step_group(q, k, v, out);
509    }
510
511    /// One decode step for the whole GQA group.  Inserts the group's
512    /// (k, v) ONCE, evicting the oldest window key into every head's far
513    /// accumulators, then writes each head's attention output.
514    /// `q_all` is `[q_heads][d]`, `out_all` is `[q_heads][dv]`.
515    pub fn step_group(&mut self, q_all: &[f32], k: &[f32], v: &[f32], out_all: &mut [f32]) {
516        let (d, dv) = (self.group.d, self.group.dv);
517        assert!(d > 0, "prefill() must run before step()");
518        let nh = self.heads.len();
519        assert_eq!(q_all.len(), nh * d);
520        assert_eq!(k.len(), d);
521        assert_eq!(v.len(), dv);
522        assert_eq!(out_all.len(), nh * dv);
523        // The current token is part of its own near window (t-j = 0),
524        // so insertion happens BEFORE any output is computed.
525        Self::advance(&mut self.group, &mut self.heads, k, v);
526        let rs = reseal_every();
527        for (h, head) in self.heads.iter_mut().enumerate() {
528            let qh = &q_all[h * d..(h + 1) * d];
529            if rs > 0 && !self.group.exact_only {
530                if head.samp_q.is_empty() {
531                    head.samp_q = vec![0.0; RESEAL_CAP * d];
532                }
533                let sp = head.samp_q_head;
534                head.samp_q[sp * d..(sp + 1) * d].copy_from_slice(qh);
535                head.samp_q_head = (sp + 1) % RESEAL_CAP;
536                head.samp_q_len = (head.samp_q_len + 1).min(RESEAL_CAP);
537            }
538            head.step(&self.group, qh, &mut out_all[h * dv..(h + 1) * dv]);
539        }
540        if rs > 0 && self.group.since_reseal >= rs && self.group.samp_len >= 2 * self.group.m_eff {
541            self.reseal();
542        }
543    }
544
545    /// Rebuild the skeleton against the CURRENT stream (Patent 17):
546    /// K-landmarks from the ring of recently evicted keys, Q-landmarks
547    /// from each head's recent queries, M re-inverted, and the far
548    /// accumulators re-warmed by re-inserting the ring. Sinks and the
549    /// window are untouched — the exact stores anchor the operator
550    /// while the approximation refreshes.
551    fn reseal(&mut self) {
552        let g = &mut self.group;
553        let (d, dv, m_eff) = (g.d, g.dv, g.m_eff);
554        let n = g.samp_len;
555        // Chronological copies (oldest first) out of the rings.
556        let start = if n == RESEAL_CAP { g.samp_head } else { 0 };
557        let mut ks = vec![0.0f32; n * d];
558        let mut vs = vec![0.0f32; n * dv];
559        for i in 0..n {
560            let idx = (start + i) % RESEAL_CAP;
561            ks[i * d..(i + 1) * d].copy_from_slice(&g.samp_k[idx * d..(idx + 1) * d]);
562            vs[i * dv..(i + 1) * dv].copy_from_slice(&g.samp_v[idx * dv..(idx + 1) * dv]);
563        }
564        let k_tilde64 = seg_means(&ks, n, d, m_eff);
565        g.k_tilde = k_tilde64.iter().map(|&x| x as f32).collect();
566        g.since_reseal = 0;
567        for head in &mut self.heads {
568            let qn = head.samp_q_len;
569            if qn < m_eff {
570                continue; // not enough queries yet — keep the old Q̃/M
571            }
572            let qstart = if qn == RESEAL_CAP {
573                head.samp_q_head
574            } else {
575                0
576            };
577            let mut qs = vec![0.0f32; qn * d];
578            for i in 0..qn {
579                let idx = (qstart + i) % RESEAL_CAP;
580                qs[i * d..(i + 1) * d].copy_from_slice(&head.samp_q[idx * d..(idx + 1) * d]);
581            }
582            let q_tilde64 = seg_means(&qs, qn, d, m_eff);
583            head.q_tilde = q_tilde64.iter().map(|&x| x as f32).collect();
584            let mut au = vec![0.0f64; m_eff * m_eff];
585            for i in 0..m_eff {
586                for j in 0..m_eff {
587                    let mut s = 0.0f64;
588                    for c in 0..d {
589                        s += q_tilde64[i * d + c] * k_tilde64[j * d + c];
590                    }
591                    au[i * m_eff + j] = (s * g.scale as f64).exp();
592                }
593            }
594            let mu64 = ridge_pinv(&au, m_eff);
595            head.mu = mu64.iter().map(|&x| x as f32).collect();
596            // Re-warm: the far field is rebuilt from the ring. Mass
597            // older than the ring is dropped — under the drifted
598            // landmarks it was mis-binned anyway.
599            head.t_hat.iter_mut().for_each(|x| *x = 0.0);
600            head.z_hat.iter_mut().for_each(|x| *x = 0.0);
601            head.m_max.iter_mut().for_each(|x| *x = f32::NEG_INFINITY);
602            head.far_len = 0;
603            for i in 0..n {
604                head.far_absorb(
605                    m_eff,
606                    d,
607                    dv,
608                    g.scale,
609                    &ks[i * d..(i + 1) * d],
610                    &vs[i * dv..(i + 1) * dv],
611                );
612            }
613        }
614    }
615
616    /// Heap bytes held by this group's state (shared window + sinks +
617    /// K̃, plus each head's skeleton and scratch) — feeds the honest
618    /// "KV+state" memory line, same discipline as counting
619    /// `linear_state` for the linear core.
620    pub fn memory_bytes(&self) -> usize {
621        self.group.memory_bytes()
622            + self
623                .heads
624                .iter()
625                .map(NystromHead::memory_bytes)
626                .sum::<usize>()
627    }
628
629    /// Push the group's (k, v) into the shared window.  In skeleton mode
630    /// a full ring first evicts its oldest key (delayed insertion — the
631    /// key leaves the exact window at this very step).
632    ///
633    /// The eviction is a GROUP event: the window is shared, so there is
634    /// exactly ONE eviction per position, not one per Q head.  The far
635    /// accumulators are per head, though, so that single evicted key is
636    /// absorbed once into EACH head — one eviction, `q_heads`
637    /// insertions.  Getting this wrong in either direction breaks the
638    /// boundary invariant (a key enters the far field at exactly the
639    /// step it leaves the window: no double count, no hole).
640    fn advance(g: &mut NystromGroup, heads: &mut [NystromHead], k: &[f32], v: &[f32]) {
641        let (d, dv) = (g.d, g.dv);
642        if !g.exact_only && g.win_len == g.w {
643            let slot = g.win_head;
644            // Every head absorbs the outgoing key BEFORE the slot is
645            // overwritten by the incoming one.
646            for h in heads.iter_mut() {
647                h.far_insert(g, slot);
648            }
649            // Reseal sampling: the evicted (k, v) joins the ring the
650            // next reseal rebuilds landmarks and far mass from.
651            if reseal_every() > 0 {
652                if g.samp_k.is_empty() {
653                    g.samp_k = vec![0.0; RESEAL_CAP * d];
654                    g.samp_v = vec![0.0; RESEAL_CAP * dv];
655                }
656                let sp = g.samp_head;
657                g.samp_k[sp * d..(sp + 1) * d].copy_from_slice(&g.win_k[slot * d..(slot + 1) * d]);
658                g.samp_v[sp * dv..(sp + 1) * dv]
659                    .copy_from_slice(&g.win_v[slot * dv..(slot + 1) * dv]);
660                g.samp_head = (sp + 1) % RESEAL_CAP;
661                g.samp_len = (g.samp_len + 1).min(RESEAL_CAP);
662                g.since_reseal += 1;
663            }
664            g.win_k[slot * d..(slot + 1) * d].copy_from_slice(k);
665            g.win_v[slot * dv..(slot + 1) * dv].copy_from_slice(v);
666            g.win_head = (g.win_head + 1) % g.w;
667        } else if g.exact_only {
668            g.win_k.extend_from_slice(k);
669            g.win_v.extend_from_slice(v);
670            g.win_len += 1;
671        } else {
672            g.win_k[g.win_len * d..(g.win_len + 1) * d].copy_from_slice(k);
673            g.win_v[g.win_len * dv..(g.win_len + 1) * dv].copy_from_slice(v);
674            g.win_len += 1;
675        }
676    }
677}
678
679impl NystromGroup {
680    /// Freeze the group-shared geometry from the prompt's keys/values.
681    /// Returns the f64 key landmarks (which the heads need at full
682    /// precision to build Au), or None in exact-only mode.
683    fn prefill_shared(
684        &mut self,
685        ks: &[f32],
686        vs: &[f32],
687        t: usize,
688        d: usize,
689        dv: usize,
690    ) -> Option<Vec<f64>> {
691        self.d = d;
692        self.dv = dv;
693        self.scale = 1.0 / (d as f32).sqrt();
694        self.win_len = 0;
695        self.win_head = 0;
696        self.sink_len = 0;
697        // A runtime seal is only admitted at the first bounded boundary
698        // (w + sink + slack + 1), so a converted prompt never enters the
699        // exact-only state. Direct Nystrom users retain the historical
700        // exact-only behavior for short prefills.
701        self.exact_only = o1_deferred_boundary(self.w, self.sink)
702            .map(|boundary| t < boundary)
703            .unwrap_or(true);
704        if self.exact_only {
705            // The end of a three-hop silence: exact-only seals are not
706            // portable to the graph (o1_views -> None), which read as
707            // "0 of 16 layers sealed" upstairs, which read as a broken
708            // seal, which read as a broken port. Say the arithmetic.
709            tracing::info!(
710                "o1 seal: exact-only (prompt t={t} <= w {} + sink {} + slack {}) — \
711                 not graph-portable; longer prompt or smaller --o1-window lifts it",
712                self.w,
713                self.sink,
714                EXACT_SLACK
715            );
716        }
717
718        if self.exact_only {
719            // Everything fits in the exact window (plus slack for a few
720            // decode steps before Vec growth); no skeleton is built and
721            // no separate sink buffer is needed — every key is already
722            // a permanent exact key in this mode.
723            self.win_k = Vec::with_capacity((t + 64) * d);
724            self.win_v = Vec::with_capacity((t + 64) * dv);
725            self.win_k.extend_from_slice(ks);
726            self.win_v.extend_from_slice(vs);
727            self.win_len = t;
728            return None;
729        }
730
731        // Sink tokens: positions 0..sink become permanent exact keys.
732        // They bypass the ring window entirely, so the delayed-insertion
733        // path can never move them into the far accumulators.
734        self.sink_len = self.sink; // skeleton mode guarantees t > sink
735        self.sink_k = ks[..self.sink * d].to_vec();
736        self.sink_v = vs[..self.sink * dv].to_vec();
737
738        // Landmarks: contiguous segment means of the prompt.  The
739        // integer split (i·t)/m matches the reference probe; the clamp
740        // keeps tiny prompts from producing duplicate landmarks.
741        let m_eff = (t / 8).clamp(4, self.m);
742        // Say so when the budget asked for is not the budget used. A
743        // prefill of 256 caps m_eff at 32, so `--o1-m 64`, `128` and
744        // `256` all run as 32 and report perplexities identical to the
745        // last digit — which reads as a saturating method rather than a
746        // clamp, and cost a sweep before it was noticed. This file's own
747        // discipline is that a file is either valid or open() fails
748        // loudly; a flag that silently does nothing is the same defect
749        // one level up.
750        if m_eff < self.m {
751            use std::sync::atomic::{AtomicBool, Ordering};
752            static SAID: AtomicBool = AtomicBool::new(false);
753            if !SAID.swap(true, Ordering::Relaxed) {
754                tracing::warn!(
755                    "o1: landmark budget m={} clamped to m_eff={} — the prefill is {t} tokens \
756                     and the skeleton takes t/8. Prefill at least {} tokens to use the budget \
757                     you asked for.",
758                    self.m,
759                    m_eff,
760                    self.m * 8
761                );
762            }
763        }
764        self.m_eff = m_eff;
765        let k_tilde64 = seg_means(ks, t, d, m_eff);
766        self.k_tilde = k_tilde64.iter().map(|&x| x as f32).collect();
767
768        self.win_k = vec![0.0; self.w * d];
769        self.win_v = vec![0.0; self.w * dv];
770        Some(k_tilde64)
771    }
772
773    fn memory_bytes(&self) -> usize {
774        (self.win_k.len()
775            + self.win_v.len()
776            + self.sink_k.len()
777            + self.sink_v.len()
778            + self.k_tilde.len())
779            * std::mem::size_of::<f32>()
780    }
781}
782
783impl NystromHead {
784    fn new() -> Self {
785        NystromHead {
786            rect: O1_DEFAULT_RECT,
787            t_hat: Vec::new(),
788            z_hat: Vec::new(),
789            m_max: Vec::new(),
790            far_len: 0,
791            q_tilde: Vec::new(),
792            mu: Vec::new(),
793            scr_s: Vec::new(),
794            scr_fh: Vec::new(),
795            scr_u: Vec::new(),
796            scr_l: Vec::new(),
797            samp_q: Vec::new(),
798            samp_q_len: 0,
799            samp_q_head: 0,
800        }
801    }
802
803    /// exact-only mode: no skeleton state at all, just room to score the
804    /// growing window.
805    fn seal_exact(&mut self, t: usize) {
806        self.far_len = 0;
807        self.scr_s = Vec::with_capacity(t + 64);
808    }
809
810    /// Freeze this head's query landmarks and mixing matrix against the
811    /// group's (already frozen) key landmarks.
812    fn seal(&mut self, g: &NystromGroup, qs: &[f32], t: usize, k_tilde64: &[f64]) {
813        let (d, dv, m_eff) = (g.d, g.dv, g.m_eff);
814        self.far_len = 0;
815        let q_tilde64 = seg_means(qs, t, d, m_eff);
816        self.q_tilde = q_tilde64.iter().map(|&x| x as f32).collect();
817
818        // Au and its regularized pseudo-inverse in f64 — one-off m×m
819        // work at prefill only; the hot path stays f32.
820        let mut au = vec![0.0f64; m_eff * m_eff];
821        for i in 0..m_eff {
822            for j in 0..m_eff {
823                let mut s = 0.0f64;
824                for c in 0..d {
825                    s += q_tilde64[i * d + c] * k_tilde64[j * d + c];
826                }
827                au[i * m_eff + j] = (s * g.scale as f64).exp();
828            }
829        }
830        let mu64 = ridge_pinv(&au, m_eff);
831        self.mu = mu64.iter().map(|&x| x as f32).collect();
832
833        self.t_hat = vec![0.0; m_eff * dv];
834        self.z_hat = vec![0.0; m_eff];
835        self.m_max = vec![f32::NEG_INFINITY; m_eff];
836        self.scr_s = vec![0.0; g.sink + g.w];
837        self.scr_fh = vec![0.0; m_eff];
838        self.scr_u = vec![0.0; m_eff];
839        self.scr_l = vec![0.0; m_eff];
840    }
841
842    /// This head's output for `q` against the group's current window and
843    /// sinks and its own far field.  The window insertion for this
844    /// position already happened at group level (`NystromState::advance`).
845    fn step(&mut self, g: &NystromGroup, q: &[f32], out: &mut [f32]) {
846        let (d, dv) = (g.d, g.dv);
847        assert_eq!(q.len(), d);
848        assert_eq!(out.len(), dv);
849
850        // Near field: exact logits over sinks + window, one shared
851        // shift.  Sinks are permanent exact keys (near mask §5b:
852        // t-j < w OR j < sink); sink_len = 0 in exact-only mode.
853        let ns = g.sink_len;
854        let skip_win = far_only() && !g.exact_only && self.far_len > 0;
855        let n = if skip_win { ns } else { ns + g.win_len };
856        self.scr_s.resize(n, 0.0);
857        let mut c = f32::NEG_INFINITY;
858        for s in 0..ns {
859            let lg = dot(q, &g.sink_k[s * d..(s + 1) * d]) * g.scale;
860            self.scr_s[s] = lg;
861            c = c.max(lg);
862        }
863        // Window scores are the decode hot loop — NEON dot (same
864        // products, regrouped sums; parity-gated by the golden tests).
865        if !skip_win {
866            for s in 0..g.win_len {
867                let lg = crate::attention::dot_f32(q, &g.win_k[s * d..(s + 1) * d]) * g.scale;
868                self.scr_s[ns + s] = lg;
869                c = c.max(lg);
870            }
871        }
872
873        // Far field: shifted skeleton (spec §3).  All exp arguments are
874        // ≤ 0 relative to the joint shift c_all, so nothing overflows.
875        let mut far_den = 0.0f32;
876        let mut c_all = c;
877        let mut have_far = false;
878        if self.far_len > 0 {
879            // Per-token row shift f over landmark scores.
880            let mut f = f32::NEG_INFINITY;
881            for a in 0..g.m_eff {
882                let s = crate::attention::dot_f32(q, &g.k_tilde[a * d..(a + 1) * d]) * g.scale;
883                self.scr_fh[a] = s;
884                f = f.max(s);
885            }
886            for a in 0..g.m_eff {
887                self.scr_fh[a] = (self.scr_fh[a] - f).exp();
888            }
889            // u = (F·e^{-f}) · M — the landmark mixing row (= FM, up to
890            // the positive factor e^{-f}).
891            for b in 0..g.m_eff {
892                let mut s = 0.0f32;
893                for a in 0..g.m_eff {
894                    s += self.scr_fh[a] * self.mu[a * g.m_eff + b];
895                }
896                // FM rectifier: every far weight is Σ_b FM[b]·E[b,j]
897                // with E ≥ 0 elementwise, so clamping this m-vector is
898                // enough to make all of them non-negative — the per-key
899                // guarantee the streaming form otherwise cannot state.
900                // The row shift e^{-f} and the flash factors below are
901                // strictly positive, so clamping here or after the
902                // rescale is the same predicate.
903                self.scr_u[b] = if self.rect == O1Rect::Fm {
904                    s.max(0.0)
905                } else {
906                    s
907                };
908            }
909            // Joint scale: the far term b carries e^{f + m_max[b]}, the
910            // near term e^{c}; take the max so every factor is ≤ 1.
911            for b in 0..g.m_eff {
912                c_all = c_all.max(f + self.m_max[b]);
913            }
914            for b in 0..g.m_eff {
915                let gain = self.scr_u[b] * (f + self.m_max[b] - c_all).exp();
916                self.scr_u[b] = gain;
917                far_den += gain * self.z_hat[b];
918            }
919            // Aggregate guard — the rectifier of `O1Rect::Aggregate`,
920            // and a second line of defence under `Fm` (where far_den is
921            // a sum of non-negative terms, so this can only fire on
922            // rounding): a negative denominator means the skeleton
923            // estimate is unusable for this row — drop the far field.
924            if far_den >= 0.0 {
925                have_far = true;
926            } else {
927                far_den = 0.0;
928            }
929        }
930
931        for o in out.iter_mut() {
932            *o = 0.0;
933        }
934        if have_far {
935            for b in 0..g.m_eff {
936                crate::attention::axpy_f32(out, &self.t_hat[b * dv..(b + 1) * dv], self.scr_u[b]);
937            }
938        }
939        let mut den = far_den;
940        for s in 0..n {
941            let p = (self.scr_s[s] - c_all).exp();
942            den += p;
943            // scr_s rows 0..ns are sinks, the rest are window entries.
944            let vv = if s < ns {
945                &g.sink_v[s * dv..(s + 1) * dv]
946            } else {
947                &g.win_v[(s - ns) * dv..(s - ns + 1) * dv]
948            };
949            crate::attention::axpy_f32(out, vv, p);
950        }
951        let den = den.max(DEN_EPS);
952        for o in out.iter_mut() {
953            *o /= den;
954        }
955    }
956
957    /// Absorb the group's window slot into THIS head's far accumulators
958    /// with the per-landmark flash shift: T̂[i]/Ẑ[i] live at scale
959    /// e^{-m_max[i]}; when a new logit raises the max, existing mass is
960    /// rescaled by e^{old-new} (exactly 0 on first insertion, since
961    /// m_max = -inf).
962    fn far_insert(&mut self, g: &NystromGroup, slot: usize) {
963        let (d, dv) = (g.d, g.dv);
964        // SAFETY of the two slices: slot < w, buffers are w-sized.
965        let k = &g.win_k[slot * d..(slot + 1) * d];
966        let v = &g.win_v[slot * dv..(slot + 1) * dv];
967        // borrow-friendly copies are avoided: far_absorb takes slices.
968        // (g is &, self is &mut — disjoint.)
969        let (m_eff, scale) = (g.m_eff, g.scale);
970        // Runs once per evicted key per head — NEON dot/axpy like the
971        // decode loop (same products, regrouped sums).
972        self.far_absorb_slices(m_eff, d, dv, scale, k, v);
973    }
974
975    /// The insertion math itself, over caller-provided (k, v) — shared
976    /// by the streaming path (window slot) and the reseal re-warm
977    /// (sample ring).
978    fn far_absorb(&mut self, m_eff: usize, d: usize, dv: usize, scale: f32, k: &[f32], v: &[f32]) {
979        self.far_absorb_slices(m_eff, d, dv, scale, k, v);
980    }
981
982    fn far_absorb_slices(
983        &mut self,
984        m_eff: usize,
985        d: usize,
986        dv: usize,
987        scale: f32,
988        k: &[f32],
989        v: &[f32],
990    ) {
991        if self.scr_l.len() < m_eff {
992            self.scr_l.resize(m_eff, 0.0);
993        }
994        for i in 0..m_eff {
995            self.scr_l[i] = crate::attention::dot_f32(&self.q_tilde[i * d..(i + 1) * d], k) * scale;
996        }
997        for i in 0..m_eff {
998            let l = self.scr_l[i];
999            if l > self.m_max[i] {
1000                let r = (self.m_max[i] - l).exp();
1001                self.z_hat[i] *= r;
1002                for e in self.t_hat[i * dv..(i + 1) * dv].iter_mut() {
1003                    *e *= r;
1004                }
1005                self.m_max[i] = l;
1006            }
1007            let e = (l - self.m_max[i]).exp();
1008            self.z_hat[i] += e;
1009            crate::attention::axpy_f32(&mut self.t_hat[i * dv..(i + 1) * dv], v, e);
1010        }
1011        self.far_len += 1;
1012    }
1013
1014    fn memory_bytes(&self) -> usize {
1015        (self.t_hat.len()
1016            + self.z_hat.len()
1017            + self.m_max.len()
1018            + self.q_tilde.len()
1019            + self.mu.len()
1020            + self.scr_s.len()
1021            + self.scr_fh.len()
1022            + self.scr_u.len()
1023            + self.scr_l.len())
1024            * std::mem::size_of::<f32>()
1025    }
1026}
1027
1028/// Contiguous segment means (the Nyströmformer landmark recipe), f64
1029/// accumulation.  The split (i·t)/m matches the Python reference.
1030fn seg_means(xs: &[f32], t: usize, d: usize, m: usize) -> Vec<f64> {
1031    let mut out = vec![0.0f64; m * d];
1032    for i in 0..m {
1033        let lo = i * t / m;
1034        let hi = (i + 1) * t / m;
1035        for j in lo..hi {
1036            for c in 0..d {
1037                out[i * d + c] += xs[j * d + c] as f64;
1038            }
1039        }
1040        let inv = 1.0 / (hi - lo) as f64;
1041        for c in 0..d {
1042            out[i * d + c] *= inv;
1043        }
1044    }
1045    out
1046}
1047
1048fn dot(a: &[f32], b: &[f32]) -> f32 {
1049    let mut s = 0.0f32;
1050    for (x, y) in a.iter().zip(b) {
1051        s += x * y;
1052    }
1053    s
1054}
1055
1056/// Regularized pseudo-inverse M = (AᵀA + λI)⁻¹ Aᵀ of a square matrix,
1057/// λ = RIDGE_REL·mean(diag(AᵀA)), solved via Cholesky.  f64 internal —
1058/// this runs once per prefill on an m×m matrix (m ≤ 32).  If Cholesky
1059/// fails (Au numerically singular despite the m_eff clamp), λ grows
1060/// tenfold — the jitter fallback of the reference probe.
1061/// pub(crate): the FCD polish trainer builds its (constant-in-backward)
1062/// mixing matrix with the SAME solver the runtime seals with.
1063pub(crate) fn ridge_pinv(a: &[f64], n: usize) -> Vec<f64> {
1064    let mut ata = vec![0.0f64; n * n];
1065    for i in 0..n {
1066        for j in 0..n {
1067            let mut s = 0.0;
1068            for k in 0..n {
1069                s += a[k * n + i] * a[k * n + j];
1070            }
1071            ata[i * n + j] = s;
1072        }
1073    }
1074    let mean_diag: f64 = (0..n).map(|i| ata[i * n + i]).sum::<f64>() / n as f64;
1075    let mut lambda = RIDGE_REL * mean_diag.max(f64::MIN_POSITIVE);
1076    for _ in 0..12 {
1077        let mut g = ata.clone();
1078        for i in 0..n {
1079            g[i * n + i] += lambda;
1080        }
1081        if let Some(l) = cholesky(&mut g, n) {
1082            // Solve G·M = Aᵀ column by column; column j of Aᵀ is row j
1083            // of A.
1084            let mut m_out = vec![0.0f64; n * n];
1085            let mut x = vec![0.0f64; n];
1086            for j in 0..n {
1087                let rhs = &a[j * n..(j + 1) * n];
1088                // Forward: L·y = rhs.
1089                for i in 0..n {
1090                    let mut s = rhs[i];
1091                    for k in 0..i {
1092                        s -= l[i * n + k] * x[k];
1093                    }
1094                    x[i] = s / l[i * n + i];
1095                }
1096                // Backward: Lᵀ·x = y.
1097                for i in (0..n).rev() {
1098                    let mut s = x[i];
1099                    for k in i + 1..n {
1100                        s -= l[k * n + i] * x[k];
1101                    }
1102                    x[i] = s / l[i * n + i];
1103                }
1104                for i in 0..n {
1105                    m_out[i * n + j] = x[i];
1106                }
1107            }
1108            return m_out;
1109        }
1110        lambda *= 10.0;
1111    }
1112    // Unreachable in practice: λ eventually dominates the diagonal.
1113    // Degrade to a scaled identity rather than poison the output.
1114    let mut fallback = vec![0.0f64; n * n];
1115    for i in 0..n {
1116        fallback[i * n + i] = 1.0 / mean_diag.max(f64::MIN_POSITIVE);
1117    }
1118    fallback
1119}
1120
1121// ── Runtime configuration (v1: runtime-level, NOT a format change) ──
1122//
1123// A layer set + {m, w, sink}, resolved in priority order:
1124//   1. CLI flag (`--o1` on run/serve/bench) — explicit user intent;
1125//   2. env `CMF_O1` (all | deepN | i,j,k | off) with CMF_O1_M /
1126//      CMF_O1_WINDOW / CMF_O1_SINK parameter overrides;
1127//   3. converter hint in the header JSON (`provenance.o1_attn`,
1128//      written by `cortiq convert --o1`) — additive metadata, the
1129//      binary envelope is untouched.
1130
1131/// Validated defaults (spec: m=32, W=128, sink=4; sink ablation ×2.39).
1132pub const O1_DEFAULT_M: usize = 32;
1133pub const O1_DEFAULT_W: usize = 128;
1134pub const O1_DEFAULT_SINK: usize = 4;
1135/// Rectifier default (see `O1Rect`).
1136pub const O1_DEFAULT_RECT: O1Rect = O1Rect::Aggregate;
1137
1138/// Which layers run the O(1) kernel.
1139#[derive(Clone, Debug, PartialEq, Eq)]
1140pub enum O1Layers {
1141    All,
1142    /// The N deepest layers (deep-N ladder of the price map; the
1143    /// early stack is the most sink-dependent, depth converts best).
1144    Deep(usize),
1145    /// Explicit layer indices.
1146    List(Vec<usize>),
1147}
1148
1149/// Per-model O(1)-attention setting.
1150#[derive(Clone, Debug)]
1151pub struct O1Cfg {
1152    pub layers: O1Layers,
1153    /// Landmark budget (≥ 4; m=64 measured WORSE — collinear segment
1154    /// means poison the pinv, so don't "help" by raising it).
1155    pub m: usize,
1156    /// Exact-window width — the main quality lever.
1157    pub w: usize,
1158    /// Permanent exact sink keys (StreamingLLM discipline, spec §5b).
1159    pub sink: usize,
1160    /// Skeleton rectifier (see `O1Rect`).
1161    pub rect: O1Rect,
1162}
1163
1164/// Three-state env reading: unset falls through to the header hint,
1165/// `off`/`0` force-disables even a header hint (the escape hatch).
1166pub enum O1Env {
1167    Unset,
1168    Off,
1169    On(O1Cfg),
1170}
1171
1172impl O1Cfg {
1173    /// Parse a layer spec: `all` | `deepN` | `i,j,k`. None = not a spec
1174    /// (also used for `off`/`0`/empty).
1175    pub fn parse_layers(spec: &str) -> Option<O1Layers> {
1176        let s = spec.trim();
1177        match s {
1178            "" | "off" | "0" | "none" => None,
1179            "all" => Some(O1Layers::All),
1180            _ => {
1181                if let Some(n) = s.strip_prefix("deep") {
1182                    return n
1183                        .parse::<usize>()
1184                        .ok()
1185                        .filter(|&n| n > 0)
1186                        .map(O1Layers::Deep);
1187                }
1188                let idx: Result<Vec<usize>, _> =
1189                    s.split(',').map(|p| p.trim().parse::<usize>()).collect();
1190                idx.ok().filter(|v| !v.is_empty()).map(O1Layers::List)
1191            }
1192        }
1193    }
1194
1195    /// Parse a rectifier spec: `agg`/`aggregate` | `fm`. None = not a
1196    /// spec.
1197    pub fn parse_rect(spec: &str) -> Option<O1Rect> {
1198        match spec.trim() {
1199            "agg" | "aggregate" => Some(O1Rect::Aggregate),
1200            "fm" => Some(O1Rect::Fm),
1201            _ => None,
1202        }
1203    }
1204
1205    /// Rectifier from an explicit value, else `CMF_O1_RECT`, else the
1206    /// default.
1207    fn rect_or_env(rect: Option<O1Rect>) -> O1Rect {
1208        rect.or_else(|| {
1209            std::env::var("CMF_O1_RECT")
1210                .ok()
1211                .as_deref()
1212                .and_then(Self::parse_rect)
1213        })
1214        .unwrap_or(O1_DEFAULT_RECT)
1215    }
1216
1217    /// Build from an explicit spec (CLI path). None = `off` or malformed.
1218    /// Explicit m/w/sink/rect beat env overrides beat validated defaults.
1219    pub fn from_spec(
1220        spec: &str,
1221        m: Option<usize>,
1222        w: Option<usize>,
1223        sink: Option<usize>,
1224        rect: Option<O1Rect>,
1225    ) -> Option<O1Cfg> {
1226        let layers = Self::parse_layers(spec)?;
1227        let env = |k: &str| std::env::var(k).ok().and_then(|v| v.parse::<usize>().ok());
1228        Some(O1Cfg {
1229            layers,
1230            // NystromState asserts m ≥ 4 and w ≥ 1 — clamp rather than
1231            // panic deep in the first prefill.
1232            m: m.or_else(|| env("CMF_O1_M")).unwrap_or(O1_DEFAULT_M).max(4),
1233            w: w.or_else(|| env("CMF_O1_WINDOW"))
1234                .unwrap_or(O1_DEFAULT_W)
1235                .max(1),
1236            sink: sink
1237                .or_else(|| env("CMF_O1_SINK"))
1238                .unwrap_or(O1_DEFAULT_SINK),
1239            rect: Self::rect_or_env(rect),
1240        })
1241    }
1242
1243    /// Converter hint from the header JSON: `{"layers": "all"|[i,…],
1244    /// "m": …, "w": …, "sink": …}`. Env parameter overrides still apply
1245    /// (the operator's knob wins over the file's suggestion).
1246    pub fn from_json(v: &serde_json::Value) -> Option<O1Cfg> {
1247        let layers = match v.get("layers") {
1248            Some(serde_json::Value::String(s)) => Self::parse_layers(s)?,
1249            Some(serde_json::Value::Array(a)) => O1Layers::List(
1250                a.iter()
1251                    .filter_map(|x| x.as_u64().map(|n| n as usize))
1252                    .collect(),
1253            ),
1254            _ => return None,
1255        };
1256        let f = |k: &str| v.get(k).and_then(|x| x.as_u64()).map(|n| n as usize);
1257        let env = |k: &str| std::env::var(k).ok().and_then(|s| s.parse::<usize>().ok());
1258        Some(O1Cfg {
1259            layers,
1260            m: env("CMF_O1_M")
1261                .or_else(|| f("m"))
1262                .unwrap_or(O1_DEFAULT_M)
1263                .max(4),
1264            w: env("CMF_O1_WINDOW")
1265                .or_else(|| f("w"))
1266                .unwrap_or(O1_DEFAULT_W)
1267                .max(1),
1268            sink: env("CMF_O1_SINK")
1269                .or_else(|| f("sink"))
1270                .unwrap_or(O1_DEFAULT_SINK),
1271            // The rectifier is a runtime property of the kernel, not a
1272            // property of the weights — a file hint cannot pin it.
1273            rect: Self::rect_or_env(None),
1274        })
1275    }
1276
1277    /// Per-layer flags over `num_layers` (indices past the end are
1278    /// silently dropped; the pipeline additionally filters non-Full
1279    /// layers — a linear layer keeps its own operator).
1280    pub fn layer_flags(&self, num_layers: usize) -> Vec<bool> {
1281        let mut flags = vec![false; num_layers];
1282        match &self.layers {
1283            O1Layers::All => flags.iter_mut().for_each(|f| *f = true),
1284            O1Layers::Deep(n) => {
1285                for f in flags.iter_mut().skip(num_layers.saturating_sub(*n)) {
1286                    *f = true;
1287                }
1288            }
1289            O1Layers::List(idx) => {
1290                for &i in idx {
1291                    if i < num_layers {
1292                        flags[i] = true;
1293                    }
1294                }
1295            }
1296        }
1297        flags
1298    }
1299}
1300
1301/// Read `CMF_O1` (+ parameter overrides) — the embedding-friendly path
1302/// for hosts that don't go through the CLI flags.
1303pub fn o1_from_env() -> O1Env {
1304    match std::env::var("CMF_O1") {
1305        Err(_) => O1Env::Unset,
1306        Ok(s) => match O1Cfg::from_spec(&s, None, None, None, None) {
1307            Some(cfg) => O1Env::On(cfg),
1308            None => O1Env::Off,
1309        },
1310    }
1311}
1312
1313/// In-place lower Cholesky of an SPD matrix; None if a pivot fails.
1314fn cholesky(g: &mut [f64], n: usize) -> Option<&[f64]> {
1315    for i in 0..n {
1316        for j in 0..=i {
1317            let mut s = g[i * n + j];
1318            for k in 0..j {
1319                s -= g[i * n + k] * g[j * n + k];
1320            }
1321            if i == j {
1322                if s <= 0.0 || !s.is_finite() {
1323                    return None;
1324                }
1325                g[i * n + i] = s.sqrt();
1326            } else {
1327                g[i * n + j] = s / g[j * n + j];
1328            }
1329        }
1330    }
1331    Some(g)
1332}