Phase 4 lane #2 (long_context_cost) measured vault.recall latency as a function of vault size N. The pre-vectorisation curve was median 875 ms at N=1k, ~9 s at N=10k — unfit for runtime use. ADR-0019 Stage 1 replaces the per-element Python dispatch loop in algebra/backend.py::vault_recall with a vectorised exact scan over the diagonal Cl(4,1) CGA inner-product metric. Per-versor serial component reduction order is preserved, so scores are bit-identical to the scalar cga_inner path. CLAUDE.md exactness is preserved; no approximate recall is introduced. Post-vectorisation: 0.217 ms at N=1k, 20.795 ms at N=100k. Slope 0.99 (linear). ~4,000-5,000x speedup at every probed N. Smoke, algebra, and runtime suites all green. Stages 2 (norm-bucketed exact pre-filter) and 3 (layered store with deterministic promotion) are documented in ADR-0019 but deferred — Stage 1 has dissolved the bottleneck at the scales relevant to current curriculum work.
5.7 KiB
ADR-0019 — Exact Vault Recall Acceleration
Status: Accepted
Date: 2026-05-16
Authors: Joshua Shay
Depends on: ADR-0016 (Capability Roadmap),
evals/long_context_cost/v1 evidence.
Context
CLAUDE.md establishes that vault recall is exact and deterministic:
Do not add cosine similarity, HNSW, ANN indexes, or approximate recall to the runtime path. Vault recall is exact and deterministic.
The Phase 4 long_context_cost lane (v1) measured per-recall
latency of the current Python vault_recall fallback against
synthetic float32 (32,) versors:
- N = 10³ → median recall ≈ 870 ms
- N = 10⁴ → see
evals/long_context_cost/results/v1_metrics.json - N = 10⁵ → see
evals/long_context_cost/results/v1_metrics.json
Even at N=10³ — well below any realistic vault size — recall is ~1 second per query, which is unfit for a per-turn runtime call. The cost shape is dominated by per-iteration NumPy dispatch, not by the algebra. Exact correctness is preserved; performance is not.
The question is how to accelerate vault recall while remaining exact — and in what order.
Decision
CORE adopts a three-stage, semantics-preserving acceleration plan for vault recall. Each stage is gated on evidence from the previous stage. No stage permits approximate recall.
Stage 1 — Vectorised exact scan (commit immediately)
Replace the per-element Python loop in
algebra/backend.py::vault_recall with a single batched
matrix-vector inner product over the stacked versor matrix
M ∈ ℝ^{N×32}. CGA inner product is bilinear; the metric
factors once and is reused.
- Exactness: required to be bit-identical to the scalar
path, verified by a correctness test that asserts
per-element equality across a fixture vault. Float32 CGA inner
products are deterministic under fixed reduction order, so we
pin reduction order (e.g.
np.einsumwith explicitoptimizeoff, or a singleM @ GqwhereGqis the metric-folded query). - Surface preserved:
vault_recallsignature, return shape, ordering, and top-K semantics unchanged. - No new state: no new index files, no new mutable cache, no background reproject.
Stage 2 — Norm-bucketed exact pre-filter (gated on Stage 1
re-run)
Triggered only if Stage 1 leaves recall super-linear past
N ≈ 10⁵ on real-content vaults. Pre-compute the L2 norm of every
stored versor; bucket by norm range. For threshold τ and query
norm q, by Cauchy–Schwarz only versors with norm ≥ τ / q can
clear the cutoff — buckets below that line are provably below
threshold. Within candidate buckets, the Stage 1 vectorised scan
runs.
- Exactness: no candidate that could beat the threshold is dropped. The bound is tight; no tolerance window.
- Determinism: bucket assignment depends only on stored versor norm; replay is preserved.
- State: norm vector cached alongside the store, updated on
every
vault.store()call; checksum hashes the bytes written per CLAUDE.md.
Stage 3 — Layered store with deterministic promotion (gated on
Stage 2 evidence)
Triggered only if working-set patterns produce a vault where most recalls hit a small recent tail and most stored versors are rarely-queried. Two tiers: fast in-memory, slow exact scan. Promotion is by deterministic rule (e.g. last-N stored, or access-count derived from replay-visible counters), never stochastic.
- Exactness: every query scans both tiers; the layer split changes cost, not result.
- Replay: promotion counters are part of replay state; same inputs ⇒ same layer assignment.
Non-options
The following are explicitly excluded by this ADR and by CLAUDE.md, and any future PR proposing them must first revisit this ADR and CLAUDE.md:
- HNSW / NSW / annoy / FAISS-IVF / any nearest-neighbour approximation
- Cosine fallback or any non-CGA metric on the recall path
- Learned indexes, embeddings, or projections trained on vault contents
- Hot-path drift repair inside
vault.recall(the bottleneck is per-iteration NumPy overhead, not numerical drift)
Blade-signature index (deferred, not rejected)
The long_context_cost contract listed a blade-signature index
as an option. It is deferred because (a) blade dominance under
Cl(4,1) sandwiches requires careful definition to stay exact, and
(b) norm-bucketing is simpler and likely sufficient. If Stage 2
proves insufficient, a future ADR may revisit signature indexing.
Consequences
- The Python fallback path becomes the vectorised path; the scalar Python loop remains only as a correctness reference in tests.
vault.store()gains an O(1) per-call cost: append norm to a pre-allocated buffer. No behavioural change in store API.- The Rust backend port (next axis after Phase 4) inherits the vectorised contract. Stage 2/3 indexes, if they land, port the same data structures.
- Replay determinism is preserved at every stage by construction.
Tests in
tests/test_trace_hash.pyand the eval replay suite must continue to pass bit-for-bit after each stage.
Evidence
evals/long_context_cost/contract.md— what we measured.evals/long_context_cost/results/v1_metrics.json— the curve.evals/long_context_cost/gaps.md— diagnosis and recommendation tree.algebra/backend.py::vault_recall— the function being accelerated.
Open questions
- Does
cga_innerfactor cleanly into a static metric matrix in the chosen embedding? If yes, Stage 1 isM @ Gqwith one precomputed Gq per query. If not, Stage 1 isnp.einsumover the multivector basis. Either way: exact, deterministic, vectorised. - What is the real-content vault size in the curriculum era? If it caps at ~10⁵, Stages 2/3 may never trigger.