starkernel: restate VM fleet heat transfer on the virtual tick
Punch list §25 item 2.1 complete. vm_physics_touch() no longer takes a wall-clock timestamp -- it reads fleet_heartbeat_tick_count internally, which is execution-paced (vm_runtime.c:143), not wall-clock. VMPhysics.last_active_ns -> last_active_tick, VMFleetTouchSample.elapsed_us -> elapsed_ticks, and a new explicit `touched` flag replaces the old `> 0` sentinel (tick 0 is a legitimate value a first touch can land on, unlike wall-clock ns). Verified: three-architecture boot (amd64 x2, aarch64, riscv64), all reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the item-0.10 baseline. No new compiler warnings in the touched files. Honestly flagged, not fixed: with Tripod pruned to Hera alone (item 0.1), vm_physics_touch()'s fan-out has no other live VM to pull heat from, so the fleet-heat-sum acceptance criterion is trivially satisfied rather than genuinely stress-tested -- a real check needs Phase 4's multi-VM fleet. fleet_transfer_slope_q48's seed (65536/3) was calibrated for elapsed microseconds and has not been re-fit for elapsed ticks; left as-is rather than guessed, deferred to item 5.1's DoE work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f2eb877691
commit
542d7dbf0d
@@ -58,7 +58,10 @@
|
||||
|
||||
typedef struct {
|
||||
uint64_t execution_heat_q48;
|
||||
uint64_t last_active_ns;
|
||||
uint64_t last_active_tick; /* fleet_heartbeat_tick_count value at last touch */
|
||||
int touched; /* 0 until the first touch; explicit rather than
|
||||
* overloading last_active_tick == 0, which is a
|
||||
* legitimate tick value a first touch can land on */
|
||||
int is_live;
|
||||
} VMPhysics;
|
||||
|
||||
@@ -72,24 +75,30 @@ typedef struct vm_physics_node {
|
||||
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
|
||||
static vm_physics_node_t *vm_physics_head = (void *)0;
|
||||
|
||||
/* One recorded touch's inputs to the transfer law (amount = elapsed_us *
|
||||
/* One recorded touch's inputs to the transfer law (amount = elapsed_ticks *
|
||||
* slope >> 16, clamped to available heat) -- not a derived heat value.
|
||||
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
|
||||
* heat-snapshot design (rev m) with this: since the transfer law is known
|
||||
* exactly, the true rate can be recovered directly from any touch that
|
||||
* wasn't clamped (rate = amount / elapsed_us), rather than curve-fitting
|
||||
* a reconstructed trajectory against a synthetic time axis. */
|
||||
* wasn't clamped (rate = amount / elapsed_ticks), rather than curve-fitting
|
||||
* a reconstructed trajectory against a synthetic time axis.
|
||||
*
|
||||
* Restated on the virtual tick (FABRIC.md item 2.1, 2026-08-04): this used
|
||||
* to be elapsed_us, wall-clock microseconds -- non-reproducible under TCG
|
||||
* per GAP-A1 (FABRIC.md §25.7.1). Now counts fleet_heartbeat_tick_count
|
||||
* ticks, which is execution-paced (incremented once per vm_tick() call,
|
||||
* confirmed at vm_runtime.c:143). */
|
||||
typedef struct {
|
||||
uint64_t elapsed_us; /* since this vm_id's previous touch; 0 if there
|
||||
* was no previous touch to compare against */
|
||||
uint64_t amount; /* elapsed_us * slope >> 16 at touch time, i.e.
|
||||
* the rate-implied transfer before any clamp;
|
||||
* 0 if not computed (first touch, zero slope,
|
||||
* or non-monotonic clock) */
|
||||
int clamped; /* 1 if the heat actually moved was capped below
|
||||
* `amount` by what the rest of the fleet held --
|
||||
* carries no rate information, excluded from
|
||||
* the estimator rather than treated as a zero */
|
||||
uint64_t elapsed_ticks; /* since this vm_id's previous touch; 0 if there
|
||||
* was no previous touch to compare against */
|
||||
uint64_t amount; /* elapsed_ticks * slope >> 16 at touch time,
|
||||
* i.e. the rate-implied transfer before any
|
||||
* clamp; 0 if not computed (first touch, zero
|
||||
* slope, or non-monotonic tick) */
|
||||
int clamped; /* 1 if the heat actually moved was capped below
|
||||
* `amount` by what the rest of the fleet held --
|
||||
* carries no rate information, excluded from
|
||||
* the estimator rather than treated as a zero */
|
||||
} VMFleetTouchSample;
|
||||
|
||||
typedef struct {
|
||||
@@ -119,15 +128,35 @@ static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
|
||||
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
|
||||
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
|
||||
* here. This was previously seeded at literal 0, which cannot bootstrap:
|
||||
* vm_physics_touch's transfer amount is elapsed_ns * slope >> 16, so a
|
||||
* vm_physics_touch's transfer amount is elapsed_ticks * slope >> 16, so a
|
||||
* zero slope moves zero heat on every touch forever, which starves
|
||||
* vm_physics_tick's regression of the very heat-trajectory signal it
|
||||
* needs to infer a better slope -- a closed loop with no way out. Found
|
||||
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
|
||||
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f). */
|
||||
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f).
|
||||
*
|
||||
* UNVALIDATED after the ns-to-tick restatement (FABRIC.md item 2.1,
|
||||
* 2026-08-04): this value was calibrated against elapsed wall-clock
|
||||
* microseconds. Elapsed fleet-heartbeat *ticks* between touches is a
|
||||
* different quantity at a different scale, and the seed has not been
|
||||
* re-fit against it. Left as-is rather than guessed at -- the acceptance
|
||||
* boot test below only checks determinism, not transfer magnitude; a real
|
||||
* re-tune is DoE work (item 5.1), not something to invent here. */
|
||||
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
|
||||
static uint64_t slope_fit_quality_q48 = 0;
|
||||
|
||||
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
|
||||
* HeartbeatState.tick_count -- see the rationale in the header. Advances
|
||||
* from whichever VM's own heartbeat cycle happens to fire, so the
|
||||
* readiness signal reflects the fleet's aggregate activity rather than
|
||||
* any one VM's (in practice, Hera's) personal word-execution rate.
|
||||
* Declared here (rather than just above vm_physics_heartbeat_tick, where
|
||||
* it used to live) so vm_physics_touch() can read the current tick
|
||||
* without a forward declaration -- it is now this counter's second
|
||||
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
|
||||
static uint64_t fleet_heartbeat_tick_count = 0;
|
||||
static uint64_t fleet_last_inference_tick = 0;
|
||||
|
||||
static vm_physics_node_t *vm_physics_find(uint32_t vm_id)
|
||||
{
|
||||
vm_physics_node_t *n = vm_physics_head;
|
||||
@@ -168,7 +197,8 @@ void vm_physics_init(uint32_t vm_id)
|
||||
|
||||
if (node) {
|
||||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||||
node->physics.last_active_ns = 0;
|
||||
node->physics.last_active_tick = 0;
|
||||
node->physics.touched = 0;
|
||||
node->physics.is_live = 1;
|
||||
return;
|
||||
}
|
||||
@@ -178,7 +208,8 @@ void vm_physics_init(uint32_t vm_id)
|
||||
|
||||
node->vm_id = vm_id;
|
||||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||||
node->physics.last_active_ns = 0;
|
||||
node->physics.last_active_tick = 0;
|
||||
node->physics.touched = 0;
|
||||
node->physics.is_live = 1;
|
||||
node->next = vm_physics_head;
|
||||
vm_physics_head = node;
|
||||
@@ -247,33 +278,32 @@ void vm_physics_retire(uint32_t vm_id)
|
||||
dying->physics.is_live = 0;
|
||||
}
|
||||
|
||||
void vm_physics_touch(uint32_t vm_id, uint64_t now_ns)
|
||||
void vm_physics_touch(uint32_t vm_id)
|
||||
{
|
||||
vm_physics_node_t *target = vm_physics_find(vm_id);
|
||||
VMFleetTouchSample sample = { 0, 0, 0 };
|
||||
vm_physics_node_t *target = vm_physics_find(vm_id);
|
||||
VMFleetTouchSample sample = { 0, 0, 0 };
|
||||
uint64_t now_tick = fleet_heartbeat_tick_count;
|
||||
|
||||
if (!target || !target->physics.is_live) return;
|
||||
|
||||
/* Restated on the virtual tick (FABRIC.md item 2.1, 2026-08-04): this
|
||||
* used to gate on target->physics.last_active_ns > 0 and scale by
|
||||
* elapsed wall-clock microseconds -- non-reproducible run to run under
|
||||
* TCG (GAP-A1, FABRIC.md §25.7.1). fleet_heartbeat_tick_count is
|
||||
* execution-paced (vm_runtime.c:143), so elapsed_ticks is now a pure
|
||||
* function of the execution stream. `touched` replaces the old
|
||||
* `> 0` sentinel: tick 0 is a legitimate value for a genuine first
|
||||
* touch to land on, so "never touched" needs its own flag rather than
|
||||
* overloading the tick value. */
|
||||
if (fleet_transfer_slope_q48 > 0 &&
|
||||
target->physics.last_active_ns > 0 &&
|
||||
now_ns > target->physics.last_active_ns) {
|
||||
target->physics.touched &&
|
||||
now_tick > target->physics.last_active_tick) {
|
||||
|
||||
/* Microseconds, not nanoseconds, matching the word engine's own
|
||||
* convention (physics_metadata_apply_linear_decay,
|
||||
* physics_metadata.c) that fleet_transfer_slope_q48's seed value
|
||||
* was explicitly calibrated to mirror (see the comment on the
|
||||
* static above). Using raw elapsed_ns here was an outright unit
|
||||
* bug, not a design choice: real touch-to-touch gaps run to
|
||||
* milliseconds, so (elapsed_ns * slope_q48) >> 16 overshot the
|
||||
* fleet's entire conserved heat (Q48_ONE) by more than 10x on the
|
||||
* very first post-seed touch, clamping to a full winner-take-all
|
||||
* transfer in a single step instead of the gradual pull this was
|
||||
* meant to model (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev n). */
|
||||
uint64_t elapsed_us = (now_ns - target->physics.last_active_ns) / 1000;
|
||||
uint64_t amount = (elapsed_us * fleet_transfer_slope_q48) >> 16;
|
||||
uint64_t elapsed_ticks = now_tick - target->physics.last_active_tick;
|
||||
uint64_t amount = (elapsed_ticks * fleet_transfer_slope_q48) >> 16;
|
||||
|
||||
sample.elapsed_us = elapsed_us;
|
||||
sample.amount = amount;
|
||||
sample.elapsed_ticks = elapsed_ticks;
|
||||
sample.amount = amount;
|
||||
|
||||
if (amount > 0) {
|
||||
/* Pull toward the touched VM, proportional to each other LIVE
|
||||
@@ -310,7 +340,8 @@ void vm_physics_touch(uint32_t vm_id, uint64_t now_ns)
|
||||
}
|
||||
}
|
||||
|
||||
target->physics.last_active_ns = now_ns;
|
||||
target->physics.last_active_tick = now_tick;
|
||||
target->physics.touched = 1;
|
||||
|
||||
/* Record this touch's transfer-law inputs, not a derived heat value
|
||||
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
|
||||
@@ -371,7 +402,7 @@ void vm_physics_tick(uint64_t now_ns)
|
||||
|
||||
(void)now_ns; /* Unlike the log-linear regression this replaced, the
|
||||
* estimator below needs no external time reference --
|
||||
* each sample already carries its own elapsed_us. */
|
||||
* each sample already carries its own elapsed_ticks. */
|
||||
|
||||
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
|
||||
* substitute a default slope. Expected to stay unwarmed (or warm
|
||||
@@ -382,17 +413,17 @@ void vm_physics_tick(uint64_t now_ns)
|
||||
/* Direct rate recovery, not curve-fitting (rev o,
|
||||
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
|
||||
* OLS approach of rev b-n): the transfer law is known exactly
|
||||
* (amount = elapsed_us * slope >> 16), so any unclamped touch can be
|
||||
* inverted directly for an exact per-sample rate. Order doesn't
|
||||
* (amount = elapsed_ticks * slope >> 16), so any unclamped touch can
|
||||
* be inverted directly for an exact per-sample rate. Order doesn't
|
||||
* matter here -- a median over the observation window, not a
|
||||
* time-indexed fit -- so this walks the fixed-size array directly
|
||||
* rather than replaying ring order from fleet_window.head. */
|
||||
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
|
||||
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
|
||||
|
||||
if (s->clamped || s->amount == 0 || s->elapsed_us == 0) continue;
|
||||
if (s->clamped || s->amount == 0 || s->elapsed_ticks == 0) continue;
|
||||
|
||||
rates[rate_count++] = (s->amount << 16) / s->elapsed_us;
|
||||
rates[rate_count++] = (s->amount << 16) / s->elapsed_ticks;
|
||||
}
|
||||
|
||||
/* slope_fit_quality_q48 as the informative fraction of the window --
|
||||
@@ -423,14 +454,6 @@ void vm_physics_tick(uint64_t now_ns)
|
||||
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
|
||||
}
|
||||
|
||||
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
|
||||
* HeartbeatState.tick_count -- see the rationale in the header. Advances
|
||||
* from whichever VM's own heartbeat cycle happens to fire, so the
|
||||
* readiness signal reflects the fleet's aggregate activity rather than
|
||||
* any one VM's (in practice, Hera's) personal word-execution rate. */
|
||||
static uint64_t fleet_heartbeat_tick_count = 0;
|
||||
static uint64_t fleet_last_inference_tick = 0;
|
||||
|
||||
void vm_physics_heartbeat_tick(uint64_t now_ns)
|
||||
{
|
||||
fleet_heartbeat_tick_count++;
|
||||
|
||||
@@ -579,7 +579,7 @@ static void mama_word_vm_step(VM *vm)
|
||||
return;
|
||||
}
|
||||
|
||||
vm_physics_touch(entry.vm_id, vm_monotonic_ns(vm));
|
||||
vm_physics_touch(entry.vm_id);
|
||||
|
||||
saved_name = console_get_vm_name();
|
||||
console_set_vm_name(entry.name);
|
||||
@@ -655,7 +655,7 @@ static void mama_word_vm_exec(VM *vm)
|
||||
return;
|
||||
}
|
||||
|
||||
vm_physics_touch(entry.vm_id, vm_monotonic_ns(vm));
|
||||
vm_physics_touch(entry.vm_id);
|
||||
|
||||
log_message(LOG_INFO, "VM-EXEC: '%s' -> '%s'", cmd_buf, vm_name);
|
||||
saved_name = console_get_vm_name();
|
||||
@@ -736,7 +736,7 @@ static void mama_word_vm_call(VM *vm)
|
||||
vm_push(vm, 0); return;
|
||||
}
|
||||
|
||||
vm_physics_touch(entry.vm_id, vm_monotonic_ns(vm));
|
||||
vm_physics_touch(entry.vm_id);
|
||||
|
||||
log_message(LOG_DEBUG, "VM-CALL: '%s' -> '%s'", cmd_buf, vm_name);
|
||||
saved_name = console_get_vm_name();
|
||||
|
||||
Reference in New Issue
Block a user