init: Rust build config, Python dispatch layer, Rust tests

This commit is contained in:
Shay 2026-05-12 19:20:42 -07:00
parent 0063259584
commit f882408e62
10 changed files with 526 additions and 0 deletions

75
algebra/backend.py Normal file
View file

@ -0,0 +1,75 @@
"""
Backend dispatch: use Rust extension (core_rs) when available,
fall back to pure Python (algebra/cl41.py etc.) transparently.
This module is the single switch. All algebra modules import from here
for performance-critical ops. Pure Python is always the fallback
the system is never broken by a missing Rust build.
Usage:
from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall
"""
import numpy as np
try:
import core_rs as _rs
_RUST = True
except ImportError:
_RUST = False
def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
if _RUST:
return np.asarray(_rs.geometric_product(A, B), dtype=np.float32)
from algebra.cl41 import geometric_product as _gp
return _gp(A, B)
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
if _RUST:
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
from algebra.versor import versor_apply as _va
return _va(V, F)
def versor_condition(F: np.ndarray) -> float:
if _RUST:
return float(_rs.versor_condition(F))
from algebra.versor import versor_condition as _vc
return _vc(F)
def normalize_to_versor(F: np.ndarray) -> np.ndarray:
if _RUST:
return np.asarray(_rs.normalize_to_versor(F), dtype=np.float32)
from algebra.versor import normalize_to_versor as _nv
return _nv(F)
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
if _RUST:
return float(_rs.cga_inner(X, Y))
from algebra.cga import cga_inner as _ci
return _ci(X, Y)
def vault_recall(versors: list, query: np.ndarray, top_k: int = 5) -> list:
"""
Top-k CGA inner product recall.
Rust path: parallel Rayon scan (releases GIL, true multithreaded).
Python path: sequential list comprehension.
"""
if _RUST:
results = _rs.vault_recall(versors, query, top_k)
# results: list of (index, score)
return results
from algebra.cga import cga_inner as _ci
scores = [(i, _ci(query, v)) for i, v in enumerate(versors)]
scores.sort(key=lambda x: -x[1])
return scores[:top_k]
def using_rust() -> bool:
"""Returns True if the Rust extension is loaded."""
return _RUST

7
core-rs/build.rs Normal file
View file

@ -0,0 +1,7 @@
// build.rs: nothing special needed for PyO3 — maturin handles the rest.
// This file exists as an extension point for future build-time codegen
// (e.g. generating the full Cl(4,1) multiplication table as a static array
// rather than computing it at OnceLock init time).
fn main() {
println!("cargo:rerun-if-changed=src/");
}

8
core-rs/pyproject.toml Normal file
View file

@ -0,0 +1,8 @@
[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
[tool.maturin]
features = ["extension-module"]
module-name = "core_rs"
python-source = "python"

81
core-rs/src/holonomy.rs Normal file
View file

@ -0,0 +1,81 @@
//! Holonomy encoder in Rust — the forward+reverse versor walk.
//!
//! This is in Rust because:
//! - Long prompts (100+ tokens) do 200+ geometric products in sequence
//! - Each geometric product is O(32^2) = 1024 multiply-adds
//! - Python overhead per call makes this 10-50x slower than necessary
//! - Rust collapses the entire walk into a single allocation-free loop
use crate::cl41::{geometric_product_raw, reverse_raw};
use crate::versor::normalize_to_versor_raw;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum HolonomyError {
#[error("Empty word list")]
Empty,
#[error("Versor error: {0}")]
Versor(String),
}
/// Compute holonomy of a word versor sequence.
///
/// Forward walk: F = w0 * w1 * ... * wn
/// Reverse walk: R = (1-alpha) * rev(wn) * ... * rev(w0)
/// Holonomy: H = normalize(F * R)
///
/// weights: per-word scalars (inverse frequency). If empty, uniform 1.0.
/// alpha: blend factor [0,1]. 0.5 recommended.
pub fn holonomy_encode_raw(
words: &[[f32; 32]],
weights: &[f32],
alpha: f32,
) -> Result<[f32; 32], HolonomyError> {
if words.is_empty() {
return Err(HolonomyError::Empty);
}
let n = words.len();
let use_weights = !weights.is_empty() && weights.len() == n;
// Forward accumulation
let mut scaled = words[0];
if use_weights {
let w = weights[0];
for x in scaled.iter_mut() { *x *= w; }
}
let mut f = normalize_to_versor_raw(&scaled)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
for k in 1..n {
let mut wk = words[k];
if use_weights {
let w = weights[k];
for x in wk.iter_mut() { *x *= w; }
}
let wk_norm = normalize_to_versor_raw(&wk)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
f = geometric_product_raw(&f, &wk_norm)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
}
// Reverse accumulation with (1-alpha) damping
let damp = 1.0 - alpha;
let mut last_rev = reverse_raw(&words[n - 1]);
for x in last_rev.iter_mut() { *x *= damp; }
let mut r = normalize_to_versor_raw(&last_rev)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
for k in (0..n - 1).rev() {
let rev_wk = reverse_raw(&words[k]);
let rev_norm = normalize_to_versor_raw(&rev_wk)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
r = geometric_product_raw(&rev_norm, &r)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
}
let h = geometric_product_raw(&f, &r)
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
normalize_to_versor_raw(&h)
.map_err(|e| HolonomyError::Versor(e.to_string()))
}

48
core-rs/src/propagate.rs Normal file
View file

@ -0,0 +1,48 @@
//! Propagation loop in Rust — tight versor_apply chain.
//!
//! propagate_n steps runs N versor_apply calls in a single Rust stack frame,
//! eliminating Python dispatch overhead for each step.
//! Used by generate/stream.py when stepping more than one token at a time
//! (e.g. prefill, speculative steps, or batch generation).
use crate::versor::versor_apply_raw;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PropagateError {
#[error("Versor error during propagation: {0}")]
Versor(String),
}
/// Run n versor_apply steps in sequence.
/// rotors: slice of n [f32;32] versors to apply in order
/// f0: initial field state
/// Returns final field state after n steps.
pub fn propagate_n_raw(
rotors: &[[f32; 32]],
f0: &[f32; 32],
) -> Result<[f32; 32], PropagateError> {
let mut f = *f0;
for v in rotors {
f = versor_apply_raw(v, &f)
.map_err(|e| PropagateError::Versor(e.to_string()))?;
}
Ok(f)
}
/// Parallel batch propagation: apply the same rotor V to a batch of field states.
/// Used for beam search or multi-hypothesis generation.
/// Returns new batch of field states.
pub fn propagate_batch_raw(
v: &[f32; 32],
fields: &[[f32; 32]],
) -> Result<Vec<[f32; 32]>, PropagateError> {
use rayon::prelude::*;
fields
.par_iter()
.map(|f| {
versor_apply_raw(v, f)
.map_err(|e| PropagateError::Versor(e.to_string()))
})
.collect()
}

48
core-rs/tests/test_cga.rs Normal file
View file

@ -0,0 +1,48 @@
#[cfg(test)]
mod tests {
use crate::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
#[test]
fn test_embedded_point_is_null() {
let p = [1.0f32, 2.0, 3.0];
let x = embed_point_raw(&p);
assert!(is_null_raw(&x, 1e-5).unwrap(), "Embedded point should be null");
}
#[test]
fn test_origin_is_null() {
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
assert!(is_null_raw(&x, 1e-5).unwrap());
}
#[test]
fn test_cga_inner_symmetry() {
let x = embed_point_raw(&[1.0, 0.0, 0.0]);
let y = embed_point_raw(&[0.0, 1.0, 0.0]);
let xy = cga_inner_raw(&x, &y).unwrap();
let yx = cga_inner_raw(&y, &x).unwrap();
assert!((xy - yx).abs() < 1e-6, "cga_inner not symmetric");
}
#[test]
fn test_cga_distance_identity() {
// Points at distance 1: cga_inner = -d^2/2 = -0.5
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
let y = embed_point_raw(&[1.0, 0.0, 0.0]);
let inner = cga_inner_raw(&x, &y).unwrap();
assert!((inner - (-0.5)).abs() < 1e-5,
"Expected -0.5 for unit-distance points, got {}", inner);
}
#[test]
fn test_null_project_restores_null() {
let p = [1.0f32, 2.0, 3.0];
let mut x = embed_point_raw(&p);
// Introduce drift
x[0] += 0.05;
x[7] -= 0.03;
let fixed = null_project_raw(&x);
assert!(is_null_raw(&fixed, 1e-5).unwrap(),
"null_project failed to restore null cone");
}
}

View file

@ -0,0 +1,69 @@
#[cfg(test)]
mod tests {
use crate::cl41::{geometric_product_raw, reverse_raw, BLADE_MASKS, MASK_TO_IDX};
fn basis(i: usize) -> [f32; 32] {
let mut v = [0f32; 32];
v[1 + i] = 1.0; // grade-1 component
v
}
fn scalar(s: f32) -> [f32; 32] {
let mut v = [0f32; 32];
v[0] = s;
v
}
#[test]
fn test_e1_squared_is_plus1() {
let e1 = basis(0);
let r = geometric_product_raw(&e1, &e1).unwrap();
// e1^2 = +1 (signature index 0 = +1)
assert!((r[0] - 1.0).abs() < 1e-6, "e1^2 should be +1, got {}", r[0]);
}
#[test]
fn test_e5_squared_is_minus1() {
let e5 = basis(4);
let r = geometric_product_raw(&e5, &e5).unwrap();
// e5^2 = -1 (signature index 4 = -1)
assert!((r[0] + 1.0).abs() < 1e-6, "e5^2 should be -1, got {}", r[0]);
}
#[test]
fn test_e1_e2_anticommute() {
let e1 = basis(0);
let e2 = basis(1);
let e1e2 = geometric_product_raw(&e1, &e2).unwrap();
let e2e1 = geometric_product_raw(&e2, &e1).unwrap();
// e1*e2 = -e2*e1
for i in 0..32 {
assert!((e1e2[i] + e2e1[i]).abs() < 1e-6,
"e1*e2 + e2*e1 != 0 at index {}", i);
}
}
#[test]
fn test_scalar_identity() {
let e1 = basis(0);
let one = scalar(1.0);
let r = geometric_product_raw(&one, &e1).unwrap();
assert!((r[1] - 1.0).abs() < 1e-6, "1*e1 should be e1");
}
#[test]
fn test_reverse_grade2_sign() {
// A grade-2 blade should flip sign under reverse
let mut a = [0f32; 32];
a[6] = 1.0; // first grade-2 component
let r = reverse_raw(&a);
assert!((r[6] + 1.0).abs() < 1e-6, "reverse of grade-2 blade should negate");
}
#[test]
fn test_reverse_grade1_unchanged() {
let e1 = basis(0);
let r = reverse_raw(&e1);
assert!((r[1] - 1.0).abs() < 1e-6, "reverse of grade-1 blade should be unchanged");
}
}

View file

@ -0,0 +1,50 @@
#[cfg(test)]
mod tests {
use crate::vault::vault_recall_raw;
use crate::versor::normalize_to_versor_raw;
fn random_versor(seed: u64) -> [f32; 32] {
let mut state = seed ^ 0xdeadbeef_cafebabe;
let mut raw = [0f32; 32];
for x in raw.iter_mut() {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
}
normalize_to_versor_raw(&raw).expect("normalize failed")
}
#[test]
fn test_recall_self() {
let versors: Vec<[f32; 32]> = (0..20).map(|i| random_versor(i as u64)).collect();
for (i, query) in versors.iter().enumerate() {
let results = vault_recall_raw(&versors, query, 1).unwrap();
assert_eq!(results[0].0, i,
"Versor {} should recall itself as top-1, got {}", i, results[0].0);
}
}
#[test]
fn test_empty_vault() {
let query = random_versor(0);
let results = vault_recall_raw(&[], &query, 5).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_top_k_count() {
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
let query = random_versor(99);
let results = vault_recall_raw(&versors, &query, 3).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_scores_descending() {
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
let query = random_versor(99);
let results = vault_recall_raw(&versors, &query, 5).unwrap();
for w in results.windows(2) {
assert!(w[0].1 >= w[1].1, "Scores not descending");
}
}
}

View file

@ -0,0 +1,59 @@
#[cfg(test)]
mod tests {
use crate::versor::{versor_apply_raw, normalize_to_versor_raw, versor_condition_raw};
fn random_versor(seed: u64) -> [f32; 32] {
// Simple LCG for deterministic test data — no external deps
let mut state = seed ^ 0xdeadbeef_cafebabe;
let mut raw = [0f32; 32];
for x in raw.iter_mut() {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
}
normalize_to_versor_raw(&raw).expect("normalize failed")
}
#[test]
fn test_normalize_produces_versor() {
for seed in 0..20u64 {
let v = random_versor(seed);
let cond = versor_condition_raw(&v).unwrap();
assert!(cond < 1e-5, "versor_condition after normalize = {}", cond);
}
}
#[test]
fn test_versor_apply_preserves_manifold() {
for seed in 0..20u64 {
let v = random_versor(seed);
let f = random_versor(seed + 1000);
let result = versor_apply_raw(&v, &f).unwrap();
let cond = versor_condition_raw(&result).unwrap();
assert!(cond < 1e-4,
"versor_apply broke manifold: condition={:.2e} at seed={}", cond, seed);
}
}
#[test]
fn test_identity_versor() {
let mut identity = [0f32; 32];
identity[0] = 1.0;
let f = random_versor(42);
let result = versor_apply_raw(&identity, &f).unwrap();
for i in 0..32 {
assert!((result[i] - f[i]).abs() < 1e-5,
"Identity apply changed component {}: {} vs {}", i, result[i], f[i]);
}
}
#[test]
fn test_composition_closed() {
let v1 = random_versor(0);
let v2 = random_versor(1);
let f = random_versor(2);
let f2 = versor_apply_raw(&v1, &f).unwrap();
let f3 = versor_apply_raw(&v2, &f2).unwrap();
let cond = versor_condition_raw(&f3).unwrap();
assert!(cond < 1e-4, "Composition broke manifold: condition={:.2e}", cond);
}
}

81
docs/RUST.md Normal file
View file

@ -0,0 +1,81 @@
# Rust Extension (core-rs)
## Why Rust
Three operations dominate CORE-AI's runtime:
1. geometric_product — O(32^2) = 1024 multiply-adds per call, called 2-3x per versor_apply
2. vault_recall scan — O(N) cga_inner calls, N = all stored versors, called once per token
3. holonomy_encode — 2 * prompt_length geometric products in sequence
None of these release the Python GIL. Rayon gives vault_recall true multithreaded
parallelism across CPU cores. The geometric product loop is cache-friendly and
compiler-autovectorized when opt-level=3 + lto=true.
## What is in Rust
| Module | Rust file | Why |
|---|---|---|
| Cl(4,1) product | cl41.rs | Hot inner loop, 1024 MADs, autovectorizable |
| Versor ops | versor.rs | 3x geometric_product per step, allocation-free |
| CGA inner product | cga.rs | Called every token decode and every vault recall |
| Vault top-k scan | vault.rs | Rayon parallel scan — GIL blocks Python threads |
| Holonomy encode | holonomy.rs | 200+ products for long prompts |
| Batch propagation | propagate.rs | Beam search / speculative decode |
## What stays in Python
| Layer | Why |
|---|---|
| VocabManifold | Word lookup, edge rotor construction — called once per token, not per step |
| SessionContext | Orchestration, not arithmetic |
| FieldState | Plain dataclass |
| PersonaMotor | Motor construction is infrequent |
## Zero-Copy Semantics
All f32 arrays are passed as numpy arrays from Python.
The Rust functions receive them as `[f32; 32]` stack arrays — copied once from
the numpy buffer into a stack frame, processed, and returned as a new numpy array.
No heap allocation inside any hot-path function.
For vault_recall, the versors list is iterated via Rayon par_iter with no cloning:
each worker holds a read-only reference to its slice element.
## Build
Requires maturin and a Rust toolchain (stable 1.75+).
```bash
cd core-rs
pip install maturin
maturin develop --release # installs core_rs into current venv
```
Or build a wheel:
```bash
maturin build --release
pip install target/wheels/*.whl
```
Verify Rust backend is active from Python:
```python
from algebra.backend import using_rust
print(using_rust()) # True if core_rs is installed
```
## Running Rust Tests
```bash
cd core-rs
cargo test
```
## Type Safety Contract
All multivectors entering the Rust layer are validated as f32 arrays of length 32
by extract_f32_slice() in lib.rs. Type errors surface immediately as Python
ValueError with a descriptive message rather than silent memory corruption.
All error types use thiserror — every failure path is a named enum variant,
not a string panic.