Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions examples/randomized_signature_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,34 @@ use std::time::Instant;
struct SplitMix64(u64);

impl SplitMix64 {
/// One raw 64-bit draw, advancing the state by the SplitMix64 gamma
/// constant. Deterministic given the seed, so every run of this bench
/// measures the same projections and the same path.
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// A draw in `[0, 1)`, taking the top 53 bits so every value is exactly
/// representable in `f64`.
fn uniform(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
/// A standard-normal draw via Box-Muller (cosine branch), clamping `u1`
/// away from zero so `ln` never returns `-inf`.
fn normal(&mut self) -> f64 {
let u1 = self.uniform().max(1e-300);
let u2 = self.uniform();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
}

/// Build the `(matrices, biases)` pair for a `d`-dimensional path and a `k`
/// -wide state: `d` stacked `k×k` Gaussian blocks plus `d` bias vectors of
/// length `k`, all scaled by `1/sqrt(k)` — the builder's own recipe, so the
/// benchmarked operand magnitudes match the consumer's.
fn projections(d: usize, k: usize, seed: u64) -> (Vec<f64>, Vec<f64>) {
let scale = (k as f64).recip().sqrt();
let mut rng = SplitMix64(seed);
Expand All @@ -44,6 +55,12 @@ fn projections(d: usize, k: usize, seed: u64) -> (Vec<f64>, Vec<f64>) {
(matrices, biases)
}

/// A `t`-step, `d`-dimensional benchmark path: a linear ramp per coordinate
/// plus a small out-of-phase cosine wobble.
///
/// The wobble matters for timing, not just realism — every increment stays
/// clear of the `1e-15` skip threshold, so no step is short-circuited and
/// both implementations do the full `O(T·d·k²)` of work being compared.
fn path(t: usize, d: usize, seed: f64) -> Vec<Vec<f64>> {
(0..=t)
.map(|i| {
Expand Down Expand Up @@ -94,13 +111,26 @@ fn scalar_encode(path: &[Vec<f64>], matrices: &[f64], biases: &[f64], k: usize)
z
}

/// Max relative error between two states, with the denominator floored at
/// `1.0` so near-zero rows report absolute rather than exploding error.
///
/// Reported alongside every timing row: a speedup is only meaningful if the
/// two implementations still agree, and this is what makes a "fast but wrong"
/// kernel visible in the bench output instead of silently applauded.
fn rel_err(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).abs() / x.abs().max(1.0))
.fold(0.0f64, f64::max)
}

/// Time one `(T, d, k)` shape both ways and print the row: scalar seconds,
/// SIMD seconds, speedup, and max relative error.
///
/// Single-shot timing, not a statistical harness — the shapes here run long
/// enough (milliseconds and up) that run-to-run noise stays well below the
/// effect being reported. Both implementations see the identical projections
/// and path, so the comparison is like-for-like.
fn bench_one(t: usize, d: usize, k: usize) {
let (matrices, biases) = projections(d, k, 0xBEEF);
let p = path(t, d, 0.3);
Expand All @@ -121,6 +151,14 @@ fn bench_one(t: usize, d: usize, k: usize) {
);
}

/// Sweep the state widths `k = 32 … 512` against the scalar baseline, then
/// run sigker's headline envelope shape (`T = 64, d = 8, k = 4096`)
/// SIMD-only and report its achieved GFLOP/s.
///
/// The envelope row omits a scalar baseline deliberately: at `k = 4096` the
/// row-major loop runs for minutes, and the row is there to show the shape is
/// reachable at all, not to claim a speedup. Its `|z|_inf` is printed so the
/// result is visibly finite rather than a NaN that timed well.
fn main() {
println!("== randomized_signature_sweep — SIMD GEMV+axpy vs. row-major scalar ==\n");

Expand Down
54 changes: 54 additions & 0 deletions src/hpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ mod e2e_tests {
use super::node::{Node, _P_, __O, SPO, S__};
use super::seal::Seal;

/// Fingerprint -> Node -> Seal: two independently seeded nodes are
/// *comparable* (non-zero overlap) and *distinct* (non-zero disagreement),
/// and a plane's Merkle root detects mutation.
///
/// The seal half is the discriminating one. `verify` returns `Wisdom` against
/// the root it was built from, then `Staunen` after a single further
/// `encounter` -- so a `merkle()` that ignored later writes, or a `verify`
/// that always agreed, fails here. The plane is rebuilt from scratch rather
/// than reusing the nodes above, to keep the root deterministic.
#[test]
fn pipeline_fingerprint_to_node_to_seal() {
// 1. Create two nodes and accumulate evidence
Expand Down Expand Up @@ -357,6 +366,13 @@ mod e2e_tests {
assert_eq!(p.verify(&root), Seal::Staunen);
}

/// Cascade search over a packed database of 50 random 256-byte fingerprints
/// must find the query itself at Hamming distance 0.
///
/// The query *is* `database[0]`, so an exact self-hit is the falsifiable
/// minimum: a cascade whose band thresholds rejected the foveal band, or
/// whose stride walked the packed buffer at the wrong vector width, would
/// return results that never contain `index == 0`.
#[test]
fn pipeline_cascade_search() {
let vec_bytes = 256;
Expand Down Expand Up @@ -384,6 +400,15 @@ mod e2e_tests {
assert!(results.iter().any(|r| r.index == 0 && r.hamming == 0));
}

/// CLAM k-NN over one-hot vectors: brute force returns exactly `k` hits with
/// the self-match ranked first at distance 0, and `ClamTree::build` covers
/// the whole input.
///
/// The data is deliberately one-hot (vector `i` sets byte `i % vec_len`), so
/// distances are uniform apart from the self-match and the ranking has a
/// single unambiguous winner. `root().cardinality == n` is the coverage
/// check -- a tree that dropped points during partitioning still builds
/// non-empty, so `!nodes.is_empty()` alone would not catch it.
#[test]
fn pipeline_clam_knn() {
let vec_len = 32;
Expand All @@ -409,6 +434,13 @@ mod e2e_tests {
assert_eq!(tree.root().cardinality, n);
}

/// Causality decomposition reads the *sign* of each qualia channel:
/// positive resonance decomposes to `Forward`, negative to `Backward`.
///
/// Three channels are set with mixed signs (warmth +, social -, sacredness +)
/// against a zero baseline, so the test discriminates in a way a single
/// channel could not: a decomposition that ignored sign, or applied one
/// direction uniformly across channels, matches on at most two of the three.
#[test]
fn pipeline_causality_decomposition() {
let mut a = PackedQualia::zero();
Expand All @@ -423,6 +455,12 @@ mod e2e_tests {
assert_eq!(dec.sacredness_dir, CausalityDirection::Forward);
}

/// Binary neural dot over 16384-bit fingerprints, pinned at both poles:
/// all-ones against all-ones is a full 16384 matches scoring `+1.0`, and
/// all-zeros against all-ones is 0 matches scoring `-1.0`.
///
/// Testing both ends fixes the whole affine mapping from match count to
/// score. Either pole alone would admit a wrong scale or offset.
#[test]
fn pipeline_bnn_inference() {
let act = Fingerprint::<256>::ones();
Expand All @@ -437,6 +475,13 @@ mod e2e_tests {
assert!((result2.score - (-1.0)).abs() < 1e-6);
}

/// Blackboard typed-slot arena: allocate two differently typed slots, mutate
/// each through `get_mut`, and read the mutations back through `get`.
///
/// Both a present and an absent key are asserted against `contains`, so the
/// membership check cannot pass by unconditionally returning `true` -- and
/// the read-back is what proves `get_mut` hands out a reference into the
/// arena rather than a copy that is dropped.
#[test]
fn pipeline_blackboard_arena() {
let mut bb = Blackboard::new();
Expand All @@ -458,6 +503,15 @@ mod e2e_tests {
assert!(!bb.contains("nonexistent"));
}

/// The whole chain in one pass: `Node` -> truth -> per-plane distance ->
/// Merkle seal -> cascade band -> BNN inference.
///
/// Where the tests above each pin one stage, this one pins that the stages
/// *compose* on the same pair of nodes -- every plane mask (`S__`, `_P_`,
/// `__O`, `SPO`) yields `Measured` rather than `Incomparable`, and the
/// fingerprints that come out the far end still score strictly inside
/// `(-1, 1)`, which a stage that silently zeroed or saturated its output
/// would violate.
#[test]
fn pipeline_full_e2e() {
// Full pipeline: Node → truth → causality → cascade → BNN
Expand Down
Loading
Loading