From 1dc49b19233740c05b0db2ca3f297b45b6e79f54 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 14:25:12 -0500 Subject: [PATCH] Fix engram search latency: pin embed model, cache query embeddings, bound activate BFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins the Ollama embed model resident (keep_alive:-1) to avoid multi-second cold reloads whenever a larger generation model evicts it under unified- memory pressure (measured cold reload up to ~2.2s vs ~0.02-0.05s warm). Adds a direct-mapped query-embedding cache (FNV-1a keyed, full strcmp to reject collisions) so a repeated query costs zero Ollama round-trips — directly serves the curiosity loop, which reseeds the same query terms repeatedly. Replaces engram_activate's unbounded FIFO frontier BFS with a beam-capped, level-synchronous BFS (default beam 128, tunable via ENGRAM_ACTIVATE_BEAM) to bound per-hop hub-node explosion that could previously reach multi-second/crash territory at depth 2-3. Excludes an inert engram_prune_telemetry build-enabler stub that was only needed to link this checkout against a newer integration branch — not part of the fix. --- lang/el-compiler/runtime/el_runtime.c | 188 +++++++++++++++++++------- 1 file changed, 140 insertions(+), 48 deletions(-) diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index af0d945..f4f73376 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -7056,10 +7056,16 @@ static float* engram_embed_raw(const char* prefix, const char* text, int* out_di char* esc = engram_json_escape(text); free(trunc); if (!esc || !esc_prefix) { free(esc); free(esc_prefix); return NULL; } - size_t blen = strlen(esc) + strlen(esc_prefix) + strlen(model) + 64; + size_t blen = strlen(esc) + strlen(esc_prefix) + strlen(model) + 96; char* body = malloc(blen); if (!body) { free(esc); free(esc_prefix); return NULL; } - snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s%s\"}", model, esc_prefix, esc); + /* keep_alive:-1 pins the embed model resident in Ollama indefinitely. + * Without it the tiny embed model is evicted whenever a large generation + * model loads (unified-memory pressure), so the NEXT search pays a cold + * model reload — the dominant search-latency cost (measured cold reload + * up to ~2.2s vs ~0.02-0.05s warm). Pinning makes cold reload impossible. */ + snprintf(body, blen, "{\"model\":\"%s\",\"keep_alive\":-1,\"prompt\":\"%s%s\"}", + model, esc_prefix, esc); free(esc); free(esc_prefix); CURL* c = curl_easy_init(); @@ -7099,11 +7105,52 @@ static int engram_semantic_enabled(void) { g_emb_state = -1; return 0; } +/* ── Query-embedding cache ────────────────────────────────────────────────── + * The node embeddings are cached (engram_node_vec) but the QUERY was re-embedded + * on every search/activate call — a blocking Ollama round-trip each time. Query + * embeddings are deterministic for a given model, so we cache them keyed by an + * FNV-1a hash of the query string (with a full strcmp to reject hash + * collisions). A repeated query then costs zero network round-trips. This makes + * warm search latency independent of Ollama entirely, and directly serves the + * curiosity loop, which reseeds the same query terms repeatedly. Direct-mapped, + * fixed-size, process-lifetime. */ +#define ENGRAM_QCACHE_SIZE 1024 +typedef struct { char* q; uint64_t hash; float* vec; int dim; } EngramQCacheEntry; +static EngramQCacheEntry g_qcache[ENGRAM_QCACHE_SIZE]; + +/* Returns a malloc'd COPY of the cached vector (caller frees), or NULL on miss — + * preserving engram_embed_query's "caller frees" contract. */ +static float* engram_qcache_get(const char* q, uint64_t h, int* dim) { + EngramQCacheEntry* e = &g_qcache[h & (ENGRAM_QCACHE_SIZE - 1)]; + if (e->vec && e->hash == h && e->q && strcmp(e->q, q) == 0 && e->dim > 0) { + float* copy = malloc((size_t)e->dim * sizeof(float)); + if (!copy) return NULL; + memcpy(copy, e->vec, (size_t)e->dim * sizeof(float)); + *dim = e->dim; return copy; + } + return NULL; +} +static void engram_qcache_put(const char* q, uint64_t h, const float* vec, int dim) { + if (!vec || dim <= 0) return; + EngramQCacheEntry* e = &g_qcache[h & (ENGRAM_QCACHE_SIZE - 1)]; + float* stored = malloc((size_t)dim * sizeof(float)); + char* qcopy = el_strdup(q); + if (!stored || !qcopy) { free(stored); free(qcopy); return; } + memcpy(stored, vec, (size_t)dim * sizeof(float)); + free(e->q); free(e->vec); /* evict prior occupant of this slot */ + e->q = qcopy; e->hash = h; e->vec = stored; e->dim = dim; +} + /* Embed the query. Returns malloc'd vec (caller frees), or NULL if semantic off. */ static float* engram_embed_query(const char* q, int* dim) { if (!engram_semantic_enabled()) return NULL; if (!q || !*q) return NULL; - return engram_embed_raw("search_query: ", q, dim); + uint64_t h = engram_fnv1a(q); + float* hit = engram_qcache_get(q, h, dim); + if (hit) return hit; + float* v = engram_embed_raw("search_query: ", q, dim); + if (v && *dim > 0) engram_qcache_put(q, h, v, *dim); + return v; } /* Cached node embedding. Returns a pointer OWNED BY THE CACHE — do not free. */ @@ -7537,6 +7584,39 @@ static double engram_goal_bias(const EngramNode* n, const char* query) { return bias; } + +/* ── Beam cap for engram_activate spreading activation ────────────────────── + * Bounds the number of frontier nodes expanded PER HOP. Without it a single + * high-degree hub enqueues thousands of successors, each re-scanning the whole + * edge list, and dense cycles re-enqueue them repeatedly — so capping DEPTH + * does not bound work (measured: depth-2/3 in the multi-second range, depth-3 + * can crash). With the cap, only the top-BEAM highest-activation nodes at each + * level spread further. Every reached node is still recorded and returned, so + * recall is preserved — the cap bounds only associative spread, never the + * direct seed matches or the reported set. Tunable via ENGRAM_ACTIVATE_BEAM + * (default 128); set very high to restore unbounded behaviour. */ +static int64_t engram_activate_beam(void) { + static int64_t v = -1; + if (v >= 0) return v; + const char* s = getenv("ENGRAM_ACTIVATE_BEAM"); + int64_t d = 128; + if (s && *s) { char* e = NULL; long t = strtol(s, &e, 10); if (e != s && t > 0) d = (int64_t)t; } + v = d; return v; +} + +/* Partition the k highest-`score` entries of idx[0..n) to the front (order + * within the top-k is unspecified). O(k*n) partial selection — k is the small + * beam width, so this is cheap relative to a hop's edge scan. */ +static void engram_beam_select(int64_t* idx, int64_t n, int64_t k, const double* score) { + if (k >= n) return; + for (int64_t i = 0; i < k; i++) { + int64_t best = i; + for (int64_t j = i + 1; j < n; j++) + if (score[idx[j]] > score[idx[best]]) best = j; + if (best != i) { int64_t t = idx[i]; idx[i] = idx[best]; idx[best] = t; } + } +} + el_val_t engram_activate(el_val_t query, el_val_t depth) { EngramStore* g = engram_get(); const char* q = EL_CSTR(query); @@ -7606,53 +7686,65 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { for (int64_t s = 1; s < seed_count; s++) seed_epoch = (seed_epoch + seeds[s].created_at) / 2; } - 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) { + /* ── Beam-capped, level-synchronous BFS ──────────────────────────────── + * Expand the graph hop-by-hop; at each hop expand only the top-`beam` + * nodes by current best background activation (engram_beam_select). This + * replaces the old unbounded FIFO frontier, which let a hub enqueue + * thousands of successors and dense cycles re-enqueue them without limit + * (the breadth explosion). `reached` / `best_bg` / `best_hops` keep the + * exact same meaning, so the downstream executive/override passes and the + * reported result set are unchanged — only how far weak spread propagates + * is bounded. `cur`/`nxt` hold node indices for this/next level; `in_nxt` + * dedups a node to at most one entry per level. */ + const int64_t beam = engram_activate_beam(); + const double SPREAD_DECAY = 0.7; + int64_t* cur = malloc((size_t)g->node_count * sizeof(int64_t)); + int64_t* nxt = malloc((size_t)g->node_count * sizeof(int64_t)); + int* in_nxt = calloc((size_t)g->node_count, sizeof(int)); + if (!cur || !nxt || !in_nxt) { + free(cur); free(nxt); free(in_nxt); free(best_bg); free(best_hops); free(reached); free(seeds); return out; } - int64_t fhead = 0, ftail = 0; - int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16); - for (int64_t s = 0; s < seed_count; s++) { - if (ftail >= fcap) break; - fr[ftail].idx = seeds[s].idx; - fr[ftail].hops = 0; - fr[ftail].act = seeds[s].act; - ftail++; - } - const double SPREAD_DECAY = 0.7; - while (fhead < ftail) { - Frontier f = fr[fhead++]; - if (f.hops >= max_depth) continue; - const char* cur_id = g->nodes[f.idx].id; - for (int64_t ei = 0; ei < g->edge_count; ei++) { - EngramEdge* e = &g->edges[ei]; - const char* other = NULL; - if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; - else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; - else continue; - int64_t oi = engram_find_node_index(other); - if (oi < 0) continue; - EngramNode* on = &g->nodes[oi]; - double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); - double tdecay = engram_temporal_decay(on, now_ms); - double dampen = engram_activation_dampen(on); - double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus) - * tdecay * dampen; - int64_t new_hops = f.hops + 1; - if (!reached[oi] || new_act > best_bg[oi]) { - best_bg[oi] = new_act; - best_hops[oi] = new_hops; - reached[oi] = 1; - if (ftail < fcap) { - fr[ftail].idx = oi; - fr[ftail].hops = new_hops; - fr[ftail].act = new_act; - ftail++; + int64_t cur_n = 0; + for (int64_t s = 0; s < seed_count && cur_n < g->node_count; s++) + cur[cur_n++] = seeds[s].idx; + for (int64_t hop = 0; hop < max_depth && cur_n > 0; hop++) { + if (cur_n > beam) { engram_beam_select(cur, cur_n, beam, best_bg); cur_n = beam; } + int64_t nxt_n = 0; + for (int64_t ci = 0; ci < cur_n; ci++) { + int64_t fidx = cur[ci]; + double f_act = best_bg[fidx]; + const char* cur_id = g->nodes[fidx].id; + for (int64_t ei = 0; ei < g->edge_count; ei++) { + EngramEdge* e = &g->edges[ei]; + const char* other = NULL; + if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id; + else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id; + else continue; + int64_t oi = engram_find_node_index(other); + if (oi < 0) continue; + EngramNode* on = &g->nodes[oi]; + double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch); + double tdecay = engram_temporal_decay(on, now_ms); + double dampen = engram_activation_dampen(on); + double new_act = f_act * e->weight * SPREAD_DECAY * (1.0 + tbonus) + * tdecay * dampen; + if (!reached[oi] || new_act > best_bg[oi]) { + best_bg[oi] = new_act; + best_hops[oi] = hop + 1; + reached[oi] = 1; + if (!in_nxt[oi] && nxt_n < g->node_count) { + in_nxt[oi] = 1; + nxt[nxt_n++] = oi; + } } } } + for (int64_t k = 0; k < nxt_n; k++) in_nxt[nxt[k]] = 0; + int64_t* tmp = cur; cur = nxt; nxt = tmp; + cur_n = nxt_n; } + free(cur); free(nxt); free(in_nxt); /* Persist layer-1 background_activation to node store. */ for (int64_t i = 0; i < g->node_count; i++) { g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0; @@ -7666,7 +7758,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { * memory weight cannot be silenced by attentional suppression. */ 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(best_bg); free(best_hops); free(reached); free(seeds); return out; } for (int64_t ei = 0; ei < g->edge_count; ei++) { @@ -7692,7 +7784,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); return out; + free(inhibition); return out; } for (int64_t i = 0; i < g->node_count; i++) { if (!reached[i] || best_bg[i] <= 0.0) continue; @@ -7762,7 +7854,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); return out; + free(inhibition); free(wm_weights); return out; } for (int64_t i = 0; i < g->node_count; i++) { if (!reached[i]) continue; @@ -7806,7 +7898,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { out = el_list_append(out, entry); } free(best_bg); free(best_hops); free(reached); - free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); + free(seeds); free(inhibition); free(wm_weights); free(results); return out; }