Files
LithosAnanake/src/ssm_jacquard.c
T

482 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "ssm_jacquard.h"
#include <string.h>
#include <stdlib.h>
/* ============================================================================
* L8 Initialization
* ============================================================================
*/
/**
* @brief Initialise the L8 Jacquard steady-state machine.
*
* Sets @c current_mode and @c pending_mode to @c initial_mode, zeroes
* @c hysteresis_counter, and sets @c table to NULL. Callers must invoke
* @c ssm_l8_init_table() separately if the adaptive UCB table is required.
*
* @param state Pointer to the L8 state to initialise
* @param initial_mode Starting mode (typically @c SSM_MODE_C0)
*/
void ssm_l8_init(ssm_l8_state_t *state, ssm_l8_mode_t initial_mode)
{
if (!state) return;
state->current_mode = initial_mode;
state->hysteresis_counter = 0;
state->pending_mode = initial_mode;
state->table = NULL; /* Caller calls ssm_l8_init_table() separately */
}
/* ============================================================================
* L8 Mode Selection Logic (Data-Driven from Top 5% DoE Analysis)
* ============================================================================
*/
/**
* @brief Update the L8 mode selector using current runtime metrics.
*
* Classifies @c metrics into four binary feature bits (L2/L3/L5/L6) derived
* from DoE top-5% analysis and computes a target mode. Applies hysteresis:
* the mode only commits after @c SSM_HYSTERESIS_TICKS consecutive ticks with
* the same target. Called by the heartbeat thread on every tick.
*
* @param metrics Runtime metrics snapshot (entropy, CV, temporal decay)
* @param state L8 state to update; @c current_mode is written on commit
*/
void ssm_l8_update(const ssm_l8_metrics_t *metrics, ssm_l8_state_t *state)
{
if (!metrics || !state) return;
/* Classify metrics into binary features */
int entropy_high = (metrics->entropy >= SSM_ENTROPY_HIGH_THRESHOLD);
int cv_high = (metrics->cv >= SSM_CV_HIGH_THRESHOLD);
int temporal_high = (metrics->temporal_decay >= SSM_TEMPORAL_DECAY_THRESHOLD);
int temporal_med = (metrics->temporal_decay >= SSM_TEMPORAL_DECAY_LOW_THRESHOLD);
/* Determine target mode using 4-bit logic (L2 L3 L5 L6) */
ssm_l8_mode_t target_mode = SSM_MODE_C0; /* Default: minimal */
/* L2 bit: Enable rolling window if high entropy (diversity) */
int L2_bit = entropy_high ? 1 : 0;
/* L3 bit: Enable linear decay if high temporal locality */
int L3_bit = temporal_high ? 1 : 0;
/* L5 bit: Enable window inference if high CV (volatility) */
int L5_bit = cv_high ? 1 : 0;
/* L6 bit: Enable decay inference if CV high AND moderate temporal */
int L6_bit = (cv_high && temporal_med) ? 1 : 0;
/* Combine bits into mode selector: L2 L3 L5 L6 */
target_mode = (ssm_l8_mode_t)((L2_bit << 3) | (L3_bit << 2) | (L5_bit << 1) | L6_bit);
/* Hysteresis: only change mode after SSM_HYSTERESIS_TICKS consecutive votes */
if (target_mode == state->pending_mode) {
/* Same target as last time, increment counter */
state->hysteresis_counter++;
if (state->hysteresis_counter >= SSM_HYSTERESIS_TICKS) {
/* Threshold reached, commit mode change */
if (target_mode != state->current_mode) {
state->current_mode = target_mode;
}
state->hysteresis_counter = 0; /* Reset for next change */
}
} else {
/* Target changed, reset hysteresis */
state->pending_mode = target_mode;
state->hysteresis_counter = 1;
}
}
/* ============================================================================
* L8 Mode Application (Set L2/L3/L5/L6 Bits)
* ============================================================================
*/
/**
* @brief Apply the current L8 mode to a runtime configuration struct.
*
* Extracts the four loop-enable bits (L2/L3/L5/L6) from
* @c state->current_mode and writes them into @c config. Used by the
* legacy (non-table) L8 path when the adaptive UCB table is not present.
*
* @param state L8 state containing the committed current mode
* @param config Runtime configuration to update
*/
void ssm_apply_mode(const ssm_l8_state_t *state, ssm_config_t *config)
{
if (!state || !config) return;
/* Extract bits from mode (L2 L3 L5 L6) */
int mode_val = (int)state->current_mode;
config->L2_rolling_window = (mode_val >> 3) & 1; /* Bit 3 */
config->L3_linear_decay = (mode_val >> 2) & 1; /* Bit 2 */
config->L5_window_inference = (mode_val >> 1) & 1; /* Bit 1 */
config->L6_decay_inference = (mode_val >> 0) & 1; /* Bit 0 */
}
/* ============================================================================
* L8 Utility Functions
* ============================================================================
*/
/**
* @brief Return a human-readable name string for the given L8 mode.
*
* Returns a static string such as @c "C4_TEMPORAL" or @c "C15_FULL_ADAPTIVE".
* Modes marked ✅ TOP 5% are the configurations favoured by the DoE study.
* Returns @c "UNKNOWN" for out-of-range values.
*
* @param mode L8 mode enumeration value
* @return Pointer to a static null-terminated string (never NULL)
*/
const char* ssm_l8_mode_name(ssm_l8_mode_t mode)
{
switch (mode) {
case SSM_MODE_C0: return "C0_MINIMAL";
case SSM_MODE_C1: return "C1_DECAY_INF";
case SSM_MODE_C2: return "C2_WINDOW_INF";
case SSM_MODE_C3: return "C3_VOLATILE";
case SSM_MODE_C4: return "C4_TEMPORAL"; /* ✅ TOP 5% */
case SSM_MODE_C5: return "C5_TEMPORAL_DECAY_INF";
case SSM_MODE_C6: return "C6_TEMPORAL_WINDOW_INF";
case SSM_MODE_C7: return "C7_FULL_INFERENCE"; /* ✅ TOP 5% */
case SSM_MODE_C8: return "C8_DIVERSE";
case SSM_MODE_C9: return "C9_DIVERSE_DECAY_INF"; /* ✅ TOP 5% */
case SSM_MODE_C10: return "C10_DIVERSE_WINDOW_INF";
case SSM_MODE_C11: return "C11_DIVERSE_INFERENCE"; /* ✅ TOP 5% */
case SSM_MODE_C12: return "C12_DIVERSE_TEMPORAL"; /* ✅ TOP 5% */
case SSM_MODE_C13: return "C13_COMPLEX";
case SSM_MODE_C14: return "C14_FULL_ADAPTIVE_NO_DECAY_INF";
case SSM_MODE_C15: return "C15_FULL_ADAPTIVE";
default: return "UNKNOWN";
}
}
/* ============================================================================
* Adaptive Table: DoE Prior Seeding
* ============================================================================
*
* Maps the DoE top-5% 4-bit modes (L2/L3/L5/L6) to the 7-bit space
* (L1=0, L4=0, L7=1 held fixed for the high-score variants).
*
* Bit layout: b6=L1, b5=L2, b4=L3, b3=L4, b2=L5, b1=L6, b0=L7
*
* Top-5% modes (4-bit → 7-bit with L1=0,L4=0,L7=1):
* C4 (L3 only): 0100 → 0 0 1 0 0 0 1 = 0x11 = 17
* C7 (L3+L5+L6): 0111 → 0 0 1 0 1 1 1 = 0x17 = 23
* C9 (L2+L6): 1001 → 0 1 0 0 0 1 1 = 0x23 = 35
* C11 (L2+L5+L6): 1011 → 0 1 0 0 1 1 1 = 0x27 = 39
* C12 (L2+L3): 1100 → 0 1 1 0 0 0 1 = 0x31 = 49
*/
/**
* @brief Seed the adaptive UCB config table with DoE prior scores.
*
* Computes benefit scores derived from the DoE top-5% analysis for all
* @c cd_tuning_word().num_configs (128) configs and hands them to
* @c cd_config_table_seed(). The five top-5% configs (C4/C7/C9/C11/C12 in
* 4-bit space → indices 17/23/35/39/49 in 7-bit space) receive 80% of
* @c cd_tuning_word().score_max; configs involving L1 or L4 (known expensive
* loops) receive 510%; all others receive 40%. Sets the starting config
* to index 17 (C4-equivalent: L3+L7 only).
*
* @param table Pointer to the CDConfigTable to seed (already initialised
* via cd_config_table_init())
*/
static void ssm_l8_seed_from_doe(CDConfigTable *table)
{
/* Top-5% config indices in 7-bit space */
static const uint8_t top5[] = { 17u, 23u, 35u, 39u, 49u };
CDTuning tuning = cd_tuning_word();
uint32_t initial_scores[128];
uint32_t score_max = tuning.score_max;
uint32_t c, i;
for (c = 0u; c < tuning.num_configs; c++) {
uint8_t bits = (uint8_t)c;
int has_l1 = (bits & SSM_CFG_L1) != 0;
int has_l4 = (bits & SSM_CFG_L4) != 0;
int is_top5 = 0;
uint32_t score;
for (i = 0u; i < 5u; i++) {
if (bits == top5[i]) { is_top5 = 1; break; }
}
if (is_top5) score = (score_max * 80u) / 100u;
else if (has_l1 && has_l4) score = (score_max * 5u) / 100u;
else if (has_l1 || has_l4) score = (score_max * 10u) / 100u;
else score = (score_max * 40u) / 100u;
initial_scores[c] = score;
}
/* Start on C4-equivalent (config index 17: L3+L7 only) */
cd_config_table_seed(table, &tuning, initial_scores, 17u);
}
/* ============================================================================
* Adaptive Table: Per-Tick Reward + UCB Selection (static)
* ============================================================================ */
/**
* @brief Score the current config from this tick's outcome and UCB-select
* this tick's config.
*
* Computes a joint convergence signal from this tick's ANOVA outcome and
* the relative delta between this tick's and the previous tick's (window,
* locality) pair. Updates the regime-specific benefit score for the current
* config using exponential decay plus a signed delta -- the decay is what
* provides smoothing over time; there's no separate multi-tick batching on
* top of it (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev r: one clock, the
* heartbeat tick -- a tick is a tick). Then runs
* UCB1 over all 128 configs for the current regime, selecting the one with
* the highest upper confidence bound as this tick's config.
*
* @param table Adaptive config table
* @param config Runtime config to update with the newly selected bits
* @param metrics This tick's metrics snapshot (for the ANOVA signal)
* @param current_window Current rolling window width, this tick
* @param locality_q16 Current locality signal from cd_classify_ids(), this tick
*/
static void ssm_l8_tick_score_and_select(CDConfigTable *table, ssm_config_t *config,
const ssm_l8_metrics_t *metrics,
uint32_t current_window, uint32_t locality_q16)
{
CDTuning tuning = cd_tuning_word();
uint8_t active_bits = table->entries[table->current_config].config_bits;
int use_joint_convergence = ((active_bits & SSM_CFG_L5) || (active_bits & SSM_CFG_L6)) ? 1 : 0;
/* --- ANOVA stability: this tick's own outcome, not an accumulated
* fraction. Neutral (32768) when inference didn't run this tick --
* no signal either way, not "unstable." */
uint32_t anova_stability = metrics->inference_ran_this_tick
? (metrics->inference_early_exited ? 65536u : 0u)
: 32768u;
uint8_t bits = cd_config_table_tick(table, &tuning, table->current_regime,
anova_stability, use_joint_convergence,
current_window, locality_q16);
/* Apply new config bits to ssm_config_t -- all 7, not just the 4
* the legacy 16-mode selector could express. */
if (config != NULL) {
config->L1_heat_tracking = (bits & SSM_CFG_L1) ? 1 : 0;
config->L2_rolling_window = (bits & SSM_CFG_L2) ? 1 : 0;
config->L3_linear_decay = (bits & SSM_CFG_L3) ? 1 : 0;
config->L4_pipelining = (bits & SSM_CFG_L4) ? 1 : 0;
config->L5_window_inference = (bits & SSM_CFG_L5) ? 1 : 0;
config->L6_decay_inference = (bits & SSM_CFG_L6) ? 1 : 0;
config->L7_adaptive_heartrate = (bits & SSM_CFG_L7) ? 1 : 0;
}
}
/* ============================================================================
* Adaptive Table: Public API
* ============================================================================
*/
/**
* @brief Allocate and initialise the adaptive UCB config table.
*
* Mallocs and initialises a @c CDConfigTable sized by @c cd_tuning_word(),
* seeds it with @c ssm_l8_seed_from_doe(), and attaches it to
* @c state->table. If either allocation fails, @c state->table is set to
* NULL and the system falls back to legacy mode (@c ssm_apply_mode()).
*
* @param state L8 state that will own the table
*/
void ssm_l8_init_table(ssm_l8_state_t *state)
{
CDConfigTable *table;
CDTuning tuning;
if (!state) return;
table = (CDConfigTable *)malloc(sizeof(CDConfigTable));
if (!table) {
state->table = NULL; /* Fall back to legacy mode */
return;
}
tuning = cd_tuning_word();
if (cd_config_table_init(table, &tuning) != 0) {
free(table);
state->table = NULL; /* Fall back to legacy mode */
return;
}
ssm_l8_seed_from_doe(table);
state->table = table;
}
/**
* @brief Free the adaptive UCB config table owned by @c state.
*
* Safe to call with @c state->table == NULL. Sets @c state->table to NULL
* after freeing so the state falls back to legacy mode.
*
* @param state L8 state whose table is freed
*/
void ssm_l8_free_table(ssm_l8_state_t *state)
{
if (!state || !state->table) return;
cd_config_table_free(state->table);
free(state->table);
state->table = NULL;
}
/**
* @brief Tick the adaptive UCB table: classify, score, and reselect.
*
* Classifies the current tick into one of 8 regimes (entropy x CV x
* locality) and stores it unconditionally, then scores the current config
* from this tick's outcome and UCB-reselects, every single call -- no
* batching, no derived counter of any kind (VM-FLEET-ATTRACTOR-DESIGN-
* 20260705.md rev r: one clock, the heartbeat tick, applied uniformly).
* The locality component of both the regime and the joint-convergence
* reward comes from cd_classify_ids() over recent_word_ids -- purely
* execution-count-derived, no wall-clock input (rev s; see header comment
* on this function's declaration for why that matters). Also mirrors the
* winning L2/L3/L5/L6 bits back into @c state->current_mode for
* diagnostic display. No-op if @c state->table is NULL.
*
* @param state L8 state with attached adaptive table
* @param metrics Current runtime metrics snapshot
* @param config Runtime config updated this tick
* @param current_window Current rolling window width
* @param recent_word_ids Recent word-execution-ID history, oldest first
* @param recent_word_ids_count Number of valid entries in recent_word_ids
*/
void ssm_l8_update_table(ssm_l8_state_t *state, const ssm_l8_metrics_t *metrics,
ssm_config_t *config,
uint32_t current_window,
const uint32_t *recent_word_ids, uint32_t recent_word_ids_count)
{
CDConfigTable *table;
CDTuning tuning = cd_tuning_word();
CDRegimeMetrics cd;
uint8_t regime;
if (!state || !state->table || !metrics) return;
table = state->table;
cd = cd_classify_ids(recent_word_ids, recent_word_ids_count,
tuning.locality_lookback);
/* Current regime (same 3-bit shape as legacy L8), stored every tick,
* unconditionally. */
regime = (uint8_t)(
((metrics->entropy >= SSM_ENTROPY_HIGH_THRESHOLD) ? 4u : 0u) |
((metrics->cv >= SSM_CV_HIGH_THRESHOLD) ? 2u : 0u) |
((cd.locality_q16 >= tuning.locality_high_threshold_q16) ? 1u : 0u)
);
table->current_regime = regime;
ssm_l8_tick_score_and_select(table, config, metrics, current_window, cd.locality_q16);
/* Mirror L2/L3/L5/L6 bits into the legacy mode field for diagnostics */
{
uint8_t bits = table->entries[table->current_config].config_bits;
state->current_mode = (ssm_l8_mode_t)(
(((bits & SSM_CFG_L2) ? 1u : 0u) << 3) |
(((bits & SSM_CFG_L3) ? 1u : 0u) << 2) |
(((bits & SSM_CFG_L5) ? 1u : 0u) << 1) |
(((bits & SSM_CFG_L6) ? 1u : 0u) << 0)
);
}
}
/**
* @brief Apply the UCB-selected config bits to a runtime configuration struct.
*
* Reads the current winning config entry from the adaptive table and writes
* all 7 of its bits into @c config. Use this instead of @c ssm_apply_mode()
* when the adaptive table is present (@c state->table != NULL).
*
* @param state L8 state with an attached, initialised adaptive table
* @param config Runtime configuration to update
*/
void ssm_apply_mode_from_table(const ssm_l8_state_t *state, ssm_config_t *config)
{
const CDConfigTable *table;
uint8_t bits;
if (!state || !state->table || !config) return;
table = state->table;
bits = table->entries[table->current_config].config_bits;
config->L1_heat_tracking = (bits & SSM_CFG_L1) ? 1 : 0;
config->L2_rolling_window = (bits & SSM_CFG_L2) ? 1 : 0;
config->L3_linear_decay = (bits & SSM_CFG_L3) ? 1 : 0;
config->L4_pipelining = (bits & SSM_CFG_L4) ? 1 : 0;
config->L5_window_inference = (bits & SSM_CFG_L5) ? 1 : 0;
config->L6_decay_inference = (bits & SSM_CFG_L6) ? 1 : 0;
config->L7_adaptive_heartrate = (bits & SSM_CFG_L7) ? 1 : 0;
}
/**
* @brief Force the adaptive table onto an externally-chosen config.
*
* Sets @c current_config directly (as if the bandit's own UCB selection had
* picked it) and applies its bits immediately. Since scoring and UCB
* reselection now run on every heartbeat tick (rev r: one clock, no
* batching of any kind), this is a one-tick nudge, not a hold: the
* forced config earns exactly one tick's worth of reward signal into its
* own regime_score, then the very next tick's ssm_l8_update_table() call
* scores it and reselects on its own, same as any other tick. Calling
* this repeatedly (e.g. once per DoE draw, faster than the VM's own
* tick cadence) keeps a config in effect in practice, not because it's
* pinned -- because it's what's currently forced, most recently.
*
* @param state L8 state with an attached, initialised adaptive table
* @param config Runtime configuration to update immediately
* @param config_idx Desired config, masked to the 128-entry (7-bit) space
*/
void ssm_l8_force_config(ssm_l8_state_t *state, ssm_config_t *config, uint8_t config_idx)
{
if (!state || !state->table || !config) return;
cd_config_table_force(state->table, config_idx & 0x7Fu);
ssm_apply_mode_from_table(state, config);
}