/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ #include "hybridmem.h" #include "hdc/hdc.h" #include "tensor/tensor.h" #include "safetensors/safetensors.h" #include #include #include #include #include #include #include #include /* ============================================================= */ /* Internal types */ /* ============================================================= */ /* Per-entry metadata stored in memory */ typedef struct { uint64_t entry_id; int layer_idx; int n_tokens; float importance; uint64_t timestamp; uint32_t access_count; int shard_idx; bool evicted; fv_vector *signature; /* HDC signature for search */ } fm_entry; /* Shard tracking */ typedef struct { char path[512]; size_t byte_size; int num_entries; } fm_shard; /* LRU shard cache — keeps up to max_open_shards fs_file handles open */ typedef struct { int shard_idx; /* which shard this slot caches (-1 = empty) */ fs_file *file; /* open file handle (mmap'd) */ uint64_t last_access; /* monotonic counter for LRU eviction */ } fm__shard_cache_slot; /* Write buffer entry — holds K/V data in memory until flushed to disk */ typedef struct { uint64_t entry_id; float *k_data; /* owned copy: [n_tokens * embed_dim] */ float *v_data; /* owned copy: [n_tokens * embed_dim] */ int n_tokens; int shard_idx; /* target shard (assigned at put time) */ } fm__write_buf_entry; /* ---- LSH index for sub-linear query ---- */ #define FM_LSH_NUM_TABLES 8 /* number of hash tables */ #define FM_LSH_BITS 14 /* bits per hash key → 16384 buckets */ #define FM_LSH_NUM_BUCKETS (1 << FM_LSH_BITS) /* Each hash table selects FM_LSH_BITS random bit positions from the HDC signature */ typedef struct { int bit_positions[FM_LSH_BITS]; /* which bits to extract */ } fm__lsh_table_config; /* Dynamic bucket: array of entry indices */ typedef struct { size_t *indices; int count; int cap; } fm__lsh_bucket; /* Full LSH index */ typedef struct { fm__lsh_table_config tables[FM_LSH_NUM_TABLES]; fm__lsh_bucket *buckets; /* [FM_LSH_NUM_TABLES * FM_LSH_NUM_BUCKETS] */ bool built; } fm__lsh_index; /* Store internal state */ struct fm_store { fm_config config; char store_dir[512]; /* Projection */ fv_projector *projector; /* Entries */ fm_entry *entries; size_t num_entries; size_t entries_capacity; /* Shards */ fm_shard *shards; int num_shards; int shards_capacity; /* Shard cache (LRU pool of open file handles) */ fm__shard_cache_slot *shard_cache; int shard_cache_cap; uint64_t shard_cache_clock; /* Write buffer — batches pending writes to avoid per-entry shard rewrites */ fm__write_buf_entry *write_buf; int write_buf_count; int write_buf_cap; /* LSH index for sub-linear query */ fm__lsh_index *lsh; /* ID counter */ uint64_t next_id; /* Stats */ size_t total_stores; size_t total_queries; size_t total_hits; size_t disk_bytes_used; /* Timestamp counter (monotonic) */ uint64_t timestamp; }; /* ============================================================= */ /* Internal helpers */ /* ============================================================= */ static int fm__ensure_dir(const char *path) { struct stat st; if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) return 0; return mkdir(path, 0755); } static void fm__shard_path(fm_store *store, int shard_idx, char *buf, size_t bufsz) { snprintf(buf, bufsz, "%s/shard_%04d.safetensors", store->store_dir, shard_idx); } /* ---- Shard cache helpers ---- */ /* Get a cached fs_file* for the given shard, opening on miss. * Returns NULL if the shard file cannot be opened. The returned * pointer is owned by the cache — caller must NOT fs_close() it. */ static fs_file* fm__shard_cache_get(fm_store *store, int shard_idx) { if (!store->shard_cache || shard_idx < 0) return NULL; /* Check for cache hit */ for (int i = 0; i < store->shard_cache_cap; i++) { if (store->shard_cache[i].shard_idx == shard_idx) { store->shard_cache[i].last_access = ++store->shard_cache_clock; return store->shard_cache[i].file; } } /* Cache miss — find a slot (empty or LRU) */ int victim = 0; uint64_t oldest = UINT64_MAX; for (int i = 0; i < store->shard_cache_cap; i++) { if (store->shard_cache[i].shard_idx < 0) { victim = i; break; } if (store->shard_cache[i].last_access < oldest) { oldest = store->shard_cache[i].last_access; victim = i; } } /* Evict victim if occupied */ if (store->shard_cache[victim].file) { fs_close(store->shard_cache[victim].file); store->shard_cache[victim].file = NULL; store->shard_cache[victim].shard_idx = -1; } /* Open shard file */ fm_shard *shard = &store->shards[shard_idx]; fs_file *f = fs_open(shard->path); if (!f) return NULL; store->shard_cache[victim].shard_idx = shard_idx; store->shard_cache[victim].file = f; store->shard_cache[victim].last_access = ++store->shard_cache_clock; return f; } /* Invalidate a shard's cache entry (e.g., after rewriting the file) */ static void fm__shard_cache_invalidate(fm_store *store, int shard_idx) { if (!store->shard_cache) return; for (int i = 0; i < store->shard_cache_cap; i++) { if (store->shard_cache[i].shard_idx == shard_idx) { if (store->shard_cache[i].file) { fs_close(store->shard_cache[i].file); } store->shard_cache[i].file = NULL; store->shard_cache[i].shard_idx = -1; return; } } } /* Close all cached shard handles */ static void fm__shard_cache_free(fm_store *store) { if (!store->shard_cache) return; for (int i = 0; i < store->shard_cache_cap; i++) { if (store->shard_cache[i].file) { fs_close(store->shard_cache[i].file); } } free(store->shard_cache); store->shard_cache = NULL; } /* ---- Write buffer helpers ---- */ /* Add K/V data to write buffer. Copies the tensor data. */ static int fm__write_buf_add(fm_store *store, uint64_t entry_id, const tm_tensor *keys, const tm_tensor *values, int shard_idx) { if (store->write_buf_count >= store->write_buf_cap) { int new_cap = store->write_buf_cap == 0 ? 64 : store->write_buf_cap * 2; fm__write_buf_entry *nb = realloc(store->write_buf, new_cap * sizeof(fm__write_buf_entry)); if (!nb) return -1; store->write_buf = nb; store->write_buf_cap = new_cap; } int n_tokens = (int)keys->shape[0]; int embed_dim = (int)keys->shape[1]; size_t data_bytes = (size_t)n_tokens * embed_dim * sizeof(float); fm__write_buf_entry *wb = &store->write_buf[store->write_buf_count]; wb->entry_id = entry_id; wb->n_tokens = n_tokens; wb->shard_idx = shard_idx; wb->k_data = malloc(data_bytes); wb->v_data = malloc(data_bytes); if (!wb->k_data || !wb->v_data) { free(wb->k_data); free(wb->v_data); return -1; } memcpy(wb->k_data, keys->data, data_bytes); memcpy(wb->v_data, values->data, data_bytes); store->write_buf_count++; return 0; } /* Load K/V from write buffer for a given entry_id. * Returns 0 on success (found in buffer), -1 if not found. */ static int fm__write_buf_load(fm_store *store, uint64_t entry_id, tm_tensor **keys_out, tm_tensor **values_out) { int embed_dim = store->config.embed_dim; for (int i = 0; i < store->write_buf_count; i++) { fm__write_buf_entry *wb = &store->write_buf[i]; if (wb->entry_id != entry_id) continue; int64_t shape[] = {wb->n_tokens, embed_dim}; size_t data_bytes = (size_t)wb->n_tokens * embed_dim * sizeof(float); tm_tensor *k = tm_zeros(shape, 2, TM_F32); tm_tensor *v = tm_zeros(shape, 2, TM_F32); if (!k || !v) { tm_free(k); tm_free(v); return -1; } memcpy(k->data, wb->k_data, data_bytes); memcpy(v->data, wb->v_data, data_bytes); *keys_out = k; *values_out = v; return 0; } return -1; } /* Flush write buffer to disk — one shard write per unique shard_idx. * Returns 0 on success, -1 on error. */ static int fm__write_buf_flush(fm_store *store) { if (store->write_buf_count == 0) return 0; if (!store->store_dir[0]) { /* No disk — just discard buffer */ for (int i = 0; i < store->write_buf_count; i++) { free(store->write_buf[i].k_data); free(store->write_buf[i].v_data); } store->write_buf_count = 0; return 0; } fm__ensure_dir(store->store_dir); /* Group by shard_idx and write each shard once */ /* Find unique shard indices */ int max_shard = -1; for (int i = 0; i < store->write_buf_count; i++) { if (store->write_buf[i].shard_idx > max_shard) max_shard = store->write_buf[i].shard_idx; } for (int si = 0; si <= max_shard; si++) { /* Count entries for this shard */ int count = 0; for (int i = 0; i < store->write_buf_count; i++) { if (store->write_buf[i].shard_idx == si) count++; } if (count == 0) continue; /* Invalidate shard cache before rewriting */ fm__shard_cache_invalidate(store, si); fm_shard *shard = &store->shards[si]; fs_writer *w = fs_writer_new(); if (!w) return -1; /* Copy existing shard contents */ fs_file *existing = fs_open(shard->path); if (existing) { int nc = 0; const char **names = fs_tensor_names(existing, &nc); if (names) { for (int i = 0; i < nc; i++) { fs_tensor_info *info = fs_get_info(existing, names[i]); if (info) { const void *data = fs_get_data(existing, names[i]); if (data) { fs_writer_add_tensor(w, names[i], info->dtype, info->shape, info->ndim, data); } fs_free_info(info); } } free(names); } fs_close(existing); } /* Add all buffered entries for this shard */ int embed_dim = store->config.embed_dim; for (int i = 0; i < store->write_buf_count; i++) { fm__write_buf_entry *wb = &store->write_buf[i]; if (wb->shard_idx != si) continue; char key_name[64], val_name[64]; snprintf(key_name, sizeof(key_name), "k_%llu", (unsigned long long)wb->entry_id); snprintf(val_name, sizeof(val_name), "v_%llu", (unsigned long long)wb->entry_id); int64_t shape[] = {wb->n_tokens, embed_dim}; fs_writer_add_f32(w, key_name, shape, 2, wb->k_data); fs_writer_add_f32(w, val_name, shape, 2, wb->v_data); } bool ok = fs_writer_save(w, shard->path); fs_writer_free(w); if (!ok) return -1; /* Update shard size */ struct stat st; if (stat(shard->path, &st) == 0) { shard->byte_size = st.st_size; } shard->num_entries += count; } /* Free buffer entries */ for (int i = 0; i < store->write_buf_count; i++) { free(store->write_buf[i].k_data); free(store->write_buf[i].v_data); } store->write_buf_count = 0; /* Recalculate disk usage */ store->disk_bytes_used = 0; for (int i = 0; i < store->num_shards; i++) { store->disk_bytes_used += store->shards[i].byte_size; } return 0; } /* Free write buffer without flushing */ static void fm__write_buf_free(fm_store *store) { for (int i = 0; i < store->write_buf_count; i++) { free(store->write_buf[i].k_data); free(store->write_buf[i].v_data); } free(store->write_buf); store->write_buf = NULL; store->write_buf_count = 0; store->write_buf_cap = 0; } /* ---- LSH index helpers ---- */ /* Simple xorshift64 PRNG for selecting random bit positions */ static uint64_t fm__lsh_rand(uint64_t *state) { uint64_t x = *state; x ^= x << 13; x ^= x >> 7; x ^= x << 17; *state = x; return x; } /* Compute hash key for a signature in table t */ static uint32_t fm__lsh_hash(const fm__lsh_table_config *tc, const fv_vector *sig) { uint32_t key = 0; for (int b = 0; b < FM_LSH_BITS; b++) { int pos = tc->bit_positions[b]; int word = pos / 64; int bit = pos % 64; if (sig->bits[word] & ((uint64_t)1 << bit)) key |= ((uint32_t)1 << b); } return key; } /* Add an entry index to a bucket */ static void fm__lsh_bucket_add(fm__lsh_bucket *bucket, size_t idx) { if (bucket->count >= bucket->cap) { int new_cap = bucket->cap == 0 ? 4 : bucket->cap * 2; size_t *ni = realloc(bucket->indices, new_cap * sizeof(size_t)); if (!ni) return; bucket->indices = ni; bucket->cap = new_cap; } bucket->indices[bucket->count++] = idx; } /* Create and initialize LSH index */ static fm__lsh_index* fm__lsh_new(int hdc_dim, uint64_t seed) { fm__lsh_index *lsh = calloc(1, sizeof(fm__lsh_index)); if (!lsh) return NULL; size_t total_buckets = (size_t)FM_LSH_NUM_TABLES * FM_LSH_NUM_BUCKETS; lsh->buckets = calloc(total_buckets, sizeof(fm__lsh_bucket)); if (!lsh->buckets) { free(lsh); return NULL; } /* Select random bit positions for each table */ uint64_t rng = seed ^ 0xDEADBEEFCAFEULL; for (int t = 0; t < FM_LSH_NUM_TABLES; t++) { for (int b = 0; b < FM_LSH_BITS; b++) { lsh->tables[t].bit_positions[b] = (int)(fm__lsh_rand(&rng) % (uint64_t)hdc_dim); } } lsh->built = true; return lsh; } /* Insert an entry into the LSH index */ static void fm__lsh_insert(fm__lsh_index *lsh, size_t entry_idx, const fv_vector *sig) { if (!lsh || !lsh->built || !sig) return; for (int t = 0; t < FM_LSH_NUM_TABLES; t++) { uint32_t key = fm__lsh_hash(&lsh->tables[t], sig); fm__lsh_bucket *bucket = &lsh->buckets[(size_t)t * FM_LSH_NUM_BUCKETS + key]; fm__lsh_bucket_add(bucket, entry_idx); } } /* Query the LSH index: returns candidate entry indices. * Caller must free(*out_indices). Returns count of candidates. */ static int fm__lsh_query(fm__lsh_index *lsh, const fv_vector *query_hv, size_t **out_indices) { if (!lsh || !lsh->built || !query_hv) { *out_indices = NULL; return 0; } /* Collect candidates from all tables into a flat array */ size_t *candidates = NULL; int n_candidates = 0; int candidates_cap = 0; for (int t = 0; t < FM_LSH_NUM_TABLES; t++) { uint32_t key = fm__lsh_hash(&lsh->tables[t], query_hv); fm__lsh_bucket *bucket = &lsh->buckets[(size_t)t * FM_LSH_NUM_BUCKETS + key]; for (int i = 0; i < bucket->count; i++) { if (n_candidates >= candidates_cap) { int new_cap = candidates_cap == 0 ? 64 : candidates_cap * 2; size_t *nc = realloc(candidates, new_cap * sizeof(size_t)); if (!nc) break; candidates = nc; candidates_cap = new_cap; } candidates[n_candidates++] = bucket->indices[i]; } } if (n_candidates == 0) { *out_indices = NULL; return 0; } /* Deduplicate by sorting then removing consecutive duplicates */ /* Simple insertion sort — n_candidates is typically small (<200) */ for (int i = 1; i < n_candidates; i++) { size_t val = candidates[i]; int j = i - 1; while (j >= 0 && candidates[j] > val) { candidates[j + 1] = candidates[j]; j--; } candidates[j + 1] = val; } int unique = 0; for (int i = 0; i < n_candidates; i++) { if (i == 0 || candidates[i] != candidates[i - 1]) { candidates[unique++] = candidates[i]; } } *out_indices = candidates; return unique; } /* Free the LSH index */ static void fm__lsh_free(fm__lsh_index *lsh) { if (!lsh) return; if (lsh->buckets) { size_t total = (size_t)FM_LSH_NUM_TABLES * FM_LSH_NUM_BUCKETS; for (size_t i = 0; i < total; i++) { free(lsh->buckets[i].indices); } free(lsh->buckets); } free(lsh); } /* Mean-pool a 2D tensor [n_tokens, embed_dim] -> [embed_dim] */ static tm_tensor* fm__mean_pool(const tm_tensor *t) { if (!t || t->ndim != 2 || t->dtype != TM_F32) return NULL; int64_t n_tokens = t->shape[0]; int64_t embed_dim = t->shape[1]; int64_t shape[1] = { embed_dim }; tm_tensor *out = tm_zeros(shape, 1, TM_F32); if (!out) return NULL; float *o = (float *)out->data; const float *d = (const float *)t->data; for (int64_t tok = 0; tok < n_tokens; tok++) { for (int64_t dim = 0; dim < embed_dim; dim++) { o[dim] += d[tok * embed_dim + dim]; } } float inv_n = 1.0f / (float)n_tokens; for (int64_t dim = 0; dim < embed_dim; dim++) { o[dim] *= inv_n; } return out; } /* Grow entries array if needed */ static int fm__grow_entries(fm_store *store) { if (store->num_entries < store->entries_capacity) return 0; size_t new_cap = store->entries_capacity == 0 ? 64 : store->entries_capacity * 2; fm_entry *new_entries = realloc(store->entries, new_cap * sizeof(fm_entry)); if (!new_entries) return -1; store->entries = new_entries; store->entries_capacity = new_cap; return 0; } /* Grow shards array if needed */ static int fm__grow_shards(fm_store *store) { if (store->num_shards < store->shards_capacity) return 0; int new_cap = store->shards_capacity == 0 ? 8 : store->shards_capacity * 2; fm_shard *new_shards = realloc(store->shards, new_cap * sizeof(fm_shard)); if (!new_shards) return -1; store->shards = new_shards; store->shards_capacity = new_cap; return 0; } /* Get or create current shard for writing */ static int fm__current_shard(fm_store *store) { if (store->num_shards > 0) { fm_shard *last = &store->shards[store->num_shards - 1]; if (last->byte_size < store->config.shard_max_bytes) { return store->num_shards - 1; } } /* Create new shard */ if (fm__grow_shards(store) != 0) return -1; int idx = store->num_shards++; fm_shard *s = &store->shards[idx]; memset(s, 0, sizeof(fm_shard)); fm__shard_path(store, idx, s->path, sizeof(s->path)); s->byte_size = 0; s->num_entries = 0; return idx; } /* Load K/V from shard for a given entry_id. * Uses shard cache to avoid repeated open/mmap/close syscalls. */ static int fm__load_from_shard(fm_store *store, int shard_idx, uint64_t entry_id, tm_tensor **keys_out, tm_tensor **values_out) { if (shard_idx < 0 || shard_idx >= store->num_shards) return -1; /* Try cached handle first, fall back to direct open */ fs_file *f = fm__shard_cache_get(store, shard_idx); bool cached = (f != NULL); if (!f) { fm_shard *shard = &store->shards[shard_idx]; f = fs_open(shard->path); if (!f) return -1; } char key_name[64], val_name[64]; snprintf(key_name, sizeof(key_name), "k_%llu", (unsigned long long)entry_id); snprintf(val_name, sizeof(val_name), "v_%llu", (unsigned long long)entry_id); /* Get tensor info for shapes */ fs_tensor_info *k_info = fs_get_info(f, key_name); fs_tensor_info *v_info = fs_get_info(f, val_name); if (!k_info || !v_info) { if (k_info) fs_free_info(k_info); if (v_info) fs_free_info(v_info); if (!cached) fs_close(f); return -1; } /* Read data (from mmap — zero-copy read) */ const float *k_data = fs_get_f32(f, key_name); const float *v_data = fs_get_f32(f, val_name); if (!k_data || !v_data) { fs_free_info(k_info); fs_free_info(v_info); if (!cached) fs_close(f); return -1; } /* Create tensors (must copy — the mmap may be evicted from cache later) */ tm_tensor *k_tensor = tm_zeros(k_info->shape, k_info->ndim, TM_F32); tm_tensor *v_tensor = tm_zeros(v_info->shape, v_info->ndim, TM_F32); if (!k_tensor || !v_tensor) { if (k_tensor) tm_free(k_tensor); if (v_tensor) tm_free(v_tensor); fs_free_info(k_info); fs_free_info(v_info); if (!cached) fs_close(f); return -1; } memcpy(k_tensor->data, k_data, k_info->data_size); memcpy(v_tensor->data, v_data, v_info->data_size); *keys_out = k_tensor; *values_out = v_tensor; fs_free_info(k_info); fs_free_info(v_info); if (!cached) fs_close(f); return 0; } /* ============================================================= */ /* Config */ /* ============================================================= */ fm_config fm_config_default(int embed_dim) { fm_config c; memset(&c, 0, sizeof(fm_config)); c.store_dir = NULL; c.embed_dim = embed_dim; c.hdc_dim = 4096; c.seed = 42; c.max_entries = 0; /* unlimited */ c.max_disk_bytes = 0; /* unlimited */ c.shard_max_bytes = 256 * 1024 * 1024; /* 256 MB */ c.max_open_shards = 4; c.quantize_stored = false; return c; } /* ============================================================= */ /* Lifecycle */ /* ============================================================= */ fm_store* fm_store_new(fm_config *config) { if (!config || config->embed_dim <= 0) return NULL; fm_store *store = calloc(1, sizeof(fm_store)); if (!store) return NULL; store->config = *config; store->next_id = 1; store->timestamp = 0; /* Set up store directory */ if (config->store_dir) { snprintf(store->store_dir, sizeof(store->store_dir), "%s", config->store_dir); fm__ensure_dir(store->store_dir); } /* Create projector for HDC encoding */ store->projector = fv_projector_new(config->embed_dim, config->hdc_dim, config->seed); if (!store->projector) { free(store); return NULL; } /* Initialize shard cache */ int cache_cap = config->max_open_shards > 0 ? config->max_open_shards : 4; store->shard_cache = calloc(cache_cap, sizeof(fm__shard_cache_slot)); if (store->shard_cache) { store->shard_cache_cap = cache_cap; for (int i = 0; i < cache_cap; i++) store->shard_cache[i].shard_idx = -1; } store->shard_cache_clock = 0; /* Initialize LSH index */ store->lsh = fm__lsh_new(config->hdc_dim, config->seed); return store; } fm_store* fm_store_open(const char *store_dir) { if (!store_dir) return NULL; /* Read index.json for config */ char index_path[600]; snprintf(index_path, sizeof(index_path), "%s/index.json", store_dir); FILE *f = fopen(index_path, "r"); if (!f) return NULL; /* Parse minimal JSON: embed_dim, hdc_dim, seed, next_id, num_shards */ char buf[4096]; size_t n = fread(buf, 1, sizeof(buf) - 1, f); buf[n] = '\0'; fclose(f); int embed_dim = 0, hdc_dim = 4096, num_shards = 0; uint64_t seed = 42, next_id = 1; /* Simple JSON parsing (no external deps) */ char *p; if ((p = strstr(buf, "\"embed_dim\""))) sscanf(p, "\"embed_dim\": %d", &embed_dim); if ((p = strstr(buf, "\"hdc_dim\""))) sscanf(p, "\"hdc_dim\": %d", &hdc_dim); if ((p = strstr(buf, "\"seed\""))) sscanf(p, "\"seed\": %llu", (unsigned long long *)&seed); if ((p = strstr(buf, "\"next_id\""))) sscanf(p, "\"next_id\": %llu", (unsigned long long *)&next_id); if ((p = strstr(buf, "\"num_shards\""))) sscanf(p, "\"num_shards\": %d", &num_shards); if (embed_dim <= 0) return NULL; fm_config config = fm_config_default(embed_dim); config.store_dir = store_dir; config.hdc_dim = hdc_dim; config.seed = seed; fm_store *store = fm_store_new(&config); if (!store) return NULL; store->next_id = next_id; /* Load entries from entries.bin */ char entries_path[600]; snprintf(entries_path, sizeof(entries_path), "%s/entries.bin", store_dir); FILE *ef = fopen(entries_path, "rb"); if (ef) { /* Read entries */ uint32_t entry_count; if (fread(&entry_count, sizeof(uint32_t), 1, ef) == 1) { for (uint32_t i = 0; i < entry_count; i++) { if (fm__grow_entries(store) != 0) break; fm_entry *e = &store->entries[store->num_entries]; memset(e, 0, sizeof(fm_entry)); /* Read fixed fields */ if (fread(&e->entry_id, sizeof(uint64_t), 1, ef) != 1) break; uint32_t layer32; if (fread(&layer32, sizeof(uint32_t), 1, ef) != 1) break; e->layer_idx = (int)layer32; uint32_t ntok; if (fread(&ntok, sizeof(uint32_t), 1, ef) != 1) break; e->n_tokens = (int)ntok; if (fread(&e->importance, sizeof(float), 1, ef) != 1) break; if (fread(&e->timestamp, sizeof(uint64_t), 1, ef) != 1) break; if (fread(&e->access_count, sizeof(uint32_t), 1, ef) != 1) break; uint32_t shard32; if (fread(&shard32, sizeof(uint32_t), 1, ef) != 1) break; e->shard_idx = (int)shard32; uint32_t pad; if (fread(&pad, sizeof(uint32_t), 1, ef) != 1) break; /* Read HDC signature */ e->signature = fv_alloc(hdc_dim); if (!e->signature) break; int words = (hdc_dim + 63) / 64; if (fread(e->signature->bits, sizeof(uint64_t), words, ef) != (size_t)words) { fv_free(e->signature); e->signature = NULL; break; } e->evicted = false; store->num_entries++; } } fclose(ef); } /* Reconstruct shard tracking */ for (int i = 0; i < num_shards; i++) { if (fm__grow_shards(store) != 0) break; fm_shard *s = &store->shards[store->num_shards]; memset(s, 0, sizeof(fm_shard)); fm__shard_path(store, i, s->path, sizeof(s->path)); struct stat st; if (stat(s->path, &st) == 0) { s->byte_size = st.st_size; store->disk_bytes_used += st.st_size; } store->num_shards++; } /* Populate LSH index with loaded entries */ if (store->lsh) { for (size_t i = 0; i < store->num_entries; i++) { fm_entry *e = &store->entries[i]; if (!e->evicted && e->signature) { fm__lsh_insert(store->lsh, i, e->signature); } } } return store; } void fm_store_free(fm_store *store) { if (!store) return; fm__lsh_free(store->lsh); fm__write_buf_free(store); fm__shard_cache_free(store); for (size_t i = 0; i < store->num_entries; i++) { fv_free(store->entries[i].signature); } free(store->entries); free(store->shards); fv_projector_free(store->projector); free(store); } int fm_store_flush(fm_store *store) { if (!store || !store->store_dir[0]) return -1; fm__ensure_dir(store->store_dir); /* Flush write buffer to disk first */ if (fm__write_buf_flush(store) != 0) return -1; /* Write index.json */ char index_path[600]; snprintf(index_path, sizeof(index_path), "%s/index.json", store->store_dir); FILE *f = fopen(index_path, "w"); if (!f) return -1; fprintf(f, "{\n"); fprintf(f, " \"embed_dim\": %d,\n", store->config.embed_dim); fprintf(f, " \"hdc_dim\": %d,\n", store->config.hdc_dim); fprintf(f, " \"seed\": %llu,\n", (unsigned long long)store->config.seed); fprintf(f, " \"next_id\": %llu,\n", (unsigned long long)store->next_id); fprintf(f, " \"num_entries\": %zu,\n", store->num_entries); fprintf(f, " \"num_shards\": %d\n", store->num_shards); fprintf(f, "}\n"); fclose(f); /* Write entries.bin */ char entries_path[600]; snprintf(entries_path, sizeof(entries_path), "%s/entries.bin", store->store_dir); FILE *ef = fopen(entries_path, "wb"); if (!ef) return -1; /* Count non-evicted entries */ uint32_t live_count = 0; for (size_t i = 0; i < store->num_entries; i++) { if (!store->entries[i].evicted) live_count++; } fwrite(&live_count, sizeof(uint32_t), 1, ef); int hdc_dim = store->config.hdc_dim; int words = (hdc_dim + 63) / 64; for (size_t i = 0; i < store->num_entries; i++) { fm_entry *e = &store->entries[i]; if (e->evicted) continue; fwrite(&e->entry_id, sizeof(uint64_t), 1, ef); uint32_t layer32 = (uint32_t)e->layer_idx; fwrite(&layer32, sizeof(uint32_t), 1, ef); uint32_t ntok = (uint32_t)e->n_tokens; fwrite(&ntok, sizeof(uint32_t), 1, ef); fwrite(&e->importance, sizeof(float), 1, ef); fwrite(&e->timestamp, sizeof(uint64_t), 1, ef); fwrite(&e->access_count, sizeof(uint32_t), 1, ef); uint32_t shard32 = (uint32_t)e->shard_idx; fwrite(&shard32, sizeof(uint32_t), 1, ef); uint32_t pad = 0; fwrite(&pad, sizeof(uint32_t), 1, ef); /* Write signature bits */ if (e->signature) { fwrite(e->signature->bits, sizeof(uint64_t), words, ef); } else { /* Write zeros if no signature */ for (int w = 0; w < words; w++) { uint64_t z = 0; fwrite(&z, sizeof(uint64_t), 1, ef); } } } fclose(ef); return 0; } /* ============================================================= */ /* Store */ /* ============================================================= */ uint64_t fm_put(fm_store *store, tm_tensor *keys, tm_tensor *values, int layer_idx, float importance) { if (!store || !keys || !values) return 0; if (keys->ndim != 2 || values->ndim != 2) return 0; if (keys->dtype != TM_F32 || values->dtype != TM_F32) return 0; if (keys->shape[0] != values->shape[0]) return 0; if (keys->shape[1] != store->config.embed_dim) return 0; if (values->shape[1] != store->config.embed_dim) return 0; /* Auto-evict if at capacity */ if (store->config.max_entries > 0 && store->num_entries >= store->config.max_entries) { fm_evict(store, store->config.max_entries - 1); } /* Mean-pool keys for HDC signature */ tm_tensor *pooled = fm__mean_pool(keys); if (!pooled) return 0; fv_vector *sig = fv_project(store->projector, pooled); tm_free(pooled); if (!sig) return 0; /* Get shard for storage */ int shard_idx = fm__current_shard(store); if (shard_idx < 0) { fv_free(sig); return 0; } uint64_t id = store->next_id++; /* Buffer K/V data in memory (deferred write — flushed on fm_store_flush) */ if (store->store_dir[0]) { fm__ensure_dir(store->store_dir); if (fm__write_buf_add(store, id, keys, values, shard_idx) != 0) { fv_free(sig); return 0; } } /* Add entry to index */ if (fm__grow_entries(store) != 0) { fv_free(sig); return 0; } fm_entry *e = &store->entries[store->num_entries++]; memset(e, 0, sizeof(fm_entry)); e->entry_id = id; e->layer_idx = layer_idx; e->n_tokens = (int)keys->shape[0]; e->importance = importance; e->timestamp = store->timestamp++; e->access_count = 0; e->shard_idx = shard_idx; e->evicted = false; e->signature = sig; /* Insert into LSH index */ fm__lsh_insert(store->lsh, store->num_entries - 1, sig); store->total_stores++; return id; } uint64_t fm_put_presig(fm_store *store, tm_tensor *keys, tm_tensor *values, int layer_idx, float importance, fv_vector *presig) { if (!store || !keys || !values || !presig) return 0; if (keys->ndim != 2 || values->ndim != 2) return 0; if (keys->dtype != TM_F32 || values->dtype != TM_F32) return 0; if (keys->shape[0] != values->shape[0]) return 0; if (keys->shape[1] != store->config.embed_dim) return 0; if (values->shape[1] != store->config.embed_dim) return 0; /* Auto-evict if at capacity */ if (store->config.max_entries > 0 && store->num_entries >= store->config.max_entries) { fm_evict(store, store->config.max_entries - 1); } /* Get shard for storage */ int shard_idx = fm__current_shard(store); if (shard_idx < 0) { fv_free(presig); return 0; } uint64_t id = store->next_id++; /* Buffer K/V data in memory */ if (store->store_dir[0]) { fm__ensure_dir(store->store_dir); if (fm__write_buf_add(store, id, keys, values, shard_idx) != 0) { fv_free(presig); return 0; } } /* Add entry to index */ if (fm__grow_entries(store) != 0) { fv_free(presig); return 0; } fm_entry *e = &store->entries[store->num_entries++]; memset(e, 0, sizeof(fm_entry)); e->entry_id = id; e->layer_idx = layer_idx; e->n_tokens = (int)keys->shape[0]; e->importance = importance; e->timestamp = store->timestamp++; e->access_count = 0; e->shard_idx = shard_idx; e->evicted = false; e->signature = presig; /* takes ownership */ /* Insert into LSH index */ fm__lsh_insert(store->lsh, store->num_entries - 1, presig); store->total_stores++; return id; } fv_projector* fm_store_projector(fm_store *store) { return store ? store->projector : NULL; } /* ============================================================= */ /* Query */ /* ============================================================= */ /* Min-heap for top-k selection: O(N log k) instead of O(N²) sort. * Maintains a heap of size k where the root is the SMALLEST score. * When a new candidate has score > root, replace root and sift down. */ typedef struct { size_t idx; float score; } fm__heap_item; static void fm__heap_sift_down(fm__heap_item *heap, int n, int i) { while (1) { int smallest = i; int left = 2 * i + 1; int right = 2 * i + 2; if (left < n && heap[left].score < heap[smallest].score) smallest = left; if (right < n && heap[right].score < heap[smallest].score) smallest = right; if (smallest == i) break; fm__heap_item tmp = heap[i]; heap[i] = heap[smallest]; heap[smallest] = tmp; i = smallest; } } static void fm__heap_sift_up(fm__heap_item *heap, int i) { while (i > 0) { int parent = (i - 1) / 2; if (heap[i].score >= heap[parent].score) break; fm__heap_item tmp = heap[i]; heap[i] = heap[parent]; heap[parent] = tmp; i = parent; } } fm_results* fm_query(fm_store *store, tm_tensor *query, int layer_idx, int top_k) { if (!store || !query) return NULL; if (query->ndim != 1 || query->dtype != TM_F32) return NULL; if (query->shape[0] != store->config.embed_dim) return NULL; if (top_k <= 0) return NULL; store->total_queries++; /* Project query to HDC */ fv_vector *query_hv = fv_project(store->projector, query); if (!query_hv) return NULL; /* Min-heap of size top_k: root = smallest score in heap. * Single allocation, zero reallocations. */ fm__heap_item *heap = malloc(top_k * sizeof(fm__heap_item)); if (!heap) { fv_free(query_hv); return NULL; } int heap_size = 0; /* Use LSH index if available and store is large enough to benefit. * For small stores (<64 entries), linear scan is faster. */ bool used_lsh = false; if (store->lsh && store->lsh->built && store->num_entries >= 64) { size_t *candidates = NULL; int n_cand = fm__lsh_query(store->lsh, query_hv, &candidates); if (n_cand > 0) { used_lsh = true; for (int ci = 0; ci < n_cand; ci++) { size_t i = candidates[ci]; if (i >= store->num_entries) continue; fm_entry *e = &store->entries[i]; if (e->evicted) continue; if (e->layer_idx != layer_idx) continue; float score = fv_similarity(query_hv, e->signature); if (heap_size < top_k) { heap[heap_size].idx = i; heap[heap_size].score = score; heap_size++; fm__heap_sift_up(heap, heap_size - 1); } else if (score > heap[0].score) { heap[0].idx = i; heap[0].score = score; fm__heap_sift_down(heap, heap_size, 0); } } free(candidates); } } /* Fallback: linear scan (used when LSH not available or store is small) */ if (!used_lsh) { for (size_t i = 0; i < store->num_entries; i++) { fm_entry *e = &store->entries[i]; if (e->evicted) continue; if (e->layer_idx != layer_idx) continue; float score = fv_similarity(query_hv, e->signature); if (heap_size < top_k) { heap[heap_size].idx = i; heap[heap_size].score = score; heap_size++; fm__heap_sift_up(heap, heap_size - 1); } else if (score > heap[0].score) { heap[0].idx = i; heap[0].score = score; fm__heap_sift_down(heap, heap_size, 0); } } } fv_free(query_hv); if (heap_size == 0) { free(heap); fm_results *r = calloc(1, sizeof(fm_results)); return r; } /* Extract heap items sorted by score descending. * Pop from heap (extract-min) into array, then reverse. */ fm__heap_item *sorted = malloc(heap_size * sizeof(fm__heap_item)); if (!sorted) { free(heap); return NULL; } int n_sorted = heap_size; for (int i = heap_size - 1; i >= 0; i--) { sorted[i] = heap[0]; heap[0] = heap[heap_size - 1]; heap_size--; if (heap_size > 0) fm__heap_sift_down(heap, heap_size, 0); } free(heap); /* Build results */ fm_results *results = calloc(1, sizeof(fm_results)); if (!results) { free(sorted); return NULL; } results->items = calloc(n_sorted, sizeof(fm_result)); if (!results->items) { free(sorted); free(results); return NULL; } results->count = 0; for (int i = 0; i < n_sorted; i++) { size_t eidx = sorted[i].idx; fm_entry *e = &store->entries[eidx]; fm_result *r = &results->items[results->count]; r->entry_id = e->entry_id; r->layer_idx = e->layer_idx; r->score = sorted[i].score; r->keys = NULL; r->values = NULL; /* Load K/V — check write buffer first, then disk */ if (store->store_dir[0]) { if (fm__write_buf_load(store, e->entry_id, &r->keys, &r->values) != 0) { if (fm__load_from_shard(store, e->shard_idx, e->entry_id, &r->keys, &r->values) != 0) { continue; /* skip this result if load fails */ } } } e->access_count++; results->count++; store->total_hits++; } free(sorted); return results; } void fm_results_free(fm_results *results) { if (!results) return; for (int i = 0; i < results->count; i++) { tm_free(results->items[i].keys); tm_free(results->items[i].values); } free(results->items); free(results); } /* ============================================================= */ /* Management */ /* ============================================================= */ fm_stats fm_stats_get(fm_store *store) { fm_stats s; memset(&s, 0, sizeof(fm_stats)); if (!store) return s; s.total_stores = store->total_stores; s.total_queries = store->total_queries; s.total_hits = store->total_hits; s.disk_bytes_used = store->disk_bytes_used; /* Include buffered (not yet flushed) data in the size estimate */ for (int i = 0; i < store->write_buf_count; i++) { s.disk_bytes_used += (size_t)store->write_buf[i].n_tokens * store->config.embed_dim * sizeof(float) * 2; } /* Count live entries */ s.num_entries = 0; for (size_t i = 0; i < store->num_entries; i++) { if (!store->entries[i].evicted) s.num_entries++; } return s; } size_t fm_store_size(fm_store *store) { if (!store) return 0; size_t count = 0; for (size_t i = 0; i < store->num_entries; i++) { if (!store->entries[i].evicted) count++; } return count; } size_t fm_evict(fm_store *store, size_t target_entries) { if (!store) return 0; size_t live = fm_store_size(store); if (live <= target_entries) return 0; size_t to_evict = live - target_entries; size_t evicted = 0; /* Score entries: lower score = evict first * Score = importance * (1 + log(1 + access_count)) */ typedef struct { size_t idx; float score; } scored; scored *ranking = malloc(live * sizeof(scored)); if (!ranking) return 0; int ri = 0; for (size_t i = 0; i < store->num_entries; i++) { if (store->entries[i].evicted) continue; fm_entry *e = &store->entries[i]; ranking[ri].idx = i; ranking[ri].score = e->importance * (1.0f + logf(1.0f + (float)e->access_count)); ri++; } /* Quickselect: partition to find the `to_evict` lowest-scored entries. * O(N) average instead of O(N²) selection sort. We don't need them * sorted — just need the bottom-k separated from the rest. */ { int k = (int)to_evict; if (k > ri) k = ri; int lo = 0, hi = ri - 1; while (lo < hi) { /* Median-of-three pivot selection */ int mid = lo + (hi - lo) / 2; if (ranking[mid].score < ranking[lo].score) { scored tmp = ranking[lo]; ranking[lo] = ranking[mid]; ranking[mid] = tmp; } if (ranking[hi].score < ranking[lo].score) { scored tmp = ranking[lo]; ranking[lo] = ranking[hi]; ranking[hi] = tmp; } if (ranking[mid].score < ranking[hi].score) { scored tmp = ranking[mid]; ranking[mid] = ranking[hi]; ranking[hi] = tmp; } float pivot = ranking[hi].score; int i = lo, j = hi - 1; for (;;) { while (i <= j && ranking[i].score <= pivot) i++; while (j >= i && ranking[j].score > pivot) j--; if (i >= j) break; scored tmp = ranking[i]; ranking[i] = ranking[j]; ranking[j] = tmp; } scored tmp = ranking[i]; ranking[i] = ranking[hi]; ranking[hi] = tmp; if (i == k - 1) break; else if (i < k - 1) lo = i + 1; else hi = i - 1; } } /* Evict lowest-scored entries (now in ranking[0..to_evict-1]) */ for (size_t i = 0; i < to_evict && i < (size_t)ri; i++) { fm_entry *e = &store->entries[ranking[i].idx]; e->evicted = true; fv_free(e->signature); e->signature = NULL; evicted++; } free(ranking); return evicted; } int fm_compact(fm_store *store) { if (!store || !store->store_dir[0]) return -1; /* Flush buffered writes to disk before compacting */ fm__write_buf_flush(store); /* Rewrite all shards, excluding evicted entries */ for (int si = 0; si < store->num_shards; si++) { fm_shard *shard = &store->shards[si]; fm__shard_cache_invalidate(store, si); fs_file *old_file = fs_open(shard->path); if (!old_file) continue; fs_writer *w = fs_writer_new(); if (!w) { fs_close(old_file); continue; } bool has_entries = false; for (size_t i = 0; i < store->num_entries; i++) { fm_entry *e = &store->entries[i]; if (e->evicted || e->shard_idx != si) continue; char key_name[64], val_name[64]; snprintf(key_name, sizeof(key_name), "k_%llu", (unsigned long long)e->entry_id); snprintf(val_name, sizeof(val_name), "v_%llu", (unsigned long long)e->entry_id); fs_tensor_info *k_info = fs_get_info(old_file, key_name); fs_tensor_info *v_info = fs_get_info(old_file, val_name); if (k_info && v_info) { const void *k_data = fs_get_data(old_file, key_name); const void *v_data = fs_get_data(old_file, val_name); if (k_data && v_data) { fs_writer_add_tensor(w, key_name, k_info->dtype, k_info->shape, k_info->ndim, k_data); fs_writer_add_tensor(w, val_name, v_info->dtype, v_info->shape, v_info->ndim, v_data); has_entries = true; } } if (k_info) fs_free_info(k_info); if (v_info) fs_free_info(v_info); } fs_close(old_file); if (has_entries) { fs_writer_save(w, shard->path); struct stat st; if (stat(shard->path, &st) == 0) { shard->byte_size = st.st_size; } } else { /* No live entries — remove shard file */ unlink(shard->path); shard->byte_size = 0; } fs_writer_free(w); } /* Recalculate disk usage */ store->disk_bytes_used = 0; for (int i = 0; i < store->num_shards; i++) { store->disk_bytes_used += store->shards[i].byte_size; } return 0; } /* ============================================================= */ /* Integration Bridge */ /* ============================================================= */ int fm_augmented_kv(tm_tensor *query, tm_tensor *cache_k, tm_tensor *cache_v, fm_store *store, int layer_idx, int top_k, tm_tensor **merged_k_out, tm_tensor **merged_v_out) { if (!cache_k || !cache_v || !merged_k_out || !merged_v_out) return -1; if (cache_k->ndim != 2 || cache_v->ndim != 2) return -1; if (cache_k->dtype != TM_F32 || cache_v->dtype != TM_F32) return -1; int64_t cache_len = cache_k->shape[0]; int64_t embed_dim = cache_k->shape[1]; /* If no store or no query, just clone cache */ if (!store || !query || top_k <= 0) { *merged_k_out = tm_clone(cache_k); *merged_v_out = tm_clone(cache_v); return (*merged_k_out && *merged_v_out) ? 0 : -1; } /* Query store */ fm_results *results = fm_query(store, query, layer_idx, top_k); if (!results || results->count == 0) { fm_results_free(results); *merged_k_out = tm_clone(cache_k); *merged_v_out = tm_clone(cache_v); return (*merged_k_out && *merged_v_out) ? 0 : -1; } /* Calculate total tokens from retrieved entries */ int64_t retrieved_tokens = 0; for (int i = 0; i < results->count; i++) { if (results->items[i].keys) { retrieved_tokens += results->items[i].keys->shape[0]; } } /* Allocate merged tensors: [cache_len + retrieved_tokens, embed_dim] */ int64_t total_len = cache_len + retrieved_tokens; int64_t merged_shape[2] = { total_len, embed_dim }; tm_tensor *mk = tm_zeros(merged_shape, 2, TM_F32); tm_tensor *mv = tm_zeros(merged_shape, 2, TM_F32); if (!mk || !mv) { tm_free(mk); tm_free(mv); fm_results_free(results); return -1; } float *mk_data = (float *)mk->data; float *mv_data = (float *)mv->data; /* Copy cache first */ memcpy(mk_data, cache_k->data, cache_len * embed_dim * sizeof(float)); memcpy(mv_data, cache_v->data, cache_len * embed_dim * sizeof(float)); /* Append retrieved entries */ int64_t offset = cache_len; for (int i = 0; i < results->count; i++) { fm_result *r = &results->items[i]; if (!r->keys || !r->values) continue; int64_t n = r->keys->shape[0]; memcpy(mk_data + offset * embed_dim, r->keys->data, n * embed_dim * sizeof(float)); memcpy(mv_data + offset * embed_dim, r->values->data, n * embed_dim * sizeof(float)); offset += n; } fm_results_free(results); *merged_k_out = mk; *merged_v_out = mv; return 0; }