Files
neuron/docs/architecture/design/perf/engram-m8-profile.md
T

141 lines
8.4 KiB
Markdown
Raw 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.
# Engram Tiered Storage — M8 Performance Profile (milestone-0 sample)
**Milestone:** M8 (ANN wired into `engram_activate` seed selection). Trunk =
worktree `/tmp/engram-tiered-wt`, branch `engram-tiered-storage`, HEAD `1507614`.
**Date:** 2026-08-12. **Author:** first full-binary build + profile of the tiered trunk.
This is **sample zero** of an accumulating per-milestone profile series (see the
BUILD-LEDGER "keep performance telemetry CONTINUOUSLY" decision). Compare future
milestones (M9, M10, …) against these numbers.
## Methodology (read this before trusting a number)
- **On a COPY, never live.** All runtime measurements used a read-only copy of the
live store booted on a **non-live port (:8798)** with a **throwaway `$HOME`**. The
live engram service (:8742, `~/.neuron/engram`) was never touched.
- **Two store substrates were used:**
1. *Live-egm copy* (`neuron.egm` 458 MB + `neuron.wal` 44 MB, copied read-only) —
**this substrate crashes both the M8 binary and the live binary on boot** (see
Integration Findings). Unusable for runtime measurement.
2. *Clean import* — a dir seeded with only `snapshot.json` (65 MB, stable 06:10),
which the binary imported into a **fresh 59.9 MB `neuron.egm`**. All healthy
runtime numbers below are from this substrate (real graph content, healthy store).
- **Build machine:** Apple Silicon (arm64), macOS. Native `cc -O2` compile; fold done
in a memory-capped (`--memory=3g --memory-swap=3g`, no swap) `linux/amd64` container
running `elc-linux-amd64`.
- Hardware/thermals uncontrolled; single run per metric unless noted. Treat as
order-of-magnitude, not benchmark-grade, except the module benchmark (Test suite).
## Build
| Metric | Value |
|---|---|
| Fold input | `engram/src/server.el` (44,273 B El, no imports) |
| Fold output | `engram.c` (30,855 B, 645 lines C), `ELC_EXIT=0`, **0 fold warnings** |
| Fold time (pure elc) | sub-second (server.el is small, importless) |
| Fold container wall | ~38 s (dominated by one-time `apt-get install libcurl4` in the throwaway container; the elc invocation itself is <1 s) |
| Compile | `cc -O2 -DHAVE_CURL engram.c el_runtime.c engram_store.c engram_vindex.c -lssl -lcrypto -lcurl -lpthread -lm` |
| Compile time | **1.83 s** wall |
| Compile warnings | **3**, all `-Wparentheses-equality` in the *folded* `engram.c` (El if-expr codegen emits `if ((x == 0))`); cosmetic. `el_runtime.c` / `engram_store.c` / `engram_vindex.c` compiled **0 warnings** — notably none around the M8 deferred `free(e_eff)` or the vindex integration. |
| Binary | **482,008 B (471 KB)** Mach-O arm64 executable |
| ANN linkage verified | `nm`: `vindex_search`, `vindex_build_from_store`, `eg_vindex_sync`, `_eg_vindex`, `store_scan_nodes`, `engram_store_boot`, `engram_activate` all present |
## Boot & footprint (clean-import substrate)
| Metric | Value |
|---|---|
| Boot from healthy `neuron.egm` | **~2 s** to listening |
| Boot from `snapshot.json` (one-time import + fresh egm) | **~9 s** |
| node_count | **13,036** (matches ledger import-dedup: 13,038 snapshot 2 dup-id entries) |
| edge_count / layer_count | 43,402 / 5 |
| embedded_count | 4,190 |
| Fresh egm size | **59.9 MB** (vs the live egm's bloated 458 MB — see Findings) |
| RSS after boot | **126.2 MB** (whole 13k-node/43k-edge/4,190-emb graph resident + fresh egm) |
## Activation latency — q="bullshit" (clean-import substrate)
50 sequential `GET /api/activate?q=bullshit&limit=10&depth=3`:
| Metric | Value |
|---|---|
| p50 | **34.35 ms** |
| p95 | **35.26 ms** |
| min / max | 33.50 ms / 9,886 ms |
| Sample | n=50 |
- The **max = 9.9 s is the first call only** — a cold query-embedding fetch
(`eg_embed_fetch` → Ollama `nomic-embed-text`, cold model load). All subsequent
calls hit the single-slot query-embedding cache (`_eg_qcache`) → **34 ms steady state**.
- **This 34 ms is the lexical/spread path, NOT the ANN seed path.** The store's 4,190
embeddings were generated by the live neuron's native embedding model; the harness's
`nomic-embed-text` query vectors are a **different vector space**, so no candidate
cleared `ENGRAM_EMBED_SEED_MIN=0.60` (`act-stats`: `dup_seeds:0`, `ctx_cos:-2.000`
sentinel) → results empty, ANN discovery produced no admitted seeds. Environmental
(embedding provenance), **not** an M8 defect. The ANN wiring still *executed*
(embed fetch succeeded, `embed_breaker_open:0`; `eg_vindex_sync` + seed block +
`free(e_eff)` all ran) **without crashing** on the real store.
## KEY M8 METRIC — ANN vs O(n) seed selection (authoritative)
Because the HTTP path can't exercise ANN seeding without embedding-space parity, the
authoritative ANN-vs-exact-scan numbers come from the **module benchmark**
(`engram/test/run_vindex_tests.sh`, PASS 1, optimised), which measures the exact
`vindex_search` code the M8 wiring calls, at full size:
| N (768-dim vectors) | Brute-force (O(n)) | ANN (HNSW) | **Speedup** |
|---|---|---|---|
| 5,000 | 3.475 ms/query | 0.353 ms/query | **9.8×** |
| 20,000 | 13.809 ms/query | 0.698 ms/query | **19.8×** |
- **recall@10 = 0.9365** at `ef_search=128` (gate ≥0.90 — **PASS**). Lower ef trades
recall for latency: ef=64→0.844, ef=32→0.750, ef=10→0.625.
- **Determinism:** two independent seeded builds give byte-identical query results.
- **`vindex_build_from_store`** over a real `engram_store`: inserts exactly the
embedded nodes, top-1 resolves to the correct node id at ~0 distance.
- **HNSW build cost (single-threaded, note for boot/index-build budgeting):**
5,000 vectors ≈ 1535 s, 20,000 vectors ≈ 75 s. In-process the index is built
**lazily on first activation** (`eg_vindex_sync`) and grown incrementally; the
M8 seed block only fires once `vindex_size ≥ ENGRAM_EMBED_SEED_K`. At the real
store's 4,190 embedded nodes this is a **one-time few-second first-activation
cost** — worth watching as the embedded set grows (a future milestone may want
to build the index at boot or persist it via `vindex_save`/`vindex_load`).
## Seed-set parity note (why there is no runtime A/B toggle)
M8 has **no ANN on/off env flag** by design (`ENGRAM_EMBED_SEED_K` is a compile-time
constant). The exact O(n) cosine scan is **preserved verbatim** and "tops up" any seed
slot the ANN leaves unfilled; every ANN candidate is admitted through the *identical*
cosine/dedup/threshold gate the exact scan uses. So ANN changes only *which nodes are
discovered and how fast*, never the final seed set — parity is **structural**, not
A/B-tested via flag. Recovered-behaviour-when-index-absent is the pre-M8 exact scan.
## Integration Findings (first full build of the tiered trunk)
1. **CRITICAL / pre-existing (NOT M8): `btree_insert` stack-buffer-overflow on
opening the live 458 MB `neuron.egm`.** `SIGABRT` (`__stack_chk_fail`) via
`btree_insert ← btree_put ← edge_place/apply_edge_put ← engram_open ←
engram_store_boot`. Reproduces on the M8 binary **and** the deployed live binary —
**the live engram :8742 was crash-looping (18 crash reports 16:18→17:44 on this
date; service refusing connections).** A clean import into a fresh 59.9 MB egm does
**not** crash (43k edges load fine), so the trigger is the specific pathological
live store: 458 MB (8× the healthy 59.9 MB) from a day of churn/tombstones with no
M5 compaction + a 44 MB un-checkpointed WAL replayed on open. The bug is in
`engram_store.c` (the edge B-tree / WAL-redo path), **upstream of everything M8
touched** (M8 lives in `el_runtime.c::engram_activate`). Fix required before any
re-cutover; `btree_insert` must bound-check regardless of on-disk content.
2. **M8 deferred `free(e_eff)`** (the flagged memory-management concern): compiled
warning-free, and the wired path executed end-to-end over HTTP on the real store
(with a real query embedding) with **no crash / no new crash report** — no
double-free or use-after-free observed. Freed on all early-return paths and exactly
once post-seed-selection.
3. **Write durability + retrieval-fix dedup**: create → checkpoint → clean SIGTERM →
restart → node found **by id and by search** (node_count 13,036→13,037 preserved).
## Caveats
- All on a copy; healthy-substrate numbers are from a re-imported store, not the live
paged store (which is currently un-bootable — Finding 1).
- Single-run metrics; no thermal control.
- Embedding-space mismatch prevented a real semantic `q=bullshit` activation in this
harness; the ANN speedup number is the module benchmark, which is the correct gauge.