Skip to main content

o1_recovery/
o1_recovery.rs

1//! Injection/recovery for the O(1) streaming operator.
2//!
3//! `--o1`'s cost is quoted as a scalar: ×1.296 perplexity against exact
4//! attention. A scalar cannot be allocated. It says the operator costs
5//! something on average over one corpus at one setting, and says nothing
6//! about which setting to spend the next landmark on.
7//!
8//! This measures the same operator the way a detection experiment
9//! measures a search: inject a signal of KNOWN strength at a KNOWN
10//! depth, and count how much of it comes back out. The needle is one
11//! (key, value) pair whose logit sits `amp` standard deviations above
12//! the background — the same construction as `tests/vacuum.rs`, run
13//! Monte Carlo over seeds. Because the background values are zero in
14//! dimension 0 and the needle's is one, the output's component 0 IS the
15//! needle's contribution: recovery = got[0] / exact[0], a fraction with
16//! no fitting and no threshold.
17//!
18//! What it is for. If recovery rises SMOOTHLY with the landmark budget,
19//! the budget is a continuous resource and can be allocated across
20//! layers by an equalizing rule. If it is a STEP — the sketch either
21//! kept the needle or did not — then allocation is a covering problem
22//! instead, and averaging landmarks across layers buys nothing. That
23//! question has to be answered before any allocation rule is chosen,
24//! which is what this prints.
25//!
26//! Measured (w=128, sink=4, depth=512, 300 trials a cell): the response
27//! is a CONTINUUM, decisively. 758–1496 of every 1500 trials in a row
28//! land strictly between 5% and 95% recovery, where a step would leave
29//! that column near zero; and the two criteria cross at different budgets
30//! (at amp 12, half the trials clear 50% by m=4 but need m=32 to clear
31//! 95%). The deficit 1−r falls as a shallow power law in m, exponent
32//! −0.18 to −0.38 across injection strengths, clustering near −1/4.
33//!
34//! That exponent is the number an allocation rule needs, and it is not
35//! the one a physical detection experiment would hand over. Equalizing a
36//! response that grows as r^β needs budget ∝ c^(1/β): at β = 1/2 — the
37//! √t law of an integration-time search — that is the familiar c², but at
38//! β = 1/4 it is c⁴, quadratically more aggressive. Assuming the physics
39//! exponent here would under-serve the weak layers by a wide margin.
40//!
41//! Two honest limits on that number. Recovery is NOT monotone in
42//! injection strength — it dips near amp 6 and recovers by amp 12 —
43//! because the statistic divides by the exact answer, and in the middle
44//! band the needle is a large minority of the softmax mass, so error in
45//! the estimated DENOMINATOR (all background) scales the ratio directly.
46//! And the keys here are iid Gaussian, which is the worst case a
47//! landmark sketch can be handed: real attention keys are strongly
48//! low-rank, which is why the method works at all. Treat β ≈ −1/4 as a
49//! floor measured on the hardest input, not as the figure for a model.
50//!
51//!     cargo run --release -p cortiq-engine --example o1_recovery
52//!     TRIALS=300 cargo run --release -p cortiq-engine --example o1_recovery
53
54use cortiq_engine::nystrom::NystromState;
55
56fn unif(s: &mut u64) -> f32 {
57    *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
58    ((*s >> 33) as f32 / (1u64 << 31) as f32) - 1.0
59}
60
61/// One trial. Returns the fraction of the needle's exact contribution
62/// that survived the streaming kernel.
63fn trial(m: usize, w: usize, sink: usize, amp: f32, depth: usize, seed: u64) -> f32 {
64    let (d, dv) = (64usize, 8usize);
65    let t = 8 * m + w + depth;
66    let mut s = seed;
67    let rd = (d as f32).sqrt();
68
69    let mut unit = |s: &mut u64| -> Vec<f32> {
70        let v: Vec<f32> = (0..d).map(|_| unif(s)).collect();
71        let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
72        v.iter().map(|x| x / n).collect()
73    };
74    let qhat = unit(&mut s);
75    let q: Vec<f32> = qhat.iter().map(|x| x * rd).collect();
76
77    let mut qs = Vec::with_capacity(t * d);
78    for _ in 0..t {
79        let u = unit(&mut s);
80        qs.extend(u.iter().map(|x| x * rd));
81    }
82
83    let mut ks: Vec<f32> = (0..t * d).map(|_| unif(&mut s) * 1.732).collect();
84    let mut vs: Vec<f32> = (0..t * dv).map(|_| unif(&mut s)).collect();
85    for j in 0..t {
86        vs[j * dv] = 0.0;
87    }
88    let p = t - depth;
89    for c in 0..d {
90        ks[p * d + c] += amp * qhat[c];
91    }
92    vs[p * dv] = 1.0;
93
94    let mut st = NystromState::new(m, w, sink);
95    st.prefill(&qs, &ks, &vs, t, d, dv);
96
97    let k_new: Vec<f32> = (0..d).map(|_| unif(&mut s) * 1.732).collect();
98    let mut v_new: Vec<f32> = (0..dv).map(|_| unif(&mut s)).collect();
99    v_new[0] = 0.0;
100    let mut got = vec![0f32; dv];
101    st.step(&q, &k_new, &v_new, &mut got);
102
103    let mut logits = Vec::with_capacity(t + 1);
104    for j in 0..t {
105        let dot: f32 = (0..d).map(|c| q[c] * ks[j * d + c]).sum();
106        logits.push(dot / rd);
107    }
108    let dot: f32 = (0..d).map(|c| q[c] * k_new[c]).sum();
109    logits.push(dot / rd);
110    let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
111    let (mut den, mut num0) = (0f64, 0f64);
112    for (j, &l) in logits.iter().enumerate() {
113        let e = ((l - mx) as f64).exp();
114        den += e;
115        num0 += e * if j == t { v_new[0] } else { vs[j * dv] } as f64;
116    }
117    let want0 = (num0 / den) as f32;
118    if want0.abs() < 1e-4 {
119        return f32::NAN;
120    }
121    got[0] / want0
122}
123
124
125/// The background channel on its own: no needle, no signal, just the
126/// skeleton's error in estimating a far field of ordinary keys.
127///
128/// The recovery statistic divides by the exact answer, so it mixes two
129/// error sources — the needle's own estimate and the denominator built
130/// from every background key. Removing the needle isolates the second,
131/// and it is the one that should follow the central-limit law: a sum
132/// over ~n far keys approximated from m landmarks.
133///
134/// Returned: ‖o_o1 − o_exact‖ / ‖o_exact‖ over the whole output vector.
135/// An AMPLITUDE, deliberately — its square is the energy, and the two
136/// differ by exactly a factor of two in any log-log slope, which is why
137/// only one of them needs measuring.
138fn background_error(m: usize, w: usize, sink: usize, depth: usize, seed: u64) -> f32 {
139    let (d, dv) = (64usize, 8usize);
140    // Sequence length is FIXED, not 8*m + …. Tying it to m — which the
141    // recovery sweep above does, to guarantee m_eff reaches m — means a
142    // larger budget also gets a longer far field, and the sweep varies
143    // two things at once. Here only m moves. `8 * 64` keeps m_eff = m up
144    // to the largest budget measured.
145    let t = 8 * 64 + w + depth;
146    let _ = m;
147    let mut s = seed;
148    let rd = (d as f32).sqrt();
149
150    let mut unit = |s: &mut u64| -> Vec<f32> {
151        let v: Vec<f32> = (0..d).map(|_| unif(s)).collect();
152        let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
153        v.iter().map(|x| x / n).collect()
154    };
155    let q: Vec<f32> = unit(&mut s).iter().map(|x| x * rd).collect();
156    let mut qs = Vec::with_capacity(t * d);
157    for _ in 0..t {
158        let u = unit(&mut s);
159        qs.extend(u.iter().map(|x| x * rd));
160    }
161    let ks: Vec<f32> = (0..t * d).map(|_| unif(&mut s) * 1.732).collect();
162    let vs: Vec<f32> = (0..t * dv).map(|_| unif(&mut s)).collect();
163
164    let mut st = NystromState::new(m, w, sink);
165    st.prefill(&qs, &ks, &vs, t, d, dv);
166    let k_new: Vec<f32> = (0..d).map(|_| unif(&mut s) * 1.732).collect();
167    let v_new: Vec<f32> = (0..dv).map(|_| unif(&mut s)).collect();
168    let mut got = vec![0f32; dv];
169    st.step(&q, &k_new, &v_new, &mut got);
170
171    let mut logits = Vec::with_capacity(t + 1);
172    for j in 0..t {
173        logits.push((0..d).map(|c| q[c] * ks[j * d + c]).sum::<f32>() / rd);
174    }
175    logits.push((0..d).map(|c| q[c] * k_new[c]).sum::<f32>() / rd);
176    let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
177    let mut den = 0f64;
178    let mut want = vec![0f64; dv];
179    for (j, &l) in logits.iter().enumerate() {
180        let e = ((l - mx) as f64).exp();
181        den += e;
182        let src = if j == t { &v_new[..] } else { &vs[j * dv..(j + 1) * dv] };
183        for (c, wc) in want.iter_mut().enumerate() {
184            *wc += e * src[c] as f64;
185        }
186    }
187    let want: Vec<f32> = want.iter().map(|x| (x / den) as f32).collect();
188    let num: f32 = got.iter().zip(&want).map(|(g, x)| (g - x) * (g - x)).sum::<f32>().sqrt();
189    let den2: f32 = want.iter().map(|x| x * x).sum::<f32>().sqrt();
190    num / den2.max(1e-9)
191}
192
193struct Cell {
194    mean: f32,
195    sd: f32,
196    /// Fraction of trials that kept at least half the needle.
197    detected: f32,
198    /// Fraction that kept at least 95% — the strict criterion.
199    strict: f32,
200    /// Trials landing strictly between 5% and 95% — the population a
201    /// step response is not allowed to have.
202    partial: usize,
203}
204
205fn cell(m: usize, w: usize, sink: usize, amp: f32, depth: usize, trials: usize) -> Cell {
206    let mut rs = Vec::with_capacity(trials);
207    for i in 0..trials {
208        let r = trial(m, w, sink, amp, depth, 0x1000 + i as u64 * 0x9E3779B9);
209        if r.is_finite() {
210            rs.push(r);
211        }
212    }
213    let n = rs.len().max(1) as f32;
214    let mean = rs.iter().sum::<f32>() / n;
215    let var = rs.iter().map(|r| (r - mean) * (r - mean)).sum::<f32>() / n;
216    Cell {
217        mean,
218        sd: var.sqrt(),
219        detected: rs.iter().filter(|&&r| r >= 0.5).count() as f32 / n,
220        strict: rs.iter().filter(|&&r| r >= 0.95).count() as f32 / n,
221        partial: rs.iter().filter(|&&r| r > 0.05 && r < 0.95).count(),
222    }
223}
224
225fn main() {
226    let trials: usize = std::env::var("TRIALS").ok().and_then(|v| v.parse().ok()).unwrap_or(48);
227    let (w, sink, depth) = (128usize, 4usize, 512usize);
228    let ms = [4usize, 8, 16, 32, 64];
229    let amps = [2.0f32, 4.0, 6.0, 8.0, 12.0];
230
231    println!("O(1) injection/recovery — w={w} sink={sink} depth={depth}, {trials} trials/cell");
232    println!("needle logit sits `amp` background sd above the field; recovery = kept / exact\n");
233
234    println!("mean recovery");
235    print!("{:>6}", "amp\\m");
236    for m in ms {
237        print!("{m:>12}");
238    }
239    println!();
240    let mut grid = Vec::new();
241    for &amp in &amps {
242        print!("{amp:>6.1}");
243        let mut row = Vec::new();
244        for &m in &ms {
245            let c = cell(m, w, sink, amp, depth, trials);
246            print!("{:>9.1}±{:<2.0}", c.mean * 100.0, c.sd * 100.0);
247            row.push(c);
248        }
249        println!();
250        grid.push((amp, row));
251    }
252
253    println!("\nfraction of trials at or above 95% recovery — the strict criterion");
254    print!("{:>6}", "amp\\m");
255    for m in ms {
256        print!("{m:>8}");
257    }
258    println!();
259    for (amp, row) in &grid {
260        print!("{amp:>6.1}");
261        for c in row {
262            print!("{:>7.0}%", c.strict * 100.0);
263        }
264        println!();
265    }
266
267    println!("\nfraction at or above 50% — the loose criterion");
268    print!("{:>6}", "amp\\m");
269    for m in ms {
270        print!("{m:>8}");
271    }
272    println!();
273    for (amp, row) in &grid {
274        print!("{amp:>6.1}");
275        for c in row {
276            print!("{:>7.0}%", c.detected * 100.0);
277        }
278        println!();
279    }
280
281
282    // ── the background channel alone ───────────────────────────────────
283    // No needle. This is the sum over ~n far keys that the skeleton
284    // estimates from m landmarks, and nothing else — the channel the
285    // recovery statistic divides by and therefore cannot see cleanly.
286    println!("\nbackground-only error (no needle) — the denominator channel isolated");
287    println!("{:>6}{:>14}{:>14}{:>12}", "m", "rel error", "sd", "log2 drop");
288    let mut prev: Option<f32> = None;
289    let mut pts: Vec<(f32, f32)> = Vec::new();
290    for &m in &ms {
291        let mut es: Vec<f32> = (0..trials)
292            .map(|i| background_error(m, w, sink, depth, 0x5000 + i as u64 * 0x9E3779B9))
293            .filter(|e| e.is_finite())
294            .collect();
295        es.sort_by(|a, b| a.partial_cmp(b).unwrap());
296        let n = es.len().max(1) as f32;
297        let mean = es.iter().sum::<f32>() / n;
298        let sd = (es.iter().map(|e| (e - mean) * (e - mean)).sum::<f32>() / n).sqrt();
299        let drop = prev.map(|p| (p / mean).log2()).unwrap_or(f32::NAN);
300        println!("{m:>6}{:>14.4}{:>14.4}{:>12.2}", mean, sd, drop);
301        prev = Some(mean);
302        pts.push(((m as f32).ln(), mean.max(1e-9).ln()));
303    }
304    let n = pts.len() as f32;
305    let (sx, sy) = pts.iter().fold((0f32, 0f32), |(a, b), (x, y)| (a + x, b + y));
306    let (mx, my) = (sx / n, sy / n);
307    let num: f32 = pts.iter().map(|(x, y)| (x - mx) * (y - my)).sum();
308    let den: f32 = pts.iter().map(|(x, _)| (x - mx) * (x - mx)).sum();
309    let beta = num / den;
310    println!(
311        "\nlog-log slope of the background error vs m: {beta:.3}  (amplitude)\n\
312         its energy would be {:.3} by identity — squaring doubles any slope, which is\n\
313         why only one of the two is worth measuring. CLT on a mean of m samples predicts\n\
314         -0.500 in amplitude; a Nystrom skeleton on a slowly-decaying kernel predicts\n\
315         nearer -0.250. That is the difference this table can actually settle.",
316        2.0 * beta
317    );
318
319    // Step or continuum? A step in m would show as the 50% and 95%
320    // criteria crossing at the SAME m — the needle is either kept whole
321    // or lost whole, with no partial states in between. A continuum
322    // shows the two criteria crossing at different m, with a spread of
323    // partial recoveries in between.
324    println!("\nshape of the response in m");
325    for (amp, row) in &grid {
326        let m50 = ms.iter().zip(row).find(|(_, c)| c.detected >= 0.5).map(|(m, _)| *m);
327        let m95 = ms.iter().zip(row).find(|(_, c)| c.strict >= 0.5).map(|(m, _)| *m);
328        // Deficit vs m on a log-log slope: a power law gives a stable
329        // exponent, a step gives a slope that runs away at the edge.
330        let slope = {
331            let pts: Vec<(f32, f32)> = ms
332                .iter()
333                .zip(row)
334                .filter(|(_, c)| c.mean < 0.999 && c.mean > 0.0)
335                .map(|(m, c)| ((*m as f32).ln(), (1.0 - c.mean).max(1e-4).ln()))
336                .collect();
337            if pts.len() < 2 {
338                f32::NAN
339            } else {
340                let n = pts.len() as f32;
341                let (sx, sy) = pts.iter().fold((0f32, 0f32), |(a, b), (x, y)| (a + x, b + y));
342                let (mx, my) = (sx / n, sy / n);
343                let num: f32 = pts.iter().map(|(x, y)| (x - mx) * (y - my)).sum();
344                let den: f32 = pts.iter().map(|(x, _)| (x - mx) * (x - mx)).sum();
345                num / den
346            }
347        };
348        let mid: usize = row.iter().map(|c| c.partial).sum();
349        println!(
350            "amp {amp:>4.1}: m@50% {:>6}  m@95% {:>6}  log-log slope of (1−r) vs m {slope:>7.2}  \
351             partial trials {mid:>4}",
352            m50.map(|v| v.to_string()).unwrap_or("—".into()),
353            m95.map(|v| v.to_string()).unwrap_or("—".into()),
354        );
355    }
356    println!(
357        "\n`partial trials` counts runs landing strictly between 5% and 95% recovery, summed over \
358         the row.\nA pure step would leave that column near zero: every trial all or nothing."
359    );
360}