Foundry/hybridmem/hybridmem.c

1455 lines
49 KiB
C

/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
/* ============================================================= */
/* 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;
}