Stage MC-0c: shared VM code made per-VM or atomic (FABRIC-3.10.8 §3.4)
- Dictionary lookup index is per VM (vm->fc_index, struct SfFcIndex): was one global index rebuilt whenever a different VM was searched, and heat reorganization could reorder another VM's buckets. Allocated on first lookup, freed by vm_cleanup(); allocation failure counted/logged, lookup falls back to a newest-first dictionary walk. - PRNG: still one stream, stepped by compare-and-swap; SEED one store. - state_variable removed (write-only; STATE is vm->mode). - Sweep additions (Captain Bob's ruling): heartbeat_export.c delta last-values moved into each VM's HeartbeatState (were statics shared by every VM -- wrong across VMs today); TOP-WORDS list heap-sized per call (was static [1000]); inference_outputs_to_string() takes a caller buffer; three failure counters atomic; crc64 table published with release/acquire. - Confirmed, unchanged: g_runtime compiled out of the kernel, g_hotwords_cache unreferenced, ttf cache core-0-only. - Probe (removed): every VM has its own index; two VMs searched alternately 2x5000 lookups: 0 rebuilds, 0 wrong-VM results, all three ISAs. - Standard sequence on amd64/aarch64/riscv64: dict_hash set unchanged, G byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
33f4090d6d
commit
355909d46f
@@ -140,8 +140,12 @@ static inline uint32_t udiv_floor(uint32_t a, uint32_t b) { return a / b; }
|
||||
static uint64_t crc64_table[256];
|
||||
static int crc64_inited = 0;
|
||||
|
||||
/* FABRIC-3.10.8 MC-0c: compute_crc64() runs outside the block lock too (the
|
||||
* log and DoE regions, capsules), so the table is published with release/
|
||||
* acquire ordering -- a core that sees crc64_inited set also sees the whole
|
||||
* table. Two cores filling it at once write identical values. */
|
||||
static void crc64_init(void) {
|
||||
if (crc64_inited) return;
|
||||
if (__atomic_load_n(&crc64_inited, __ATOMIC_ACQUIRE)) return;
|
||||
const uint64_t poly = 0x42F0E1EBA9EA3693ULL;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
uint64_t crc = (uint64_t) i;
|
||||
@@ -149,11 +153,11 @@ static void crc64_init(void) {
|
||||
crc = (crc & 1) ? ((crc >> 1) ^ poly) : (crc >> 1);
|
||||
crc64_table[i] = crc;
|
||||
}
|
||||
crc64_inited = 1;
|
||||
__atomic_store_n(&crc64_inited, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
uint64_t compute_crc64(const uint8_t *data, size_t len) {
|
||||
if (!crc64_inited) crc64_init();
|
||||
if (!__atomic_load_n(&crc64_inited, __ATOMIC_ACQUIRE)) crc64_init();
|
||||
uint64_t crc = 0xFFFFFFFFFFFFFFFFULL;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t idx = (uint8_t)(crc ^ data[i]);
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
#include "log.h"
|
||||
#include "platform_time.h"
|
||||
|
||||
/* sf_fc_list/sf_fc_count/sf_fc_cap are declared in dictionary_heat_optimization.h */
|
||||
/* struct SfFcIndex (vm->fc_index, per VM since MC-0c) is declared in dictionary_heat_optimization.h */
|
||||
|
||||
/* ============================================================================
|
||||
* Heat Percentile Calculation
|
||||
@@ -161,7 +161,7 @@ void dict_update_heat_percentiles(VM *vm) {
|
||||
/**
|
||||
* @brief Three-pass heat-stratified dictionary lookup, hottest tier first.
|
||||
*
|
||||
* Searches the first-character bucket @c sf_fc_list[(unsigned char)name[0]]
|
||||
* Searches the first-character bucket @c vm->fc_index->list[(unsigned char)name[0]]
|
||||
* for a word whose @c name and @c name_len match @p name / @p len, scanning
|
||||
* in three priority tiers ordered by execution heat:
|
||||
*
|
||||
@@ -196,9 +196,15 @@ void dict_update_heat_percentiles(VM *vm) {
|
||||
DictEntry *dict_find_word_heat_aware(VM *vm, const char *name, size_t len) {
|
||||
if (!vm || !name || len == 0) return NULL;
|
||||
|
||||
/* MC-0c: vm's own index. vm_find_word(), the only caller, has already
|
||||
* brought it up to date (and walks the dictionary itself if there is
|
||||
* none). */
|
||||
struct SfFcIndex *ix = vm->fc_index;
|
||||
if (!ix) return NULL;
|
||||
|
||||
const unsigned char first = (unsigned char)name[0];
|
||||
DictEntry **bucket = sf_fc_list[first];
|
||||
size_t n = sf_fc_count[first];
|
||||
DictEntry **bucket = ix->list[first];
|
||||
size_t n = ix->count[first];
|
||||
|
||||
if (!bucket || n == 0) return NULL;
|
||||
|
||||
@@ -249,9 +255,9 @@ static int compare_dict_entry_heat_desc(const void *a, const void *b) {
|
||||
* Holds @c vm->dict_lock for the entire operation to prevent concurrent
|
||||
* @c DictEntry frees (from word redefinition or GC) from invalidating the
|
||||
* pointer array while it is being sorted. For each non-empty bucket in
|
||||
* @c sf_fc_list[0..255]:
|
||||
* @c vm->fc_index->list[0..255]:
|
||||
*
|
||||
* 1. Reads @c sf_fc_count[bucket_id] and @c sf_fc_cap[bucket_id]; skips the
|
||||
* 1. Reads the bucket's @c count and @c cap; skips the
|
||||
* bucket if @c count > @c cap (indicates a TOCTOU race — logs a warning).
|
||||
* 2. Allocates a temporary copy of the bucket pointer array.
|
||||
* 3. Re-verifies @c count has not changed (TOCTOU double-check); if it has,
|
||||
@@ -277,14 +283,18 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
* Without this, main thread can free DictEntry objects while we're sorting pointers */
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
|
||||
/* MC-0c: this VM's own buckets -- before, whichever VM's dictionary had
|
||||
* been searched last. */
|
||||
struct SfFcIndex *ix = vm->fc_index;
|
||||
|
||||
/* Reorganize each bucket by heat (hottest words first) */
|
||||
for (int bucket_id = 0; bucket_id < 256; bucket_id++) {
|
||||
if (!sf_fc_list[bucket_id] || sf_fc_count[bucket_id] == 0) continue;
|
||||
for (int bucket_id = 0; ix && bucket_id < SF_FC_BUCKETS; bucket_id++) {
|
||||
if (!ix->list[bucket_id] || ix->count[bucket_id] == 0) continue;
|
||||
|
||||
/* ASan fix: Verify count doesn't exceed capacity before accessing (2025-12-09)
|
||||
* Race condition: Main thread may have changed count/capacity while we read it */
|
||||
size_t count = sf_fc_count[bucket_id];
|
||||
size_t cap = sf_fc_cap[bucket_id];
|
||||
size_t count = ix->count[bucket_id];
|
||||
size_t cap = ix->cap[bucket_id];
|
||||
if (count > cap) {
|
||||
log_message(LOG_WARN,
|
||||
"dict_reorganize_buckets_by_heat: skipping bucket %d - count=%zu exceeds capacity=%zu (race condition)",
|
||||
@@ -297,7 +307,7 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
if (!bucket_copy) continue;
|
||||
|
||||
/* Re-verify count hasn't changed (TOCTOU protection) */
|
||||
size_t count_recheck = sf_fc_count[bucket_id];
|
||||
size_t count_recheck = ix->count[bucket_id];
|
||||
if (count_recheck != count || count_recheck > cap) {
|
||||
log_message(LOG_WARN,
|
||||
"dict_reorganize_buckets_by_heat: bucket %d count changed (%zu -> %zu), aborting",
|
||||
@@ -306,14 +316,14 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
memcpy(bucket_copy, sf_fc_list[bucket_id], count * sizeof(DictEntry *));
|
||||
memcpy(bucket_copy, ix->list[bucket_id], count * sizeof(DictEntry *));
|
||||
|
||||
/* Sort by descending heat (use cached count to avoid TOCTOU race) */
|
||||
qsort(bucket_copy, count, sizeof(DictEntry *),
|
||||
compare_dict_entry_heat_desc);
|
||||
|
||||
/* Copy back to original bucket (still use cached count) */
|
||||
memcpy(sf_fc_list[bucket_id], bucket_copy, count * sizeof(DictEntry *));
|
||||
memcpy(ix->list[bucket_id], bucket_copy, count * sizeof(DictEntry *));
|
||||
|
||||
free(bucket_copy);
|
||||
}
|
||||
|
||||
+81
-59
@@ -61,19 +61,10 @@
|
||||
#endif
|
||||
/* LIKELY/UNLIKELY macros are defined in vm.h */
|
||||
|
||||
#ifndef SF_FC_BUCKETS
|
||||
#define SF_FC_BUCKETS 256
|
||||
#endif
|
||||
|
||||
/* Buckets of entry pointers, sizes, and capacities (we reuse memory). */
|
||||
/* Note: sf_fc_list, sf_fc_count, and sf_fc_cap are not static to allow access from
|
||||
* dictionary_heat_optimization.c for heat-aware bucket reorganization with proper
|
||||
* capacity checks (ASan fix: 2025-12-09). */
|
||||
DictEntry **sf_fc_list[SF_FC_BUCKETS];
|
||||
size_t sf_fc_count[SF_FC_BUCKETS];
|
||||
size_t sf_fc_cap[SF_FC_BUCKETS];
|
||||
|
||||
static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
|
||||
/* The first-character lookup index is per VM (vm->fc_index, struct SfFcIndex
|
||||
* in dictionary_heat_optimization.h; FABRIC-3.10.8 Stage MC-0c). Buckets
|
||||
* reuse their memory across rebuilds. */
|
||||
static uint32_t g_fc_index_alloc_failures; /* atomic: any core can count one */
|
||||
|
||||
/* FABRIC-3.9.0.md §8.1 (Stage S-1b): stable word ids are no longer capped at
|
||||
* DICTIONARY_SIZE. The id tables grow with the dictionary; a word that can't
|
||||
@@ -84,7 +75,7 @@ static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
|
||||
|
||||
static uint32_t g_word_id_refusals;
|
||||
|
||||
uint32_t vm_dictionary_word_id_refusals(void) { return g_word_id_refusals; }
|
||||
uint32_t vm_dictionary_word_id_refusals(void) { return __atomic_load_n(&g_word_id_refusals, __ATOMIC_RELAXED); }
|
||||
|
||||
/* Grow both id tables to at least `need` entries, all-or-nothing: both new
|
||||
* tables are allocated before either old one is touched, so a failure leaves
|
||||
@@ -160,7 +151,7 @@ int vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
|
||||
entry->word_id = word_id;
|
||||
|
||||
if (word_id == WORD_ID_INVALID) {
|
||||
g_word_id_refusals++;
|
||||
__atomic_add_fetch(&g_word_id_refusals, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "word id table can't grow: '%.*s' refused",
|
||||
(int)entry->name_len, entry->name);
|
||||
return -1;
|
||||
@@ -248,21 +239,39 @@ static inline void *sf_xrealloc(void *p, size_t nbytes) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static void sf_fc_clear_all(void) {
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
|
||||
sf_free(sf_fc_list[i]);
|
||||
sf_fc_list[i] = NULL;
|
||||
sf_fc_count[i] = 0;
|
||||
sf_fc_cap[i] = 0;
|
||||
struct SfFcIndex *vm_dictionary_index(VM *vm) {
|
||||
if (!vm) return NULL;
|
||||
if (!vm->fc_index) {
|
||||
vm->fc_index = (struct SfFcIndex *) sf_calloc(1, sizeof(struct SfFcIndex));
|
||||
if (!vm->fc_index) {
|
||||
/* Lookups stay correct without it (vm_find_word() walks the
|
||||
* dictionary); counted and logged, retried on the next lookup. */
|
||||
uint32_t n = __atomic_add_fetch(&g_fc_index_alloc_failures, 1, __ATOMIC_RELAXED);
|
||||
if (n == 1 || (n & (n - 1)) == 0)
|
||||
log_message(LOG_ERROR, "dictionary index: can't allocate (x%u) -- "
|
||||
"lookups walk the dictionary", (unsigned) n);
|
||||
}
|
||||
}
|
||||
return vm->fc_index;
|
||||
}
|
||||
|
||||
uint32_t vm_dictionary_index_alloc_failures(void) {
|
||||
return __atomic_load_n(&g_fc_index_alloc_failures, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void vm_dictionary_index_free(VM *vm) {
|
||||
if (!vm || !vm->fc_index) return;
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_free(vm->fc_index->list[i]);
|
||||
sf_free(vm->fc_index);
|
||||
vm->fc_index = NULL;
|
||||
}
|
||||
|
||||
/* Ensure bucket i has at least `need` capacity; grow geometrically. */
|
||||
static int sf_fc_reserve(size_t i, size_t need) {
|
||||
if (sf_fc_cap[i] >= need) return 1;
|
||||
size_t newcap = sf_fc_cap[i] ? sf_fc_cap[i] : 8;
|
||||
static int sf_fc_reserve(struct SfFcIndex *ix, size_t i, size_t need) {
|
||||
if (ix->cap[i] >= need) return 1;
|
||||
size_t newcap = ix->cap[i] ? ix->cap[i] : 8;
|
||||
while (newcap < need) newcap <<= 1;
|
||||
DictEntry **old = sf_fc_list[i];
|
||||
DictEntry **old = ix->list[i];
|
||||
DictEntry **newv = (DictEntry **) sf_xrealloc(old, newcap * sizeof(*newv));
|
||||
if (!newv) return 0;
|
||||
/* Kernel sf_realloc is a bump allocator that does not copy; patch that up.
|
||||
@@ -270,66 +279,67 @@ static int sf_fc_reserve(size_t i, size_t need) {
|
||||
* from freed memory (UB; glibc overwrites freed block with heap metadata). */
|
||||
#ifdef __STARKERNEL__
|
||||
{
|
||||
size_t old_count = sf_fc_count[i];
|
||||
size_t old_count = ix->count[i];
|
||||
if (newv != old && old && old_count > 0)
|
||||
memcpy(newv, old, old_count * sizeof(*newv));
|
||||
}
|
||||
#endif
|
||||
sf_fc_list[i] = newv;
|
||||
sf_fc_cap[i] = newcap;
|
||||
ix->list[i] = newv;
|
||||
ix->cap[i] = newcap;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Full rebuild: count → reserve → fill oldest→newest (so backward scan = newest-first). */
|
||||
static void sf_rebuild_fc_index(VM *vm) {
|
||||
static void sf_rebuild_fc_index(VM *vm, struct SfFcIndex *ix) {
|
||||
/* Reset counts, keep capacity to avoid churn. */
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_fc_count[i] = 0;
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) ix->count[i] = 0;
|
||||
|
||||
/* Count first chars. */
|
||||
for (DictEntry *e = vm->latest; e; e = e->link) {
|
||||
unsigned fc = (unsigned char) e->name[0];
|
||||
sf_fc_count[fc]++;
|
||||
ix->count[fc]++;
|
||||
}
|
||||
|
||||
/* Reserve memory once per bucket. */
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
|
||||
if (sf_fc_count[i] && !sf_fc_reserve(i, sf_fc_count[i])) {
|
||||
if (ix->count[i] && !sf_fc_reserve(ix, i, ix->count[i])) {
|
||||
/* OOM for this bucket → treat as empty; we’ll just miss the fast path. */
|
||||
sf_fc_count[i] = 0;
|
||||
ix->count[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Back-fill so [0]=oldest, [count-1]=newest. */
|
||||
size_t fill_at[SF_FC_BUCKETS];
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = sf_fc_count[i];
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = ix->count[i];
|
||||
|
||||
for (DictEntry *e = vm->latest; e; e = e->link) {
|
||||
unsigned fc = (unsigned char) e->name[0];
|
||||
if (sf_fc_count[fc] == 0 || !sf_fc_list[fc]) continue;
|
||||
sf_fc_list[fc][--fill_at[fc]] = e;
|
||||
if (ix->count[fc] == 0 || !ix->list[fc]) continue;
|
||||
ix->list[fc][--fill_at[fc]] = e;
|
||||
}
|
||||
|
||||
sf_cached_latest = vm->latest;
|
||||
ix->cached_latest = vm->latest;
|
||||
ix->rebuilds++;
|
||||
}
|
||||
|
||||
/* Fast path: if exactly one new word was pushed, append it without rebuilding. */
|
||||
static void sf_try_fast_append(VM *vm) {
|
||||
if (!vm || vm->latest == sf_cached_latest) return;
|
||||
static void sf_try_fast_append(VM *vm, struct SfFcIndex *ix) {
|
||||
if (!vm || vm->latest == ix->cached_latest) return;
|
||||
DictEntry *newest = vm->latest;
|
||||
/* single push if the new head links to the previously cached head */
|
||||
if (newest && newest->link == sf_cached_latest) {
|
||||
if (newest && newest->link == ix->cached_latest) {
|
||||
unsigned fc = (unsigned char) newest->name[0];
|
||||
size_t n = sf_fc_count[fc];
|
||||
if (sf_fc_reserve(fc, n + 1)) {
|
||||
size_t n = ix->count[fc];
|
||||
if (sf_fc_reserve(ix, fc, n + 1)) {
|
||||
/* Keep oldest→…→newest ordering: append at the end. */
|
||||
sf_fc_list[fc][n] = newest;
|
||||
sf_fc_count[fc] = n + 1;
|
||||
sf_cached_latest = newest;
|
||||
ix->list[fc][n] = newest;
|
||||
ix->count[fc] = n + 1;
|
||||
ix->cached_latest = newest;
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Otherwise fall back to a full rebuild (multiple changes, deletes, etc.). */
|
||||
sf_rebuild_fc_index(vm);
|
||||
sf_rebuild_fc_index(vm, ix);
|
||||
}
|
||||
|
||||
/* --- API ----------------------------------------------------------------- */
|
||||
@@ -401,12 +411,32 @@ DictEntry *vm_find_word(VM *vm, const char *name, size_t len) {
|
||||
/* Thread safety: Acquire dict_lock for read access to dictionary */
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
|
||||
/* This VM's own index (MC-0c). Without one (allocation refused, counted
|
||||
* in vm_dictionary_index()), walk the dictionary newest-first: the
|
||||
* first visible match is the newest definition. */
|
||||
struct SfFcIndex *ix = vm_dictionary_index(vm);
|
||||
if (UNLIKELY(!ix)) {
|
||||
DictEntry *w;
|
||||
for (w = vm->latest; w; w = w->link) {
|
||||
if ((size_t) w->name_len != len || memcmp(w->name, name, len) != 0) continue;
|
||||
#ifdef WORD_HIDDEN
|
||||
if (w->flags & WORD_HIDDEN) continue;
|
||||
#endif
|
||||
#ifdef WORD_SMUDGED
|
||||
if (w->flags & WORD_SMUDGED) continue;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
return w;
|
||||
}
|
||||
|
||||
/* Keep the index in sync with dictionary head, cheaply if possible. */
|
||||
if (UNLIKELY(sf_cached_latest != vm->latest)) sf_try_fast_append(vm);
|
||||
if (UNLIKELY(ix->cached_latest != vm->latest)) sf_try_fast_append(vm, ix);
|
||||
|
||||
const unsigned char first = (unsigned char) name[0];
|
||||
DictEntry **bucket = sf_fc_list[first];
|
||||
size_t n = sf_fc_count[first];
|
||||
DictEntry **bucket = ix->list[first];
|
||||
size_t n = ix->count[first];
|
||||
if (UNLIKELY(!bucket || n == 0)) {
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
return NULL;
|
||||
@@ -662,13 +692,5 @@ void vm_unpin_execution_heat(VM *vm) {
|
||||
physics_metadata_refresh_state(vm->latest);
|
||||
}
|
||||
|
||||
/* If you ever need to hard-reset (e.g., switching vocabularies wholesale) */
|
||||
/**
|
||||
* @brief Resets the dictionary index
|
||||
*
|
||||
* Clears all index data and cached state. Used when switching vocabularies.
|
||||
*/
|
||||
void vm_dictionary_index_reset(void) {
|
||||
sf_cached_latest = NULL;
|
||||
sf_fc_clear_all();
|
||||
}
|
||||
/* MC-0c: vm_dictionary_index_reset() (the global index's hard reset, no
|
||||
* callers) is gone; a VM's index is freed with the VM, vm_dictionary_index_free(). */
|
||||
|
||||
+12
-13
@@ -70,13 +70,14 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
* raw counter on riscv64). Use nominal tick period instead. */
|
||||
snapshot->elapsed_ns = hb->tick_count * (uint64_t)HEARTBEAT_TICK_NS;
|
||||
|
||||
static uint64_t last_tick_count = 0;
|
||||
uint64_t delta_ticks = hb->tick_count - last_tick_count;
|
||||
uint64_t delta_ticks = hb->tick_count - hb->export_last_tick_count;
|
||||
snapshot->tick_interval_ns = delta_ticks * (uint64_t)HEARTBEAT_TICK_NS;
|
||||
last_tick_count = hb->tick_count;
|
||||
hb->export_last_tick_count = hb->tick_count;
|
||||
|
||||
/* Delta metrics — computed from per-VM counters using static last-value tracking.
|
||||
* On VM reset (current < last), treat current value as the full delta. */
|
||||
/* Delta metrics — computed from per-VM counters against this VM's own
|
||||
* previous values (hb->export_last_*; FABRIC-3.10.8 MC-0c -- were statics
|
||||
* shared by every VM). On VM reset (current < last), treat current value
|
||||
* as the full delta. */
|
||||
|
||||
/* Cache metrics from hotwords cache */
|
||||
snapshot->cache_hits_delta = 0; /* TODO: wire hotwords cache hit counter */
|
||||
@@ -85,12 +86,11 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
/* Word execution delta — words executed since the previous heartbeat tick.
|
||||
* vm->heartbeat.words_executed is incremented by vm_core.c on every dispatch. */
|
||||
{
|
||||
static uint64_t last_words_executed = 0;
|
||||
uint64_t cur = hb->words_executed;
|
||||
snapshot->word_executions_delta = (uint32_t)(cur >= last_words_executed
|
||||
? cur - last_words_executed
|
||||
snapshot->word_executions_delta = (uint32_t)(cur >= hb->export_last_words_executed
|
||||
? cur - hb->export_last_words_executed
|
||||
: cur);
|
||||
last_words_executed = cur;
|
||||
hb->export_last_words_executed = cur;
|
||||
}
|
||||
|
||||
/* Hot word count - walk dictionary and count words above threshold */
|
||||
@@ -141,12 +141,11 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
* High rate = selector chose well and the system settled.
|
||||
* Low rate = selector is still searching. */
|
||||
{
|
||||
static uint64_t last_early_exit_count = 0;
|
||||
uint64_t cur = hb->early_exit_count;
|
||||
snapshot->predicted_label_hits = (uint32_t)(cur >= last_early_exit_count
|
||||
? cur - last_early_exit_count
|
||||
snapshot->predicted_label_hits = (uint32_t)(cur >= hb->export_last_early_exit_count
|
||||
? cur - hb->export_last_early_exit_count
|
||||
: cur);
|
||||
last_early_exit_count = cur;
|
||||
hb->export_last_early_exit_count = cur;
|
||||
}
|
||||
|
||||
/* Jitter: tick-based timing has no wall-clock deviation to measure.
|
||||
|
||||
@@ -756,18 +756,21 @@ void inference_engine_run(InferenceInputs *inputs, InferenceOutputs *outputs)
|
||||
/**
|
||||
* @brief Format inference outputs as a human-readable string.
|
||||
*
|
||||
* Writes into a static 256-byte buffer — not thread-safe, for diagnostics only.
|
||||
* Writes into the caller's buffer (FABRIC-3.10.8 MC-0c: was a static
|
||||
* 256-byte buffer shared by every VM and core).
|
||||
* Format: @c "window=N var=F slope=F quality=F (cached|full)".
|
||||
*
|
||||
* @param outputs Inference outputs to format (NULL produces @c "(null)")
|
||||
* @return Pointer to a static string buffer
|
||||
* @param buf Destination (256 bytes holds any output)
|
||||
* @param cap Size of @p buf
|
||||
* @return @p buf, or "" if @p buf is NULL or @p cap is 0
|
||||
*/
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs)
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs, char *buf, size_t cap)
|
||||
{
|
||||
static char buf[256];
|
||||
if (!buf || cap == 0) return "";
|
||||
|
||||
if (!outputs) {
|
||||
snprintf(buf, sizeof(buf), "(null)");
|
||||
snprintf(buf, cap, "(null)");
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -775,7 +778,7 @@ const char* inference_outputs_to_string(const InferenceOutputs *outputs)
|
||||
double slope_dbl = q48_to_double(outputs->adaptive_decay_slope);
|
||||
double quality_dbl = q48_to_double(outputs->slope_fit_quality_q48);
|
||||
|
||||
snprintf(buf, sizeof(buf),
|
||||
snprintf(buf, cap,
|
||||
"window=%u var=%.6f slope=%.6f quality=%.6f %s",
|
||||
outputs->adaptive_window_width,
|
||||
var_dbl,
|
||||
|
||||
@@ -212,7 +212,7 @@ int transition_metrics_init(WordTransitionMetrics *metrics) {
|
||||
*/
|
||||
static uint64_t g_transition_grow_failures;
|
||||
|
||||
uint64_t transition_metrics_grow_failures(void) { return g_transition_grow_failures; }
|
||||
uint64_t transition_metrics_grow_failures(void) { return __atomic_load_n(&g_transition_grow_failures, __ATOMIC_RELAXED); }
|
||||
|
||||
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size) {
|
||||
if (!metrics || next_word_id >= dict_size) {
|
||||
@@ -234,7 +234,7 @@ int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word
|
||||
}
|
||||
grown = (uint64_t *)calloc(new_cap, sizeof(uint64_t));
|
||||
if (!grown) {
|
||||
g_transition_grow_failures++;
|
||||
__atomic_add_fetch(&g_transition_grow_failures, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "transition_heat can't grow: dropped");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -312,6 +312,9 @@ void vm_cleanup(VM* vm)
|
||||
vm->hotwords_cache = NULL;
|
||||
}
|
||||
|
||||
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
|
||||
vm_dictionary_index_free(vm);
|
||||
|
||||
/* Clean up rolling window of truth */
|
||||
rolling_window_cleanup(&vm->rolling_window);
|
||||
|
||||
|
||||
@@ -394,6 +394,9 @@ void vm_cleanup(VM* vm)
|
||||
vm->hotwords_cache = NULL;
|
||||
}
|
||||
|
||||
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
|
||||
vm_dictionary_index_free(vm);
|
||||
|
||||
/* Clean up rolling window of truth */
|
||||
rolling_window_cleanup(&vm->rolling_window);
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@
|
||||
#include "../../include/word_registry.h"
|
||||
#include "../../include/log.h"
|
||||
|
||||
/* Global state for dictionary manipulation */
|
||||
static cell_t state_variable = 0; /* STATE - compilation state */
|
||||
/* FABRIC-3.10.8 MC-0c: the file-global state_variable ("STATE") that lived
|
||||
* here is gone. It was written (INTERPRET) and never read -- STATE is
|
||||
* vm->mode, already per VM. */
|
||||
|
||||
/* Helper function to calculate aligned address */
|
||||
static uintptr_t align_address(uintptr_t addr) {
|
||||
@@ -360,7 +361,6 @@ void dictionary_m_word_interpret(VM *vm) {
|
||||
|
||||
/* Set interpretation mode */
|
||||
vm->mode = MODE_INTERPRET;
|
||||
state_variable = 0;
|
||||
|
||||
/* The actual interpretation happens through vm_interpret() calls */
|
||||
/* This word exists mainly for completeness and mode setting */
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
#include "platform_time.h"
|
||||
#include "word_registry.h"
|
||||
|
||||
/* Forward declarations */
|
||||
extern DictEntry **sf_fc_list[256];
|
||||
extern size_t sf_fc_count[256];
|
||||
|
||||
/* ============================================================================
|
||||
* Benchmark: Dictionary Lookup Performance
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "../../include/platform_time.h"
|
||||
#include "../../include/profiler.h"
|
||||
#include "../../include/block_subsystem.h"
|
||||
#include "../../include/platform_alloc.h" /* sf_malloc()/sf_free() -- TOP-WORDS (MC-0c) */
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -228,17 +229,33 @@ void starforth_word_top_words(VM* vm)
|
||||
printf("Top %ld most frequently used words:\n", (long)n);
|
||||
printf("==================================\n");
|
||||
|
||||
/* Collect all words with execution_heat > 0 (static: avoids large kernel stack frame) */
|
||||
static DictEntry* words_with_heat[1000];
|
||||
/* Collect all words with execution_heat > 0. FABRIC-3.10.8 MC-0c: sized
|
||||
* to this dictionary and allocated per call -- was a static [1000]
|
||||
* (shared by every VM, and silently cut at 1000 words). */
|
||||
int word_count = 0;
|
||||
|
||||
for (DictEntry* entry = vm->latest; entry && word_count < 1000; entry = entry->link)
|
||||
for (DictEntry* entry = vm->latest; entry; entry = entry->link)
|
||||
{
|
||||
if (entry->execution_heat > 0)
|
||||
if (entry->execution_heat > 0) word_count++;
|
||||
}
|
||||
DictEntry** words_with_heat = NULL;
|
||||
if (word_count > 0)
|
||||
{
|
||||
words_with_heat = (DictEntry**)sf_malloc((size_t)word_count * sizeof(DictEntry*));
|
||||
if (!words_with_heat)
|
||||
{
|
||||
words_with_heat[word_count++] = entry;
|
||||
vm->error = 1;
|
||||
log_message(LOG_ERROR, "TOP-WORDS: can't allocate a list of %d words", word_count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
int k = 0;
|
||||
for (DictEntry* entry = vm->latest; entry && k < word_count; entry = entry->link)
|
||||
{
|
||||
if (entry->execution_heat > 0) words_with_heat[k++] = entry;
|
||||
}
|
||||
word_count = k;
|
||||
}
|
||||
|
||||
/* Simple bubble sort by execution_heat (descending) */
|
||||
for (int i = 0; i < word_count - 1; i++)
|
||||
@@ -261,6 +278,7 @@ void starforth_word_top_words(VM* vm)
|
||||
DictEntry* entry = words_with_heat[i];
|
||||
printf("%d. %.*s: %ld\n", i + 1, (int)entry->name_len, entry->name, (long)entry->execution_heat);
|
||||
}
|
||||
sf_free(words_with_heat);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -653,9 +671,18 @@ void starforth_word_init(VM* vm)
|
||||
*/
|
||||
static uint64_t prng_next(void)
|
||||
{
|
||||
/* LCG: state = (a * state + c) mod m, where m = 2^64 (implicit) */
|
||||
g_prng_state = g_prng_state * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
return g_prng_state;
|
||||
/* LCG: state = (a * state + c) mod m, where m = 2^64 (implicit).
|
||||
* FABRIC-3.10.8 MC-0c: one stream for every VM (as accepted at Stage H),
|
||||
* stepped with compare-and-swap so two cores never draw the same value
|
||||
* or lose a step. Which core draws which value is ordering, not
|
||||
* physics (MC-R4). */
|
||||
uint64_t cur = __atomic_load_n(&g_prng_state, __ATOMIC_RELAXED);
|
||||
uint64_t next;
|
||||
do {
|
||||
next = cur * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
} while (!__atomic_compare_exchange_n(&g_prng_state, &cur, next, 0,
|
||||
__ATOMIC_RELAXED, __ATOMIC_RELAXED));
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,15 +702,16 @@ void starforth_word_seed(VM* vm)
|
||||
}
|
||||
|
||||
cell_t seed = vm_pop(vm);
|
||||
g_prng_state = (uint64_t)seed;
|
||||
uint64_t s = (uint64_t)seed;
|
||||
|
||||
/* Ensure non-zero state (LCG weakness) */
|
||||
if (g_prng_state == 0)
|
||||
if (s == 0)
|
||||
{
|
||||
g_prng_state = 1;
|
||||
s = 1;
|
||||
}
|
||||
__atomic_store_n(&g_prng_state, s, __ATOMIC_RELAXED); /* MC-0c: one store, not two */
|
||||
|
||||
log_message(LOG_DEBUG, "SEED: PRNG seeded with %lu", (unsigned long)g_prng_state);
|
||||
log_message(LOG_DEBUG, "SEED: PRNG seeded with %lu", (unsigned long)s);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -80,7 +80,7 @@ static inline int ensure_input(VM *vm) {
|
||||
* counted here -- never silently truncated. */
|
||||
static uint32_t g_word_too_long;
|
||||
|
||||
uint32_t string_word_word_refusals(void) { return g_word_too_long; }
|
||||
uint32_t string_word_word_refusals(void) { return __atomic_load_n(&g_word_too_long, __ATOMIC_RELAXED); }
|
||||
|
||||
/* Helper function to convert string to number (base-10 only here) */
|
||||
static int convert_string_to_number(const char *str, size_t len, cell_t *result) {
|
||||
@@ -254,7 +254,7 @@ void string_word_word(VM *vm) {
|
||||
if (word_len > WORD_BUFFER_SIZE - 2) {
|
||||
/* Refused whole: the delimiter goes back on the stack and >IN is
|
||||
* left where it was, so nothing is consumed. */
|
||||
g_word_too_long++;
|
||||
__atomic_add_fetch(&g_word_too_long, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "WORD: token over 255 chars refused");
|
||||
vm_push(vm, c);
|
||||
vm->error = 1;
|
||||
|
||||
Reference in New Issue
Block a user