From 926816c11648a5a109c8c78308bea049f2612d03 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:49:10 +0000 Subject: [PATCH 1/9] D-RPF-TABLE-1: numeric, replay-drift and decision axes for NarsTables resolutions vs A, plus a throughput bench Compares direct ISA revision (A) with NarsTables::build(n) for n in {1,2,4,8,16} through the real implementations. The A replay goes through step_with, which is replay_step's body with the law as a parameter and is pinned bit-exact against replay_step for every table; the A decision goes through decide, pinned against counterfactual_replay the same way. Pins: the numeric surface on the R3 domain, per-step drift over 256x64 chains on two corpora, decision flips over every cut, A's own saturation, that only F/C ever differ, that the W0 corpus cannot flip the frequency bar, and that chain_counterfactual's load-bearing fixture is load-bearing under B1 only. The bench example times micro revision, replay_step (dependent and batched) and cold-cache lookups, with a deliberately slow A as its anti-vacuity arm. Tests only; no production change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- .../examples/d_rpf_table_1_bench.rs | 331 +++++++ .../tests/d_rpf_table_1.rs | 881 ++++++++++++++++++ 2 files changed, 1212 insertions(+) create mode 100644 crates/lance-graph-planner/examples/d_rpf_table_1_bench.rs create mode 100644 crates/lance-graph-planner/tests/d_rpf_table_1.rs diff --git a/crates/lance-graph-planner/examples/d_rpf_table_1_bench.rs b/crates/lance-graph-planner/examples/d_rpf_table_1_bench.rs new file mode 100644 index 000000000..cb835309b --- /dev/null +++ b/crates/lance-graph-planner/examples/d_rpf_table_1_bench.rs @@ -0,0 +1,331 @@ +//! **D-RPF-TABLE-1, axis 4: what does the revision table buy in throughput?** +//! +//! ```text +//! cargo run -p lance-graph-planner --example d_rpf_table_1_bench --release +//! ``` +//! +//! Compares direct confidence-aware revision (A, `CausalEdge64::revision`) +//! with `NarsTables::build(n).revise` for n in {1, 2, 4, 8, 16}, at two +//! levels: +//! +//! - **micro**: one revision per input pair, inputs read from a 1 Mi-entry +//! array so nothing constant-folds, results summed into a sink. +//! - **replay**: the real `replay_step` for tables; for A, the same step with +//! the table lookup replaced by `CausalEdge64::revision` +//! (`tests/d_rpf_table_1.rs` proves that step bit-exact against +//! `replay_step` for every table). `dep` is one dependent 64-step chain +//! looped (latency-bound, the W0 shape); `batch` interleaves 256 chains so +//! independent steps can overlap (throughput-bound). +//! +//! Two input distributions: `uniform` (whole grid) and `w0` (f 128..=255, +//! c 128..=227, the distribution `dcr_w0_replay_budget` sized the kernel on). +//! `cold` scrubs a 512 MiB buffer before a short run, so the first table +//! accesses miss every cache level this machine has. +//! +//! **Anti-vacuity:** `A-slow` adds a deliberate f64 recompute to every A +//! revision. The harness must show it slower than A, or it cannot tell +//! kernels apart at all. +//! +//! Each number is the median of 7 runs. + +use std::hint::black_box; +use std::time::Instant; + +use causal_edge::edge::InferenceType; +use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_planner::chain_replay::{replay_step, ComposeTables}; + +const N: usize = 1 << 20; +const RUNS: usize = 7; +const LEVELS: [usize; 5] = [1, 2, 4, 8, 16]; + +struct Lcg(u64); +impl Lcg { + fn next(&mut self) -> u64 { + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + self.0 >> 33 + } + fn byte(&mut self, lo: u8, hi: u8) -> u8 { + lo + (self.next() % (hi as u64 - lo as u64 + 1)) as u8 + } +} + +#[derive(Clone, Copy)] +enum Dist { + Uniform, + W0, +} + +impl Dist { + fn pick(self, r: &mut Lcg) -> (u8, u8) { + match self { + Dist::Uniform => (r.byte(0, 255), r.byte(0, 255)), + Dist::W0 => (r.byte(128, 255), r.byte(128, 227)), + } + } +} + +const OPS: [InferenceType; 4] = [ + InferenceType::Deduction, + InferenceType::Induction, + InferenceType::Abduction, + InferenceType::Revision, +]; + +#[allow(deprecated)] // v2 `pack` ignores temporal +fn edge(f: u8, c: u8, op: InferenceType) -> CausalEdge64 { + CausalEdge64::pack( + 1, + 2, + 3, + f, + c, + CausalMask::SPO, + 0, + op, + PlasticityState::ALL_HOT, + 0, + ) +} + +fn pairs(d: Dist, seed: u64) -> Vec<(CausalEdge64, CausalEdge64)> { + let mut r = Lcg(seed); + (0..N) + .map(|_| { + let (f1, c1) = d.pick(&mut r); + let (f2, c2) = d.pick(&mut r); + ( + edge(f1, c1, InferenceType::Revision), + edge(f2, c2, InferenceType::Revision), + ) + }) + .collect() +} + +fn median(mut v: Vec) -> f64 { + v.sort_by(|a, b| a.partial_cmp(b).unwrap()); + v[v.len() / 2] +} + +/// The revision a step uses: A, A-slow, or a table. +#[derive(Clone, Copy)] +enum Law<'a> { + A, + ASlow, + Table(&'a NarsTables), +} + +#[inline(always)] +fn revise(law: Law<'_>, a: CausalEdge64, b: CausalEdge64) -> (u8, u8) { + match law { + Law::A => { + let r = a.revision(b); + (r.frequency_u8(), r.confidence_u8()) + } + Law::ASlow => { + let r = a.revision(b); + // Deliberate extra work: an f64 recompute of the same law. + let (c1, c2) = ( + a.confidence_u8() as f64 / 255.0, + b.confidence_u8() as f64 / 255.0, + ); + let mut x = c1 / (1.0 - c1 + 1e-9) + c2 / (1.0 - c2 + 1e-9); + for _ in 0..16 { + x = black_box(x).sqrt() * x.cbrt(); + } + let bump = u8::from(x.is_nan()); + (r.frequency_u8() ^ bump, r.confidence_u8()) + } + Law::Table(t) => { + let p = t.revise( + a.frequency_u8(), + a.confidence_u8(), + b.frequency_u8(), + b.confidence_u8(), + ); + (unpack_f(p), unpack_c(p)) + } + } +} + +fn micro(law: Law<'_>, input: &[(CausalEdge64, CausalEdge64)]) -> f64 { + let ns: Vec = (0..RUNS) + .map(|_| { + let t0 = Instant::now(); + let mut sink = 0u64; + for &(a, b) in input { + let (f, c) = revise(law, black_box(a), black_box(b)); + sink = sink.wrapping_add(f as u64 + ((c as u64) << 8)); + } + black_box(sink); + t0.elapsed().as_secs_f64() * 1e9 / input.len() as f64 + }) + .collect(); + median(ns) +} + +/// The replay step for `law`: `replay_step` for tables, the same body with +/// `revision` for A. +#[inline(always)] +fn step( + law: Law<'_>, + running: CausalEdge64, + w: CausalEdge64, + c: ComposeTables<'_>, +) -> CausalEdge64 { + match law { + Law::Table(t) => replay_step(running, w, t, c).expect("executable"), + _ => { + let (f, cc) = revise(law, running, w); + let mut out = running.forward(w, c.s, c.p, c.o).expect("executable"); + out.set_frequency_u8(f); + out.set_confidence_u8(cc); + out + } + } +} + +fn weights(d: Dist, n: usize, seed: u64) -> Vec { + let mut r = Lcg(seed); + (0..n) + .map(|_| { + let (f, c) = d.pick(&mut r); + edge(f, c, OPS[(r.next() % 4) as usize]) + }) + .collect() +} + +/// One dependent chain over 64 cyclic weights (the W0 shape). +fn replay_dep(law: Law<'_>, w: &[CausalEdge64], seed: CausalEdge64, c: ComposeTables<'_>) -> f64 { + let iters = N; + let ns: Vec = (0..RUNS) + .map(|_| { + let mut running = seed; + let t0 = Instant::now(); + for i in 0..iters { + running = step(law, running, black_box(w[i & 63]), c); + } + black_box(running.0); + t0.elapsed().as_secs_f64() * 1e9 / iters as f64 + }) + .collect(); + median(ns) +} + +/// 256 independent chains advanced in lockstep. +fn replay_batch( + law: Law<'_>, + w: &[CausalEdge64], + seeds: &[CausalEdge64], + c: ComposeTables<'_>, +) -> f64 { + let rounds = N / seeds.len(); + let ns: Vec = (0..RUNS) + .map(|_| { + let mut run = seeds.to_vec(); + let t0 = Instant::now(); + for r in 0..rounds { + for (j, x) in run.iter_mut().enumerate() { + *x = step(law, *x, black_box(w[(r * 7 + j) % w.len()]), c); + } + } + black_box(run[0].0); + t0.elapsed().as_secs_f64() * 1e9 / (rounds * seeds.len()) as f64 + }) + .collect(); + median(ns) +} + +/// First accesses after a 512 MiB scrub, so the table is out of every cache. +fn cold(law: Law<'_>, input: &[(CausalEdge64, CausalEdge64)]) -> f64 { + let mut scrub = vec![0u8; 512 << 20]; + let ns: Vec = (0..RUNS) + .map(|k| { + for (i, b) in scrub.iter_mut().enumerate().step_by(64) { + *b = b.wrapping_add((i + k) as u8); + } + black_box(&scrub); + let n = 4096; + let t0 = Instant::now(); + let mut sink = 0u64; + for &(a, b) in &input[k * n..(k + 1) * n] { + let (f, c) = revise(law, black_box(a), black_box(b)); + sink = sink.wrapping_add(f as u64 + ((c as u64) << 8)); + } + black_box(sink); + t0.elapsed().as_secs_f64() * 1e9 / n as f64 + }) + .collect(); + median(ns) +} + +fn compose_tables() -> [Box<[u8; 256 * 256]>; 3] { + let mut r = Lcg(0x9E37_79B9_7F4A_7C15); + core::array::from_fn(|_| { + let mut t = Box::new([0u8; 256 * 256]); + for v in t.iter_mut() { + *v = r.next() as u8; + } + t + }) +} + +fn main() { + let ct = compose_tables(); + let compose = ComposeTables { + s: &ct[0], + p: &ct[1], + o: &ct[2], + }; + let built: Vec = LEVELS.iter().map(|&n| NarsTables::build(n)).collect(); + let mut laws: Vec<(String, Law<'_>, usize)> = + vec![("A".into(), Law::A, 0), ("A-slow".into(), Law::ASlow, 0)]; + for (n, t) in LEVELS.iter().zip(&built) { + laws.push((format!("B{n}"), Law::Table(t), t.byte_size())); + } + + println!( + "{:<7} {:>10} | {:>13} {:>13} | {:>13} {:>13} | {:>13} {:>13} | {:>13}", + "law", + "bytes", + "micro unif", + "micro w0", + "replay dep u", + "replay dep w0", + "replay bat u", + "replay bat w0", + "cold unif" + ); + for d in [Dist::Uniform, Dist::W0] { + // Warm the inputs once so page faults are not charged to the first law. + black_box(pairs(d, 1).len()); + } + let pu = pairs(Dist::Uniform, 0xA11C_E5ED); + let pw = pairs(Dist::W0, 0xB0B_5EED); + let wu = weights(Dist::Uniform, 64, 0x51DE); + let ww = weights(Dist::W0, 64, 0x051E_D270_B5A1_11E5); + let su: Vec<_> = pairs(Dist::Uniform, 7) + .iter() + .take(256) + .map(|p| p.0) + .collect(); + let sw: Vec<_> = pairs(Dist::W0, 7).iter().take(256).map(|p| p.0).collect(); + for (name, law, bytes) in &laws { + println!( + "{:<7} {:>10} | {:>10.2} ns {:>10.2} ns | {:>10.2} ns {:>10.2} ns | {:>10.2} ns {:>10.2} ns | {:>10.2} ns", + name, + bytes, + micro(*law, &pu), + micro(*law, &pw), + replay_dep(*law, &wu, su[0], compose), + replay_dep(*law, &ww, sw[0], compose), + replay_batch(*law, &wu, &su, compose), + replay_batch(*law, &ww, &sw, compose), + cold(*law, &pu), + ); + } +} diff --git a/crates/lance-graph-planner/tests/d_rpf_table_1.rs b/crates/lance-graph-planner/tests/d_rpf_table_1.rs new file mode 100644 index 000000000..30d5402da --- /dev/null +++ b/crates/lance-graph-planner/tests/d_rpf_table_1.rs @@ -0,0 +1,881 @@ +//! D-RPF-TABLE-1: does the confidence-inert `NarsTables::build(1)` fast path +//! earn its semantic cost? +//! +//! Three of the four axes live here (numeric surface, replay drift, +//! downstream decisions); the fourth, throughput, is the +//! `d_rpf_table_1_bench` example, because a timing is not a test assertion. +//! +//! Laws compared, all through their real implementations: +//! - A `CausalEdge64::revision` (confidence-aware ISA revision) +//! - Bn `NarsTables::build(n).revise` for n in {1, 2, 4, 8, 16} +//! +//! The replay for A cannot go through `replay_step`, which takes a table. It +//! goes through `step_with`, which is `replay_step`'s body with the truth law +//! as a parameter. `step_with_a_table_is_replay_step` proves the mirror is +//! bit-exact against the real `replay_step` for every table resolution, so a +//! difference between A and Bn can only come from the law. +//! +//! Report: `.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md`. + +use causal_edge::edge::InferenceType; +use causal_edge::isa::IsaFault; +use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_planner::chain_counterfactual::{ + counterfactual_replay, cut_step, CutContext, DEFAULT_FREQUENCY_BAR, +}; +use lance_graph_planner::chain_replay::{replay_step, ChainStep, ComposeTables}; +use lance_graph_planner::pearl::Reaction; + +const LEVELS: [usize; 5] = [1, 2, 4, 8, 16]; + +/// A revision law over the u8 truth grid. +type Law<'a> = Box (u8, u8) + 'a>; + +#[allow(deprecated)] // v2 `pack` ignores temporal; not under test +fn edge(f: u8, c: u8, op: InferenceType) -> CausalEdge64 { + CausalEdge64::pack( + 1, + 2, + 3, + f, + c, + CausalMask::SPO, + 0, + op, + PlasticityState::ALL_HOT, + 0, + ) +} + +fn law_a() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = + edge(f1, c1, InferenceType::Revision).revision(edge(f2, c2, InferenceType::Revision)); + (r.frequency_u8(), r.confidence_u8()) + }) +} + +fn law_table(t: &NarsTables) -> Law<'_> { + Box::new(move |f1, c1, f2, c2| { + let p = t.revise(f1, c1, f2, c2); + (unpack_f(p), unpack_c(p)) + }) +} + +fn tables() -> Vec { + LEVELS.iter().map(|&n| NarsTables::build(n)).collect() +} + +// ─── Axis 1: the numeric surface ──────────────────────────────────────── + +/// The R3 domain of D-RPF-CONF-0, for comparability. +const FS: [u8; 5] = [0, 51, 128, 204, 255]; +const CELLS: u64 = (FS.len() * FS.len() * 256 * 256) as u64; + +fn is_boundary(c: u8) -> bool { + c == 0 || c >= 254 +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Surface { + differ: u64, + differ_broad: u64, + max_df: u8, + max_dc: u8, + max_df_broad: u8, + max_dc_broad: u8, + sum_df: u64, + sum_dc: u64, + /// p50 / p95 / p99 of |dF| and |dC| over all cells. + pct_df: [u8; 3], + pct_dc: [u8; 3], + distinct_c: usize, +} + +fn percentiles(hist: &[u64; 256]) -> [u8; 3] { + let mut out = [0u8; 3]; + for (k, q) in [0.50f64, 0.95, 0.99].iter().enumerate() { + let want = (q * CELLS as f64).ceil() as u64; + let mut acc = 0; + for (v, n) in hist.iter().enumerate() { + acc += n; + if acc >= want { + out[k] = v as u8; + break; + } + } + } + out +} + +fn surface(law: &Law<'_>, reference: &Law<'_>) -> Surface { + let mut s = Surface { + differ: 0, + differ_broad: 0, + max_df: 0, + max_dc: 0, + max_df_broad: 0, + max_dc_broad: 0, + sum_df: 0, + sum_dc: 0, + pct_df: [0; 3], + pct_dc: [0; 3], + distinct_c: 0, + }; + let (mut hf, mut hc) = ([0u64; 256], [0u64; 256]); + let mut seen_c = [false; 256]; + for &f1 in &FS { + for &f2 in &FS { + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + let (fa, ca) = reference(f1, c1, f2, c2); + let (fb, cb) = law(f1, c1, f2, c2); + seen_c[cb as usize] = true; + let (df, dc) = (fa.abs_diff(fb), ca.abs_diff(cb)); + hf[df as usize] += 1; + hc[dc as usize] += 1; + s.sum_df += df as u64; + s.sum_dc += dc as u64; + s.max_df = s.max_df.max(df); + s.max_dc = s.max_dc.max(dc); + if df != 0 || dc != 0 { + s.differ += 1; + if !is_boundary(c1) && !is_boundary(c2) { + s.differ_broad += 1; + s.max_df_broad = s.max_df_broad.max(df); + s.max_dc_broad = s.max_dc_broad.max(dc); + } + } + } + } + } + } + s.pct_df = percentiles(&hf); + s.pct_dc = percentiles(&hc); + s.distinct_c = seen_c.iter().filter(|x| **x).count(); + s +} + +fn all_surfaces() -> Vec { + let a = law_a(); + let ts = tables(); + ts.iter().map(|t| surface(&law_table(t), &a)).collect() +} + +#[test] +fn axis1_report() { + let ts = tables(); + for (n, t) in LEVELS.iter().zip(&ts) { + println!( + "B{n:<2} tables={:>3} bytes={:>9}", + t.revision.len(), + t.byte_size() + ); + } + for (n, s) in LEVELS.iter().zip(all_surfaces()) { + println!( + "B{n:<2} differ={:>7} broad={:>7} max=({},{}) broad_max=({},{}) \ + mean=({:.3},{:.3}) p50/95/99 dF={:?} dC={:?} distinct_c={}", + s.differ, + s.differ_broad, + s.max_df, + s.max_dc, + s.max_df_broad, + s.max_dc_broad, + s.sum_df as f64 / CELLS as f64, + s.sum_dc as f64 / CELLS as f64, + s.pct_df, + s.pct_dc, + s.distinct_c + ); + } +} + +// ─── Axes 2 and 3: replay drift and downstream decisions ──────────────── + +/// `replay_step`'s body with the truth law as a parameter: forward for the +/// palettes, mask, direction and plasticity, then the law's truth written +/// back. `step_with_a_table_is_replay_step` pins it against the real one. +fn step_with( + law: &Law<'_>, + running: CausalEdge64, + weight: CausalEdge64, + compose: ComposeTables<'_>, +) -> Result { + let (f, c) = law( + running.frequency_u8(), + running.confidence_u8(), + weight.frequency_u8(), + weight.confidence_u8(), + ); + let mut out = running.forward(weight, compose.s, compose.p, compose.o)?; + out.set_frequency_u8(f); + out.set_confidence_u8(c); + Ok(out) +} + +struct Lcg(u64); +impl Lcg { + fn next(&mut self) -> u64 { + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + self.0 >> 33 + } + fn byte(&mut self, lo: u8, hi: u8) -> u8 { + lo + (self.next() % (hi as u64 - lo as u64 + 1)) as u8 + } +} + +fn compose_tables() -> [Box<[u8; 256 * 256]>; 3] { + let mut r = Lcg(0x9E37_79B9_7F4A_7C15); + core::array::from_fn(|_| { + let mut t = Box::new([0u8; 256 * 256]); + for v in t.iter_mut() { + *v = r.next() as u8; + } + t + }) +} + +/// The ops a recorded weight can carry that the ISA executes. +const OPS: [InferenceType; 4] = [ + InferenceType::Deduction, + InferenceType::Induction, + InferenceType::Abduction, + InferenceType::Revision, +]; + +/// Two workloads. `Uniform` spans the whole truth grid. `W0` is the +/// distribution `dcr_w0_replay_budget` measured the replay kernel on +/// (f in 128..=255, c in 128..=227), i.e. the workload the fast path was +/// sized against. +#[derive(Debug, Clone, Copy)] +enum Corpus { + Uniform, + W0, +} + +struct Chain { + seed: CausalEdge64, + steps: Vec, +} + +fn corpus(kind: Corpus, chains: usize, len: usize) -> Vec { + let mut r = Lcg(match kind { + Corpus::Uniform => 0x0D17_AB1E_0000_0001, + Corpus::W0 => 0x051E_D270_B5A1_11E5, + }); + let pick = |r: &mut Lcg| match kind { + Corpus::Uniform => (r.byte(0, 255), r.byte(0, 255)), + Corpus::W0 => (r.byte(128, 255), r.byte(128, 227)), + }; + (0..chains) + .map(|_| { + let (f, c) = pick(&mut r); + let seed = edge(f, c, InferenceType::Revision); + let steps = (0..len) + .map(|_| { + let (f, c) = pick(&mut r); + let op = OPS[(r.next() % 4) as usize]; + (0x90u8, edge(f, c, op)) + }) + .collect(); + Chain { seed, steps } + }) + .collect() +} + +/// Every running truth after each step. +fn trajectory(law: &Law<'_>, ch: &Chain, compose: ComposeTables<'_>) -> Vec { + let mut running = ch.seed; + ch.steps + .iter() + .map(|&(_, w)| { + running = step_with(law, running, w, compose).expect("ops are executable"); + running + }) + .collect() +} + +fn compose_of(t: &[Box<[u8; 256 * 256]>; 3]) -> ComposeTables<'_> { + ComposeTables { + s: &t[0], + p: &t[1], + o: &t[2], + } +} + +/// The mirror is the real replay step. Without this every A-vs-Bn replay +/// number below could be a difference between two step functions. +#[test] +fn step_with_a_table_is_replay_step() { + let c = compose_tables(); + let compose = compose_of(&c); + let chains = corpus(Corpus::Uniform, 64, 64); + for t in tables() { + let law = law_table(&t); + for ch in &chains { + let (mut mine, mut real) = (ch.seed, ch.seed); + for &(_, w) in &ch.steps { + mine = step_with(&law, mine, w, compose).unwrap(); + real = replay_step(real, w, &t, compose).unwrap(); + assert_eq!(mine, real, "c_levels={}", t.c_levels); + } + } + } +} + +const CHECKPOINTS: [usize; 7] = [1, 2, 4, 8, 16, 32, 64]; + +#[derive(Debug, Clone, PartialEq)] +struct Drift { + /// Per checkpoint: (chains whose truth differs from A, mean |dF|, mean |dC|). + at: Vec<(usize, f64, f64)>, + /// Chains whose truth first differs from A at step 1. + first_at_step1: usize, + /// Chains that never differ within 64 steps. + never: usize, + /// Median first-divergence step over the chains that diverge. + median_first: usize, + /// Fields other than F/C that differ from A at any step (palettes, mask, + /// direction, plasticity, Epi5). Must be zero: the law touches only F/C. + other_field_diffs: usize, +} + +const FC_MASK: u64 = 0xFFFF << 24; + +fn drift(law: &Law<'_>, chains: &[Chain], compose: ComposeTables<'_>) -> Drift { + let a = law_a(); + let mut at = vec![(0usize, 0f64, 0f64); CHECKPOINTS.len()]; + let mut firsts = Vec::new(); + let (mut first_at_step1, mut never, mut other) = (0, 0, 0); + for ch in chains { + let ta = trajectory(&a, ch, compose); + let tb = trajectory(law, ch, compose); + let first = ta.iter().zip(&tb).position(|(x, y)| x != y); + match first { + None => never += 1, + Some(0) => { + first_at_step1 += 1; + firsts.push(1); + } + Some(i) => firsts.push(i + 1), + } + other += ta + .iter() + .zip(&tb) + .filter(|(x, y)| (x.0 & !FC_MASK) != (y.0 & !FC_MASK)) + .count(); + for (k, &cp) in CHECKPOINTS.iter().enumerate() { + let (x, y) = (ta[cp - 1], tb[cp - 1]); + if x != y { + at[k].0 += 1; + } + at[k].1 += x.frequency_u8().abs_diff(y.frequency_u8()) as f64; + at[k].2 += x.confidence_u8().abs_diff(y.confidence_u8()) as f64; + } + } + for e in &mut at { + e.1 /= chains.len() as f64; + e.2 /= chains.len() as f64; + } + firsts.sort_unstable(); + Drift { + at, + first_at_step1, + never, + median_first: firsts.get(firsts.len() / 2).copied().unwrap_or(0), + other_field_diffs: other, + } +} + +#[test] +fn axis2_report() { + let c = compose_tables(); + let compose = compose_of(&c); + let ts = tables(); + for kind in [Corpus::Uniform, Corpus::W0] { + let chains = corpus(kind, 256, 64); + for (n, t) in LEVELS.iter().zip(&ts) { + let d = drift(&law_table(t), &chains, compose); + println!( + "{kind:?} B{n:<2} first@1={} never={} median_first={} other={}", + d.first_at_step1, d.never, d.median_first, d.other_field_diffs + ); + for (cp, (k, df, dc)) in CHECKPOINTS.iter().zip(&d.at) { + println!(" step {cp:>2}: differ {k:>3}/256 mean dF {df:6.2} mean dC {dc:6.2}"); + } + } + } +} + +/// How often A itself reaches c = 255 (after which its frequency is frozen: +/// `w = MAX` dominates every later revision) and how often a step hits the +/// singular cell (255,255), which is G1 and collapses to (0,0). +fn a_saturation(chains: &[Chain], compose: ComposeTables<'_>) -> (Vec, usize) { + let a = law_a(); + let mut sat = vec![0usize; CHECKPOINTS.len()]; + let mut singular = 0; + for ch in chains { + let t = trajectory(&a, ch, compose); + let mut prev = ch.seed; + for (i, (e, &(_, w))) in t.iter().zip(&ch.steps).enumerate() { + if prev.confidence_u8() == 255 && w.confidence_u8() == 255 { + singular += 1; + } + prev = *e; + if let Some(k) = CHECKPOINTS.iter().position(|&cp| cp == i + 1) { + if e.confidence_u8() == 255 { + sat[k] += 1; + } + } + } + } + (sat, singular) +} + +#[test] +fn axis2_a_saturation_report() { + let c = compose_tables(); + let compose = compose_of(&c); + for kind in [Corpus::Uniform, Corpus::W0] { + let (sat, singular) = a_saturation(&corpus(kind, 256, 64), compose); + println!("{kind:?}: A at c=255 by checkpoint {sat:?} of 256; singular steps {singular}"); + } +} + +// ─── Axis 3: decisions ────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Class { + LoadBearing, + TruthOnly, + Inert, +} + +fn class(r: Reaction) -> Class { + match r { + Reaction::LoadBearing { .. } => Class::LoadBearing, + Reaction::TruthOnly { .. } => Class::TruthOnly, + Reaction::Inert { .. } => Class::Inert, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Decision { + factual: bool, + cut: bool, + load_bearing: bool, + reaction: Reaction, + /// The two arms' terminal frequencies are equal, so a TruthOnly can only + /// have come from confidence. + freq_equal: bool, +} + +fn terminal( + law: &Law<'_>, + seed: CausalEdge64, + steps: &[ChainStep], + compose: ComposeTables<'_>, +) -> (u8, u8) { + let ch = Chain { + seed, + steps: steps.to_vec(), + }; + trajectory(law, &ch, compose) + .last() + .map_or((0, 0), |e| (e.frequency_u8(), e.confidence_u8())) +} + +/// The cut decision exactly as `chain_counterfactual` + `pearl::reason` +/// make it: verdict = terminal frequency >= the bar (an empty arm is +/// Inconsistent and reads (0,0)); load-bearing = the verdicts differ; +/// reaction = `Reaction::classify` on the two terminal truths. +fn decide(law: &Law<'_>, ch: &Chain, index: usize, compose: ComposeTables<'_>) -> Decision { + let cut = cut_step(&ch.steps, index).expect("in range"); + let f = terminal(law, ch.seed, &ch.steps, compose); + let c = if cut.is_empty() { + (0, 0) + } else { + terminal(law, ch.seed, &cut, compose) + }; + let factual = !ch.steps.is_empty() && f.0 >= DEFAULT_FREQUENCY_BAR; + let cut_ok = !cut.is_empty() && c.0 >= DEFAULT_FREQUENCY_BAR; + let load_bearing = factual != cut_ok; + Decision { + factual, + cut: cut_ok, + load_bearing, + reaction: Reaction::classify(load_bearing, f, c), + freq_equal: f.0 == c.0, + } +} + +/// The decision mirror is the real counterfactual path for every table. +#[test] +fn decide_with_a_table_is_counterfactual_replay() { + let c = compose_tables(); + let compose = compose_of(&c); + for t in tables() { + let law = law_table(&t); + for ch in corpus(Corpus::Uniform, 64, 8).iter() { + for i in 0..ch.steps.len() { + let d = decide(&law, ch, i, compose); + let cf = counterfactual_replay( + &ch.steps, + i, + ch.seed, + CutContext { + tables: &t, + compose, + owner: 3, + base_seq: 100, + bar: DEFAULT_FREQUENCY_BAR, + }, + ) + .unwrap() + .unwrap(); + assert_eq!(d.load_bearing, cf.role.is_load_bearing()); + let tf = cf + .factual + .last() + .map(|r| (r.edge.frequency_u8(), r.edge.confidence_u8())); + let tc = cf + .counterfactual + .last() + .map(|r| (r.edge.frequency_u8(), r.edge.confidence_u8())); + assert_eq!( + d.reaction, + Reaction::classify( + cf.role.is_load_bearing(), + tf.unwrap_or((0, 0)), + tc.unwrap_or((0, 0)) + ) + ); + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +struct Flips { + cuts: usize, + /// The chain's own verdict differs from A's. + verdict: usize, + load_bearing: usize, + /// The Reaction variant differs from A's. + reaction_class: usize, + /// Of those, cuts where A says TruthOnly with equal frequencies (so the + /// truth moved only in confidence) and the law says Inert. + confidence_only: usize, +} + +fn flips(law: &Law<'_>, chains: &[Chain], compose: ComposeTables<'_>) -> Flips { + let a = law_a(); + let mut out = Flips::default(); + for ch in chains { + for i in 0..ch.steps.len() { + let (da, db) = (decide(&a, ch, i, compose), decide(law, ch, i, compose)); + out.cuts += 1; + out.verdict += usize::from(da.factual != db.factual); + out.load_bearing += usize::from(da.load_bearing != db.load_bearing); + if class(da.reaction) != class(db.reaction) { + out.reaction_class += 1; + if class(da.reaction) == Class::TruthOnly + && da.freq_equal + && class(db.reaction) == Class::Inert + { + out.confidence_only += 1; + } + } + } + } + out +} + +#[test] +fn axis3_report() { + let c = compose_tables(); + let compose = compose_of(&c); + let ts = tables(); + for kind in [Corpus::Uniform, Corpus::W0] { + for len in [2usize, 8] { + let chains = corpus(kind, 256, len); + for (n, t) in LEVELS.iter().zip(&ts) { + println!( + "{kind:?} len={len} B{n:<2} {:?}", + flips(&law_table(t), &chains, compose) + ); + } + } + } + // The repository's own counterfactual fixtures (chain_counterfactual tests). + let lb: Vec = vec![ + (0x91, edge(250, 250, InferenceType::Deduction)), + (0x92, edge(40, 30, InferenceType::Deduction)), + ]; + let red: Vec = (0..4) + .map(|i| (0x90 + i as u8, edge(250, 250, InferenceType::Deduction))) + .collect(); + let seed = edge(200, 200, InferenceType::Deduction); + let a = law_a(); + for (name, steps, cut) in [("load-bearing", &lb, 0usize), ("redundant", &red, 2)] { + let ch = Chain { + seed, + steps: steps.clone(), + }; + println!("{name}: A {:?}", decide(&a, &ch, cut, compose)); + for (n, t) in LEVELS.iter().zip(&ts) { + println!( + "{name}: B{n} {:?}", + decide(&law_table(t), &ch, cut, compose) + ); + } + } +} + +// ─── Pins ─────────────────────────────────────────────────────────────── + +/// Axis 1, per level: (revision tables, bytes, cells differing, broad cells +/// differing, broad max dF, broad max dC, distinct output c). +#[rustfmt::skip] +const AXIS1: [(usize, usize, u64, u64, u8, u8, usize); 5] = [ + ( 1, 262_144, 1_636_550, 1_598_385, 128, 168, 1), + ( 4, 655_360, 1_634_496, 1_596_341, 127, 100, 3), + ( 16, 2_228_224, 1_629_527, 1_591_392, 125, 55, 10), + ( 64, 8_519_680, 1_616_783, 1_578_704, 121, 28, 32), + (256, 33_685_504, 1_585_273, 1_547_346, 113, 14, 92), +]; + +#[test] +fn axis1_is_pinned() { + let ts = tables(); + let got: Vec<_> = ts + .iter() + .zip(all_surfaces()) + .map(|(t, s)| { + ( + t.revision.len(), + t.byte_size(), + s.differ, + s.differ_broad, + s.max_df_broad, + s.max_dc_broad, + s.distinct_c, + ) + }) + .collect(); + assert_eq!(got, AXIS1.to_vec()); +} + +/// Error falls with resolution on the mean, but no level reaches A: even 16 +/// buckets differ on 96.8% of cells and by over 100 frequency codes in the +/// broad domain. That is what "full precision" must not be read to mean. +#[test] +fn axis1_no_resolution_reaches_a() { + for s in all_surfaces() { + assert!(s.differ * 100 > CELLS * 96, "{s:?}"); + assert!(s.max_df_broad > 100, "{s:?}"); + } +} + +/// One AXIS2 row. +type Axis2Row = (&'static str, usize, usize, usize, usize, usize, u64, u64); + +/// Axis 2, per corpus and level: (chains diverging at step 1, chains never +/// diverging, median first divergence, non-F/C field diffs, mean |dF| and +/// mean |dC| at step 64 in hundredths). 256 chains x 64 steps. +#[rustfmt::skip] +const AXIS2: [Axis2Row; 10] = [ + ("Uniform", 1, 256, 0, 1, 0, 4386, 8325), + ("Uniform", 2, 256, 0, 1, 0, 4275, 4557), + ("Uniform", 4, 254, 0, 1, 0, 3953, 2414), + ("Uniform", 8, 254, 0, 1, 0, 3377, 1252), + ("Uniform", 16, 245, 0, 1, 0, 2805, 620), + ("W0", 1, 256, 0, 1, 0, 1634, 8100), + ("W0", 2, 255, 0, 1, 0, 1633, 3200), + ("W0", 4, 252, 0, 1, 0, 1186, 1891), + ("W0", 8, 251, 0, 1, 0, 656, 974), + ("W0", 16, 248, 0, 1, 0, 363, 469), +]; + +#[test] +fn axis2_is_pinned() { + let c = compose_tables(); + let compose = compose_of(&c); + let ts = tables(); + let mut got = Vec::new(); + for (name, kind) in [("Uniform", Corpus::Uniform), ("W0", Corpus::W0)] { + let chains = corpus(kind, 256, 64); + for (n, t) in LEVELS.iter().zip(&ts) { + let d = drift(&law_table(t), &chains, compose); + let last = d.at[CHECKPOINTS.len() - 1]; + got.push(( + name, + *n, + d.first_at_step1, + d.never, + d.median_first, + d.other_field_diffs, + (last.1 * 100.0).round() as u64, + (last.2 * 100.0).round() as u64, + )); + } + } + assert_eq!(got, AXIS2.to_vec()); +} + +/// The law touches only F/C: palettes, mask, direction, plasticity and the +/// epistemic bits agree with A at every step of every chain. So Epi5 and the +/// Pearl operator selection (bits 40..42) cannot be moved by the table. +#[test] +fn axis2_only_truth_bytes_differ() { + let c = compose_tables(); + let compose = compose_of(&c); + let ts = tables(); + let chains = corpus(Corpus::Uniform, 64, 64); + for t in &ts { + assert_eq!(drift(&law_table(t), &chains, compose).other_field_diffs, 0); + } +} + +/// A itself saturates on the uniform corpus: 43 of 256 chains sit at c = 255 +/// by step 64 (frequency frozen from then on) and 13 steps hit the G1 cell. +/// The W0 corpus never reaches it. +#[test] +fn axis2_a_saturation_is_pinned() { + let c = compose_tables(); + let compose = compose_of(&c); + assert_eq!( + a_saturation(&corpus(Corpus::Uniform, 256, 64), compose), + (vec![1, 2, 6, 7, 12, 24, 43], 13) + ); + assert_eq!( + a_saturation(&corpus(Corpus::W0, 256, 64), compose), + (vec![0; 7], 0) + ); +} + +/// Axis 3, per corpus, chain length and level: decision flips against A over +/// every cut of 256 chains. +#[rustfmt::skip] +const AXIS3: [(&str, usize, usize, Flips); 20] = [ + ("Uniform", 2, 1, Flips { cuts: 512, verdict: 104, load_bearing: 125, reaction_class: 156, confidence_only: 1 }), + ("Uniform", 2, 2, Flips { cuts: 512, verdict: 66, load_bearing: 68, reaction_class: 101, confidence_only: 3 }), + ("Uniform", 2, 4, Flips { cuts: 512, verdict: 32, load_bearing: 43, reaction_class: 84, confidence_only: 1 }), + ("Uniform", 2, 8, Flips { cuts: 512, verdict: 16, load_bearing: 21, reaction_class: 58, confidence_only: 2 }), + ("Uniform", 2, 16, Flips { cuts: 512, verdict: 6, load_bearing: 9, reaction_class: 35, confidence_only: 2 }), + ("Uniform", 8, 1, Flips { cuts: 2048, verdict: 776, load_bearing: 244, reaction_class: 905, confidence_only: 16 }), + ("Uniform", 8, 2, Flips { cuts: 2048, verdict: 552, load_bearing: 205, reaction_class: 751, confidence_only: 13 }), + ("Uniform", 8, 4, Flips { cuts: 2048, verdict: 368, load_bearing: 183, reaction_class: 646, confidence_only: 13 }), + ("Uniform", 8, 8, Flips { cuts: 2048, verdict: 168, load_bearing: 105, reaction_class: 502, confidence_only: 13 }), + ("Uniform", 8, 16, Flips { cuts: 2048, verdict: 96, load_bearing: 77, reaction_class: 426, confidence_only: 15 }), + ("W0", 2, 1, Flips { cuts: 512, verdict: 0, load_bearing: 0, reaction_class: 8, confidence_only: 1 }), + ("W0", 2, 2, Flips { cuts: 512, verdict: 0, load_bearing: 0, reaction_class: 7, confidence_only: 1 }), + ("W0", 2, 4, Flips { cuts: 512, verdict: 0, load_bearing: 0, reaction_class: 6, confidence_only: 1 }), + ("W0", 2, 8, Flips { cuts: 512, verdict: 0, load_bearing: 0, reaction_class: 4, confidence_only: 1 }), + ("W0", 2, 16, Flips { cuts: 512, verdict: 0, load_bearing: 0, reaction_class: 1, confidence_only: 0 }), + ("W0", 8, 1, Flips { cuts: 2048, verdict: 0, load_bearing: 0, reaction_class: 632, confidence_only: 54 }), + ("W0", 8, 2, Flips { cuts: 2048, verdict: 0, load_bearing: 0, reaction_class: 628, confidence_only: 54 }), + ("W0", 8, 4, Flips { cuts: 2048, verdict: 0, load_bearing: 0, reaction_class: 338, confidence_only: 43 }), + ("W0", 8, 8, Flips { cuts: 2048, verdict: 0, load_bearing: 0, reaction_class: 244, confidence_only: 36 }), + ("W0", 8, 16, Flips { cuts: 2048, verdict: 0, load_bearing: 0, reaction_class: 238, confidence_only: 45 }), +]; + +#[test] +fn axis3_is_pinned() { + let c = compose_tables(); + let compose = compose_of(&c); + let ts = tables(); + let mut got = Vec::new(); + for (name, kind) in [("Uniform", Corpus::Uniform), ("W0", Corpus::W0)] { + for len in [2usize, 8] { + let chains = corpus(kind, 256, len); + for (n, t) in LEVELS.iter().zip(&ts) { + got.push((name, len, *n, flips(&law_table(t), &chains, compose))); + } + } + } + assert_eq!(got, AXIS3.to_vec()); +} + +/// The W0 corpus cannot flip a verdict at all: every input frequency is +/// at least 128, so every weighted mean is too, and every chain clears the bar of +/// 128 under every law. Its zero verdict flips are structural, not evidence of +/// stability. +#[test] +fn w0_corpus_cannot_flip_the_frequency_bar() { + let c = compose_tables(); + let compose = compose_of(&c); + let a = law_a(); + for ch in corpus(Corpus::W0, 256, 8) { + let t = trajectory(&a, &ch, compose); + assert!(t.iter().all(|e| e.frequency_u8() >= DEFAULT_FREQUENCY_BAR)); + } +} + +/// The repository's load-bearing fixture (`chain_counterfactual`'s +/// `cutting_a_load_bearing_edge_flips_the_verdict`: seed 200/200, steps +/// 250/250 then 40/30, cut 0) is load-bearing under B1 ONLY. Under A and +/// under every other resolution both arms clear the bar. +#[test] +fn the_load_bearing_fixture_is_load_bearing_only_under_b1() { + let c = compose_tables(); + let compose = compose_of(&c); + let ch = Chain { + seed: edge(200, 200, InferenceType::Deduction), + steps: vec![ + (0x91, edge(250, 250, InferenceType::Deduction)), + (0x92, edge(40, 30, InferenceType::Deduction)), + ], + }; + let a = decide(&law_a(), &ch, 0, compose); + assert_eq!( + a.reaction, + Reaction::TruthOnly { + factual: 246, + counterfactual: 194 + } + ); + let lb: Vec = tables() + .iter() + .map(|t| decide(&law_table(t), &ch, 0, compose).load_bearing) + .collect(); + assert_eq!(lb, vec![true, false, false, false, false]); +} + +/// Anti-vacuity for the flip counter: a counter that compared only +/// frequencies would miss TruthOnly decisions that come from confidence +/// alone. This fixture has A moving confidence with frequency held, so a +/// confidence-blind classifier calls it Inert while `Reaction::classify` +/// does not. +#[test] +fn the_flip_counter_sees_confidence_only_changes() { + let c = compose_tables(); + let compose = compose_of(&c); + // Cutting one of two identical steps leaves the frequency where it was + // and lowers only the accumulated confidence. + let w = edge(200, 120, InferenceType::Revision); + let ch = Chain { + seed: edge(200, 120, InferenceType::Revision), + steps: vec![(0x90, w), (0x91, w)], + }; + let d = decide(&law_a(), &ch, 1, compose); + assert!(d.freq_equal, "{d:?}"); + assert_eq!(class(d.reaction), Class::TruthOnly, "{d:?}"); + let blind = if d.freq_equal && !d.load_bearing { + Class::Inert + } else { + class(d.reaction) + }; + assert_ne!(blind, class(d.reaction)); + // And B1 is exactly that blind classifier here. + let t1 = NarsTables::build(1); + assert_eq!( + class(decide(&law_table(&t1), &ch, 1, compose).reaction), + Class::Inert + ); +} From 910d457a1872be5257d3049ed9a068630e61ba0b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:50:37 +0000 Subject: [PATCH 2/9] Correct NarsTables precision and cache-residency comments 16 c-levels is the maximum table resolution, not numerical full precision: D-RPF-TABLE-1 measures 16 levels differing from direct revision on ~97% of cells and by up to 113 frequency codes in the broad domain. The 128 KiB and 32 MiB tables also do not fit L1 and L2 respectively on the bench host (48 KiB L1d, 2 MiB L2 per core); the lib.rs table size was half the real one. Comment changes only; no behaviour change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- crates/causal-edge/src/lib.rs | 2 +- crates/causal-edge/src/tables.rs | 18 +++++++++++++----- .../src/cache/nars_engine.rs | 8 +++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/causal-edge/src/lib.rs b/crates/causal-edge/src/lib.rs index 8a504d69f..c79fdedb0 100644 --- a/crates/causal-edge/src/lib.rs +++ b/crates/causal-edge/src/lib.rs @@ -39,7 +39,7 @@ //! Frequency (u8): f = val / 255.0, 256 quantization levels. //! Confidence (u8): c = val / 255.0, 256 quantization levels. //! At u8 resolution, revision/deduction/induction/abduction can be -//! precomputed as 256×256 lookup tables (64 KB each, fits L1 cache). +//! precomputed as 256×256 lookup tables (128 KiB each, u16 entries). //! //! ## Plasticity //! diff --git a/crates/causal-edge/src/tables.rs b/crates/causal-edge/src/tables.rs index 8d08519e2..d3f815b57 100644 --- a/crates/causal-edge/src/tables.rs +++ b/crates/causal-edge/src/tables.rs @@ -3,8 +3,10 @@ //! At u8 quantization (256 levels for f and c), every NARS operation //! can be precomputed as a 256×256 lookup table. //! -//! Size per table: 256 × 256 × 2 bytes (output f,c packed as u16) = 128 KB. -//! Fits L1 cache. Every NARS inference becomes a single memory read. +//! Size per table: 256 × 256 × 2 bytes (output f,c packed as u16) = 128 KiB. +//! Every NARS inference becomes a single memory read. Note that 128 KiB is +//! larger than a typical L1d (48 KiB on the D-RPF-TABLE-1 bench host); where a +//! table actually sits is measured there, not assumed here. /// Packed (frequency_u8, confidence_u8) output as u16. /// Low byte = frequency, high byte = confidence. @@ -34,7 +36,8 @@ pub fn unpack_c(p: PackedTruth) -> u8 { /// For the full 4D table (f1, c1, f2, c2), we use a two-level approach: /// 1. Quantize c into 16 buckets (4 bits) /// 2. Build 16×16 = 256 tables of 256×256 entries each -/// Total: 256 × 128KB = 32 MB — fits L2 cache. +/// Total: 256 × 128 KiB = 32 MiB, which on the D-RPF-TABLE-1 bench host is +/// larger than L2 (2 MiB per core) and lives in L3. /// /// For the fast path, we use a single table with average c: /// revision_fast[f1 * 256 + f2] → packed truth for c = c_mean. @@ -53,8 +56,13 @@ impl NarsTables { /// /// `c_levels`: number of confidence quantiles for revision tables. /// Total footprint is `(c_levels² + 1) × 128 KB` — the deduction table is - /// always allocated, independent of `c_levels`. Use 16 for full precision - /// (~32.1 MB), 1 for fast path (256 KB, not 128 KB). + /// always allocated, independent of `c_levels`. 16 is the **maximum table + /// resolution** (16 confidence buckets per side, ~32.1 MB), NOT numerical + /// full precision: each bucket uses its midpoint confidence, and against + /// direct revision (`CausalEdge64::revision`) 16 levels still differ on + /// ~97% of cells and by up to 113 frequency codes away from the boundary + /// (D-RPF-TABLE-1, `lance-graph-planner/tests/d_rpf_table_1.rs`). 1 is the + /// fast path (256 KB, not 128 KB) and ignores confidence entirely. pub fn build(c_levels: usize) -> Self { let c_levels = c_levels.clamp(1, 16); diff --git a/crates/lance-graph-planner/src/cache/nars_engine.rs b/crates/lance-graph-planner/src/cache/nars_engine.rs index 099564308..f9de444bd 100644 --- a/crates/lance-graph-planner/src/cache/nars_engine.rs +++ b/crates/lance-graph-planner/src/cache/nars_engine.rs @@ -431,11 +431,12 @@ pub fn style_score( /// Closed-loop NARS feedback engine. /// -/// Dual-path: hot path uses NarsTables (u16 lookup, L1 cache), +/// Dual-path: hot path uses NarsTables (u16 lookup), /// cold path uses f32 Truth for configuration and analysis. pub struct NarsEngine { pub distances: SpoDistances, - /// Precomputed NARS lookup tables (128 KB, L1 cache resident). + /// Precomputed NARS lookup tables (256 KiB at the default 1 level: one + /// 128 KiB revision table plus the 128 KiB deduction table). /// Hot path: revision/deduction as single memory read. pub tables: NarsTables, /// Skepticism: grows with consecutive high-confidence outputs. @@ -461,7 +462,8 @@ impl NarsEngine { /// `(c_levels² + 1) × 128 KB` — the revision tables plus the always-present /// 128 KB deduction table: **1 → 256 KB** (the `new` default, confidence /// inert — NOT 128 KB; the deduction table is allocated unconditionally), - /// **4 → ~2.1 MB**, **16 → ~32.1 MB** (full precision). Pick deliberately; + /// **4 → ~2.1 MB**, **16 → ~32.1 MB** (maximum table resolution, still not equal to direct + /// revision — D-RPF-TABLE-1). Pick deliberately; /// the default is the fast path, not the accurate one. pub fn with_c_levels(distances: SpoDistances, c_levels: usize) -> Self { Self { From 12e6413a60520b022848b7b1d4d4b09a10ff5d18 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:51:36 +0000 Subject: [PATCH 3/9] tables.rs: separate the size note from the numbered list (clippy doc_lazy_continuation) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- crates/causal-edge/src/tables.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/causal-edge/src/tables.rs b/crates/causal-edge/src/tables.rs index d3f815b57..d4019a7a9 100644 --- a/crates/causal-edge/src/tables.rs +++ b/crates/causal-edge/src/tables.rs @@ -36,6 +36,7 @@ pub fn unpack_c(p: PackedTruth) -> u8 { /// For the full 4D table (f1, c1, f2, c2), we use a two-level approach: /// 1. Quantize c into 16 buckets (4 bits) /// 2. Build 16×16 = 256 tables of 256×256 entries each +/// /// Total: 256 × 128 KiB = 32 MiB, which on the D-RPF-TABLE-1 bench host is /// larger than L2 (2 MiB per core) and lives in L3. /// From e86e6a7206436636387539d6ec44975e9ebc0693 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:52:55 +0000 Subject: [PATCH 4/9] T2: pin that Pearl bits 40..42 are a subset mask, not an ordinal ladder PO (0b011) is numerically below SO (0b101) but ranks above it, the two are incomparable subsets, and no field >= k threshold selects 'Intervention or above'. A paired test shows the same check passes for a numeric ranking. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- .../causal-edge/tests/t2_pearl_subset_mask.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 crates/causal-edge/tests/t2_pearl_subset_mask.rs diff --git a/crates/causal-edge/tests/t2_pearl_subset_mask.rs b/crates/causal-edge/tests/t2_pearl_subset_mask.rs new file mode 100644 index 000000000..16eb72a90 --- /dev/null +++ b/crates/causal-edge/tests/t2_pearl_subset_mask.rs @@ -0,0 +1,58 @@ +//! T2 (CE64 coresearch, 2026-10-08): bits 40..42 are a SUBSET mask over the +//! S/P/O planes, not an ordinal ladder. Reading the field as a number and +//! testing `>= k` ranks the masks wrongly. + +use causal_edge::CausalMask; + +/// The rung a mask's own documentation assigns it, parsed from +/// `pearl_level()`, or `None` for masks the ladder does not name. +fn rung(m: CausalMask) -> Option { + let s = m.pearl_level(); + s.strip_prefix("Level ") + .and_then(|r| r.chars().next()) + .and_then(|c| c.to_digit(10)) + .map(|d| d as u8) +} + +#[test] +fn po_is_numerically_below_so_but_ranks_above_it() { + let (po, so) = (CausalMask::PO as u8, CausalMask::SO as u8); + assert!(po < so, "PO={po:#05b} SO={so:#05b}"); + assert_eq!(rung(CausalMask::PO), Some(2)); + assert_eq!(rung(CausalMask::SO), Some(1)); + assert!(rung(CausalMask::PO) > rung(CausalMask::SO)); +} + +/// PO and SO are incomparable subsets: neither contains the other, so no +/// numeric order on the field can agree with the rung order for both. +#[test] +fn po_and_so_are_incomparable_subsets() { + let (po, so) = (CausalMask::PO as u8, CausalMask::SO as u8); + assert_ne!(po & so, po); + assert_ne!(po & so, so); + assert_eq!(CausalMask::SPO as u8, po | so); +} + +/// Whether some `field >= k` admits exactly the named masks whose rank is at +/// least that of PO, under the ranking `rank`. +fn some_threshold_matches(rank: impl Fn(CausalMask) -> Option) -> bool { + let named = [CausalMask::SO, CausalMask::PO, CausalMask::SPO]; + (0u8..=8).any(|k| { + named + .iter() + .all(|&m| ((m as u8) >= k) == (rank(m) >= rank(CausalMask::PO))) + }) +} + +/// No threshold on the raw field selects "Intervention or above". +#[test] +fn no_numeric_threshold_reproduces_the_ladder() { + assert!(!some_threshold_matches(rung)); +} + +/// The check can pass: if the rung WERE the field's numeric value, a +/// threshold would work. Without this, the test above could not fail. +#[test] +fn the_threshold_check_passes_for_a_numeric_ranking() { + assert!(some_threshold_matches(|m| Some(m as u8))); +} From 1b8d99d13b66727892609c50ccfe005f2dd8b590 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:53:09 +0000 Subject: [PATCH 5/9] T4: pin learn against isa::contracts::LEARN over all eight plasticity patterns Frozen planes keep A's archetype, hot planes adopt the more confident observation's, and Pearl, Direction, Inference, Witness and Epi5 pass through bit-exactly. A paired test shows a hot plane really moves. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- .../tests/t4_learn_plasticity_patterns.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/causal-edge/tests/t4_learn_plasticity_patterns.rs diff --git a/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs new file mode 100644 index 000000000..44fa9b177 --- /dev/null +++ b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs @@ -0,0 +1,86 @@ +//! T4 (CE64 coresearch, 2026-10-08): `learn` against `isa::contracts::LEARN`, +//! bit-exact over all eight plasticity patterns. +//! +//! The contract declares Pearl, Direction, Inference, Witness and Epistemic +//! as pass-through from A. Per plane, a FROZEN plane keeps A's archetype and +//! a HOT plane adopts the observation's when the observation is more +//! confident. The fixture keeps the revised confidence below 0.7, so the +//! plasticity field itself is not changed by the confidence transitions. + +use causal_edge::edge::InferenceType; +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; + +/// Bits 40..49 (Pearl, Direction, Inference) and 53..63 (Witness, Epi5). +const PASS: u64 = (0x3FF << 40) | (0x7FF << 53); +const PLAST_SHIFT: u32 = 50; + +#[allow(deprecated)] // v2 `pack` ignores temporal; not under test +fn edge(s: u8, p: u8, o: u8, c: u8, plast: PlasticityState) -> CausalEdge64 { + let mut e = CausalEdge64::pack( + s, + p, + o, + 200, + c, + CausalMask::SO, + 0b101, + InferenceType::Abduction, + plast, + 0, + ); + // Non-zero witness and epistemic bits, so pass-through is observable. + e.0 |= 0b10_1101_0110_1u64 << 53; + e +} + +#[test] +fn learn_passes_frozen_planes_and_declared_fields_for_every_pattern() { + for bits in 0u8..8 { + let plast = PlasticityState::from_bits(bits); + let a = edge(1, 2, 3, 51, plast); // c = 0.2 + let obs = edge(9, 8, 7, 77, PlasticityState::ALL_HOT); // c ~ 0.3 + let mut out = a; + out.learn(obs, 0); + + assert_eq!( + out.0 & PASS, + a.0 & PASS, + "pass-through, plasticity {bits:03b}" + ); + assert_eq!( + out.0 >> PLAST_SHIFT & 0b111, + u64::from(bits), + "plasticity {bits:03b}" + ); + let want = |hot: bool, mine: u8, theirs: u8| if hot { theirs } else { mine }; + assert_eq!( + out.s_idx(), + want(plast.s_hot(), 1, 9), + "S, plasticity {bits:03b}" + ); + assert_eq!( + out.p_idx(), + want(plast.p_hot(), 2, 8), + "P, plasticity {bits:03b}" + ); + assert_eq!( + out.o_idx(), + want(plast.o_hot(), 3, 7), + "O, plasticity {bits:03b}" + ); + assert!( + out.confidence_u8() > 51 && out.confidence_u8() < 179, + "{}", + out.confidence_u8() + ); + } +} + +/// The fixture can see a change: a hot plane really does move, so the +/// frozen-plane assertions above are not satisfied by `learn` doing nothing. +#[test] +fn learn_moves_a_hot_plane() { + let mut out = edge(1, 2, 3, 51, PlasticityState::ALL_HOT); + out.learn(edge(9, 8, 7, 77, PlasticityState::ALL_HOT), 0); + assert_eq!((out.s_idx(), out.p_idx(), out.o_idx()), (9, 8, 7)); +} From 93d6d32dfb0438b9c769f5e08f3357d92fb48fa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:53:29 +0000 Subject: [PATCH 6/9] T4: clippy byte grouping Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- crates/causal-edge/tests/t4_learn_plasticity_patterns.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs index 44fa9b177..f283b81ee 100644 --- a/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs +++ b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs @@ -29,7 +29,7 @@ fn edge(s: u8, p: u8, o: u8, c: u8, plast: PlasticityState) -> CausalEdge64 { 0, ); // Non-zero witness and epistemic bits, so pass-through is observable. - e.0 |= 0b10_1101_0110_1u64 << 53; + e.0 |= 0b101_1010_1101_u64 << 53; e } From b413163df4c4fb7867f12fae54f52f7059b07738 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 05:56:55 +0000 Subject: [PATCH 7/9] D-RPF-TABLE-1: board entry with the Pareto table, boundaries and kill conditions Also corrects #1424's claim that the replay path emits B1 in production: no production code constructs a CutContext or reads NarsEngine::tables. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- .claude/board/SUPERSESSION-INDEX.md | 2 +- ...6-10-09-d-rpf-conf-0-confidence-surface.md | 6 + ...2026-10-09-d-rpf-table-1-fast-path-cost.md | 271 ++++++++++++++++++ .claude/board/entries/README.md | 3 +- 4 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 .claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md diff --git a/.claude/board/SUPERSESSION-INDEX.md b/.claude/board/SUPERSESSION-INDEX.md index 8b57f7542..8657cc50b 100644 --- a/.claude/board/SUPERSESSION-INDEX.md +++ b/.claude/board/SUPERSESSION-INDEX.md @@ -94,7 +94,7 @@ a licence to act on it. | **READ** | `mul-ewa-trust-propagation-v1` | `GateDecision` | PROPOSED — PLAN/BOARD ONLY. Measure-before-c | 1/3 | | **READ** | `nan-ci-mode-v1` | `GateDecision` | PROPOSAL — **`D-NCI-1..5` are unbuilt. No Na | 1/8 | | **READ** | `persistence-artifact-backed-commit-v1` | `persist_cycle` | RATIFIED (operator ruling 2026-08-09). Phase | 0/0 | -| **READ** | `post-teardown-buildup-survey-v1` | `StepMask` | SURVEY, read-only, plan-only (no code, no te | 1/5 | +| **READ** | `post-teardown-buildup-survey-v1` | `StepMask` | SURVEY, read-only, plan-only (no code, no te | 2/5 | | **READ** | `r2il-bpe-typed-genetic-recombination-v1` | `GateDecision` | PROPOSAL, §7's three falsifiers now RUN (see | 1/1 | | **READ** | `scientific-kg-substrate-v1` | `GateDecision` | PROPOSED — **scoping doc**, no code. Records | 3/9 | | **READ** | `self-reasoning-substrate-v1` | `GateDecision` | PROPOSED — doc-only. No code, no contract ch | 4/5 | diff --git a/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md b/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md index 07b6bd037..9b1ac81d4 100644 --- a/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md +++ b/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md @@ -109,6 +109,12 @@ not definitions.) Every call site used for a live/not-live claim was read. production functions. Every in-tree caller passes `build(1)`, except `tests/chain_confidence.rs` (16). So on the replay/counterfactual path the emitted truth is **B(1)**. + + > ⊘ Corrected by D-RPF-TABLE-1 (`2026-10-09-d-rpf-table-1-fast-path-cost.md`): + > the replay path emits whatever tables its caller passes, and every caller + > in the tree is a test, example or probe. No production code constructs a + > `CutContext` or reads `NarsEngine::tables`, so B(1) changes no production + > decision today. - Not in the census, found while verifying it: `contract/high_heel.rs:240` `revise_truth` is another LIN copy with a 0.99 cap. The census sweep of differently named pooling functions was incidental, not exhaustive, so this diff --git a/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md b/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md new file mode 100644 index 000000000..e1069972e --- /dev/null +++ b/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md @@ -0,0 +1,271 @@ +# 2026-10-09 — D-RPF-TABLE-1: what did the confidence-inert fast path buy? + +**Status:** MEASURED. Tests, one bench example, comment corrections and two +isolated guards (T2, T4). No production semantics changed; no G1 or G2 +policy chosen. +**Harness:** `crates/lance-graph-planner/tests/d_rpf_table_1.rs` (numeric, +replay drift, decisions; 15 tests) and +`crates/lance-graph-planner/examples/d_rpf_table_1_bench.rs` (throughput). +**Parent:** `2026-10-09-d-rpf-conf-0-confidence-surface.md` (#1424). Its +T1/T3/R1/G4/R3 results are taken as given and not re-measured. + +Laws: **A** = `CausalEdge64::revision` (confidence-aware). **Bn** = +`NarsTables::build(n).revise`, n ∈ {1, 2, 4, 8, 16}. All through the real +implementations. A's replay uses `step_with` (= `replay_step` with the law as +a parameter) and A's decisions use `decide` (= the `counterfactual_replay` + +`Reaction::classify` rule); both are pinned bit-exact against the real +functions for every table, so A-vs-Bn differences come only from the law. + +## Where B1 actually runs (correcting #1424) + +#1424 said "the replay/counterfactual path emits B1". Read in code: + +- `replay_chain` / `counterfactual_replay` / `pearl::reason` take the tables + from the caller. **No production code constructs a `CutContext`.** Every + caller is a test, an example or a probe, and all of them pass `build(1)` + except `tests/chain_confidence.rs` (16). +- `NarsEngine::new` allocates `build(1)` (256 KiB per engine; production + instance in `strategy/chat_bundle.rs:42`), but **no production code reads + `NarsEngine::tables`**: `revise_fast` / `deduce_fast` have only test + callers, and `on_response` revises with `truth_revision` (law C). +- `cognitive-shader-driver` does a `tables.revise` per hit, but **discards + the result** (`_revised_truth`, "observed only"), and nothing calls + `with_nars_tables` outside tests. + +So today B1 changes **no production decision**. It is the default of every +replay fixture and probe and of `NarsEngine`. G2 is therefore a question about +what future wiring should use, and about what the existing fixtures pin. + +## History (classification) + +- `NarsTables` and the "16 for full precision" comment: `de61aa5d` + (2026-03-28, "CausalEdge64 — 64-bit causal neuron crate"). +- `NarsEngine::new → build(1)`: `1d355e16` (2026-03-31, "wire causal-edge + protocol into NarsEngine hot path"), commented "fast path: 1 c-level = + 128 KB" and "L1-resident". That hot path indexed the **deduction** table + until `6e5e674a` (2026-09-10). No benchmark accompanies either commit. +- `replay_step` with the table: `badd0894` (2026-08-31, D-DCR-1). Its message + cites "the kernel W0 measured at ~35 ns"; `dcr_w0_replay_budget` timed the + table+forward kernel at `build(1)` only and never compared it with direct + revision. + +Classification: **a deliberate production default chosen on an unmeasured +speed/size assumption**, not a benchmark shortcut. It is not an accident, and +it was never measured against the law it replaces. + +## Axis 1 — numeric surface vs A (R3 domain, 1,638,400 cells) + +| law | tables | bytes | cells ≠ A | broad ≠ A | broad max dF | broad max dC | mean dF | mean dC | p50/p95/p99 dF | p50/p95/p99 dC | distinct c | +|---|---|---|---|---|---|---|---|---|---|---|---| +| B1 | 1 | 262,144 | 1,636,550 | 1,598,385 | 128 | 168 | 32.72 | 47.65 | 24/99/123 | 46/98/136 | 1 | +| B2 | 4 | 655,360 | 1,634,496 | 1,596,341 | 127 | 100 | 16.76 | 25.25 | 9/65/99 | 23/56/68 | 3 | +| B4 | 16 | 2,228,224 | 1,629,527 | 1,591,392 | 125 | 55 | 8.93 | 12.92 | 4/35/66 | 12/28/35 | 10 | +| B8 | 64 | 8,519,680 | 1,616,783 | 1,578,704 | 121 | 28 | 4.68 | 6.51 | 2/18/37 | 6/14/17 | 32 | +| B16 | 256 | 33,685,504 | 1,585,273 | 1,547,346 | 113 | 14 | 2.41 | 3.27 | 1/9/19 | 3/7/9 | 92 | + +Mean error roughly halves per doubling. No level approaches A: even B16 +differs on 96.8% of cells, and **broad max dF stays above 100 at every level**, +because a bucket midpoint misstates the relative weight of two inputs whose +confidences sit far apart inside their buckets. + +## Axis 2 — replay drift (256 chains × 64 steps, two corpora) + +`Uniform` spans the grid; `W0` is the distribution `dcr_w0_replay_budget` +sized the kernel on (f 128..=255, c 128..=227). + +- **Every table diverges from A at step 1** on 245–256 of 256 chains. No chain + under any table stays equal to A for 64 steps. +- Mean |dF| / |dC| at step 64 (hundredths in the pin): + +| law | Uniform dF | Uniform dC | W0 dF | W0 dC | +|---|---|---|---|---| +| B1 | 43.86 | 83.25 | 16.34 | 81.00 | +| B2 | 42.75 | 45.57 | 16.33 | 32.00 | +| B4 | 39.53 | 24.14 | 11.86 | 18.91 | +| B8 | 33.77 | 12.52 | 6.56 | 9.74 | +| B16 | 28.05 | 6.20 | 3.63 | 4.69 | + +- Frequency error **grows** with chain length on the uniform corpus at every + resolution (B16: 2.2 at step 1, 28.1 at step 64). Error is not monotone in + steps on W0 (B16 peaks at step 16). +- Only F/C ever differ: palettes, mask, direction, plasticity, witness and + Epi5 agree with A at every step (pinned). So the table cannot move Epi5 or + Pearl operator selection. +- A itself saturates on the uniform corpus: 43/256 chains are at c = 255 by + step 64 (frequency frozen from then on), and 13 steps hit the G1 cell. W0 + never does. + +## Axis 3 — decisions (every cut of 256 chains) + +Flips against A. "verdict" = the chain's own frequency-bar verdict; +"LB" = load-bearing; "class" = `Reaction` variant; "c-only" = A said TruthOnly +with equal frequencies and the law said Inert. + +| corpus, len | law | cuts | verdict | LB | class | c-only | +|---|---|---|---|---|---|---| +| Uniform, 8 | B1 | 2048 | 776 (37.9%) | 244 (11.9%) | 905 (44.2%) | 16 | +| Uniform, 8 | B2 | 2048 | 552 | 205 | 751 | 13 | +| Uniform, 8 | B4 | 2048 | 368 | 183 | 646 | 13 | +| Uniform, 8 | B8 | 2048 | 168 | 105 | 502 | 13 | +| Uniform, 8 | B16 | 2048 | 96 (4.7%) | 77 (3.8%) | 426 (20.8%) | 15 | +| Uniform, 2 | B1 | 512 | 104 | 125 | 156 | 1 | +| Uniform, 2 | B16 | 512 | 6 | 9 | 35 | 2 | +| W0, 8 | B1 | 2048 | 0 | 0 | 632 (30.9%) | 54 | +| W0, 8 | B16 | 2048 | 0 | 0 | 238 (11.6%) | 45 | + +(All 20 rows are pinned in `AXIS3`.) + +- **The W0 zeros are structural, not stability.** Every W0 frequency is + ≥ 128, so every weighted mean is too, and no chain can fall below the 128 + bar under any law (pinned in `w0_corpus_cannot_flip_the_frequency_bar`). +- **The repository's own load-bearing fixture is load-bearing under B1 only.** + `chain_counterfactual::cutting_a_load_bearing_edge_flips_the_verdict` + (seed 200/200, steps 250/250 then 40/30, cut 0) gives B1 133 → 120, across + the bar. A gives 246 → 194, and B2…B16 give 207–244 → 184–198: both arms + clear the bar, so the cut is **not** load-bearing. The fixture was found by + sweeping under B1 and pins a property of B1, not of revision. +- `DEFAULT_FREQUENCY_BAR`'s doc justifies reading frequency because + confidence "saturates" at 170 in every chain. 170 is not saturation: it is + B1 discarding both confidences and returning one constant. Under A, + confidence varies along every chain. The frequency-over-confidence choice may + still be right; its stated evidence is B1's constant. +- Chain admission reads no truth, so the law cannot affect it. + +## Axis 4 — throughput + +Machine: 4 vCPU Xeon @ 2.1 GHz; per core 48 KiB L1d, 2 MiB L2; 260 MiB shared +L3. Median of 7 runs per cell; the table shows the median of 3 such runs (the +spread across runs is in the transcript and stays within the noted ranges). + +| law | memory | micro unif ns | micro w0 ns | replay dep ns | replay batch ns | cold ns | +|---|---|---|---|---|---|---| +| A | 0 | 12.1 | 12.1 | 32.7 | 15.7 | 11.5 | +| B1 | 256 KiB | 3.4 | 8.5 | 7.4 | 7.0 | 19.0 | +| B2 | 640 KiB | 3.8 | 8.6 | 7.4 | 7.4 | 28.2 | +| B4 | 2.1 MiB | 13.4 | 3.6 | 7.7 | 7.2 | 36.8 | +| B8 | 8.1 MiB | 17.5 | 6.0 | 8.4 | 7.3 | 51.7 | +| B16 | 32.1 MiB | 29.2 | 12.4 | 8.9 | 9.8 | 60.3 | +| A-slow (anti-vacuity) | 0 | ~480 | ~470 | ~500 | ~475 | ~455 | + +Reading: + +- **Replay, hot table:** every resolution is 2–4.4× faster than A. The + dependent replay is latency-bound and A's float divisions sit on the chain; + the table is one load. B16 is within ~1.2× of B1 here. +- **Random access (micro, uniform):** only B1 and B2 beat A (3.5×). B4 ties A; + **B8 and B16 are slower than A** (17.5 and 29.2 ns vs 12.1), because random + indices miss L2. +- **Cold:** **A beats every table**, B1 included (11.5 vs 19.0 ns). +- The replay rows use 64 cyclic weights (the W0 probe's shape), so the touched + table entries stay hot. A replay whose weights are spread over the grid + behaves like the micro-uniform row, not the replay row. +- Unexplained, recorded: B1/B2 micro are slower on `w0` inputs than on + `uniform` (8.5 vs 3.4 ns) and B4 the reverse, consistently across runs. + +## Pareto table + +| law | memory | replay dep / batch ns | micro unif ns | cold ns | broad max dF / dC | mean dF / dC | Uniform len 8 flips: verdict / LB / class | +|---|---|---|---|---|---|---|---| +| A | 0 | 32.7 / 15.7 | 12.1 | 11.5 | 0 / 0 | 0 / 0 | 0 / 0 / 0 | +| B1 | 256 KiB | 7.4 / 7.0 | 3.4 | 19.0 | 128 / 168 | 32.7 / 47.6 | 776 / 244 / 905 | +| B2 | 640 KiB | 7.4 / 7.4 | 3.8 | 28.2 | 127 / 100 | 16.8 / 25.3 | 552 / 205 / 751 | +| B4 | 2.1 MiB | 7.7 / 7.2 | 13.4 | 36.8 | 125 / 55 | 8.9 / 12.9 | 368 / 183 / 646 | +| B8 | 8.1 MiB | 8.4 / 7.3 | 17.5 | 51.7 | 121 / 28 | 4.7 / 6.5 | 168 / 105 / 502 | +| B16 | 32.1 MiB | 8.9 / 9.8 | 29.2 | 60.3 | 113 / 14 | 2.4 / 3.3 | 96 / 77 / 426 | + +## The pre-registered boundaries, evaluated + +| boundary | result | +|---|---| +| B1 survives if ≥ 1.5× faster in replay **and** planner outcomes stable | **Fails.** The speed half holds (2.2–4.4× replay, 3.6× random micro). The stability half does not: 37.9% verdict flips, 11.9% load-bearing flips and 44.2% Reaction-class flips on uniform 8-step chains, and the repository's own load-bearing fixture flips. | +| Some Bn faster while decision-stable | **Fails at every n.** B16 is the closest: 4.7% verdict, 3.8% LB, 20.8% class flips on uniform 8-step chains. | +| Table approach wounded if A replay ≥ best table / 1.1 | **Not wounded on hot replay** (A is 2–4× slower there). **Wounded off the hot path:** on random access B8/B16 are slower than A, B4 ties, and on a cold cache A beats every table. | +| B16 "full precision" falsified if material divergence remains | **Falsified.** 96.8% of cells differ, broad max dF 113, decision flips at every length. Comments corrected to "maximum table resolution". | + +So the answer to "what did we buy by throwing confidence away": **2–4× +throughput on a hot, dependent replay, and 3.6× on random lookups — for a law +that flips about 4 in 10 verdicts on spread inputs.** Paying for resolution +does not rescue it: B16 keeps the hot-replay speed, loses the random-access +and cold speed, and still flips decisions. + +## The replay contract (item 11) + +`replay_step` runs `forward` (opcode = the weight's inference field: palettes +compose, mask intersects, direction and plasticity from the weight, F/C from +that opcode's truth function), then overwrites F/C with the table's revision. + +- They are different operations: forward computes the **rule** the weight + declares (deduction, induction, …); the table computes **revision** of the + running truth with the weight. The overwrite is deliberate (`badd0894`: + "no second truth register"). +- forward's own F/C computation is discarded every step. Its other outputs + are kept, and its opcode decode still matters (it faults on Counterfactual + and Intervention weights). Possible compute waste, not removed here. +- The output edge's inference field names the weight's opcode (e.g. + Deduction) while its F/C carry a revision. Recorded as a label/value + mismatch, not changed. + +## Comment corrections landed + +- `tables.rs`: "Use 16 for full precision" → "maximum table resolution … NOT + numerical full precision", with the measured gap; "Fits L1 cache" and + "32 MB — fits L2" replaced by the measured cache picture. +- `lib.rs`: tables are 128 KiB (u16 entries), not 64 KB, and do not fit L1. +- `nars_engine.rs`: "(128 KB, L1 cache resident)" and "(full precision)" + corrected. No behaviour change. + +## Independent guards (#1423) + +- **T2** (`causal-edge/tests/t2_pearl_subset_mask.rs`): PO (0b011) < SO + (0b101) numerically while PO ranks above SO; PO and SO are incomparable + subsets; no `field >= k` threshold selects "Intervention or above"; a paired + test shows the check passes for a numeric ranking. +- **T4** (`causal-edge/tests/t4_learn_plasticity_patterns.rs`): `learn` over + all eight plasticity patterns keeps frozen planes, moves hot planes, and + passes Pearl/Direction/Inference/Witness/Epi5 bit-exactly. + +## Disable runs (all red as required) + +- B1 replaced by A → 8 tests fail (surface, drift, flips, fixture, both + mirror tests). +- All `c_levels` forced to 1 → surface, drift, flips and fixture pins fail. +- Real `replay_step` stops writing the table result → both mirror-faithfulness + tests fail. (The drift/decision pins measure the laws through the mirror, so + they do not move; the mirror tests are what tie them to production.) +- Flip counter made blind to confidence → the confidence-only fixture, the + flips pin and the decision-mirror test fail. +- Bench: A-slow measures ~40× slower than A, so the harness separates kernels. +- T4: frozen-S check removed → fails; `learn` clearing the Pearl bits → fails. +- T2: rung read from the numeric field → 2 of 4 fail. + +## Kill conditions for what this leaves open + +- **B1 as a default** is dead unless a workload is shown whose inputs keep + frequencies on one side of every decision bar, *and* whose decisions do not + read `Reaction`'s class. W0 shows the first half can hold; the second did not + (30.9% class flips). +- **A table at any resolution** survives only for hot, dependent replay where + the decisions it feeds are frequency-bar verdicts on inputs that cannot + straddle the bar. Otherwise A is as fast or faster (random, cold) and exact. +- **The load-bearing fixture** must be re-derived under whichever law + production adopts. Under A it is not load-bearing. +- **`DEFAULT_FREQUENCY_BAR`'s rationale** must be re-stated without B1's + constant before any change to the replay law. + +``` +STATUS: measured | OUTCOME: B1 buys 2-4.4x hot-replay and 3.6x random-lookup +speed and loses ~38% of verdicts on spread inputs; no table resolution is +decision-stable; A wins cold and beats B8/B16 on random access; no production +code reads any NarsTables revision today; B16 "full precision" corrected; +T2/T4 guards landed | OPEN: G2 (which law future wiring uses), G1, +load-bearing fixture and frequency-bar rationale under the chosen law, +forward's discarded truth +``` + +MIRROR +- BLIND SPOT: two synthetic corpora; no real recorded chain set exists in the + tree to replay. The replay bench uses 64 cyclic weights, which flatters every + table. +- BIAS CHECK: the question was framed against B1, which made "B16 fixes it" + the easy counter-story; the micro-uniform and cold rows are what refute it. +- STILL OPEN: why B1/B2 micro lookups are slower on narrower inputs. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index b65fce9a6..7bb767a15 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -280 entries, 2026-08-06 .. 2026-10-09. +281 entries, 2026-08-06 .. 2026-10-09. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-09 | `D-RPF-TABLE-1` | | [2026-10-09-d-rpf-table-1-fast-path-cost.md](2026-10-09-d-rpf-table-1-fast-path-cost.md) | | 2026-10-09 | `D-RPF-CONF-0` | | [2026-10-09-d-rpf-conf-0-confidence-surface.md](2026-10-09-d-rpf-conf-0-confidence-surface.md) | | 2026-10-08 | `register128-signed-readings` | | [2026-10-08-register128-signed-readings.md](2026-10-08-register128-signed-readings.md) | | 2026-10-08 | `D-ART-2` | | [2026-10-08-recipe-probe-phaselut-foldcost.md](2026-10-08-recipe-probe-phaselut-foldcost.md) | From 8dccf428ed99038e0322d53578e5ba66122f9253 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:34:12 +0000 Subject: [PATCH 8/9] T4: gate on causal-edge-v2-layout The masks pin the v2 bit layout; under --no-default-features v1 places plasticity at 49..51 and temporal at 52..63, which learn rewrites, so the test failed there. Gated like ce64_isa_contract / ce64_isa_golden / ce64_op_contract. Codex review on #1426. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- crates/causal-edge/tests/t4_learn_plasticity_patterns.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs index f283b81ee..e0ed90c6b 100644 --- a/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs +++ b/crates/causal-edge/tests/t4_learn_plasticity_patterns.rs @@ -6,6 +6,11 @@ //! a HOT plane adopts the observation's when the observation is more //! confident. The fixture keeps the revised confidence below 0.7, so the //! plasticity field itself is not changed by the confidence transitions. +//! +//! The masks below are the v2 bit layout (plasticity 50..52, witness and +//! Epi5 53..63); v1 puts plasticity at 49..51 and temporal at 52..63, which +//! `learn` rewrites. Gated like the other layout-specific contract tests. +#![cfg(feature = "causal-edge-v2-layout")] use causal_edge::edge::InferenceType; use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; From 7b1d93a12d55c8d913cb65c4176ca03b6919523f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:42:52 +0000 Subject: [PATCH 9/9] D-RPF-TABLE-1 entry: split the replay speedup by column Every resolution is 3.7-4.4x faster than A on the dependent replay but only 1.6-2.2x on the batch replay (B16 batch is ~1.6x). The conclusion now gives B1's figures per column. CodeRabbit review on #1426. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2EuNHrariL6B3G3agyASK --- .../entries/2026-10-09-d-rpf-table-1-fast-path-cost.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md b/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md index e1069972e..80cc2c442 100644 --- a/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md +++ b/.claude/board/entries/2026-10-09-d-rpf-table-1-fast-path-cost.md @@ -149,7 +149,8 @@ spread across runs is in the transcript and stays within the noted ranges). Reading: -- **Replay, hot table:** every resolution is 2–4.4× faster than A. The +- **Replay, hot table:** every resolution is 3.7–4.4× faster than A on the + dependent replay and 1.6–2.2× faster on the batch replay. The dependent replay is latency-bound and A's float divisions sit on the chain; the table is one load. B16 is within ~1.2× of B1 here. - **Random access (micro, uniform):** only B1 and B2 beat A (3.5×). B4 ties A; @@ -182,8 +183,8 @@ Reading: | Table approach wounded if A replay ≥ best table / 1.1 | **Not wounded on hot replay** (A is 2–4× slower there). **Wounded off the hot path:** on random access B8/B16 are slower than A, B4 ties, and on a cold cache A beats every table. | | B16 "full precision" falsified if material divergence remains | **Falsified.** 96.8% of cells differ, broad max dF 113, decision flips at every length. Comments corrected to "maximum table resolution". | -So the answer to "what did we buy by throwing confidence away": **2–4× -throughput on a hot, dependent replay, and 3.6× on random lookups — for a law +So the answer to "what did we buy by throwing confidence away": **4.4× +throughput on a hot, dependent replay (2.2× batched), and 3.6× on random lookups — for a law that flips about 4 in 10 verdicts on spread inputs.** Paying for resolution does not rescue it: B16 keeps the hot-replay speed, loses the random-access and cold speed, and still flips decisions.