core/docs/decisions/ADR-0019-exact-vault-recall-acceleration.md
Shay 9e1add43a1 feat(phase4): long-context-cost lane + ADR-0019 Stage 1 vault recall vectorisation
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.
2026-05-16 16:39:30 -07:00

147 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.einsum` with explicit `optimize`
off, or a single `M @ Gq` where `Gq` is the metric-folded
query).
- **Surface preserved:** `vault_recall` signature, 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 CauchySchwarz 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.py` and 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_inner` factor cleanly into a static metric matrix in
the chosen embedding? If yes, Stage 1 is `M @ Gq` with one
precomputed Gq per query. If not, Stage 1 is `np.einsum` over
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.