Repository navigation
A pass skips the timer write and the ask swaps it does not owe - #1263
Conversation
Found while #1263's timer skip turned the interactive policy test red: main breaks the same bound in 12 of 400 explored runs, the skip in 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
355a050 to
000bc1d
Compare
Review of #1263 at 000bc1d (on #1266's 578c9df)Architecture. The responsibilities are split sensibly.
Size.
What I was asked to judgeThe skip's logic.
The architectures.
The interrupt-versus-flag race. It is real, and is blocker 1. The corpus.
BLOCKER
SUGGESTION
PR description, for the orchestrator
Landing preconditions
SEND BACK: blockers 1 and 2. |
000bc1d to
9ca0126
Compare
8f2d2db to
338d134
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9ca0126 to
489a142
Compare
Review of #1263, round 2, at 489a142 (code-identical to 9ca0126)Scope: round 1's findings, the order question, and the commits since 000bc1d: Architecture. It is unchanged since round 1, and the fixes are local:
This round adds +5/−2 lines of architecture code, +9 in Round-1 blockers
The orchestrator's question: does the flag order need more than the guest timer rows? No.
Owed on metal: recorded in the TSC-deadline record, with rows, benches, what main must show, and "the speed-up is not yet measured on metal". No timing claim rests on the simulator or a guest:
Checks I ran at 489a142:
BLOCKERNone. SUGGESTION
PR description, for the orchestrator
Landing preconditions
LAND: no blocker remains in this PR's own changes, subject to the preconditions above. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
489a142 to
bdd559e
Compare
9a1a013 to
4534ce3
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bdd559e to
5be7e5a
Compare
4534ce3 to
e24deb8
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5be7e5a to
b125067
Compare
Every pass wrote its timer and swapped all sixteen ask words, whether or not anything had changed. On the T14 the probes put the timer's clock read, conversion, write and trace at about 110 ns a pass and the empty drain, which is the ask swaps, at about 75; a yield with nothing to yield to probed 620 ns. - A running CPU leaves a one-shot that is still to fire and due no later than it needs. An early fire is a spurious pass, never a missed deadline, so the timer is written once a request, after a fire, or when something is owed sooner. `Machine::timer_pending` says whether the last arm's interrupt is still to be taken: both architectures keep a per-CPU flag that the arm sets and every timer entry and stop clears, and the simulator answers from its modelled one-shot. A CPU going idle still arms exactly, so nothing wakes it early. - An ask word read empty is not swapped. An ask landing after the read rings the doorbell after it, and the pass that ring owes takes it. - A task's mask and its reach are one value with one constructor, so the queue cannot count a reach its mask has moved from. The TSC-deadline record loses "re-armed every pass" from its name: what a deadline would save on a running CPU is now per arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading an ask word before swapping it made a take that leaves the word set representable, and no test held that a taken ask is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…request Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A running CPU that leaves an earlier one-shot pending fires it where it used to be rewritten, so the simulator offers timer steps where it did not, and two committed decision traces no longer drove their old protocols into the failure they were recorded for. They are found again (`kernel-sim find`, a search for I8, and `shrink`): `old_steal_port_i8` from seed 10, `old_migrate_kept_the_corpse_i14` from seed 70, its notes kept. Both fail as committed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pass from a system call runs with interrupts open, so a fire taken between the hardware write and the flag's store cleared a flag that the store then raised again, over a timer the fire had re-armed a quantum on: every later pass that needed nothing sooner skipped its write, and the running request's end waited for the quantum, up to 10 ms late. The flag is now raised first, so a fire between the two clears it and the next pass writes again. AArch64's comparator write is a `nomem` asm, so a compiler fence holds the store before it. The setter is `pub(super)` on both architectures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`# expect: fail I14` and its kind say which invariant a failing trace must still break; the corpus test checks it, so a replay that fails only some other way is no longer read as the case it was kept for. The three old-protocol traces name theirs. `old_steal_port_i8` is found again at this head (seed 5, shrunk); the other two still break theirs, I1 and I14's killed-then-migrated task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b125067 to
49095e6
Compare
|
Orchestrator: landing under the benches-down rule. Review r2 LAND. Head 49095e6 is the same eight commits rebased onto main after #1266 ( |
Draft, on main now that #1260 and #1266 have landed. Head 49095e6, on main 75bdec3: its own code is 9ca0126's. It lands on its run 43090913024 at 9ca0126 and #1266's 43099723883, which covers the only base code that differs. #1266 fixes the policy defect this branch's timer skip had surfaced, so the record this branch carried for it is dropped (closed there). The metal A/B is owed (below), the benches being down.
Problem
Every scheduler pass wrote its timer and swapped all sixteen of its ask words (one per 64 CPUs), whether or not anything had changed. The wakeperf diagnosis's segment probes (main 422915e, one boot a bench) put a yield with nothing to yield to at 620 ns on the T14 and 541 on the AMD bench. Of that:
The same two items are paid by every pass of
pingpong.*andipc.reqrep.*. Evidence: https://github.com/ToyOSOrg/evidence/tree/3ddc686338d20b13528b597a7437e3b027479728/diagnosis (tables/fine-split.txt,tables/*-seg-grouped.txt).What changes
SchedPass::apply_timer_no_later_than). An early fire is a spurious pass, never a missed deadline, so the timer is written once a request, after a fire, or when something is owed sooner.cpu.armedstays what the hardware holds, so invariant T (the armed instant is no later than the earliest event owed) and the simulator's I3 hold as before.Machine::timer_pending. Both architectures keep a per-CPU flag:arm_one_shotsets it, andstop_timerand every timer entry clear it (x86-64'srearm_lastandring0_fire, AArch64'srearmandrearm_in_kernel).arm_withinleaves it, since what it arms is never later than what it found. The simulator answers from its modelled one-shot, which a fire disarms. A machine that cannot tell answersfalseand writes every arm.nomemasm, so a compiler fence holds the store before it. The setter ispub(super)on both. No host model can see this order: the simulator derivestimer_pendingfrom its modelled one-shot.now: a one-shot already due, whose interrupt is latched but not yet taken, is armed again rather than relied on.CpuHandle::take_asks). An ask sets its bit and then rings the doorbell. A pass clears the doorbell's edge before it reads the words. So an ask either lands before that clear, and its bit is visible to the load, or it rings after it and owes a later pass, which takes it.kernel-loom'sloom_sleepdrives exactly this race.affinity::LoadedMask, one constructor), from The run queue's operations cost less and decide as before #1260's review. The queue can no longer count a reach its mask has moved from; The run queue's operations cost less and decide as before #1260's mutation M6 is now unrepresentable.issues/the-timer-is-a-lapic-count-re-armed-every-pass-never-a-tsc-deadline.mdbecomesissues/the-timer-is-a-lapic-count-never-a-tsc-deadline.md.What this does of the TSC-deadline track:
Not changed: the pass's other per-pass work. That is the run queue's pick and put-back (#1260), the published load, weight and stamp (the stamp feeds the staleness every placement reads, so its cadence is a decision), and the drain's mailbox. The direct hand-off within a job is designed after this A/B, not built here.
Checks
At 9ca0126, rebased onto #1266's head 8f2d2db (logs: the evidence's
checks/pr2r-*).cargo test -p kernel --lib, and with--features sched-checkcargo clippyon the kernel library's two host shapes, on-p kernelforx86_64-unknown-noneandaarch64-unknown-none-softfloat(each default andboot-actuators,test-actuators), and onkernel-sim --all-targets, the adopted lints,-D warningsbuild/clippy.rsthat compile these files, both architectures' timer code includedcargo run -- --ci host modelskernel-simruns passes against its modelled one-shot and asserts I3, andkernel-loom'sloom_sleepdrives the ask racering0_timer_in_syscall, and on AArch64virt_timer_preempts,virt_ring0_timer_in_syscall,virt_timer_floor,virt_irq_stormNew tests:
a_running_cpu_leaves_a_pending_one_shot_due_no_later: nothing is written while the request's end stands; a fired one-shot is armed again; a sleeper's earlier deadline is written.a_cpu_going_idle_writes_its_deadline_over_a_pending_earlier_one_shot.a_taken_ask_is_gone.Mutations, each applied to the committed head, run and restored:
a_running_cpu_leaves…)a_cpu_going_idle…)a_running_cpu_leaves…)a_taken_ask_is_gone)a_steal_probe_goes_to_surplus_and_not_to_a_cpu_full_of_corpses, its first failing test, andloom_sleep'san_ask_is_taken_by_the_pass_its_ring_owesfailsNothing on the host drives the flag's architecture writers: a missing clear would leave a running CPU's timer unwritten after a fire. The guest rows above boot both architectures' timer paths, and the A/B's 16 boots run every row on the benches.
The policy test, and the corpus
an_interactive_wake_waits_out_at_most_one_requestwas red here before the rebase, a defect on main this skip's extra timer steps let 40 seeds reach. A thread's request follows its bursts, so a brief sleeper is due before a hog #1266 fixes it (a thread's request follows its bursts), and the test now sweeps 400 seeds: green on the rebased head.# expect: fail I14), and the corpus test checks it: a header naming another invariant reds it. At 9ca0126:old_steal_port_i8is found again: a search ofkernel-sim run old_steal_port <seed>for the first run whose violations include I8 (seed 5), thenshrink.old_migrate_kept_the_corpse_i14still replays to I14's case,TaskKey(4) was killed and then migrated to cpu1; it was regenerated at the first rebase from seed 70, the first thatkernel-sim findreported failing, which was I14.old_steal_port_i1still fails I1.Owed on metal
Both metal benches are down, so metal is no longer a landing condition here. The speed-up is not yet measured on metal. Recorded as owed:
system,sched,ipcandtime, with a Linux reference timed through the TSC (issues/the-timer-is-a-lapic-count-never-a-tsc-deadline.md): main must readtime.yield,pingpong.*andipc.reqrep.*faster by more than 3% on both, the timer and periodic-wake rows within 3%, and no scheduler, IPC, wake, yield, spawn or timer row slower.Risks
arm_one_shotand leaves the flag set could leave a running CPU waiting for a fire later than it needs. Every writer today is named above.🤖 Generated with Claude Code