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