engram: fix lazy-embed index gap (#20) and make activate's cosine scan lazy; extract vindex harvest primitive with a bench/oracle harness
El SDK CI - dev / build-and-test (pull_request) Successful in 6m42s
El SDK CI - dev / build-and-test (pull_request) Successful in 6m42s
Adds an O(1) "seen" bitmap so lazily-embedded older nodes get picked up incrementally instead of only on a full rebuild (embed-gap #20). Replaces engram_activate's O(N*D) cosine prescan with a lazy-memoized cosine cache (eg_cosq_at), proven bit-identical to the old path. Extracts a clean vindex_harvest_from_store primitive (read-only vector harvest, careful malloc/ownership/error-path handling) reused by both index-build and the new vindex_bench.c — a read-only proof harness comparing brute-force vs HNSW recall/latency on both the real store and synthetic data. .nsbx-env intentionally excluded — local sandbox config (ports, paths, dev-only placeholder key), not checked in.
This commit is contained in:
+91
-29
@@ -9352,6 +9352,21 @@ static double engram_goal_bias(const EngramNode* n, const char* query) {
|
||||
static VIndex* _eg_vindex = NULL;
|
||||
static int32_t _eg_vindex_dim = 0;
|
||||
static int64_t _eg_vindex_built_nc = 0; /* g->node_count at last (re)build */
|
||||
static uint8_t* _eg_vindex_seen = NULL;/* per-ordinal: 1 iff inserted into _eg_vindex */
|
||||
static int64_t _eg_vindex_seen_cap = 0;
|
||||
|
||||
/* Grow the per-ordinal "indexed" bitmap to hold at least `need` entries, zeroing
|
||||
* the new tail. Returns 0 on success, -1 on OOM (caller keeps the old map). */
|
||||
static int eg_vindex_seen_ensure(int64_t need) {
|
||||
if (need <= _eg_vindex_seen_cap) return 0;
|
||||
int64_t nc = _eg_vindex_seen_cap ? _eg_vindex_seen_cap : 1024;
|
||||
while (nc < need) nc *= 2;
|
||||
uint8_t* ns = (uint8_t*)realloc(_eg_vindex_seen, (size_t)nc);
|
||||
if (!ns) return -1;
|
||||
memset(ns + _eg_vindex_seen_cap, 0, (size_t)(nc - _eg_vindex_seen_cap));
|
||||
_eg_vindex_seen = ns; _eg_vindex_seen_cap = nc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static VIndex* eg_vindex_sync(EngramStore* g, int32_t dim) {
|
||||
if (!g || dim <= 0) return _eg_vindex;
|
||||
@@ -9360,22 +9375,32 @@ static VIndex* eg_vindex_sync(EngramStore* g, int32_t dim) {
|
||||
if (_eg_vindex && (_eg_vindex_dim != dim || g->node_count < _eg_vindex_built_nc)) {
|
||||
vindex_free(_eg_vindex);
|
||||
_eg_vindex = NULL; _eg_vindex_dim = 0; _eg_vindex_built_nc = 0;
|
||||
free(_eg_vindex_seen); _eg_vindex_seen = NULL; _eg_vindex_seen_cap = 0;
|
||||
}
|
||||
if (!_eg_vindex) {
|
||||
VIndex* idx = vindex_create((int)dim, 0, 0);
|
||||
if (!idx) return NULL;
|
||||
if (eg_vindex_seen_ensure(g->node_count)) { vindex_free(idx); return NULL; }
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->emb && n->emb_dim == dim)
|
||||
(void)vindex_insert(idx, (uint64_t)i, n->emb);
|
||||
if (n->emb && n->emb_dim == dim && vindex_insert(idx, (uint64_t)i, n->emb) == 0)
|
||||
_eg_vindex_seen[i] = 1;
|
||||
}
|
||||
_eg_vindex = idx; _eg_vindex_dim = dim; _eg_vindex_built_nc = g->node_count;
|
||||
} else if (g->node_count > _eg_vindex_built_nc) {
|
||||
/* Incremental: index newly-appended nodes that already carry an emb. */
|
||||
for (int64_t i = _eg_vindex_built_nc; i < g->node_count; i++) {
|
||||
} else if (eg_vindex_seen_ensure(g->node_count) == 0) {
|
||||
/* Incremental (embed-gap #20 fix): index EVERY node that now carries an emb
|
||||
* but is not yet in the index — whether newly APPENDED or lazily EMBEDDED on
|
||||
* an OLDER ordinal by the backfill loop. The previous tail-only scan left a
|
||||
* lazily-embedded older node invisible until the next full rebuild; that node
|
||||
* was silently absent from route_nearest / autoconnect (which have NO exact
|
||||
* top-up, unlike engram_activate) and lowered activation recall. This O(node_count)
|
||||
* presence check carries no D factor, so it is negligible beside the cosq scan on
|
||||
* the same path. On seen-map OOM we fall through unchanged (index simply not grown). */
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (_eg_vindex_seen[i]) continue;
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->emb && n->emb_dim == dim)
|
||||
(void)vindex_insert(_eg_vindex, (uint64_t)i, n->emb);
|
||||
if (n->emb && n->emb_dim == dim && vindex_insert(_eg_vindex, (uint64_t)i, n->emb) == 0)
|
||||
_eg_vindex_seen[i] = 1;
|
||||
}
|
||||
_eg_vindex_built_nc = g->node_count;
|
||||
}
|
||||
@@ -9438,6 +9463,25 @@ static int eg_geo_prime_max(void){ /* cap primed members added to the front
|
||||
if (v < 0) v = 0; if (v > 256) v = 256; return v;
|
||||
}
|
||||
|
||||
/* Lazy per-node cosine vs the effective query (M8.1 activate-latency fix,
|
||||
* 2026-08-14). Computes cos(node_i, query) ON DEMAND and memoizes it, replacing
|
||||
* the full O(N·D) prescan that dominated live activate latency (~330ms + CPU peg
|
||||
* + restart thrash). Returns exactly the value the prescan produced for any node
|
||||
* it is asked about; nodes never asked about are never computed — and the prescan
|
||||
* version never READ them either (Pass-3 skips !reached[i]; the exact top-up only
|
||||
* runs when the ANN underfills), so engram_activate stays BIT-IDENTICAL while
|
||||
* touching only ANN candidates + propagation-reached nodes. Degrades to the -2.0
|
||||
* "no/!=dim embedding" sentinel exactly as the prescan did. Callers guard with
|
||||
* `if (cosq)`, so cosq/cos_done are non-NULL here. */
|
||||
static inline double eg_cosq_at(EngramStore* g, double* cosq, unsigned char* cos_done,
|
||||
const float* qv, int32_t dim, int64_t i) {
|
||||
if (cos_done[i]) return cosq[i];
|
||||
EngramNode* n = &g->nodes[i];
|
||||
cosq[i] = (n->emb && n->emb_dim == dim && qv) ? eg_cosine(n->emb, qv, dim) : -2.0;
|
||||
cos_done[i] = 1;
|
||||
return cosq[i];
|
||||
}
|
||||
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
EngramStore* g = engram_get();
|
||||
const char* q = EL_CSTR(query);
|
||||
@@ -9537,15 +9581,26 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
* twice, coherently — HippoRAG). cosq stays NULL when the embedder is
|
||||
* unavailable; every consumer degrades to pure lexical behavior. */
|
||||
double* cosq = NULL;
|
||||
unsigned char* cos_done = NULL; /* M8.1: which cosq[i] have been computed (lazy) */
|
||||
float* cosq_qv = NULL; /* M8.1: OWNED copy of the effective query vector,
|
||||
* kept alive past the e_eff free (9889) for the
|
||||
* lazy cosq reads in Pass-2/Pass-3 propagation. */
|
||||
if (q_emb) {
|
||||
const float* qv = e_eff ? e_eff : q_emb;
|
||||
cosq = calloc((size_t)g->node_count, sizeof(double));
|
||||
if (cosq) {
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
cosq[i] = (n->emb && n->emb_dim == q_dim)
|
||||
? eg_cosine(n->emb, qv, q_dim) : -2.0;
|
||||
}
|
||||
cosq = calloc((size_t)g->node_count, sizeof(double));
|
||||
cos_done = calloc((size_t)g->node_count, 1);
|
||||
if (cosq && cos_done && qv) {
|
||||
cosq_qv = malloc((size_t)q_dim * sizeof(float));
|
||||
if (cosq_qv) memcpy(cosq_qv, qv, (size_t)q_dim * sizeof(float));
|
||||
}
|
||||
/* M8.1 activate-latency fix: NO full O(N·D) prescan here. cosq is filled
|
||||
* lazily via eg_cosq_at() only for ANN candidates + propagation-reached
|
||||
* nodes — bit-identical to the prescan (see eg_cosq_at). If any alloc
|
||||
* failed, degrade to embedder-down behaviour: every `if (cosq)` consumer
|
||||
* skips and activation falls back to pure lexical, exactly as before. */
|
||||
if (!cosq || !cos_done || !cosq_qv) {
|
||||
free(cosq); free(cos_done); free(cosq_qv);
|
||||
cosq = NULL; cos_done = NULL; cosq_qv = NULL;
|
||||
}
|
||||
}
|
||||
/* NOTE (M8): e_eff is NOT freed here anymore — the effective query vector
|
||||
@@ -9558,7 +9613,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t));
|
||||
int* reached = calloc((size_t)g->node_count, sizeof(int));
|
||||
if (!best_bg || !best_hops || !reached) {
|
||||
free(best_bg); free(best_hops); free(reached); free(cosq); free(e_eff); return out;
|
||||
free(best_bg); free(best_hops); free(reached); free(cosq); free(cos_done); free(cosq_qv); free(e_eff); return out;
|
||||
}
|
||||
|
||||
/* ── LAYER 1: broad fan-out (background activation) ─────────────────
|
||||
@@ -9569,7 +9624,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry));
|
||||
int64_t seed_count = 0;
|
||||
if (!seeds) {
|
||||
free(best_bg); free(best_hops); free(reached); free(cosq); free(e_eff); return out;
|
||||
free(best_bg); free(best_hops); free(reached); free(cosq); free(cos_done); free(cosq_qv); free(e_eff); return out;
|
||||
}
|
||||
/* Tokenize once: a node seeds if it matches ANY query token, and its seed
|
||||
* activation is scaled by token coverage (fraction of distinct query
|
||||
@@ -9662,7 +9717,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
if (bi < 0 || bi >= g->node_count) continue; /* stale id guard */
|
||||
if (reached[bi]) continue; /* lexically seeded */
|
||||
if (seed_dup && seed_dup[bi]) continue;
|
||||
double bc = cosq[bi]; /* EXACT cosine — parity gate */
|
||||
double bc = eg_cosq_at(g, cosq, cos_done, cosq_qv, q_dim, bi); /* EXACT cosine — parity gate (lazy) */
|
||||
if (!(bc > ENGRAM_EMBED_SEED_MIN)) continue;
|
||||
EngramNode* n = &g->nodes[bi];
|
||||
uint64_t key = eg_content_key(n);
|
||||
@@ -9704,7 +9759,8 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (reached[i]) continue;
|
||||
if (seed_dup && seed_dup[i]) continue;
|
||||
if (cosq[i] > bc) { bc = cosq[i]; bi = i; }
|
||||
double ci = eg_cosq_at(g, cosq, cos_done, cosq_qv, q_dim, i);
|
||||
if (ci > bc) { bc = ci; bi = i; }
|
||||
}
|
||||
if (bi < 0) break;
|
||||
EngramNode* n = &g->nodes[bi];
|
||||
@@ -9880,7 +9936,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
typedef struct { int64_t idx; int64_t hops; double act; } Frontier;
|
||||
Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier));
|
||||
if (!fr) {
|
||||
free(best_bg); free(best_hops); free(reached); free(seeds); free(cosq); return out;
|
||||
free(best_bg); free(best_hops); free(reached); free(seeds); free(cosq); free(cos_done); free(cosq_qv); return out;
|
||||
}
|
||||
int64_t fhead = 0, ftail = 0;
|
||||
int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16);
|
||||
@@ -9982,9 +10038,12 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
* information, no penalty); cosq == NULL (embedder down) means
|
||||
* no gating at all — same graceful degradation as seeding. */
|
||||
double qgate = 1.0;
|
||||
if (cosq && cosq[oi] > -1.5) {
|
||||
double c = cosq[oi] > 0.0 ? cosq[oi] : 0.0;
|
||||
qgate = ENGRAM_QGATE_FLOOR + (1.0 - ENGRAM_QGATE_FLOOR) * c;
|
||||
if (cosq) {
|
||||
double coi = eg_cosq_at(g, cosq, cos_done, cosq_qv, q_dim, oi);
|
||||
if (coi > -1.5) {
|
||||
double c = coi > 0.0 ? coi : 0.0;
|
||||
qgate = ENGRAM_QGATE_FLOOR + (1.0 - ENGRAM_QGATE_FLOOR) * c;
|
||||
}
|
||||
}
|
||||
/* ── ACT-R fan effect (2026-08-11 self-review) ──
|
||||
* Symmetric degree normalization over the (source, target) pair.
|
||||
@@ -10036,7 +10095,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
double* inhibition = calloc((size_t)g->node_count, sizeof(double));
|
||||
if (!inhibition) {
|
||||
free(best_bg); free(best_hops); free(reached); free(seeds); free(fr);
|
||||
free(cosq); return out;
|
||||
free(cosq); free(cos_done); free(cosq_qv); return out;
|
||||
}
|
||||
for (int64_t ei = 0; ei < g->edge_count; ei++) {
|
||||
EngramEdge* e = &g->edges[ei];
|
||||
@@ -10061,7 +10120,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
double* wm_weights = calloc((size_t)g->node_count, sizeof(double));
|
||||
if (!wm_weights) {
|
||||
free(best_bg); free(best_hops); free(reached); free(seeds);
|
||||
free(fr); free(inhibition); free(cosq); return out;
|
||||
free(fr); free(inhibition); free(cosq); free(cos_done); free(cosq_qv); return out;
|
||||
}
|
||||
/* Per-call breakthrough budget (2026-08-02) — see ENGRAM_BREAKTHROUGH_BUDGET. */
|
||||
int64_t bt_budget = ENGRAM_BREAKTHROUGH_BUDGET;
|
||||
@@ -10135,9 +10194,12 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
* dragged in), not the semantic one (what the query actually
|
||||
* means); relevance to the current query is not stale merely
|
||||
* because the node was in WM a moment ago. */
|
||||
if (cosq && cosq[i] > ENGRAM_EMBED_S0) {
|
||||
raw_wm += ENGRAM_EMBED_WM_WEIGHT
|
||||
* (cosq[i] - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
|
||||
if (cosq) {
|
||||
double ci = eg_cosq_at(g, cosq, cos_done, cosq_qv, q_dim, i);
|
||||
if (ci > ENGRAM_EMBED_S0) {
|
||||
raw_wm += ENGRAM_EMBED_WM_WEIGHT
|
||||
* (ci - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
|
||||
}
|
||||
}
|
||||
/* Threshold gate: must exceed per-type threshold to enter working
|
||||
* memory. Type threshold replaces the old flat 0.2 filter. */
|
||||
@@ -10817,7 +10879,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
int64_t rcount = 0;
|
||||
if (!results) {
|
||||
free(best_bg); free(best_hops); free(reached); free(seeds);
|
||||
free(fr); free(inhibition); free(wm_weights); free(cosq);
|
||||
free(fr); free(inhibition); free(wm_weights); free(cosq); free(cos_done); free(cosq_qv);
|
||||
free(was_wm); return out;
|
||||
}
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
@@ -10885,7 +10947,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
}
|
||||
free(best_bg); free(best_hops); free(reached);
|
||||
free(seeds); free(fr); free(inhibition); free(wm_weights); free(results);
|
||||
free(cosq);
|
||||
free(cosq); free(cos_done); free(cosq_qv);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user