From da4cb1470242c85ed2d6bacffdc80d22863e16ad Mon Sep 17 00:00:00 2001 From: Robert Allan James Date: Mon, 3 Aug 2026 23:04:09 -0400 Subject: [PATCH] =?UTF-8?q?FABRIC.md=20=C2=A726:=20rule=20the=20physical?= =?UTF-8?q?=20heartbeat=20must=20be=20adaptive,=20not=20fixed-rate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loop #7 (vm_runtime.c:703-752) already computes an execution-derived adaptive period and it is orphaned -- nothing in the kernel build ever reads tick_target_ns. Traces the pthread-based consumer it was written for (deliberately disabled for kernel builds, no pthread on bare metal) and confirms via the §18.5 argument that consuming it via the physical re-arm period does not reopen GAP-A1, since the decision to change the period stays execution-derived and the virtual tick still owns all patron state. Rules the scale mismatch (10us hosted base vs 10ms/100Hz kernel base) in favor of reusing Loop #7's decision logic against the kernel-appropriate base, and the mechanism (single-writer/single-reader shared state, no thread/scheduler) against §21.1's existing free-locking finding. Amends item 0.8 to include this -- no new punch-list item, this is inside what 0.8 already builds. Co-Authored-By: Claude Sonnet 5 --- FABRIC.md | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/FABRIC.md b/FABRIC.md index ef3d3d8..73edba2 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -2117,15 +2117,28 @@ on until there is a tick on all three architectures (§16.1, §16.5).* each tick. *Done when:* `heartbeat_ticks()` advances on aarch64 at the configured rate. -- [ ] **0.8 — Converge the three architectures on one tick path.** +- [ ] **0.8 — Converge the three architectures on one tick path, and make the physical + heartbeat adaptive.** One `heartbeat_tick()` call site per architecture; the ISR does counter, timestamp and flag only. **Per the GAP-A1 ruling, the hardware tick drives instrumentation only:** the bottom half services TIME-TRUST bookkeeping, and the engine (`vm_tick()`, decay, inference) stays on the virtual tick — execution-paced, exactly as today. Nothing that feeds patron state reads the hardware counter. + + **Per §26 (ruled):** the physical re-arm period is no longer a fixed 100 Hz constant. + `heartbeat.c` owns the current adaptive period (`heartbeat_set_adaptive_period_ns()` / + `heartbeat_next_period_ns()`); Loop #7's existing site in `vm_runtime.c` calls the setter + with its stable/volatile-derived value, rescaled to the 10 ms kernel base per §26.3 (not + the 10 µs hosted `HEARTBEAT_TICK_NS`); each architecture's re-arm function reads the + getter and converts to its own raw counter units instead of using a hardcoded period. No + new concurrency primitive — single writer (mainline), single reader (ISR), same shape + §21.1 already found free on one hart. *Done when:* all three architectures drive the same TIME-TRUST bottom half; no loop math - runs in interrupt context; and `vm_tick()`'s call sites are unchanged. - *Refs:* §16.4 (as ruled), §18.4, §21.2. + runs in interrupt context; `vm_tick()`'s call sites are unchanged; **and** the re-arm + period measurably varies with Loop #7's stable/volatile signal in a live QEMU run (not + merely computed and discarded) — verified the same way item 0.6's IRQ-storm question was + verified: direct measurement, not assumption. + *Refs:* §16.4 (as ruled), §18.4, §21.2, §26. - [ ] **0.9 — Write the concurrency constraint at the mutex stub.** Add a comment at `src/starkernel/vm/host/shim.c:415` stating that the no-op is correct @@ -2451,3 +2464,122 @@ pin-and-panic, and §25.0's rules themselves. None of them moved. means. B1/B2 are ten-minute reads. C and D are mechanical once A1 is ruled. Nothing should go to a coding model before C1, C2 and C3 are fixed at minimum — those are the ones it will hit in its first hour. + +--- + +## 26. The hardware heartbeat must itself be adaptive — RULED 2026-08-03 + +Raised mid-0.8: the punch list, as written, makes the hardware tick a fixed-rate +instrument (100 Hz, unconditionally, on all three architectures — the `apic_timer_init(..., +100)` calls built across items 0.1–0.7). That is correct for what §18.4/§18.5 require of +the *engine* — the virtual tick stays execution-paced regardless. But it leaves the +*physical* heartbeat monotonic, and the physical heartbeat was never supposed to be +monotonic. Captain Bob: *"the heartbeat is adaptive... it spreads the heartbeat out when +your heart goes faster when you're running... it's gotta be an adaptive heartbeat, that's +the whole thing to cage variance."* + +### 26.1 The finding — an adaptive-rate engine already exists, and it is orphaned + +`vm_runtime.c:703-752` ("Loop #7 — Adaptive Heartrate") computes a bounded adaptive period, +`vm->heartbeat.tick_target_ns`, from the same ANOVA-driven stability signal Loop #5 already +uses: early-exit (stable) slows it down, full-inference (volatile) speeds it up, ±25% per +step, clamped to `[¼×, 4×]` of a configured base. This is real, executing code, faithful to +the design in the sibling StarForth repo's own +`docs/working/architecture/03-architecture/heartbeat-system/architecture.md` (Option A vs. +Option B). It is not a proposal — it already runs, on every `vm_tick()`. + +It is orphaned. `tick_target_ns` is written to `vm->heartbeat.worker->tick_ns` +(`include/vm.h:286,289`), and `worker` is always `NULL` in kernel builds — nothing in +`src/starkernel/` reads `tick_target_ns` at all. The mechanism that was meant to *consume* +it, a `pthread_create()`-based background worker (`heartbeat_thread_main()`, vendored into +`vm_bootstrap.c:310`), is deliberately and redundantly disabled for kernel builds +(`Makefile.starkernel:293,338`, `HEARTBEAT_THREAD_ENABLED` forced to `0` twice) — correctly: +this is a bare-metal single-hart kernel, there is no pthread implementation, and the +mechanism's own history (`segfault-analysis.md` in the StarForth repo) is a real +concurrent-access bug against `RollingWindowOfTruth`, fixed by a mutex a single hart doesn't +need and can't cheaply provide (§21.2, item 0.9). + +So: the *decision logic* is real, tested by inheritance, and currently produces a number +nothing downstream ever reads. Phase 0 as written would ship a heartbeat that looks adaptive +in the source tree and is not adaptive on the wire. + +### 26.2 This does not reopen GAP-A1 + +§18.5 already proved the general shape of this argument for Loop #5 and is directly +reusable for Loop #7: **adaptation is safe exactly when its inputs are execution-derived**, +because then the *decision* to change the period is itself a deterministic function of the +execution stream, not of wall-clock jitter. §18.5 point 3 already establishes that every +`InferenceInputs` field feeding this ANOVA machinery — rolling window, trajectory length, +prefetch hit rate, hot/stale word counts, total heat, word count — is execution-derived with +zero timing input. Loop #7's stable/volatile classification is downstream of that same +machinery. Nothing new needs proving there. + +What must not change, and does not under this design: + +- The **virtual tick stays the engine's clock** (§18.4, unchanged). Adjusting the physical + re-arm period changes *when TIME-TRUST samples land and how often the idle path wakes* — + it does not move decay, reap, or inference off the virtual tick onto the hardware one. +- The hardware tick's **output** still feeds nothing that reaches the parity hash (§18.5 + points 1, 5, unchanged) — only its *input* (the period it's told to re-arm at) becomes + execution-derived instead of fixed. +- What was already true and already inert under §18.5 — that wall-clock interrupt *arrival* + timing is not reproducible run to run — stays true and stays inert. Nothing patron-facing + ever depended on it; this design doesn't change that. + +### 26.3 The scale mismatch, and the ruling + +`tick_target_ns`'s configured base, `HEARTBEAT_TICK_NS` (`include/starforth_config.h:71`), +is `10000ULL` — 10 microseconds. Its live range under Loop #7's ±25%/`[¼×,4×]` bounds is +therefore 2.5µs–40µs. The hardware timer configured throughout items 0.1–0.7 runs at 100 Hz +— 10 milliseconds. That is a three-orders-of-magnitude mismatch: reprogramming the physical +re-arm to the literal `tick_target_ns` value would fire the timer 25,000–400,000 times a +second, which on a bare-metal single-hart kernel means the core spends effectively all its +time in trap entry/exit and the REPL is never reached. `HEARTBEAT_TICK_NS` was tuned for a +hosted OS thread's sleep granularity, not a bare-metal ISR period. + +**RULED (Captain Bob, 2026-08-03):** same relationship, kernel-appropriate scale. The +mechanism must be real and load-bearing — a genuine, measurable effect on the physical +re-arm period, the same kind of effect the original hosted pthread experiments showed — but +computed against the 10 ms / 100 Hz base already established for this kernel, not the 10 µs +hosted base. Loop #7's *decision logic* (stable → slower, volatile → faster, ±25% per step, +clamped `[¼×, 4×]`) is reused unmodified; only the base it is applied to changes. + +### 26.4 The mechanism — no thread needed + +Captain Bob authorized building a kernel-native thread/task if one were required +("if we gotta run a thread or whatever, it doesn't matter"). One is not required, and adding +one would need a preemptive scheduler this single-hart kernel does not have (§21.2 already +rules out real locking for exactly this reason). The existing shape gets there without new +infrastructure: + +- `vm_tick()` already calls Loop #7 on the **mainline path** (execution-paced, never in + interrupt context) and already produces a fresh `tick_target_ns` there. +- Item 0.8 already introduces a shared `heartbeat.c` owning the top/bottom-half split. That + file is the natural owner of one new piece of state: the *current adaptive period*, set by + a new `heartbeat_set_adaptive_period_ns(uint64_t ns)` (called from `vm_runtime.c`'s Loop #7 + site, scaled to the kernel base per §26.3) and read by a new `heartbeat_next_period_ns(void)`. +- Each architecture's existing re-arm function (`apic_timer_rearm()`, `riscv64_timer_rearm()`, + the aarch64 equivalent) already runs in interrupt context at the top of every tick (§18.4's + "one tick" call sites, unchanged). It converts `heartbeat_next_period_ns()` to that + architecture's raw counter units — a conversion each already does today for its fixed + period — instead of using a hardcoded constant. + +No new concurrency: the write happens on the mainline execution path, the read happens in +interrupt context, and the value read is whatever was last written — the same single-writer/ +single-reader shape every other piece of ISR-read, mainline-written state in this kernel +already has (§21.1's finding that locking here is already free, because nothing here is +actually concurrent on one hart). No pthread, no kernel task, no scheduler. + +### 26.5 Open, deferred + +- Multi-VM: today Hera is the only VM, so "whose `tick_target_ns` drives the one physical + timer" has one answer. Not resolved for when Hermes/Artemis return — deferred, not + applicable yet (consistent with §20.5's other Tripod-return deferrals). +- `HEARTBEAT_TICK_NS`'s name and its hosted-scale value are unchanged by this ruling — the + kernel-side base (10 ms) is a **separate** constant, not a redefinition of the hosted one. + Naming the kernel constant is an implementation detail of the item that builds this, not a + document-level open question. + +**RULED.** Item 0.8 is amended below to include this; no new punch-list item is needed — +this is squarely inside what 0.8 already builds (`heartbeat.c`, the three re-arm call +sites).