482 lines
20 KiB
C
482 lines
20 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 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) 2023–2025 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 5–10%; 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);
|
||
} |