[Track 2] Parallel-scoped global clock fixes timebase-desync livelock
In --parallel mode a long run livelocked: the scheduler spun "advanced to deadline 3000 waking hw=2 idx=0" ~14k times in microseconds. Root cause: each guest thread owns ctx.timebase (+1/instr in step_block), and all kernel deadline arithmetic read Scheduler::ctx(hw_id).timebase as "now". But the parallel worker extracts its PpcContext via mem::replace(ctx_mut_ref, PpcContext::new()) — leaving a ZEROED timebase in the slot while it steps unlocked — and advance_all_timebases_to only walks runqueue (never idle_ctx). So the coordinator's coord_pre_round drain and a woken thread's parse_timeout could read a zeroed/stale basis decoupled from the deadline the scheduler just advanced to. The thread re-armed the same constant deadline forever; the global clock never moved. Fix: add a single monotonic Scheduler::global_clock, advanced by the per-block retired-instruction count on each parallel writeback and floored up by advance_all_timebases_to. Kernel deadline reads route through KernelState::now_basis_at(hw_id), which returns global_clock ONLY when parallel_active; lockstep keeps reading the exact pre-existing ctx(hw_id).timebase expression, so the deterministic lockstep trace is byte-identical (sylpheed_n50m golden unchanged, zero re-baseline). Verified: - 50M --parallel run completes (was: hung). Deadlines now strictly increasing 5.4M -> 49.1M (18097 unique of 18116; max repeat 2) vs pre-fix constant 3000 x ~14000. - sylpheed_n50m golden byte-identical via plain `check` (no persist). - Full suite 665/665 green. Note: an intermittent parallel hang/crash (~1-2/20 at -n 5M) is pre-existing (master 1/20, this build 2/20 — within noise) and distinct from the timebase livelock: it is a parallel-race class (e.g. the unsafe block_ptr deref in run_execution_parallel). Tracked separately; lockstep remains the recommendation for long runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1251,6 +1251,26 @@ impl KernelState {
|
||||
self.pending_timer_fires.first().map(|&(d, _)| d)
|
||||
}
|
||||
|
||||
/// Coherent "now" basis for deadline arithmetic, gated on execution mode.
|
||||
///
|
||||
/// In **lockstep** (`parallel_active == false`) this returns exactly the
|
||||
/// pre-existing per-thread `ctx(hw_id).timebase` each call site read
|
||||
/// before, so the deterministic lockstep trace is byte-identical (no
|
||||
/// golden re-baseline). In **parallel** (`parallel_active == true`) the
|
||||
/// per-thread timebases are incoherent (workers extract/zero their slots
|
||||
/// while stepping unlocked), so we return the scheduler's single
|
||||
/// monotonic `global_clock` instead — the basis that breaks the
|
||||
/// timebase-desync livelock. Callers pass the `hw_id` they would have
|
||||
/// used for the lockstep `ctx()` read (slot 0 for coordinator-side
|
||||
/// drains, the current thread's slot for in-guest waits).
|
||||
pub fn now_basis_at(&self, hw_id: u8) -> u64 {
|
||||
if self.parallel_active {
|
||||
self.scheduler.global_clock()
|
||||
} else {
|
||||
self.scheduler.ctx(hw_id).timebase
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire every timer whose deadline is `<= now` (derived from slot 0's
|
||||
/// timebase, matching `parse_timeout`'s "current thread" fallback).
|
||||
/// For each fire: mark the timer `signaled=true`, clear its
|
||||
@@ -1259,7 +1279,7 @@ impl KernelState {
|
||||
/// fired — the caller uses this to decide whether the scheduler round
|
||||
/// needs a follow-up `advance_to_next_wake_if_due` step.
|
||||
pub fn fire_due_timers(&mut self) -> bool {
|
||||
let now = self.scheduler.ctx(0).timebase;
|
||||
let now = self.now_basis_at(0);
|
||||
let mut fired = false;
|
||||
loop {
|
||||
let Some(&(deadline, handle)) = self.pending_timer_fires.first() else {
|
||||
|
||||
Reference in New Issue
Block a user