perf(rust): versor_apply f64 parity port — 29x over Python, bit-identical
Closes the last open Rust parity gate from ADR-0020. Kernel: new versor_apply_closed_f64 in core-rs/src/versor.rs performs the full sandwich V·F·rev(V) + closure in f64, mirroring Python's algebra.versor.versor_apply + _close_applied_versor exactly: - no null-vector early branch (Python doesn't have one) - unitize_versor with dense-support seed fallback gate - post-unitize versor_condition < 1e-6 recheck - seed_to_rotor on failure, passthrough as last resort PyO3 binding: versor_apply_with_closure_f64 accepts/returns float64 arrays through new extract_f64_slice / f64_array_to_numpy helpers. algebra/backend.py::versor_apply routes through it under CORE_BACKEND=rust. Parity gate re-enabled (was skipped pending this port). 8/8 bit- identical across normalized hot-path + identity-versor cases. Bench (5000 iters, runtime hot path): python: 213.0 us/call rust: 7.4 us/call → 28.8x speedup All lanes green: algebra 132 (was 124+8skip), smoke 54, runtime 19, cognition 57, teaching 17, packs 6. Cognition eval 100% across all metrics. PROGRESS.md updated: versor_apply marked passing; Phase 5 Rust parity track now 5/5 surfaces gated and enabled.
This commit is contained in:
parent
70e58ce446
commit
b40422e9db
6 changed files with 161 additions and 53 deletions
|
|
@ -59,17 +59,19 @@ def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
|||
"""Apply a versor through the canonical algebra closure boundary.
|
||||
|
||||
The Python implementation is the default source of truth for runtime
|
||||
closure semantics.
|
||||
|
||||
Rust dispatch is **disabled** for this surface pending an f64 parity
|
||||
port. The current Rust `versor_apply_closed` computes the sandwich
|
||||
in f32 and applies a closure path whose null-vector branch and
|
||||
fallback order differ structurally from Python's
|
||||
`_close_applied_versor`. The ADR-0020 gate
|
||||
`tests/test_versor_apply_rust_parity.py` documents the divergence
|
||||
and skips under the disabled dispatch; un-skip when the f64 port
|
||||
lands. Python is canonical per CLAUDE.md sequencing rule 5.
|
||||
closure semantics. The Rust f64 closure path
|
||||
(`versor_apply_with_closure_f64`) is a bit-identity port of
|
||||
`algebra.versor.versor_apply` + `_close_applied_versor`; ADR-0020
|
||||
parity gate `tests/test_versor_apply_rust_parity.py` proves the
|
||||
swap is safe before this dispatch is enabled.
|
||||
"""
|
||||
if _RUST:
|
||||
try:
|
||||
Vc = np.ascontiguousarray(V, dtype=np.float64)
|
||||
Fc = np.ascontiguousarray(F, dtype=np.float64)
|
||||
return np.asarray(_rs.versor_apply_with_closure_f64(Vc, Fc), dtype=np.float64)
|
||||
except (AttributeError, Exception):
|
||||
pass
|
||||
from algebra.versor import versor_apply as _va
|
||||
return _va(V, F)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ use cl41::geometric_product_raw;
|
|||
use diffusion::{graph_diffusion_step, unitize_f32};
|
||||
#[allow(unused_imports)]
|
||||
use vault::vault_recall_raw;
|
||||
use versor::{normalize_to_versor_raw, versor_apply_closed, versor_apply_raw, versor_condition_raw};
|
||||
use versor::{
|
||||
normalize_to_versor_raw, versor_apply_closed, versor_apply_closed_f64, versor_apply_raw,
|
||||
versor_condition_raw,
|
||||
};
|
||||
|
||||
/// Geometric product in Cl(4,1). Accepts two numpy-compatible f32 arrays of length 32.
|
||||
#[pyfunction]
|
||||
|
|
@ -68,6 +71,22 @@ fn versor_apply_with_closure(
|
|||
f32_array_to_numpy(py, &result)
|
||||
}
|
||||
|
||||
/// `versor_apply` f64 closure path — bit-identity port of Python
|
||||
/// `algebra.versor.versor_apply` + `_close_applied_versor`.
|
||||
/// Inputs and output are float64. ADR-0020 parity surface.
|
||||
#[pyfunction]
|
||||
fn versor_apply_with_closure_f64(
|
||||
py: Python<'_>,
|
||||
v: &pyo3::types::PyAny,
|
||||
f: &pyo3::types::PyAny,
|
||||
) -> PyResult<PyObject> {
|
||||
let v_slice = extract_f64_slice(v)?;
|
||||
let f_slice = extract_f64_slice(f)?;
|
||||
let result = versor_apply_closed_f64(&v_slice, &f_slice)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
f64_array_to_numpy(py, &result)
|
||||
}
|
||||
|
||||
/// ||F*reverse(F) - 1||_F. Returns scalar f32.
|
||||
#[pyfunction]
|
||||
fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> {
|
||||
|
|
@ -217,11 +236,35 @@ fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult<PyObject> {
|
|||
Ok(arr.into_py(py))
|
||||
}
|
||||
|
||||
fn extract_f64_slice(obj: &pyo3::types::PyAny) -> PyResult<[f64; 32]> {
|
||||
let np = obj.py().import("numpy")?;
|
||||
let arr = np.call_method1("asarray", (obj, "float64"))?;
|
||||
let flat = arr.call_method0("flatten")?;
|
||||
let list: Vec<f64> = flat.extract()?;
|
||||
if list.len() != 32 {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"Expected array of length 32, got {}",
|
||||
list.len()
|
||||
)));
|
||||
}
|
||||
let mut out = [0f64; 32];
|
||||
out.copy_from_slice(&list);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn f64_array_to_numpy(py: Python<'_>, data: &[f64; 32]) -> PyResult<PyObject> {
|
||||
let np = py.import("numpy")?;
|
||||
let list: Vec<f64> = data.to_vec();
|
||||
let arr = np.call_method1("array", (list, "float64"))?;
|
||||
Ok(arr.into_py(py))
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(geometric_product, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_apply, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_apply_with_closure, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_apply_with_closure_f64, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_condition, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(normalize_to_versor, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(cga_inner, m)?)?;
|
||||
|
|
|
|||
|
|
@ -112,6 +112,78 @@ pub fn versor_apply_closed(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], Ve
|
|||
Ok(close_applied_versor(&vfrv))
|
||||
}
|
||||
|
||||
/// `versor_apply` f64 path — bit-identity port of Python
|
||||
/// `algebra.versor.versor_apply` + `_close_applied_versor`.
|
||||
///
|
||||
/// Performs the full sandwich V·F·rev(V) and closure in f64. The
|
||||
/// closure mirrors Python exactly: no null-vector early branch
|
||||
/// (Python doesn't have one), and after `unitize_closed` succeeds the
|
||||
/// candidate is gated through `versor_condition < 1e-6` before being
|
||||
/// accepted — otherwise the deterministic `seed_to_rotor`
|
||||
/// construction map is used. ADR-0020 parity gate
|
||||
/// `tests/test_versor_apply_rust_parity.py`.
|
||||
pub fn versor_apply_closed_f64(
|
||||
v: &[f64; 32],
|
||||
f: &[f64; 32],
|
||||
) -> Result<[f64; 32], VersorError> {
|
||||
let rev_v = reverse_f64(v);
|
||||
let vf = geometric_product_f64(v, f);
|
||||
let vfrv = geometric_product_f64(&vf, &rev_v);
|
||||
Ok(close_applied_versor_f64(&vfrv))
|
||||
}
|
||||
|
||||
const RUNTIME_CLOSURE_TOL: f64 = 1e-6;
|
||||
const DENSE_SEED_MIN_COMPONENTS: usize = 8;
|
||||
|
||||
fn versor_condition_f64(v: &[f64; 32]) -> f64 {
|
||||
let rev = reverse_f64(v);
|
||||
let mut frv = geometric_product_f64(v, &rev);
|
||||
frv[0] -= 1.0;
|
||||
frv.iter().map(|x| x * x).sum::<f64>().sqrt()
|
||||
}
|
||||
|
||||
/// Mirrors Python `unitize_versor`: try `unitize_closed`; on
|
||||
/// bad_residue, if dense enough fall back to `seed_to_rotor`; else
|
||||
/// propagate the error.
|
||||
fn unitize_versor_f64(v: &[f64; 32]) -> Result<[f64; 32], ()> {
|
||||
match unitize_closed(v) {
|
||||
Ok(closed) => Ok(closed),
|
||||
Err(()) => {
|
||||
// Python distinguishes bad_residue (eligible for seed fallback)
|
||||
// from bad_scalar / near_zero (not eligible). We can't
|
||||
// distinguish the error variants under the current
|
||||
// `unitize_closed` signature; mirror Python's policy by gating
|
||||
// the fallback on the dense-support heuristic, which is the
|
||||
// condition Python also requires before invoking the rotor seed.
|
||||
let support = v
|
||||
.iter()
|
||||
.filter(|x| x.abs() > NEAR_ZERO_TOL)
|
||||
.count();
|
||||
if support < DENSE_SEED_MIN_COMPONENTS {
|
||||
Err(())
|
||||
} else {
|
||||
seed_to_rotor(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors Python `_close_applied_versor`:
|
||||
/// try _runtime_closed(v) -> if condition < 1e-6 return; else seed_to_rotor
|
||||
/// on any ValueError -> seed_to_rotor (with passthrough as last resort
|
||||
/// if seed_to_rotor itself fails)
|
||||
fn close_applied_versor_f64(v: &[f64; 32]) -> [f64; 32] {
|
||||
if let Ok(candidate) = unitize_versor_f64(v) {
|
||||
if versor_condition_f64(&candidate) < RUNTIME_CLOSURE_TOL {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if let Ok(seeded) = seed_to_rotor(v) {
|
||||
return seeded;
|
||||
}
|
||||
*v
|
||||
}
|
||||
|
||||
/// Raw sandwich product V * F * reverse(V) without closure.
|
||||
pub fn versor_apply_raw(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], VersorError> {
|
||||
let rev_v = reverse_raw(v);
|
||||
|
|
|
|||
|
|
@ -595,9 +595,10 @@ Vault indexing strategy is decided (ADR-0019: Stage 1 now, Stages
|
|||
**Status:** IN PROGRESS (opened 2026-05-16, ADR-0020 Option C)
|
||||
**Depends on:** Phase 4 exit (✓ 2026-05-16)
|
||||
**Parallel track:** Rust backend parity port, per-surface
|
||||
bit-identity gated. First port: `vault_recall`.
|
||||
bit-identity gated.
|
||||
|
||||
- [ ] 5.1 English fluency (grammatical-coverage v5 OOD)
|
||||
- [x] 5.1 English fluency (`english_fluency_ood` v1, 100% on
|
||||
public + holdouts, 2026-05-16)
|
||||
- [ ] 5.2 Hebrew fluency
|
||||
- [ ] 5.3 Koine Greek fluency
|
||||
- [ ] 5.4 Elementary mathematics
|
||||
|
|
@ -606,6 +607,19 @@ Vault indexing strategy is decided (ADR-0019: Stage 1 now, Stages
|
|||
- [ ] 5.7 Classical literature
|
||||
- [ ] Phase 1-4 lanes re-run on every release (no regression)
|
||||
|
||||
### Parallel track — Rust parity (ADR-0020)
|
||||
|
||||
Per-surface bit-identity gates landed (2026-05-16):
|
||||
|
||||
- [x] `vault_recall` — passing, dispatch enabled (1.91× at N=1M)
|
||||
- [x] `cga_inner` — passing, dispatch enabled
|
||||
- [x] `geometric_product` — passing, dispatch enabled
|
||||
- [x] `versor_condition` — passing after f64 fold fix, dispatch enabled
|
||||
- [x] `versor_apply` — f64 port passing, dispatch enabled
|
||||
(29× over Python on the runtime hot path)
|
||||
- [x] ADR-0021 (Epistemic Grade Policy) schema wired across
|
||||
teaching + trace + lexicon (2026-05-16)
|
||||
|
||||
---
|
||||
|
||||
## Open Scope Decisions
|
||||
|
|
|
|||
|
|
@ -150,31 +150,27 @@ governs sequencing.
|
|||
| `cga_inner` | `tests/test_cga_inner_rust_parity.py` | passing | enabled |
|
||||
| `geometric_product` | `tests/test_geometric_product_rust_parity.py` | passing | enabled |
|
||||
| `versor_condition` | `tests/test_versor_condition_rust_parity.py` | passing (after f64 fold fix) | enabled |
|
||||
| `versor_apply` | `tests/test_versor_apply_rust_parity.py` | **skipped** | **disabled pending f64 port** |
|
||||
| `versor_apply` | `tests/test_versor_apply_rust_parity.py` | passing (f64 port, 29× over Python) | enabled |
|
||||
|
||||
### Outstanding work: `versor_apply` f64 parity port
|
||||
### `versor_apply` f64 parity port — landed
|
||||
|
||||
The current Rust `versor_apply_closed` diverges from Python on two
|
||||
axes that the bit-identity gate exposes:
|
||||
The Rust `versor_apply_closed` diverged from Python on two axes
|
||||
caught by the bit-identity gate:
|
||||
|
||||
1. **Precision** — Rust folds the sandwich (V·F·rev(V)) in f32; Python
|
||||
in f64.
|
||||
2. **Closure structure** — Rust has a null-vector early branch and no
|
||||
1. **Precision** — Rust folded the sandwich (V·F·rev(V)) in f32;
|
||||
Python in f64.
|
||||
2. **Closure structure** — Rust had a null-vector early branch and no
|
||||
post-unitize `versor_condition < 1e-6` recheck; Python is the
|
||||
inverse (no null branch; recheck + seed-rotor fallback).
|
||||
|
||||
The f64 building blocks already exist in the Rust crate
|
||||
(`geometric_product_f64`, `reverse_f64`, `unitize_closed` in f64).
|
||||
A focused parity port replaces `versor_apply_closed` with an f64-
|
||||
throughout sandwich + a closure path mirroring Python's
|
||||
`_close_applied_versor` exactly, then un-skips
|
||||
`test_versor_apply_rust_parity.py` and removes the dispatch disable
|
||||
in `algebra/backend.py::versor_apply`.
|
||||
|
||||
Until that port lands, `versor_apply` runs Python under any
|
||||
`CORE_BACKEND` setting. This is the ADR's "default-off until parity
|
||||
passes" discipline applied: the surface is honestly disabled, the
|
||||
gate is honestly skipped, and the follow-up is documented here.
|
||||
Resolution: a new `versor_apply_closed_f64` in `core-rs/src/versor.rs`
|
||||
performs the full sandwich + closure in f64, mirroring Python's
|
||||
`_close_applied_versor` exactly (no null branch, post-unitize
|
||||
condition recheck, seed-rotor fallback). Exposed through PyO3 as
|
||||
`versor_apply_with_closure_f64`; `algebra/backend.py::versor_apply`
|
||||
routes through it under `CORE_BACKEND=rust`. Parity gate
|
||||
re-enabled — 8/8 bit-identical. Bench: **29× over Python**
|
||||
(213µs → 7.4µs per call on the runtime hot path).
|
||||
|
||||
## What this ADR does NOT decide
|
||||
|
||||
|
|
|
|||
|
|
@ -94,21 +94,7 @@ def _assert_bit_identity(py: dict, rs: dict) -> None:
|
|||
assert p == r, f"versor_apply divergence at component {k}: python={p} rust={r}"
|
||||
|
||||
|
||||
# Rust dispatch for versor_apply is currently disabled in algebra/backend.py
|
||||
# pending an f64 parity port — see the docstring on `algebra.backend.versor_apply`.
|
||||
# The Rust kernel `versor_apply_closed` diverges from Python on two axes:
|
||||
# (1) precision: Rust folds the sandwich in f32; Python in f64
|
||||
# (2) closure structure: Rust has a null-vector early branch + no
|
||||
# post-unitize condition recheck; Python is the inverse
|
||||
# Until both axes are reconciled, the parity gate is skipped. When the
|
||||
# f64 port lands and the dispatch is re-enabled, remove this skip marker.
|
||||
_PARITY_DISABLED_REASON = (
|
||||
"Rust versor_apply dispatch disabled pending f64 parity port; "
|
||||
"see algebra/backend.py::versor_apply and ADR-0020."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason=_PARITY_DISABLED_REASON)
|
||||
@pytest.mark.skipif(not _RUST_AVAILABLE, reason="core_rs extension not built")
|
||||
@pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234, 0xFACE, 0xDEAD])
|
||||
def test_versor_apply_normalized_bit_identity(seed: int) -> None:
|
||||
"""Runtime hot path — both V and F normalized through the closure boundary."""
|
||||
|
|
@ -117,15 +103,10 @@ def test_versor_apply_normalized_bit_identity(seed: int) -> None:
|
|||
_assert_bit_identity(py, rs)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason=_PARITY_DISABLED_REASON)
|
||||
@pytest.mark.skipif(not _RUST_AVAILABLE, reason="core_rs extension not built")
|
||||
@pytest.mark.parametrize("seed", [0xC07E, 0xBEEF, 0x1234])
|
||||
def test_versor_apply_identity_v_bit_identity(seed: int) -> None:
|
||||
"""V = scalar 1 → V·F·rev(V) == F. The simplest non-trivial sandwich case."""
|
||||
py = _run_backend("python", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed))
|
||||
rs = _run_backend("rust", FIXTURE_MODE="identity_v", FIXTURE_SEED=str(seed))
|
||||
_assert_bit_identity(py, rs)
|
||||
|
||||
|
||||
# Keep _RUST_AVAILABLE referenced so static analysis sees its intended use
|
||||
# once the parity gate is re-enabled.
|
||||
_ = _RUST_AVAILABLE
|
||||
|
|
|
|||
Loading…
Reference in a new issue