746 lines
34 KiB
Markdown
746 lines
34 KiB
Markdown
# L8 Jacquard: Adaptive 128-Config Heat-Ranked Selector
|
||
|
||
**Status:** Design proposal — awaiting review and approval before implementation
|
||
**Author:** Claude Code session, June 2026
|
||
**Affects:** `include/ssm_jacquard.h`, `src/ssm_jacquard.c`, `src/inference_engine.c`, `include/vm.h`
|
||
|
||
---
|
||
|
||
## 1. Executive Summary
|
||
|
||
The current L8 Jacquard selector is a **static 4-bit threshold classifier**: three metric
|
||
thresholds produce a 4-bit pattern that maps to one of 16 fixed modes controlling L2/L3/L5/L6.
|
||
It has no memory, no learning, and no ability to discover that the DoE-derived thresholds may
|
||
not be optimal for a specific deployed workload.
|
||
|
||
This document proposes replacing it with a **heat-ranked 8×128 adaptive config table** —
|
||
the same structure used by the hotword cache applied to configuration selection. The 128
|
||
entries cover all 2^7 DoE configurations (L1–L7). Benefit scores are updated via negative
|
||
feedback from the coupled (window, slope) convergence signal that the inference engine already
|
||
computes. DoE priors seed the initial scores so there is no cold-start penalty.
|
||
|
||
**Estimated additional overhead:** ~3–4 ns per heartbeat tick (0.03–0.04% of the 10 µs tick
|
||
period). Memory: ~10 KB, fully L1-cached.
|
||
|
||
**Verdict:** Worthwhile, conditional on the reward signal design described in Section 5.
|
||
See Section 9 for the full benefit/cost analysis.
|
||
|
||
---
|
||
|
||
## 2. What Exists Today
|
||
|
||
### 2.1 The Seven Loops
|
||
|
||
| Loop | Name | Current State | Reason |
|
||
|------|------|--------------|--------|
|
||
| L1 | Execution Heat Tracking | Permanently OFF | Harmful in 86% of top DoE configs |
|
||
| L2 | Rolling Window History | L8-controlled | ON if entropy > 0.75 |
|
||
| L3 | Linear Decay | L8-controlled | ON if temporal_decay > 0.5 |
|
||
| L4 | Pipelining Metrics | Permanently OFF | Harmful in 100% of top DoE configs |
|
||
| L5 | Window Width Inference | L8-controlled | ON if CV > 0.15 |
|
||
| L6 | Decay Slope Inference | L8-controlled | ON if CV > 0.15 AND temporal_decay > 0.3 |
|
||
| L7 | Adaptive Heartrate | Permanently ON | Beneficial in 71% of top DoE configs |
|
||
| L8 | Jacquard Policy Engine | — | Drives L2/L3/L5/L6 |
|
||
|
||
### 2.2 Current L8 Logic (`ssm_l8_update`, 156 lines total)
|
||
|
||
```c
|
||
int L2_bit = (entropy >= SSM_ENTROPY_HIGH_THRESHOLD); // 0.75
|
||
int L3_bit = (temporal >= SSM_TEMPORAL_DECAY_THRESHOLD); // 0.5
|
||
int L5_bit = (cv >= SSM_CV_HIGH_THRESHOLD); // 0.15
|
||
int L6_bit = (cv >= 0.15 && temporal >= SSM_TEMPORAL_DECAY_LOW_THRESHOLD);
|
||
|
||
target_mode = (L2<<3) | (L3<<2) | (L5<<1) | L6;
|
||
// hysteresis: commit only after SSM_HYSTERESIS_TICKS=5 consecutive votes
|
||
```
|
||
|
||
Four thresholds hardcoded from the DoE. No state beyond the hysteresis counter and current mode.
|
||
|
||
### 2.3 Top-5% DoE Validated Modes
|
||
|
||
| Mode | Bits (L2 L3 L5 L6) | Description |
|
||
|------|-------------------|-------------|
|
||
| C4 | 0 1 0 0 | Temporal locality only |
|
||
| C7 | 0 1 1 1 | Decay + both inferences |
|
||
| C9 | 1 0 0 1 | Rolling window + decay-slope inference |
|
||
| C11 | 1 0 1 1 | Rolling window + both inferences |
|
||
| C12 | 1 1 0 0 | Rolling window + decay, no inference |
|
||
|
||
These modes will seed the table with high initial benefit scores (see Section 6).
|
||
|
||
---
|
||
|
||
## 3. Signal Chain and Coupled Feedback Topology
|
||
|
||
Understanding the signal chain is prerequisite to understanding the reward signal.
|
||
The three variables — decay slope, window width, and heat trajectory — form a coupled
|
||
feedback system, not three independent loops.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ COUPLED FEEDBACK SYSTEM │
|
||
│ │
|
||
│ decay_slope_q48 │
|
||
│ │ │
|
||
│ ▼ (applied each heartbeat tick) │
|
||
│ DictEntry.execution_heat ◄── decays at slope rate │
|
||
│ │ │
|
||
│ ▼ (word IDs recorded in execution order) │
|
||
│ RollingWindowOfTruth ◄── width = effective_window_size │
|
||
│ │ │
|
||
│ ▼ Phase 2B: linearize → heat trajectory[] │
|
||
│ Inference Engine │
|
||
│ │ │
|
||
│ ├─► Phase 2A: variance stable? ──► early-exit (most ticks) │
|
||
│ │ │
|
||
│ ├─► Phase 2C (L5): Levene's test │
|
||
│ │ └──► new effective_window_size ──────────────┐ │
|
||
│ │ │ │
|
||
│ └─► Phase 2D (L6): log regression │ │
|
||
│ └──► new decay_slope_q48 ────────────────────┼──┐ │
|
||
│ │ │ │
|
||
│ ┌────────────────────────────────────────────────────────┘ │ │
|
||
│ │ window feeds back into how much trajectory │ │
|
||
│ │ Phase 2B extracts next time │ │
|
||
│ │ │ │
|
||
│ └──► effective_window_size │ │
|
||
│ │ │
|
||
│ slope feeds back into heat distribution │ │
|
||
│ rolling window captures │ │
|
||
│ └──── decay_slope_q48 ◄──────────────────────────────────────┘ │
|
||
│ │
|
||
│ Parallel: adaptive pass adjusts window based on diversity │
|
||
│ (growth_rate_q48 vs threshold → shrink or grow) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.1 Why the Coupling Matters
|
||
|
||
- A steeper decay slope → words cool faster → rolling window sees a more homogeneous
|
||
heat distribution → trajectory is less noisy → Levene's inflection shifts left
|
||
(narrower window sufficient) → Phase 2C infers a narrower window.
|
||
|
||
- A narrower window → Phase 2B extracts a shorter trajectory → log regression in
|
||
Phase 2D has fewer samples → slope estimate is noisier → less stable decay_slope_q48.
|
||
|
||
- They iterate toward a **joint attractor**: the window is exactly wide enough to give
|
||
the slope estimator reliable data, and the slope is exactly steep enough that the window
|
||
captures a stationary heat distribution. Neither is pushing the other any more.
|
||
|
||
This coupling is what makes the reward signal design non-trivial. See Section 5.
|
||
|
||
---
|
||
|
||
## 4. Proposed Architecture
|
||
|
||
### 4.1 Core Concept
|
||
|
||
Replace the 4-bit threshold classifier with a stratified heat-ranked table:
|
||
|
||
```
|
||
regime_scores[8][128] — benefit score per (regime, config)
|
||
regime_trials[8][128] — trial count per (regime, config)
|
||
```
|
||
|
||
The **8 regime bins** are the same 3-bit metric partition the current L8 already computes:
|
||
|
||
```c
|
||
regime = (entropy_high << 2) | (cv_high << 1) | temporal_high
|
||
```
|
||
|
||
Eight bins, one per combination of the three binary threshold tests. This is the same
|
||
partition as current L8 — we are not introducing new thresholds.
|
||
|
||
The **128 config entries** cover the full 2^7 DoE space: all combinations of L1–L7 as
|
||
independent binary variables. L1 and L4 are included with very low initial scores —
|
||
not disabled by fiat, but deprioritized by evidence.
|
||
|
||
### 4.2 Data Structures
|
||
|
||
```c
|
||
/* One entry in the config table */
|
||
typedef struct {
|
||
uint8_t config_bits; /* 7-bit mask: b6=L1, b5=L2, b4=L3, b3=L4,
|
||
b2=L5, b1=L6, b0=L7 */
|
||
uint32_t benefit_score; /* Heat analog — rises when stable, falls when not */
|
||
uint32_t trial_count; /* Total completed trials under this config */
|
||
uint64_t last_active_ns; /* Monotonic timestamp — supports score decay */
|
||
uint32_t prev_window; /* effective_window_size at end of last trial */
|
||
uint64_t prev_slope; /* decay_slope_q48 at end of last trial */
|
||
} SsmConfigEntry; /* ~32 bytes per entry */
|
||
|
||
/* Full stratified table */
|
||
typedef struct {
|
||
SsmConfigEntry entries[128]; /* Config metadata, static */
|
||
uint32_t regime_scores[8][128]; /* Benefit score per regime per config */
|
||
uint32_t regime_trials[8][128]; /* Trial count per regime per config */
|
||
uint8_t current_config; /* Index of active config (0–127) */
|
||
uint8_t current_regime; /* Regime bin at start of current trial */
|
||
uint32_t trial_tick_count; /* Ticks elapsed in current trial (relative time) */
|
||
uint32_t anova_exits_this_trial; /* Phase 2A early-exits accumulated */
|
||
uint32_t total_regime_trials[8]; /* Total trials per regime (for UCB ln term) */
|
||
} SsmConfigTable;
|
||
```
|
||
|
||
**Memory footprint:**
|
||
|
||
| Structure | Size |
|
||
|---|---|
|
||
| `SsmConfigEntry[128]` | 128 × 32 = 4,096 bytes |
|
||
| `regime_scores[8][128]` (uint32_t) | 4,096 bytes |
|
||
| `regime_trials[8][128]` (uint32_t) | 4,096 bytes |
|
||
| Trial state + totals | ~64 bytes |
|
||
| **Total** | **~12 KB** |
|
||
|
||
The entire table fits in L1 cache (32–64 KB typical). The access pattern is regime-local:
|
||
each trial-end touches 128 contiguous uint32_t values for the current regime, then stops.
|
||
This is the same cache-friendly pattern the hotword cache exploits.
|
||
|
||
### 4.3 `ssm_l8_state_t` Changes
|
||
|
||
Current `ssm_l8_state_t` has: `current_mode`, `hysteresis_counter`, `pending_mode`.
|
||
|
||
New `ssm_l8_state_t` adds a pointer to the config table (keeping the existing fields
|
||
for backward compatibility during transition, removing them once validated):
|
||
|
||
```c
|
||
typedef struct {
|
||
ssm_l8_mode_t current_mode; /* Keep for compatibility */
|
||
uint32_t hysteresis_counter; /* Keep for compatibility */
|
||
ssm_l8_mode_t pending_mode; /* Keep for compatibility */
|
||
SsmConfigTable *table; /* NULL = use legacy threshold mode */
|
||
} ssm_l8_state_t;
|
||
```
|
||
|
||
Gated behind `table != NULL` so the old path remains functional during development
|
||
and testing.
|
||
|
||
---
|
||
|
||
## 5. Reward Signal Design
|
||
|
||
### 5.1 The Circularity Problem
|
||
|
||
A naive reward signal based on correction magnitude within a trial will produce wrong results:
|
||
|
||
- A config with **L5+L6 enabled** always shows non-zero `Δwindow` and `Δslope` — because
|
||
the inference engine is running and making corrections. Large corrections on the first trial
|
||
after a config switch may reflect genuine convergence toward the attractor, not config failure.
|
||
|
||
- A config with **L5+L6 disabled** always shows zero `Δwindow` and `Δslope` — because
|
||
nothing is running to update them. This would be incorrectly rewarded as "stable."
|
||
|
||
**The fix:** measure **joint convergence across consecutive trials**, not correction magnitude
|
||
within a trial. The system is converging when the corrections are getting *smaller* from one
|
||
trial to the next — not when they are zero in one trial.
|
||
|
||
### 5.2 Joint Error Signal
|
||
|
||
At the end of each trial, record the current (window, slope) pair as `(prev_window, prev_slope)`
|
||
in the `SsmConfigEntry`. At the end of the *next* trial under the same config and same regime,
|
||
compute joint error:
|
||
|
||
```
|
||
Δw = |window_trial_N+1 - window_trial_N| / window_trial_N
|
||
Δs = |slope_trial_N+1 - slope_trial_N| / slope_trial_N
|
||
|
||
joint_error = sqrt(Δw² + Δs²) / sqrt(2) ← normalized RMS in [0, 1]
|
||
|
||
stability = 1.0 - clamp(joint_error, 0.0, 1.0)
|
||
```
|
||
|
||
When the window and slope have stopped moving relative to each other, `joint_error → 0`
|
||
and `stability → 1.0`. When they are still adjusting, `joint_error > 0` and
|
||
`stability < 1.0`.
|
||
|
||
### 5.3 ANOVA Rate as Secondary Signal
|
||
|
||
The ANOVA early-exit rate (Phase 2A) is a fast, cheap proxy for system stability:
|
||
it fires when variance hasn't changed enough to justify full inference. It is a necessary
|
||
but not sufficient condition for joint attractor convergence:
|
||
|
||
```
|
||
anova_stability = anova_exits_this_trial / trial_ticks ∈ [0, 1]
|
||
```
|
||
|
||
### 5.4 Combined Stability Score
|
||
|
||
```
|
||
combined_stability = w_joint × stability + w_anova × anova_stability
|
||
```
|
||
|
||
#### What each signal actually measures
|
||
|
||
`stability` (from `joint_error`) is **output convergence**: did the inference engine's
|
||
corrections reach a fixed point? It requires at least one of L5 or L6 to be active —
|
||
if neither is running, `Δwindow = Δslope = 0` always and the signal collapses (Section 5.7).
|
||
|
||
`anova_stability` is **input stationarity**: is the execution trajectory quiet enough that
|
||
the inference engine decided not to bother computing? It fires regardless of L5/L6 state,
|
||
making it the only signal available when both are disabled.
|
||
|
||
These are not redundant. A config can show:
|
||
- High `anova_stability`, low `stability`: input is quiet (ANOVA skips often) but corrections
|
||
keep churning — window and slope are wandering on a flat input landscape. Indicates
|
||
misconfigured L5/L6 or an under-constrained attractor.
|
||
- Low `anova_stability`, high `stability`: input is noisy/dynamic but corrections have
|
||
settled — the config found a stable operating point despite volatile input. Desirable.
|
||
- Both high: ideal. Both low: still converging, not yet settled.
|
||
|
||
#### The partial-inference bias problem
|
||
|
||
When only one of L5 or L6 is active, `joint_error` is only half-informed:
|
||
- L5-on, L6-off: `Δslope = 0` always (slope frozen) → `joint_error = |Δw|/sqrt(2)`,
|
||
biased toward 1.0 because the frozen component contributes zero.
|
||
- L5-off, L6-on: symmetric bias in the other dimension.
|
||
|
||
A fixed weight like 0.75 treats both the one-active and both-active cases identically,
|
||
overstating the signal quality in the one-active case.
|
||
|
||
#### Option A: Fixed weights (simpler, conservative)
|
||
|
||
```
|
||
w_joint = 0.75, w_anova = 0.25
|
||
```
|
||
|
||
This is the design-conservative choice. The joint signal dominates when it is available;
|
||
ANOVA fills in when it cannot run (full fallback in Section 5.7). The 3:1 ratio reflects
|
||
the ordering: output convergence is what we ultimately care about; input stationarity
|
||
is a proxy that is easier to measure. These weights need DoE validation — they are an
|
||
informed initial guess, not a derived result.
|
||
|
||
#### Option B: Activity-weighted (principled, slightly more complex)
|
||
|
||
```c
|
||
active_outputs = L5_enabled + L6_enabled; /* ∈ {0, 1, 2} */
|
||
w_joint = active_outputs / 2; /* 0.0, 0.5, or 1.0 */
|
||
w_anova = 1 - w_joint;
|
||
```
|
||
|
||
When both L5 and L6 are active: 100% joint (both components of `joint_error` valid).
|
||
When one is active: 50/50 (half the joint signal is meaningful, ANOVA fills the rest).
|
||
When neither is active: 100% ANOVA (collapses cleanly to the Section 5.7 special case,
|
||
eliminating the need for a separate branch).
|
||
|
||
This is strictly more correct than Option A for the partial-inference cases. It costs
|
||
three integer operations per trial end. The tradeoff: it makes the effective weight
|
||
a function of the current config, which complicates reasoning about the reward surface
|
||
when comparing configs with different L5/L6 settings.
|
||
|
||
#### Decision required
|
||
|
||
This is the most consequential tunable in the design. **Recommend starting with Option A
|
||
at the conservative 0.75/0.25 split and adding instrumented logging of both components**
|
||
so the effective ratio can be observed over real workload runs before any adjustment.
|
||
Option B should be considered if the logs show systematic bias in the partial-inference
|
||
configs (e.g., L5-on/L6-off configs consistently overscoring their L5-on/L6-on neighbors).
|
||
|
||
### 5.5 Negative Feedback Reward Update
|
||
|
||
```
|
||
benefit_delta = GAIN × (combined_stability - 0.5) × 2.0
|
||
|
||
new_score = old_score × DECAY_FACTOR + benefit_delta
|
||
new_score = clamp(new_score, SCORE_MIN, SCORE_MAX)
|
||
```
|
||
|
||
- `combined_stability > 0.5`: the config is driving the system toward its attractor → positive
|
||
delta → benefit_score rises.
|
||
- `combined_stability < 0.5`: the config is preventing convergence → negative delta →
|
||
benefit_score falls.
|
||
- `combined_stability = 0.5`: neutral → no update.
|
||
|
||
`DECAY_FACTOR < 1.0` ensures scores decay over time, preventing a config from holding
|
||
the top position indefinitely based on past performance.
|
||
|
||
#### DECAY_FACTOR stability analysis
|
||
|
||
The half-life of a benefit score is expressed in **trials** — the natural relative-time unit
|
||
for this system. Wall-clock conversion is L7's concern and is not the right framing here.
|
||
|
||
```
|
||
half_life_trials = ln(0.5) / ln(DECAY_FACTOR)
|
||
```
|
||
|
||
At `DECAY_FACTOR = 0.99`:
|
||
|
||
```
|
||
half_life_trials ≈ 69 trials
|
||
```
|
||
|
||
The key assumption: **regime dwell time, measured in trials**. A config earns its score
|
||
over many trials in a given regime. If the system typically dwells in a regime for ~200 trials
|
||
before the metric signature shifts, a 69-trial half-life means scores from the previous dwell
|
||
carry moderate weight into the next — which is appropriate. If regimes shift every ~20 trials,
|
||
the half-life is too long and the table remembers the wrong regime. If regimes dwell for
|
||
~2,000 trials, the half-life is fine and the exploration bonus from UCB prevents stagnation.
|
||
|
||
**Effective-zero crossover:**
|
||
|
||
```
|
||
0.99^n = 0.01 → n ≈ 458 trials
|
||
```
|
||
|
||
This is the memory horizon — how many trials back the table looks. Flag `SSM_SCORE_DECAY_FACTOR`
|
||
as a first-class tunable. Calibrate it against observed regime dwell time in trials, not
|
||
against wall-clock expectations.
|
||
|
||
### 5.6 Minimum Trial Duration
|
||
|
||
Each trial must contain at least two inference cycles — one to establish `(prev_window,
|
||
prev_slope)` and one to measure how much they moved. Since inference runs every
|
||
`HEARTBEAT_INFERENCE_FREQUENCY = 1,000` ticks:
|
||
|
||
```
|
||
SSM_MIN_TRIAL_TICKS = 2 × HEARTBEAT_INFERENCE_FREQUENCY = 2,000 ticks
|
||
```
|
||
|
||
**This is a relative-time measure, not a wall-clock measure — intentionally so.**
|
||
|
||
The tick is the fundamental unit of relative time in this system. L7 (adaptive heartrate)
|
||
varies the *wall-clock duration* of a tick based on observed workload. That variation is the
|
||
system's own response to workload; it is not noise. A trial lasting 2,000 ticks at a slower
|
||
L7 heartrate is a trial over a longer relative-time span, which corresponds to more actual
|
||
system activity — it is a larger, not smaller, sample. Anchoring to wall clock here would
|
||
import an absolute time dependency into a system built deliberately around relative time with
|
||
CV promises.
|
||
|
||
The correct framing: `SSM_MIN_TRIAL_TICKS` is a promise — "this trial will not end until
|
||
the system has processed at least 2,000 of its own ticks." What that means in nanoseconds
|
||
is L7's concern, not the adaptive selector's.
|
||
|
||
`SSM_MIN_TRIAL_TICKS` is a first-class tunable in `starforth_config.h`. Larger values give
|
||
more stable reward estimates; smaller values allow faster adaptation to regime changes.
|
||
|
||
### 5.7 Special Case: L5 and L6 Both Disabled
|
||
|
||
When both L5 and L6 are disabled in the current config, `Δwindow` and `Δslope` from the
|
||
inference engine are always zero (the values are frozen). The joint error signal is
|
||
meaningless — both numerator and denominator are zero.
|
||
|
||
In this case, fall back to ANOVA rate alone:
|
||
|
||
```c
|
||
if (!L5_enabled && !L6_enabled) {
|
||
combined_stability = anova_stability;
|
||
}
|
||
```
|
||
|
||
This correctly evaluates "stability without active inference" — does the system at least
|
||
not need correcting when left alone?
|
||
|
||
---
|
||
|
||
## 6. DoE Prior Seeding
|
||
|
||
The 38,400-run DoE provides empirical priors. These seed `regime_scores` at initialization
|
||
so the selector immediately gravitates toward known-good configs on first deployment.
|
||
|
||
### 6.1 Seeding Strategy
|
||
|
||
| Config category | Initial score | Reasoning |
|
||
|---|---|---|
|
||
| Top-5% configs (C4, C7, C9, C11, C12) in their 4-bit form | `SCORE_MAX × 0.80` | DoE-validated |
|
||
| Other configs with L1=0, L4=0 | `SCORE_MAX × 0.40` | Not validated but not harmful |
|
||
| Configs with L1=1 OR L4=1 | `SCORE_MAX × 0.10` | DoE found harmful on average |
|
||
| Configs with L1=1 AND L4=1 | `SCORE_MAX × 0.05` | Doubly harmful |
|
||
|
||
Note: the top-5% configs from the DoE are defined in 4-bit (L2/L3/L5/L6) space. For the
|
||
7-bit table, each top-5% 4-bit config maps to multiple 7-bit entries (L1=0, L4=0, L7=1 held
|
||
fixed). All such mappings receive the same high initial score.
|
||
|
||
### 6.2 Regime-Specific Seeding
|
||
|
||
The DoE ran across all regimes combined. If regime-specific DoE data is available in the
|
||
future, `regime_scores[r]` can be seeded independently per regime. For now, all 8 regimes
|
||
receive the same initial scores — the table will differentiate them over time through runtime
|
||
learning.
|
||
|
||
---
|
||
|
||
## 7. Config Selection: UCB Algorithm
|
||
|
||
### 7.1 Selection Score
|
||
|
||
At each trial end, the selector computes a UCB (Upper Confidence Bound) score for all 128
|
||
configs in the current regime and picks the highest:
|
||
|
||
```
|
||
ucb_score[r][c] = regime_scores[r][c]
|
||
+ UCB_K × sqrt(ln(total_regime_trials[r]) / regime_trials[r][c])
|
||
```
|
||
|
||
- `regime_scores[r][c]`: exploitation term — how well config C has performed in regime R
|
||
- `UCB_K × sqrt(...)`: exploration term — bonus for under-tried configs; shrinks as
|
||
`regime_trials[r][c]` grows toward `total_regime_trials[r]`
|
||
|
||
**UCB_K** controls exploration aggressiveness. Suggest starting at `UCB_K = 0.1 × SCORE_MAX`.
|
||
Too large: wastes trials on known-bad configs. Too small: never explores.
|
||
|
||
### 7.2 Implementation Cost
|
||
|
||
- `ln(total_regime_trials[r])`: computed **once** per trial end, reused for all 128 entries
|
||
- Per config: 1 divide + 1 integer sqrt (~10 cycles) + 1 multiply + 1 add
|
||
- 128 configs × ~4 ops × 2 ns = **~1–2 µs per trial end**
|
||
- Amortized over 2,000-tick trial: **~0.5–1 ns per tick**
|
||
|
||
Integer sqrt via Newton's method (4–5 iterations, convergence guaranteed for uint32_t):
|
||
no floating point, no libm dependency. Compatible with freestanding kernel builds.
|
||
|
||
### 7.3 Bootstrap: First Two Trials
|
||
|
||
The first trial under any (regime, config) pair has `regime_trials[r][c] = 0`,
|
||
making the UCB exploration term undefined (`ln/0`). Bootstrap rule:
|
||
|
||
```
|
||
if regime_trials[r][c] == 0:
|
||
ucb_score[r][c] = SCORE_MAX // treat unexplored as maximally interesting
|
||
```
|
||
|
||
This ensures every config gets at least one trial in each regime before the UCB formula
|
||
takes over — equivalent to the "try everything once" initialization of UCB1.
|
||
|
||
---
|
||
|
||
## 8. Re-Ranking and Selection Path
|
||
|
||
Mirrors `dict_adaptive_optimization_pass` / `dict_reorganize_buckets_by_heat` exactly.
|
||
|
||
### 8.1 Per-Trial-End Sequence
|
||
|
||
```
|
||
1. Compute regime bin for current metrics (same 3 comparisons as today)
|
||
2. Compute joint_error and anova_stability for completed trial
|
||
3. Update regime_scores[current_regime][current_config] with benefit_delta
|
||
4. Increment regime_trials[current_regime][current_config]
|
||
5. Apply score decay: regime_scores[r][c] *= DECAY_FACTOR
|
||
6. Compute ucb_score for all 128 configs in current_regime
|
||
7. Select next_config = argmax(ucb_score)
|
||
8. Apply next_config's 7-bit L1–L7 mask to ssm_config
|
||
9. Reset trial state: trial_tick_count=0, anova_exits_this_trial=0
|
||
10. Record current (window, slope) into entries[current_config].prev_*
|
||
```
|
||
|
||
### 8.2 Per-Tick Sequence (replaces current ssm_l8_update)
|
||
|
||
```
|
||
1. Increment trial_tick_count
|
||
2. If inference ran this tick: increment anova_exits_this_trial (if early-exited)
|
||
3. If trial_tick_count >= SSM_MIN_TRIAL_TICKS:
|
||
run per-trial-end sequence (steps 1–10 above)
|
||
```
|
||
|
||
Trial duration is a relative-time promise in ticks. L7 adaptive heartrate affects wall-clock
|
||
duration but not relative-time semantics. See Section 5.6 for full rationale.
|
||
|
||
---
|
||
|
||
## 9. Overhead Estimates
|
||
|
||
All figures assume 3 GHz CPU, base heartrate tick, 1,000-tick inference frequency,
|
||
2,000-tick minimum trial period. Trial duration in wall-clock time varies with L7.
|
||
|
||
### 9.1 Per-Tick Additions
|
||
|
||
| Operation | Cost | Notes |
|
||
|---|---|---|
|
||
| Regime bin computation | 0 ns | Identical to today's 3 comparisons |
|
||
| `trial_tick_count++` | ~0.3 ns | 1 integer add |
|
||
| ANOVA exit accumulation | ~0.3 ns | 1 conditional add |
|
||
| Trial-end check | ~0.3 ns | 1 compare |
|
||
| **Per-tick total** | **~1 ns** | 0.01% of 10 µs tick |
|
||
|
||
### 9.2 Per-Trial-End Additions (every 2,000 ticks = 20 ms)
|
||
|
||
| Operation | Cost | Amortized/tick |
|
||
|---|---|---|
|
||
| Joint error computation (Δw, Δs, RMS) | ~50 ns | 0.025 ns |
|
||
| ANOVA rate computation | ~10 ns | 0.005 ns |
|
||
| Combined stability + benefit_delta | ~30 ns | 0.015 ns |
|
||
| Score update + decay (1 array write) | ~15 ns | 0.0075 ns |
|
||
| UCB over 128 entries (int sqrt ×128) | ~1,500 ns | 0.75 ns |
|
||
| Argmax selection | ~50 ns | 0.025 ns |
|
||
| **Per-trial-end total** | **~1,655 ns** | **~0.83 ns/tick at base heartrate** |
|
||
|
||
### 9.3 Memory Access
|
||
|
||
| | Size | Cache behavior |
|
||
|---|---|---|
|
||
| `regime_scores[r][*]` (128 uint32_t) | 512 bytes per regime | ~8 cache lines, stays hot |
|
||
| `SsmConfigEntry[128]` | 4,096 bytes | Fits in L1; accessed sequentially |
|
||
| Full table | ~12 KB | Fits in L1 (32–64 KB typical) |
|
||
|
||
### 9.4 Summary Against Existing Costs
|
||
|
||
| Component | ns/tick amortized | % of 10 µs tick |
|
||
|---|---|---|
|
||
| Existing `ssm_l8_update` (today) | ~4 ns | 0.04% |
|
||
| Existing inference engine (20–50 µs every 1,000 ticks) | 20–50 ns | 0.20–0.50% |
|
||
| **New per-tick accumulation** | **~1 ns** | **0.01%** |
|
||
| **New per-trial-end (amortized)** | **~0.83 ns** | **0.008%** |
|
||
| **New L8 total addition** | **~2 ns** | **0.02%** |
|
||
| **New L8 total (replacing old L8)** | **~2 ns** | **0.02%** |
|
||
|
||
The new L8 is marginally *cheaper* than the current one in per-tick terms
|
||
(today's `ssm_l8_update` runs every tick; the new one accumulates cheaply per tick and
|
||
runs the heavy logic only at trial end, amortized over 2,000 ticks).
|
||
|
||
The existing Levene's test `malloc` inside Phase 2C costs more per call than the entire
|
||
new L8 reward + UCB selection combined.
|
||
|
||
---
|
||
|
||
## 10. Files Changed
|
||
|
||
| File | Change | Lines added (est.) |
|
||
|---|---|---|
|
||
| `include/ssm_jacquard.h` | Add `SsmConfigEntry`, `SsmConfigTable`; extend `ssm_l8_state_t` | +80 |
|
||
| `src/ssm_jacquard.c` | Add `ssm_l8_init_table()`, `ssm_l8_seed_from_doe()`, `ssm_l8_update_trial()`, `ssm_l8_select_next_config()`, `ssm_rerank_ucb()`; keep old `ssm_l8_update` as legacy path | +250 |
|
||
| `src/inference_engine.c` | Add `window_delta_q48` and `slope_delta_q48` to `InferenceOutputs`; populate them (already computed internally, just not exposed) | +10 |
|
||
| `include/vm.h` (via `inference_engine.h`) | Two new fields on `InferenceOutputs` | +5 |
|
||
| `src/ssm_jacquard.c` | DoE prior seeding table (static const array of 128 seed scores) | +40 |
|
||
|
||
**No changes to:** `rolling_window_of_truth.c`, `heartbeat_export.c`, `physics_runtime.c`,
|
||
`framebuffer.c`, `vt100.c`, any kernel or boot code.
|
||
|
||
Total estimated addition: **~385 lines**, isolated entirely within the L8 and inference
|
||
engine files.
|
||
|
||
---
|
||
|
||
## 11. What Doesn't Change
|
||
|
||
- The five inference engine phases (2A–2D) are architecturally unchanged. Two small
|
||
outputs are added (`window_delta_q48`, `slope_delta_q48`) — values already computed
|
||
internally that are simply not currently surfaced.
|
||
- The `ssm_config_t` L1–L7 bit flags are unchanged. Everything downstream reads them
|
||
the same way.
|
||
- The rolling window, heartbeat, heat tracking, hotword cache, dictionary optimization
|
||
— all unchanged.
|
||
- The kernel build (freestanding) is unaffected. `SsmConfigTable` uses only `stdint.h`
|
||
types, no libc allocation (table is statically sized), no floating point
|
||
(all arithmetic in Q48.16 or integer).
|
||
|
||
---
|
||
|
||
## 12. Open Questions for Captain Bob
|
||
|
||
These require decisions before coding begins:
|
||
|
||
**Q1: Include L1 and L4 in the 7-bit space?**
|
||
Current plan: yes, seeded at 5–10% of `SCORE_MAX`. The DoE found them harmful on average
|
||
but the table can discover exceptions if they exist. Risk: UCB exploration may occasionally
|
||
select them and hurt performance during that trial. Mitigation: low seed + score decay means
|
||
they stay near the bottom. Alternative: exclude them (use 5-bit space, 32 configs) and keep
|
||
L7 hardwired to ON (as today).
|
||
|
||
**Q2: Tunable constants — bake in or expose via `starforth_config.h`?**
|
||
Proposed new constants:
|
||
- `SSM_MIN_TRIAL_TICKS` (suggest 2000 — relative-time floor, guarantees ≥ 2 inference
|
||
cycles; wall-clock meaning is L7's concern, not ours; see Section 5.6)
|
||
- `SSM_SCORE_MAX` (suggest 65535 to match Q48.16 range intuition)
|
||
- `SSM_SCORE_DECAY_FACTOR` (suggest 0.99 ≈ Q48.16 64881; half-life ~69 trials —
|
||
calibrate against observed regime dwell time in trials; see Section 5.5)
|
||
- `SSM_UCB_K` (suggest `SSM_SCORE_MAX / 10`)
|
||
- `SSM_REWARD_GAIN` (suggest `SSM_SCORE_MAX / 4`)
|
||
- `SSM_JOINT_WEIGHT` (suggest 0.75 for Option A; see Section 5.4)
|
||
- `SSM_ANOVA_WEIGHT` (suggest 0.25 for Option A; or use activity-weighted Option B)
|
||
|
||
All of these map naturally alongside the existing `ROLLING_WINDOW_SIZE`, `SSM_HYSTERESIS_TICKS`
|
||
etc. in `include/starforth_config.h`.
|
||
|
||
**Q3: Regime-specific seeding now or later?**
|
||
Current plan: all 8 regimes seeded identically from DoE priors. Regime-specific DoE
|
||
analysis (if the raw run data is available broken out by metric regime) would improve
|
||
cold-start behavior. Can be deferred — the table learns regime-specific scores at runtime.
|
||
|
||
**Q4: Legacy threshold mode as compile-time flag or runtime gate?**
|
||
Suggested: runtime gate (`table != NULL` in `ssm_l8_state_t`). Allows A/B testing the
|
||
old vs new L8 without recompilation. Alternative: compile-time `#ifdef SSM_L8_ADAPTIVE`
|
||
to keep the kernel build lean.
|
||
|
||
**Q5: How to validate correctness before running the full DoE?**
|
||
Suggested validation sequence (hosted VM only):
|
||
1. `make -f Makefile.starkernel fbtest` — confirms no regression in framebuffer
|
||
2. `make test` — confirms all 936+ FORTH words still pass
|
||
3. Run `--doe` mode for 300 reps each with and without `SSM_L8_ADAPTIVE` defined;
|
||
compare variance and mean execution time. The adaptive selector should produce equal
|
||
or better variance at equal or lower mean time after a warm-up period of ~1000 trials.
|
||
|
||
---
|
||
|
||
## 13. Benefit / Cost Assessment
|
||
|
||
### 13.1 Concrete Expected Benefits
|
||
|
||
**1. Regime-specific optimality.**
|
||
The DoE established which configs win *on average*. This table learns which config wins
|
||
*for this specific workload in this specific regime*. For FORTH programs with unusual heat
|
||
distributions (e.g., heavily recursive, deeply compositional, or with very stable vs very
|
||
bursty execution patterns), the optimal config per regime may differ from the DoE average.
|
||
The table discovers this without re-running the DoE.
|
||
|
||
**2. No manual tuning for new workloads.**
|
||
Current L8: if the DoE thresholds are miscalibrated for a specific FORTH application, the
|
||
classifier is stuck in the wrong mode permanently. The adaptive table self-corrects within
|
||
a few hundred trial periods (a few seconds of runtime at 20ms trials).
|
||
|
||
**3. Joint convergence signal closes the feedback loop.**
|
||
The inference engine currently produces (window, slope) outputs with no feedback about
|
||
whether those outputs are causing the system to converge. The reward signal adds this
|
||
feedback, making the overall system a genuine closed-loop controller rather than
|
||
open-loop inference.
|
||
|
||
**4. Architecturally consistent.**
|
||
The hotword cache, bucket reorganization, and adaptive window already use heat-ranked
|
||
structures. L8 becoming heat-ranked completes the pattern — everything in the SSM is
|
||
adaptive and data-driven, not hardcoded.
|
||
|
||
### 13.2 Honest Caveats
|
||
|
||
**1. The 0% algorithmic variance result.**
|
||
The DoE showed the system converges to the same attractor states from any of the 128
|
||
starting configurations. If the attractor is already well-characterized and robust, the
|
||
adaptive table is solving a problem that doesn't significantly affect final-state behavior
|
||
for most workloads. The benefit is primarily in *how quickly* the system reaches its
|
||
attractor, not *where* the attractor is.
|
||
|
||
**2. L1 and L4 almost certainly stay at the bottom.**
|
||
The DoE evidence against them is overwhelming (86% and 100% harmful respectively). The
|
||
exploration of L1 and L4 slots in the 128-entry space is methodologically correct but
|
||
practically likely to produce no new findings. The useful learning is within the 16-config
|
||
subspace of {L2, L3, L5, L6} combinations, stratified by regime — which the current L8
|
||
already approximates with fixed thresholds.
|
||
|
||
**3. Reward signal parameter sensitivity.**
|
||
The weights (`w_joint`, `w_anova`), gain, decay factor, UCB_K, and trial duration interact.
|
||
The DoE that validated the current system was designed to avoid exactly this kind of
|
||
parameter sensitivity. A second DoE over the adaptive L8's parameters may be needed to
|
||
find the optimal operating point — or the parameters can be set conservatively and left
|
||
fixed, accepting slightly suboptimal but stable behavior.
|
||
|
||
**4. Complexity increase is real.**
|
||
`ssm_jacquard.c` is currently 156 clean lines. The addition is ~385 lines of nontrivial
|
||
logic in the heartbeat thread. Bugs here affect the adaptation rate, not correctness
|
||
(worst case: the selector sticks on a suboptimal config, not on an incorrect config),
|
||
but the surface area grows meaningfully.
|
||
|
||
### 13.3 Verdict
|
||
|
||
**Worth doing.** The overhead is negligible by any measure. The architectural coherence
|
||
with the existing hotword cache is a genuine advantage. The joint convergence reward signal
|
||
is a principled design that closes a feedback loop the current system leaves open.
|
||
|
||
The primary condition: **get the reward signal right before writing any other code**.
|
||
Specifically, resolve Q1 (L1/L4 inclusion), confirm the `window_delta` / `slope_delta`
|
||
output additions to `InferenceOutputs`, and agree on the `min_trial_ticks` value.
|
||
Everything else is mechanical once those are fixed.
|
||
|
||
The primary risk is not overhead or correctness — it is parameter sensitivity in the
|
||
reward signal. Suggest conservative initial values (low GAIN, high DECAY, moderate UCB_K)
|
||
and validate against the existing DoE baseline before changing any parameters.
|
||
|
||
---
|
||
|
||
*End of design document. Awaiting review and approval.*
|