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/// Streaming Nyström attention state for ONE GQA group.
113///
114/// State splits along the GQA grain, because the operator does:
115///
116/// * SHARED per KV group (`NystromGroup`) — the exact window ring, the
117/// sink buffer and the key landmarks K̃. Under GQA every Q head of a
118/// group reads the SAME k/v rows, so all three are bit-identical
119/// across the group; storing them once per group instead of once per
120/// Q head is the point of this split (identical arithmetic,
121/// ×heads_per_kv less window memory). K̃ = seg_means(ks, t, d, m_eff)
122/// is a pure function of the group's keys and of `t` (which fixes
123/// m_eff), so it is shareable for the same reason the keys are.
124/// * PRIVATE per Q head (`NystromHead`) — the far accumulators T̂/Ẑ and
125/// their per-landmark running maxima, the QUERY landmarks Q̃, and the
126/// mixing matrix M = pinv(exp(Q̃K̃ᵀ/√d)). Q̃ is built from that head's
127/// own queries, so M and the far field it drives are per-Q-head and
128/// cannot be shared: the far mass a head accumulates is contracted
129/// against its own query landmarks.
130///
131/// Lifecycle: `new(m, w, sink)` → `prefill(prompt)` once → `step()` per
132/// decode token (single-head façade), or `new_group`/`prefill_group`/
133/// `step_group` for a whole GQA group at once. All buffers are flat
134/// `Vec<f32>`, row-major; the skeleton path performs no allocations
135/// inside `step()`.
136#[derive(Clone, Debug)]
137pub struct NystromState {
138 group: NystromGroup,
139 heads: Vec<NystromHead>,
140}
141
142/// The part of the state a GQA group shares: everything derived from
143/// the group's KEYS and VALUES alone (see `NystromState`).
144#[derive(Clone, Debug)]
145struct NystromGroup {
146 /// Landmark budget (m) — effective count may be lower (`m_eff`).
147 m: usize,
148 /// Exact-window width in keys.
149 w: usize,
150 /// Permanent exact sink keys at positions 0..sink (spec §5b).
151 sink: usize,
152 d: usize,
153 dv: usize,
154 /// Effective landmark count: clamp(t/8, 4, m) at prefill. Derived
155 /// from the prompt length, hence equal for every head of the group.
156 m_eff: usize,
157 /// Short-prompt mode: window holds ALL keys, no skeleton. The
158 /// buffer grows on decode, so this mode may allocate in `step()` —
159 /// acceptable for the ≤ w+8-token degenerate case.
160 exact_only: bool,
161 scale: f32,
162 /// Window keys `[cap][d]` — ring buffer in skeleton mode (cap = w),
163 /// append-only in exact-only mode.
164 win_k: Vec<f32>,
165 /// Window values `[cap][dv]`.
166 win_v: Vec<f32>,
167 win_len: usize,
168 /// Ring slot of the OLDEST window entry (0 while not yet full).
169 win_head: usize,
170 /// Sink keys `[sink_len][d]` — filled once at prefill, immutable.
171 sink_k: Vec<f32>,
172 /// Sink values `[sink_len][dv]`.
173 sink_v: Vec<f32>,
174 /// Number of stored sink tokens (0 in exact-only mode, where every
175 /// key is permanent-exact anyway).
176 sink_len: usize,
177 /// Key landmarks `[m_eff][d]` — segment means of the group's keys.
178 k_tilde: Vec<f32>,
179}
180
181/// The part of the state that is private to one Q head: everything that
182/// touches that head's QUERIES (see `NystromState`).
183#[derive(Clone, Debug)]
184struct NystromHead {
185 /// How the indefinite skeleton is rectified (see `O1Rect`).
186 rect: O1Rect,
187 /// Far numerator `[m_eff][dv]`, stored at scale e^{-m_max[i]}.
188 t_hat: Vec<f32>,
189 /// Far denominator `[m_eff]`, same scale.
190 z_hat: Vec<f32>,
191 /// Per-landmark running max of far logits q̃_i·k_j/√d.
192 m_max: Vec<f32>,
193 /// Number of keys absorbed into the far field.
194 far_len: usize,
195 /// Query landmarks `[m_eff][d]` (segment means of the prefill).
196 q_tilde: Vec<f32>,
197 /// Regularized pseudo-inverse of Au = exp(Q̃·K̃ᵀ/√d), `[m_eff][m_eff]`.
198 mu: Vec<f32>,
199 // Scratch preallocated at prefill so skeleton-mode step() is
200 // allocation-free. Per head rather than per group: the heads of a
201 // group write it independently, and it is ~0.5 KB.
202 scr_s: Vec<f32>,
203 scr_fh: Vec<f32>,
204 scr_u: Vec<f32>,
205 scr_l: Vec<f32>,
206}
207
208/// Borrowed view of a sealed group's state for the GPU upload — every
209/// slice the device mirror needs, in the layout the kernels index.
210/// `exact_only` groups (degenerate short prompts) are not portable and
211/// make the caller refuse the GPU path for the layer.
212pub struct O1DeviceView<'a> {
213 pub m_eff: usize,
214 pub w: usize,
215 pub sink_len: usize,
216 pub d: usize,
217 pub dv: usize,
218 pub exact_only: bool,
219 pub scale: f32,
220 pub win_len: usize,
221 pub win_head: usize,
222 pub far_len: usize,
223 pub win_k: &'a [f32],
224 pub win_v: &'a [f32],
225 pub sink_k: &'a [f32],
226 pub sink_v: &'a [f32],
227 pub k_tilde: &'a [f32],
228 pub heads: Vec<O1HeadView<'a>>,
229}
230
231pub struct O1HeadView<'a> {
232 pub rect_fm: bool,
233 pub t_hat: &'a [f32],
234 pub z_hat: &'a [f32],
235 pub m_max: &'a [f32],
236 pub q_tilde: &'a [f32],
237 pub mu: &'a [f32],
238}
239
240impl NystromState {
241 pub fn device_view(&self) -> O1DeviceView<'_> {
242 let g = &self.group;
243 O1DeviceView {
244 m_eff: g.m_eff,
245 w: g.w,
246 sink_len: g.sink_len,
247 d: g.d,
248 dv: g.dv,
249 exact_only: g.exact_only,
250 scale: g.scale,
251 win_len: g.win_len,
252 win_head: g.win_head,
253 far_len: self.heads.first().map_or(0, |h| h.far_len),
254 win_k: &g.win_k,
255 win_v: &g.win_v,
256 sink_k: &g.sink_k,
257 sink_v: &g.sink_v,
258 k_tilde: &g.k_tilde,
259 heads: self
260 .heads
261 .iter()
262 .map(|h| O1HeadView {
263 rect_fm: h.rect == O1Rect::Fm,
264 t_hat: &h.t_hat,
265 z_hat: &h.z_hat,
266 m_max: &h.m_max,
267 q_tilde: &h.q_tilde,
268 mu: &h.mu,
269 })
270 .collect(),
271 }
272 }
273}
274
275impl NystromState {
276 /// Single-head state (`heads_per_kv == 1`, and the shape the kernel
277 /// unit tests use).
278 ///
279 /// `m` — landmark budget (≥ 4; see `O1_DEFAULT_M`),
280 /// `w` — exact window width (validated setting is 128),
281 /// `sink` — permanent exact sink keys (validated default is 4;
282 /// 0 reproduces the sink-free kernel bit-for-bit).
283 /// Rectifier defaults to `O1_DEFAULT_RECT`; override with
284 /// `with_rect` (the golden-parity test pins it explicitly).
285 pub fn new(m: usize, w: usize, sink: usize) -> Self {
286 Self::new_group(m, w, sink, 1)
287 }
288
289 /// State for one GQA group of `q_heads` query heads sharing a KV
290 /// head. The window/sink/K̃ are stored ONCE for the group; each Q
291 /// head keeps its own far field, Q̃ and M.
292 pub fn new_group(m: usize, w: usize, sink: usize, q_heads: usize) -> Self {
293 assert!(m >= 4, "landmark budget must be at least 4");
294 assert!(w >= 1, "window must hold at least one key");
295 assert!(q_heads >= 1, "a GQA group needs at least one query head");
296 NystromState {
297 group: NystromGroup {
298 m,
299 w,
300 sink,
301 d: 0,
302 dv: 0,
303 m_eff: 0,
304 exact_only: true,
305 scale: 0.0,
306 win_k: Vec::new(),
307 win_v: Vec::new(),
308 win_len: 0,
309 win_head: 0,
310 sink_k: Vec::new(),
311 sink_v: Vec::new(),
312 sink_len: 0,
313 k_tilde: Vec::new(),
314 },
315 heads: (0..q_heads).map(|_| NystromHead::new()).collect(),
316 }
317 }
318
319 /// Select the skeleton rectifier for every head of the group
320 /// (builder; see `O1Rect`).
321 pub fn with_rect(mut self, rect: O1Rect) -> Self {
322 for h in &mut self.heads {
323 h.rect = rect;
324 }
325 self
326 }
327
328 /// Query heads in this group.
329 pub fn num_q_heads(&self) -> usize {
330 self.heads.len()
331 }
332
333 /// Keys absorbed into head `head`'s far field. Exposed for the
334 /// delayed-insertion invariant test: eviction is a GROUP event, but
335 /// each head must absorb the evicted key EXACTLY once, so this must
336 /// equal the number of evictions — never a multiple of it.
337 pub fn far_len(&self, head: usize) -> usize {
338 self.heads[head].far_len
339 }
340
341 /// Absorb the whole prompt for a single-head state — see
342 /// `prefill_group`.
343 pub fn prefill(&mut self, qs: &[f32], ks: &[f32], vs: &[f32], t: usize, d: usize, dv: usize) {
344 assert_eq!(self.heads.len(), 1, "use prefill_group for a GQA group");
345 self.prefill_group(&[qs], ks, vs, t, d, dv);
346 }
347
348 /// Absorb the whole prompt for a GQA group: freeze each head's
349 /// landmarks and M, then replay the prompt through the step() state
350 /// semantics (window fill + delayed far insertion). `qs[h]` is that
351 /// head's `[t][d]` query block; `ks` is `[t][d]` and `vs` is
352 /// `[t][dv]` — the group's shared keys/values, row-major.
353 pub fn prefill_group(
354 &mut self,
355 qs: &[&[f32]],
356 ks: &[f32],
357 vs: &[f32],
358 t: usize,
359 d: usize,
360 dv: usize,
361 ) {
362 assert_eq!(qs.len(), self.heads.len(), "one query block per head");
363 for q in qs {
364 assert_eq!(q.len(), t * d);
365 }
366 assert_eq!(ks.len(), t * d);
367 assert_eq!(vs.len(), t * dv);
368
369 let Some(k_tilde64) = self.group.prefill_shared(ks, vs, t, d, dv) else {
370 // exact-only: no skeleton, no far field — nothing per head
371 // beyond the score scratch.
372 for h in &mut self.heads {
373 h.seal_exact(t);
374 }
375 return;
376 };
377 for (h, q) in self.heads.iter_mut().zip(qs) {
378 h.seal(&self.group, q, t, &k_tilde64);
379 }
380 // Replay the post-sink prompt ONCE for the group: each key
381 // enters the shared window, evicting the (j-w)-th into every
382 // head's far field.
383 for j in self.group.sink..t {
384 Self::advance(
385 &mut self.group,
386 &mut self.heads,
387 &ks[j * d..(j + 1) * d],
388 &vs[j * dv..(j + 1) * dv],
389 );
390 }
391 }
392
393 /// One decode step for a single-head state — see `step_group`.
394 pub fn step(&mut self, q: &[f32], k: &[f32], v: &[f32], out: &mut [f32]) {
395 assert_eq!(self.heads.len(), 1, "use step_group for a GQA group");
396 self.step_group(q, k, v, out);
397 }
398
399 /// One decode step for the whole GQA group. Inserts the group's
400 /// (k, v) ONCE, evicting the oldest window key into every head's far
401 /// accumulators, then writes each head's attention output.
402 /// `q_all` is `[q_heads][d]`, `out_all` is `[q_heads][dv]`.
403 pub fn step_group(&mut self, q_all: &[f32], k: &[f32], v: &[f32], out_all: &mut [f32]) {
404 let (d, dv) = (self.group.d, self.group.dv);
405 assert!(d > 0, "prefill() must run before step()");
406 let nh = self.heads.len();
407 assert_eq!(q_all.len(), nh * d);
408 assert_eq!(k.len(), d);
409 assert_eq!(v.len(), dv);
410 assert_eq!(out_all.len(), nh * dv);
411 // The current token is part of its own near window (t-j = 0),
412 // so insertion happens BEFORE any output is computed.
413 Self::advance(&mut self.group, &mut self.heads, k, v);
414 for (h, head) in self.heads.iter_mut().enumerate() {
415 head.step(
416 &self.group,
417 &q_all[h * d..(h + 1) * d],
418 &mut out_all[h * dv..(h + 1) * dv],
419 );
420 }
421 }
422
423 /// Heap bytes held by this group's state (shared window + sinks +
424 /// K̃, plus each head's skeleton and scratch) — feeds the honest
425 /// "KV+state" memory line, same discipline as counting
426 /// `linear_state` for the linear core.
427 pub fn memory_bytes(&self) -> usize {
428 self.group.memory_bytes()
429 + self
430 .heads
431 .iter()
432 .map(NystromHead::memory_bytes)
433 .sum::<usize>()
434 }
435
436 /// Push the group's (k, v) into the shared window. In skeleton mode
437 /// a full ring first evicts its oldest key (delayed insertion — the
438 /// key leaves the exact window at this very step).
439 ///
440 /// The eviction is a GROUP event: the window is shared, so there is
441 /// exactly ONE eviction per position, not one per Q head. The far
442 /// accumulators are per head, though, so that single evicted key is
443 /// absorbed once into EACH head — one eviction, `q_heads`
444 /// insertions. Getting this wrong in either direction breaks the
445 /// boundary invariant (a key enters the far field at exactly the
446 /// step it leaves the window: no double count, no hole).
447 fn advance(g: &mut NystromGroup, heads: &mut [NystromHead], k: &[f32], v: &[f32]) {
448 let (d, dv) = (g.d, g.dv);
449 if !g.exact_only && g.win_len == g.w {
450 let slot = g.win_head;
451 // Every head absorbs the outgoing key BEFORE the slot is
452 // overwritten by the incoming one.
453 for h in heads.iter_mut() {
454 h.far_insert(g, slot);
455 }
456 g.win_k[slot * d..(slot + 1) * d].copy_from_slice(k);
457 g.win_v[slot * dv..(slot + 1) * dv].copy_from_slice(v);
458 g.win_head = (g.win_head + 1) % g.w;
459 } else if g.exact_only {
460 g.win_k.extend_from_slice(k);
461 g.win_v.extend_from_slice(v);
462 g.win_len += 1;
463 } else {
464 g.win_k[g.win_len * d..(g.win_len + 1) * d].copy_from_slice(k);
465 g.win_v[g.win_len * dv..(g.win_len + 1) * dv].copy_from_slice(v);
466 g.win_len += 1;
467 }
468 }
469}
470
471impl NystromGroup {
472 /// Freeze the group-shared geometry from the prompt's keys/values.
473 /// Returns the f64 key landmarks (which the heads need at full
474 /// precision to build Au), or None in exact-only mode.
475 fn prefill_shared(
476 &mut self,
477 ks: &[f32],
478 vs: &[f32],
479 t: usize,
480 d: usize,
481 dv: usize,
482 ) -> Option<Vec<f64>> {
483 self.d = d;
484 self.dv = dv;
485 self.scale = 1.0 / (d as f32).sqrt();
486 self.win_len = 0;
487 self.win_head = 0;
488 self.sink_len = 0;
489 self.exact_only = t <= self.w + self.sink + EXACT_SLACK;
490
491 if self.exact_only {
492 // Everything fits in the exact window (plus slack for a few
493 // decode steps before Vec growth); no skeleton is built and
494 // no separate sink buffer is needed — every key is already
495 // a permanent exact key in this mode.
496 self.win_k = Vec::with_capacity((t + 64) * d);
497 self.win_v = Vec::with_capacity((t + 64) * dv);
498 self.win_k.extend_from_slice(ks);
499 self.win_v.extend_from_slice(vs);
500 self.win_len = t;
501 return None;
502 }
503
504 // Sink tokens: positions 0..sink become permanent exact keys.
505 // They bypass the ring window entirely, so the delayed-insertion
506 // path can never move them into the far accumulators.
507 self.sink_len = self.sink; // skeleton mode guarantees t > sink
508 self.sink_k = ks[..self.sink * d].to_vec();
509 self.sink_v = vs[..self.sink * dv].to_vec();
510
511 // Landmarks: contiguous segment means of the prompt. The
512 // integer split (i·t)/m matches the reference probe; the clamp
513 // keeps tiny prompts from producing duplicate landmarks.
514 let m_eff = (t / 8).clamp(4, self.m);
515 self.m_eff = m_eff;
516 let k_tilde64 = seg_means(ks, t, d, m_eff);
517 self.k_tilde = k_tilde64.iter().map(|&x| x as f32).collect();
518
519 self.win_k = vec![0.0; self.w * d];
520 self.win_v = vec![0.0; self.w * dv];
521 Some(k_tilde64)
522 }
523
524 fn memory_bytes(&self) -> usize {
525 (self.win_k.len()
526 + self.win_v.len()
527 + self.sink_k.len()
528 + self.sink_v.len()
529 + self.k_tilde.len())
530 * std::mem::size_of::<f32>()
531 }
532}
533
534impl NystromHead {
535 fn new() -> Self {
536 NystromHead {
537 rect: O1_DEFAULT_RECT,
538 t_hat: Vec::new(),
539 z_hat: Vec::new(),
540 m_max: Vec::new(),
541 far_len: 0,
542 q_tilde: Vec::new(),
543 mu: Vec::new(),
544 scr_s: Vec::new(),
545 scr_fh: Vec::new(),
546 scr_u: Vec::new(),
547 scr_l: Vec::new(),
548 }
549 }
550
551 /// exact-only mode: no skeleton state at all, just room to score the
552 /// growing window.
553 fn seal_exact(&mut self, t: usize) {
554 self.far_len = 0;
555 self.scr_s = Vec::with_capacity(t + 64);
556 }
557
558 /// Freeze this head's query landmarks and mixing matrix against the
559 /// group's (already frozen) key landmarks.
560 fn seal(&mut self, g: &NystromGroup, qs: &[f32], t: usize, k_tilde64: &[f64]) {
561 let (d, dv, m_eff) = (g.d, g.dv, g.m_eff);
562 self.far_len = 0;
563 let q_tilde64 = seg_means(qs, t, d, m_eff);
564 self.q_tilde = q_tilde64.iter().map(|&x| x as f32).collect();
565
566 // Au and its regularized pseudo-inverse in f64 — one-off m×m
567 // work at prefill only; the hot path stays f32.
568 let mut au = vec![0.0f64; m_eff * m_eff];
569 for i in 0..m_eff {
570 for j in 0..m_eff {
571 let mut s = 0.0f64;
572 for c in 0..d {
573 s += q_tilde64[i * d + c] * k_tilde64[j * d + c];
574 }
575 au[i * m_eff + j] = (s * g.scale as f64).exp();
576 }
577 }
578 let mu64 = ridge_pinv(&au, m_eff);
579 self.mu = mu64.iter().map(|&x| x as f32).collect();
580
581 self.t_hat = vec![0.0; m_eff * dv];
582 self.z_hat = vec![0.0; m_eff];
583 self.m_max = vec![f32::NEG_INFINITY; m_eff];
584 self.scr_s = vec![0.0; g.sink + g.w];
585 self.scr_fh = vec![0.0; m_eff];
586 self.scr_u = vec![0.0; m_eff];
587 self.scr_l = vec![0.0; m_eff];
588 }
589
590 /// This head's output for `q` against the group's current window and
591 /// sinks and its own far field. The window insertion for this
592 /// position already happened at group level (`NystromState::advance`).
593 fn step(&mut self, g: &NystromGroup, q: &[f32], out: &mut [f32]) {
594 let (d, dv) = (g.d, g.dv);
595 assert_eq!(q.len(), d);
596 assert_eq!(out.len(), dv);
597
598 // Near field: exact logits over sinks + window, one shared
599 // shift. Sinks are permanent exact keys (near mask §5b:
600 // t-j < w OR j < sink); sink_len = 0 in exact-only mode.
601 let ns = g.sink_len;
602 let n = ns + g.win_len;
603 self.scr_s.resize(n, 0.0);
604 let mut c = f32::NEG_INFINITY;
605 for s in 0..ns {
606 let lg = dot(q, &g.sink_k[s * d..(s + 1) * d]) * g.scale;
607 self.scr_s[s] = lg;
608 c = c.max(lg);
609 }
610 // Window scores are the decode hot loop — NEON dot (same
611 // products, regrouped sums; parity-gated by the golden tests).
612 for s in 0..g.win_len {
613 let lg = crate::attention::dot_f32(q, &g.win_k[s * d..(s + 1) * d]) * g.scale;
614 self.scr_s[ns + s] = lg;
615 c = c.max(lg);
616 }
617
618 // Far field: shifted skeleton (spec §3). All exp arguments are
619 // ≤ 0 relative to the joint shift c_all, so nothing overflows.
620 let mut far_den = 0.0f32;
621 let mut c_all = c;
622 let mut have_far = false;
623 if self.far_len > 0 {
624 // Per-token row shift f over landmark scores.
625 let mut f = f32::NEG_INFINITY;
626 for a in 0..g.m_eff {
627 let s = crate::attention::dot_f32(q, &g.k_tilde[a * d..(a + 1) * d]) * g.scale;
628 self.scr_fh[a] = s;
629 f = f.max(s);
630 }
631 for a in 0..g.m_eff {
632 self.scr_fh[a] = (self.scr_fh[a] - f).exp();
633 }
634 // u = (F·e^{-f}) · M — the landmark mixing row (= FM, up to
635 // the positive factor e^{-f}).
636 for b in 0..g.m_eff {
637 let mut s = 0.0f32;
638 for a in 0..g.m_eff {
639 s += self.scr_fh[a] * self.mu[a * g.m_eff + b];
640 }
641 // FM rectifier: every far weight is Σ_b FM[b]·E[b,j]
642 // with E ≥ 0 elementwise, so clamping this m-vector is
643 // enough to make all of them non-negative — the per-key
644 // guarantee the streaming form otherwise cannot state.
645 // The row shift e^{-f} and the flash factors below are
646 // strictly positive, so clamping here or after the
647 // rescale is the same predicate.
648 self.scr_u[b] = if self.rect == O1Rect::Fm {
649 s.max(0.0)
650 } else {
651 s
652 };
653 }
654 // Joint scale: the far term b carries e^{f + m_max[b]}, the
655 // near term e^{c}; take the max so every factor is ≤ 1.
656 for b in 0..g.m_eff {
657 c_all = c_all.max(f + self.m_max[b]);
658 }
659 for b in 0..g.m_eff {
660 let gain = self.scr_u[b] * (f + self.m_max[b] - c_all).exp();
661 self.scr_u[b] = gain;
662 far_den += gain * self.z_hat[b];
663 }
664 // Aggregate guard — the rectifier of `O1Rect::Aggregate`,
665 // and a second line of defence under `Fm` (where far_den is
666 // a sum of non-negative terms, so this can only fire on
667 // rounding): a negative denominator means the skeleton
668 // estimate is unusable for this row — drop the far field.
669 if far_den >= 0.0 {
670 have_far = true;
671 } else {
672 far_den = 0.0;
673 }
674 }
675
676 for o in out.iter_mut() {
677 *o = 0.0;
678 }
679 if have_far {
680 for b in 0..g.m_eff {
681 crate::attention::axpy_f32(out, &self.t_hat[b * dv..(b + 1) * dv], self.scr_u[b]);
682 }
683 }
684 let mut den = far_den;
685 for s in 0..n {
686 let p = (self.scr_s[s] - c_all).exp();
687 den += p;
688 // scr_s rows 0..ns are sinks, the rest are window entries.
689 let vv = if s < ns {
690 &g.sink_v[s * dv..(s + 1) * dv]
691 } else {
692 &g.win_v[(s - ns) * dv..(s - ns + 1) * dv]
693 };
694 crate::attention::axpy_f32(out, vv, p);
695 }
696 let den = den.max(DEN_EPS);
697 for o in out.iter_mut() {
698 *o /= den;
699 }
700 }
701
702 /// Absorb the group's window slot into THIS head's far accumulators
703 /// with the per-landmark flash shift: T̂[i]/Ẑ[i] live at scale
704 /// e^{-m_max[i]}; when a new logit raises the max, existing mass is
705 /// rescaled by e^{old-new} (exactly 0 on first insertion, since
706 /// m_max = -inf).
707 fn far_insert(&mut self, g: &NystromGroup, slot: usize) {
708 let (d, dv) = (g.d, g.dv);
709 // Runs once per evicted key per head — NEON dot/axpy like the
710 // decode loop (same products, regrouped sums).
711 for i in 0..g.m_eff {
712 self.scr_l[i] = crate::attention::dot_f32(
713 &self.q_tilde[i * d..(i + 1) * d],
714 &g.win_k[slot * d..(slot + 1) * d],
715 ) * g.scale;
716 }
717 for i in 0..g.m_eff {
718 let l = self.scr_l[i];
719 if l > self.m_max[i] {
720 let r = (self.m_max[i] - l).exp();
721 self.z_hat[i] *= r;
722 for e in self.t_hat[i * dv..(i + 1) * dv].iter_mut() {
723 *e *= r;
724 }
725 self.m_max[i] = l;
726 }
727 let e = (l - self.m_max[i]).exp();
728 self.z_hat[i] += e;
729 crate::attention::axpy_f32(
730 &mut self.t_hat[i * dv..(i + 1) * dv],
731 &g.win_v[slot * dv..(slot + 1) * dv],
732 e,
733 );
734 }
735 self.far_len += 1;
736 }
737
738 fn memory_bytes(&self) -> usize {
739 (self.t_hat.len()
740 + self.z_hat.len()
741 + self.m_max.len()
742 + self.q_tilde.len()
743 + self.mu.len()
744 + self.scr_s.len()
745 + self.scr_fh.len()
746 + self.scr_u.len()
747 + self.scr_l.len())
748 * std::mem::size_of::<f32>()
749 }
750}
751
752/// Contiguous segment means (the Nyströmformer landmark recipe), f64
753/// accumulation. The split (i·t)/m matches the Python reference.
754fn seg_means(xs: &[f32], t: usize, d: usize, m: usize) -> Vec<f64> {
755 let mut out = vec![0.0f64; m * d];
756 for i in 0..m {
757 let lo = i * t / m;
758 let hi = (i + 1) * t / m;
759 for j in lo..hi {
760 for c in 0..d {
761 out[i * d + c] += xs[j * d + c] as f64;
762 }
763 }
764 let inv = 1.0 / (hi - lo) as f64;
765 for c in 0..d {
766 out[i * d + c] *= inv;
767 }
768 }
769 out
770}
771
772fn dot(a: &[f32], b: &[f32]) -> f32 {
773 let mut s = 0.0f32;
774 for (x, y) in a.iter().zip(b) {
775 s += x * y;
776 }
777 s
778}
779
780/// Regularized pseudo-inverse M = (AᵀA + λI)⁻¹ Aᵀ of a square matrix,
781/// λ = RIDGE_REL·mean(diag(AᵀA)), solved via Cholesky. f64 internal —
782/// this runs once per prefill on an m×m matrix (m ≤ 32). If Cholesky
783/// fails (Au numerically singular despite the m_eff clamp), λ grows
784/// tenfold — the jitter fallback of the reference probe.
785/// pub(crate): the FCD polish trainer builds its (constant-in-backward)
786/// mixing matrix with the SAME solver the runtime seals with.
787pub(crate) fn ridge_pinv(a: &[f64], n: usize) -> Vec<f64> {
788 let mut ata = vec![0.0f64; n * n];
789 for i in 0..n {
790 for j in 0..n {
791 let mut s = 0.0;
792 for k in 0..n {
793 s += a[k * n + i] * a[k * n + j];
794 }
795 ata[i * n + j] = s;
796 }
797 }
798 let mean_diag: f64 = (0..n).map(|i| ata[i * n + i]).sum::<f64>() / n as f64;
799 let mut lambda = RIDGE_REL * mean_diag.max(f64::MIN_POSITIVE);
800 for _ in 0..12 {
801 let mut g = ata.clone();
802 for i in 0..n {
803 g[i * n + i] += lambda;
804 }
805 if let Some(l) = cholesky(&mut g, n) {
806 // Solve G·M = Aᵀ column by column; column j of Aᵀ is row j
807 // of A.
808 let mut m_out = vec![0.0f64; n * n];
809 let mut x = vec![0.0f64; n];
810 for j in 0..n {
811 let rhs = &a[j * n..(j + 1) * n];
812 // Forward: L·y = rhs.
813 for i in 0..n {
814 let mut s = rhs[i];
815 for k in 0..i {
816 s -= l[i * n + k] * x[k];
817 }
818 x[i] = s / l[i * n + i];
819 }
820 // Backward: Lᵀ·x = y.
821 for i in (0..n).rev() {
822 let mut s = x[i];
823 for k in i + 1..n {
824 s -= l[k * n + i] * x[k];
825 }
826 x[i] = s / l[i * n + i];
827 }
828 for i in 0..n {
829 m_out[i * n + j] = x[i];
830 }
831 }
832 return m_out;
833 }
834 lambda *= 10.0;
835 }
836 // Unreachable in practice: λ eventually dominates the diagonal.
837 // Degrade to a scaled identity rather than poison the output.
838 let mut fallback = vec![0.0f64; n * n];
839 for i in 0..n {
840 fallback[i * n + i] = 1.0 / mean_diag.max(f64::MIN_POSITIVE);
841 }
842 fallback
843}
844
845// ── Runtime configuration (v1: runtime-level, NOT a format change) ──
846//
847// A layer set + {m, w, sink}, resolved in priority order:
848// 1. CLI flag (`--o1` on run/serve/bench) — explicit user intent;
849// 2. env `CMF_O1` (all | deepN | i,j,k | off) with CMF_O1_M /
850// CMF_O1_WINDOW / CMF_O1_SINK parameter overrides;
851// 3. converter hint in the header JSON (`provenance.o1_attn`,
852// written by `cortiq convert --o1`) — additive metadata, the
853// binary envelope is untouched.
854
855/// Validated defaults (spec: m=32, W=128, sink=4; sink ablation ×2.39).
856pub const O1_DEFAULT_M: usize = 32;
857pub const O1_DEFAULT_W: usize = 128;
858pub const O1_DEFAULT_SINK: usize = 4;
859/// Rectifier default (see `O1Rect`).
860pub const O1_DEFAULT_RECT: O1Rect = O1Rect::Aggregate;
861
862/// Which layers run the O(1) kernel.
863#[derive(Clone, Debug, PartialEq, Eq)]
864pub enum O1Layers {
865 All,
866 /// The N deepest layers (deep-N ladder of the price map; the
867 /// early stack is the most sink-dependent, depth converts best).
868 Deep(usize),
869 /// Explicit layer indices.
870 List(Vec<usize>),
871}
872
873/// Per-model O(1)-attention setting.
874#[derive(Clone, Debug)]
875pub struct O1Cfg {
876 pub layers: O1Layers,
877 /// Landmark budget (≥ 4; m=64 measured WORSE — collinear segment
878 /// means poison the pinv, so don't "help" by raising it).
879 pub m: usize,
880 /// Exact-window width — the main quality lever.
881 pub w: usize,
882 /// Permanent exact sink keys (StreamingLLM discipline, spec §5b).
883 pub sink: usize,
884 /// Skeleton rectifier (see `O1Rect`).
885 pub rect: O1Rect,
886}
887
888/// Three-state env reading: unset falls through to the header hint,
889/// `off`/`0` force-disables even a header hint (the escape hatch).
890pub enum O1Env {
891 Unset,
892 Off,
893 On(O1Cfg),
894}
895
896impl O1Cfg {
897 /// Parse a layer spec: `all` | `deepN` | `i,j,k`. None = not a spec
898 /// (also used for `off`/`0`/empty).
899 pub fn parse_layers(spec: &str) -> Option<O1Layers> {
900 let s = spec.trim();
901 match s {
902 "" | "off" | "0" | "none" => None,
903 "all" => Some(O1Layers::All),
904 _ => {
905 if let Some(n) = s.strip_prefix("deep") {
906 return n
907 .parse::<usize>()
908 .ok()
909 .filter(|&n| n > 0)
910 .map(O1Layers::Deep);
911 }
912 let idx: Result<Vec<usize>, _> =
913 s.split(',').map(|p| p.trim().parse::<usize>()).collect();
914 idx.ok().filter(|v| !v.is_empty()).map(O1Layers::List)
915 }
916 }
917 }
918
919 /// Parse a rectifier spec: `agg`/`aggregate` | `fm`. None = not a
920 /// spec.
921 pub fn parse_rect(spec: &str) -> Option<O1Rect> {
922 match spec.trim() {
923 "agg" | "aggregate" => Some(O1Rect::Aggregate),
924 "fm" => Some(O1Rect::Fm),
925 _ => None,
926 }
927 }
928
929 /// Rectifier from an explicit value, else `CMF_O1_RECT`, else the
930 /// default.
931 fn rect_or_env(rect: Option<O1Rect>) -> O1Rect {
932 rect.or_else(|| {
933 std::env::var("CMF_O1_RECT")
934 .ok()
935 .as_deref()
936 .and_then(Self::parse_rect)
937 })
938 .unwrap_or(O1_DEFAULT_RECT)
939 }
940
941 /// Build from an explicit spec (CLI path). None = `off` or malformed.
942 /// Explicit m/w/sink/rect beat env overrides beat validated defaults.
943 pub fn from_spec(
944 spec: &str,
945 m: Option<usize>,
946 w: Option<usize>,
947 sink: Option<usize>,
948 rect: Option<O1Rect>,
949 ) -> Option<O1Cfg> {
950 let layers = Self::parse_layers(spec)?;
951 let env = |k: &str| std::env::var(k).ok().and_then(|v| v.parse::<usize>().ok());
952 Some(O1Cfg {
953 layers,
954 // NystromState asserts m ≥ 4 and w ≥ 1 — clamp rather than
955 // panic deep in the first prefill.
956 m: m.or_else(|| env("CMF_O1_M")).unwrap_or(O1_DEFAULT_M).max(4),
957 w: w.or_else(|| env("CMF_O1_WINDOW"))
958 .unwrap_or(O1_DEFAULT_W)
959 .max(1),
960 sink: sink
961 .or_else(|| env("CMF_O1_SINK"))
962 .unwrap_or(O1_DEFAULT_SINK),
963 rect: Self::rect_or_env(rect),
964 })
965 }
966
967 /// Converter hint from the header JSON: `{"layers": "all"|[i,…],
968 /// "m": …, "w": …, "sink": …}`. Env parameter overrides still apply
969 /// (the operator's knob wins over the file's suggestion).
970 pub fn from_json(v: &serde_json::Value) -> Option<O1Cfg> {
971 let layers = match v.get("layers") {
972 Some(serde_json::Value::String(s)) => Self::parse_layers(s)?,
973 Some(serde_json::Value::Array(a)) => O1Layers::List(
974 a.iter()
975 .filter_map(|x| x.as_u64().map(|n| n as usize))
976 .collect(),
977 ),
978 _ => return None,
979 };
980 let f = |k: &str| v.get(k).and_then(|x| x.as_u64()).map(|n| n as usize);
981 let env = |k: &str| std::env::var(k).ok().and_then(|s| s.parse::<usize>().ok());
982 Some(O1Cfg {
983 layers,
984 m: env("CMF_O1_M")
985 .or_else(|| f("m"))
986 .unwrap_or(O1_DEFAULT_M)
987 .max(4),
988 w: env("CMF_O1_WINDOW")
989 .or_else(|| f("w"))
990 .unwrap_or(O1_DEFAULT_W)
991 .max(1),
992 sink: env("CMF_O1_SINK")
993 .or_else(|| f("sink"))
994 .unwrap_or(O1_DEFAULT_SINK),
995 // The rectifier is a runtime property of the kernel, not a
996 // property of the weights — a file hint cannot pin it.
997 rect: Self::rect_or_env(None),
998 })
999 }
1000
1001 /// Per-layer flags over `num_layers` (indices past the end are
1002 /// silently dropped; the pipeline additionally filters non-Full
1003 /// layers — a linear layer keeps its own operator).
1004 pub fn layer_flags(&self, num_layers: usize) -> Vec<bool> {
1005 let mut flags = vec![false; num_layers];
1006 match &self.layers {
1007 O1Layers::All => flags.iter_mut().for_each(|f| *f = true),
1008 O1Layers::Deep(n) => {
1009 for f in flags.iter_mut().skip(num_layers.saturating_sub(*n)) {
1010 *f = true;
1011 }
1012 }
1013 O1Layers::List(idx) => {
1014 for &i in idx {
1015 if i < num_layers {
1016 flags[i] = true;
1017 }
1018 }
1019 }
1020 }
1021 flags
1022 }
1023}
1024
1025/// Read `CMF_O1` (+ parameter overrides) — the embedding-friendly path
1026/// for hosts that don't go through the CLI flags.
1027pub fn o1_from_env() -> O1Env {
1028 match std::env::var("CMF_O1") {
1029 Err(_) => O1Env::Unset,
1030 Ok(s) => match O1Cfg::from_spec(&s, None, None, None, None) {
1031 Some(cfg) => O1Env::On(cfg),
1032 None => O1Env::Off,
1033 },
1034 }
1035}
1036
1037/// In-place lower Cholesky of an SPD matrix; None if a pivot fails.
1038fn cholesky(g: &mut [f64], n: usize) -> Option<&[f64]> {
1039 for i in 0..n {
1040 for j in 0..=i {
1041 let mut s = g[i * n + j];
1042 for k in 0..j {
1043 s -= g[i * n + k] * g[j * n + k];
1044 }
1045 if i == j {
1046 if s <= 0.0 || !s.is_finite() {
1047 return None;
1048 }
1049 g[i * n + i] = s.sqrt();
1050 } else {
1051 g[i * n + j] = s / g[j * n + j];
1052 }
1053 }
1054 }
1055 Some(g)
1056}