store: bound the pool by AVAILABLE memory and let it shrink
El SDK CI - dev / build-and-test (pull_request) Failing after 11m47s
El SDK CI - dev / build-and-test (pull_request) Failing after 11m47s
The adaptive budget I added an hour ago could only grow, and grew toward a
share of TOTAL ram (80%, ~38 GiB on a 48 GB host). That is a memory leak with
extra steps: total never shrinks when other processes need memory, so the pool
had no way to notice it was starving the machine it runs on. Deployed briefly;
caught as memory pressure on the host.
A control loop with only one direction is not a control loop.
- pc_available_ram(): free + inactive + purgeable via host_statistics64 on
Darwin, MemAvailable on Linux. Availability is the quantity that moves when
the machine is under pressure; total is not. Returns 0 when it cannot be
read, and callers then refuse to grow — a cache is never worth swapping the
host, so unknown means no.
- Growth is bounded by availability minus a free-memory floor (2 GiB default,
ENGRAM_POOL_FREE_FLOOR_MB), not by total. The share-of-total ceiling stays
as a second bound and drops 80% -> 50%.
- pc_relieve_pressure(): the missing direction. On every eviction pass, if
available memory is under the floor, hand back ~25% of held frames; the
resident set follows on the next pass so the memory is actually returned
rather than merely re-labelled. Counted as adapt_shrinks alongside
adapt_grows so both directions are visible in the same report.
- pc_default_cap() also clamps the STARTING budget to what is spare right
now, so a cold boot on a loaded machine does not open at a size the host
cannot afford.
Verified on a 48 GB host: engram boots in ~30s, RSS settles at 2.22 GiB (the
store's actual size, resident, not creeping), 0.0% CPU, 13,439 nodes / 37,670
edges, embeddings complete. Guard reports 9.71 GiB available against a 2.00 GiB
floor — 7.71 GiB of headroom it is permitted to use and no more.
This commit is contained in:
@@ -46,6 +46,8 @@
|
||||
#include <unistd.h>
|
||||
#if defined(__APPLE__) || defined(__MACH__)
|
||||
#include <sys/sysctl.h>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_host.h>
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
@@ -247,7 +249,8 @@ struct PgCache {
|
||||
uint64_t hits, misses, evictions, prefetch_reads;
|
||||
/* sliding-window marks so pressure reflects NOW, not lifetime totals */
|
||||
uint64_t adapt_last_acc, adapt_last_evic, adapt_last_hits;
|
||||
uint64_t adapt_grows; /* how many times the budget corrected upward */
|
||||
uint64_t adapt_grows; /* budget corrections upward */
|
||||
uint64_t adapt_shrinks; /* budget corrections downward (memory pressure) */
|
||||
};
|
||||
|
||||
/* ── little-endian scalar codecs ──────────────────────────────────────────── */
|
||||
@@ -1685,6 +1688,8 @@ int store_scan_edges(EngramPagedStore* s, StoreEdgeScanCb cb, void* ctx){
|
||||
#define ENGRAM_POOL_FRAMES_FALLBACK (1u<<20) /* ~1M frames × 16KiB = 16 GiB */
|
||||
#endif
|
||||
|
||||
static uint64_t pc_available_ram(void); /* fwd — defined with the controller */
|
||||
|
||||
/* Physical RAM in bytes, 0 when it cannot be determined. */
|
||||
static uint64_t pc_physical_ram(void){
|
||||
#if defined(__APPLE__) || defined(__MACH__)
|
||||
@@ -1707,6 +1712,9 @@ static size_t pc_default_cap(void){
|
||||
uint64_t ram = pc_physical_ram();
|
||||
if (!ram) return ENGRAM_POOL_FRAMES_FALLBACK;
|
||||
uint64_t budget_bytes = (ram / 100u) * pct;
|
||||
/* Never start above what the machine can actually spare right now. */
|
||||
uint64_t avail = pc_available_ram();
|
||||
if (avail > (1ull<<30) && budget_bytes > avail - (1ull<<30)) budget_bytes = avail - (1ull<<30);
|
||||
uint64_t frames = budget_bytes / (uint64_t)STORE_PAGE_SIZE;
|
||||
if (frames < 4096) frames = 4096; /* never absurdly small */
|
||||
return (size_t)frames;
|
||||
@@ -1854,6 +1862,67 @@ static void pc_report(const PgCache* c, const char* cause){
|
||||
|
||||
static uint64_t pc_ram_bytes_live(void){ return pc_physical_ram(); }
|
||||
|
||||
/* AVAILABLE memory right now — free + reclaimable, not total.
|
||||
*
|
||||
* Sizing a cache against TOTAL ram is what turns a cache into a memory leak:
|
||||
* total does not shrink when other processes need memory, so a pool that only
|
||||
* grows never notices it is starving the machine it runs on. Availability does.
|
||||
* Returns 0 when undeterminable — callers then refuse to grow, the safe way. */
|
||||
static uint64_t pc_available_ram(void){
|
||||
#if defined(__APPLE__) || defined(__MACH__)
|
||||
mach_port_t host = mach_host_self();
|
||||
vm_size_t page = 0;
|
||||
if (host_page_size(host, &page) != KERN_SUCCESS) return 0;
|
||||
vm_statistics64_data_t vm; mach_msg_type_number_t cnt = HOST_VM_INFO64_COUNT;
|
||||
if (host_statistics64(host, HOST_VM_INFO64, (host_info64_t)&vm, &cnt) != KERN_SUCCESS) return 0;
|
||||
uint64_t avail = (uint64_t)vm.free_count + (uint64_t)vm.inactive_count
|
||||
+ (uint64_t)vm.purgeable_count;
|
||||
return avail * (uint64_t)page;
|
||||
#else
|
||||
FILE* f = fopen("/proc/meminfo", "r");
|
||||
if (!f) return 0;
|
||||
char line[256]; unsigned long long kb = 0;
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (sscanf(line, "MemAvailable: %llu kB", &kb) == 1) break;
|
||||
fclose(f);
|
||||
return (uint64_t)kb * 1024ull;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Shrink the budget when the machine is short on memory.
|
||||
*
|
||||
* A pool that can only grow is a leak with extra steps. This is the other half
|
||||
* of the control loop: if free memory drops below a floor, hand frames back.
|
||||
* The resident set follows on the next eviction pass, so the memory is actually
|
||||
* returned rather than merely re-labelled. */
|
||||
#ifndef ENGRAM_POOL_FREE_FLOOR_BYTES
|
||||
#define ENGRAM_POOL_FREE_FLOOR_BYTES (2ull*1024ull*1024ull*1024ull) /* 2 GiB */
|
||||
#endif
|
||||
static int pc_relieve_pressure(PgCache* c){
|
||||
uint64_t avail = pc_available_ram();
|
||||
if (!avail) return 0;
|
||||
uint64_t floor_b = ENGRAM_POOL_FREE_FLOOR_BYTES;
|
||||
const char* fe = getenv("ENGRAM_POOL_FREE_FLOOR_MB");
|
||||
if (fe && *fe){ unsigned long v = strtoul(fe, NULL, 10); if (v) floor_b = (uint64_t)v * 1024ull * 1024ull; }
|
||||
if (avail >= floor_b) return 0; /* machine has room */
|
||||
if (!c->cap || c->count == 0) return 0;
|
||||
size_t was = c->cap;
|
||||
size_t want = c->count - (c->count / 4); /* give back ~25% of what we hold */
|
||||
if (want < 4096) want = 4096;
|
||||
if (want >= c->cap) return 0;
|
||||
c->cap = want;
|
||||
c->adapt_shrinks++;
|
||||
fprintf(stderr,
|
||||
"[engram] memory pressure: %.2f GiB available (floor %.2f GiB) — shrinking pool "
|
||||
"budget %zu -> %zu frames (%.2f -> %.2f GiB) and releasing frames.\n",
|
||||
(double)avail/(1024.0*1024.0*1024.0), (double)floor_b/(1024.0*1024.0*1024.0),
|
||||
was, c->cap,
|
||||
(double)was * (double)STORE_PAGE_SIZE/(1024.0*1024.0*1024.0),
|
||||
(double)c->cap* (double)STORE_PAGE_SIZE/(1024.0*1024.0*1024.0));
|
||||
fflush(stderr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void pc_adapt_budget(PgCache* c){
|
||||
if (!c->cap) return; /* unlimited: nothing to adapt */
|
||||
if (getenv("ENGRAM_POOL_FRAMES")) return; /* explicit operator override wins */
|
||||
@@ -1872,12 +1941,26 @@ static void pc_adapt_budget(PgCache* c){
|
||||
if (d_evic * 3 < d_acc) return; /* < 1/3 of accesses evict: healthy */
|
||||
if (d_hits * 4 < d_acc) return; /* little reuse: a scan, not pressure */
|
||||
|
||||
uint64_t ram = pc_ram_bytes_live(); /* live, not a boot-time constant */
|
||||
/* Growth is bounded by what is AVAILABLE, never by total RAM. Sizing against
|
||||
* total is how a cache starves its own host: total never shrinks when other
|
||||
* processes need memory. Refuse to grow at all if availability is unknown or
|
||||
* already under the floor — a cache is never worth swapping the machine. */
|
||||
uint64_t avail = pc_available_ram();
|
||||
uint64_t floor_b = ENGRAM_POOL_FREE_FLOOR_BYTES;
|
||||
const char* fe = getenv("ENGRAM_POOL_FREE_FLOOR_MB");
|
||||
if (fe && *fe){ unsigned long v = strtoul(fe, NULL, 10); if (v) floor_b = (uint64_t)v * 1024ull * 1024ull; }
|
||||
if (!avail || avail <= floor_b) return;
|
||||
uint64_t ram = pc_ram_bytes_live();
|
||||
if (!ram) return;
|
||||
unsigned pct = 80; /* hard ceiling for autonomous growth */
|
||||
unsigned pct = 50; /* ceiling as a share of TOTAL, belt-and-braces */
|
||||
const char* mp = getenv("ENGRAM_POOL_MAX_PCT");
|
||||
if (mp && *mp){ unsigned long v = strtoul(mp, NULL, 10); if (v > 0 && v <= 95) pct = (unsigned)v; }
|
||||
size_t ceiling = (size_t)(((ram / 100u) * pct) / (uint64_t)STORE_PAGE_SIZE);
|
||||
/* and never grow into the free-memory floor */
|
||||
uint64_t headroom = avail - floor_b;
|
||||
size_t ceil_avail = (size_t)((c->count * (uint64_t)STORE_PAGE_SIZE + headroom)
|
||||
/ (uint64_t)STORE_PAGE_SIZE);
|
||||
if (ceil_avail < ceiling) ceiling = ceil_avail;
|
||||
if (c->cap >= ceiling) return; /* already at the machine's limit */
|
||||
|
||||
size_t want = c->cap + (c->cap / 2) + 1; /* ×1.5, geometric */
|
||||
@@ -1908,7 +1991,7 @@ static void pc_evict_to_budget(PgCache* c){
|
||||
}
|
||||
if (!freed) break; /* nothing evictable — allowed to exceed cap */
|
||||
}
|
||||
pc_adapt_budget(c);
|
||||
if (!pc_relieve_pressure(c)) pc_adapt_budget(c);
|
||||
}
|
||||
|
||||
static PgEnt* pc_get(EngramPagedStore* s, uint64_t id){
|
||||
|
||||
Reference in New Issue
Block a user