FABRIC-3.10.8 K-8: free the vocabulary indexes and inference outputs at vm_cleanup

The ~3.4 KB/VM heap residual left after K-4 was each dead VM's CONTEXT
and FORTH first-character bucket arrays (84 blocks, 3,400 bytes per
worker), plus last_inference_outputs. New vocabulary_free_indexes();
both vm_cleanup()s call it and free the outputs. Per-VM residual is now
232 bytes: the registry node (kept by design) and the vm_physics node
(open finding). 3-ISA acceptance clean, hash 0x99b01134c3d3049c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-26 09:21:16 -04:00
co-authored by Claude Opus 5.5
parent acea36ea55
commit f29658a65d
15 changed files with 46014 additions and 0 deletions
+31
View File
@@ -673,3 +673,34 @@ riscv64's slowest completion halved (15.1 → 7.7 ms), consistent with requests
| riscv64 | `logs/20260926-082042` | same; **0 `timed out` lines in the log** |
**K-6 closes §8 defect 3.**
### 4.8 K-8 — the ~3.4 KB/VM residual: found and freed (2026-09-26)
**Investigation** (temporary probe: a kmalloc heap walk and two Mama words, `K8-SNAP` (record every used block) and `K8-DIFF` (list the used blocks that are new since the snapshot, with their first 32 bytes)). amd64. Each run did two warm-up worker cycles (WORKER-BIRTH, SEND, KILL), then snapshot, one cycle, diff, twice.
- **Before the fix** (`logs/20260926-083533`): each cycle left **84 new blocks, 3,400 bytes**, identical in both cycles. That is exactly K-4's 95,200 bytes per 28-VM round.
- **What they were:** the dead VM's vocabulary search indexes. These are the first-character bucket arrays for CONTEXT and FORTH (`vm->vocab.ctx_fc` / `forth_fc`), built lazily by `fc_rebuild()` and never freed by `vm_cleanup()`.
- Reading `vm_cleanup()` turned up one more: `vm->last_inference_outputs` was never freed either.
**Change.**
- New `vocabulary_free_indexes(vm)` (`vocabulary_words.c`) frees both indexes' buckets and clears the cached heads.
- Both `vm_cleanup()`s (kernel `vm_core.c`, hosted `vm_bootstrap.c`) call it after the dictionary index, then free `last_inference_outputs`.
**Evidence, after the fix** (same probe, `logs/20260926-084642`): each cycle leaves **2 blocks, 232 bytes**, identical in both cycles. Both are keyed by the dead VM's id:
| Block | Size | What | Status |
|---|---|---|---|
| registry node (`vm_node_t`), state DEAD | 168 B | the VM's registry entry | kept by design: nodes are never freed after boot, so entry pointers stay valid |
| `vm_physics_node_t` | 64 B | the VM's fleet-physics node | the MC-0 finding "`vm_physics` nodes are never removed"; **open, awaiting a ruling** |
- The unexplained residual is gone: 3,400 → 232 bytes per VM, all of it identified.
- K-1's "within one allocation" criterion, as written, cannot be met while those two nodes stay after death. The registry node is a design decision; the physics node depends on the ruling above.
**Standard sequence** (clean build, with the probe reverted; only the four fix files differ from K-6).
| ISA | log | result |
|---|---|---|
| amd64 | `logs/20260926-085034` | hash `0x99b01134c3d3049c`, G A=40 B=40, HHH, all ok `3`, ledger unchanged, 0 B idle, 0 / 0 |
| aarch64 | `logs/20260926-090043` | same |
| riscv64 | `logs/20260926-091122` | same; 0 `timed out` lines |
**K-8 closes the unexplained part of K-1's heap criterion.**
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9
View File
@@ -315,6 +315,15 @@ void vm_cleanup(VM* vm)
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
vm_dictionary_index_free(vm);
/* Its vocabulary search indexes, and its last inference outputs
* (FABRIC-3.10.8 K-8: neither was freed -- the vocabulary buckets were
* ~3.4 KB per dead VM). */
vocabulary_free_indexes(vm);
if (vm->last_inference_outputs) {
vm_host_free(vm, vm->last_inference_outputs);
vm->last_inference_outputs = NULL;
}
/* Clean up rolling window of truth */
rolling_window_cleanup(&vm->rolling_window);
+8
View File
@@ -398,6 +398,14 @@ void vm_cleanup(VM* vm)
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
vm_dictionary_index_free(vm);
/* Its vocabulary search indexes and last inference outputs
* (FABRIC-3.10.8 K-8: neither was freed). */
vocabulary_free_indexes(vm);
if (vm->last_inference_outputs) {
sf_free(vm->last_inference_outputs);
vm->last_inference_outputs = NULL;
}
/* Clean up rolling window of truth */
rolling_window_cleanup(&vm->rolling_window);
@@ -98,4 +98,13 @@ void vocabulary_word_definitions(VM *vm);
*/
void vocabulary_word_forth(VM *vm);
/*
* @brief Free this VM's vocabulary search indexes (vm_cleanup())
* @details FABRIC-3.10.8 K-8: the CONTEXT and FORTH first-character bucket
* arrays (vm->vocab.ctx_fc / forth_fc) were never freed -- about
* 3.4 KB left behind by every VM that died.
* @param vm Pointer to the virtual machine instance
*/
void vocabulary_free_indexes(VM *vm);
#endif /* VOCABULARY_WORDS_H */
+11
View File
@@ -104,6 +104,17 @@ static void fc_free(DictEntry ***lists, size_t *counts) {
}
}
/* FABRIC-3.10.8 K-8: vm_cleanup() calls this -- the two indexes' bucket
* arrays were never freed when a VM died (84 blocks, 3,400 bytes, per
* worker). */
void vocabulary_free_indexes(VM *vm) {
if (!vm) return;
fc_free(vm->vocab.ctx_fc, vm->vocab.ctx_n);
fc_free(vm->vocab.forth_fc, vm->vocab.forth_n);
vm->vocab.ctx_cached_head = NULL;
vm->vocab.forth_cached_head = NULL;
}
static void fc_rebuild(DictEntry *head, DictEntry ***lists, size_t *counts, DictEntry **cached_head) {
fc_free(lists, counts);