Repository navigation
Conversation
The rule moved a task on the second wake in a row from another LLC, and said a server with a client in each LLC never moved. It did: only a wake that finds the server parked reaches the rule, and which client's request that is depends on timing. The simulator's own server_of_two_llcs moved it 36 times over 40 seeds, and with independent, even arrivals a run of two moves it on 1 parked wake in 6. The u16 the owner admitted now holds the LLC in ten bits and the run's count in six (WakerStreak), and the wake that makes WAKER_RUN in a row from another LLC moves the task. With independent, even arrivals a run of k moves a server on 1 parked wake in 2^(k+1) - 2, checked by direct simulation: 1 in 6 at 2, 30 at 4, 126 at 6, 510 at 8. A split pair pays k split round trips once, about 1.7 us each on the AMD bench, so k = 8 costs a pair about 14 us and keeps a server's moves to one wake in 510. Measured in the simulator with the scenarios at 24 rounds (40 seeds, server moves in all / most in one run / runs that moved; then runs where the split pair moved exactly once): k=1 407/45/34 40 k=4 17/1/17 40 k=10 5/1/5 40 k=2 60/5/27 40 k=6 10/1/10 40 k=12 5/1/5 40 k=3 31/3/24 40 k=8 5/1/5 40 Past 8 the server's 5 moves stay put, so the streak no longer drives them. The simulator's clients alternate more than independent ones would, which is why it moves less than the formula; the formula is the bound the constant cites. The server test now requires at most one move per run and at most 10 over 40 seeds; k = 4 and k = 1 both red it. The move chose among sleepers that were answering, which accepts a CPU a kick is already on its way to, and the scan always starts at the waker. Two runs ending in one drain both went to the same CPU, which then forwarded one on. The move now takes only a sleeping CPU with no kick pending (Doorbell::sleeping_unrung, as a wake forward does), and scans the waker's LLC alone (CpuHandles::idle_in_llc) rather than every ring with the other LLCs filtered out. A host test ends two runs in one drain; the old predicate reds it. post and post_boosted named the waker before post_as disabled preemption, so a migration in between named the previous CPU; post_as names it now, as post_n, revoke and post_in_place do. The issue record describes main after landing and records the server's remaining move rate. Controls, each applied and reversed in place: the old predicate reds the two-run test; k = 1 and k = 4 red the server test; no rule reds split_pair, the run test and the two-run test; ignoring the mask reds the mask test and the server test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8ab5159 to
e8e9682
Compare
b8bd0ea to
1178599
Compare
Review r2 of #1129 at
|
SYS_MESSAGE_CALL is SYS_MESSAGE_SEND and then a blocking SYS_MESSAGE_RECV on the same connection, in one syscall. A refused send answers as a send does with nothing sent; once the request is queued the receive answers as a receive does, into the same two windows. A protocol whose peer answers each request in turn makes the next message its answer. The request carries no handles: an error could come from either half, so a caller could not know whether they had moved. The call no longer tells the scheduler that its caller parks next: #1111 dropped WakeCause::parks_next (e938fe3), and #1129's waker-LLC affinity carries the waker on every wake instead. What is left is one syscall entry and exit per round trip. Consumers: the file server's client (toyos::fs's request and reply) and osbench's ipc.reqrep rows, whose request's last frame and answer's first are one call. Connection::call takes the answer straight into the caller's buffer, as recv_into does. messages_are_whole checks that a call takes the answer its peer sends, with the answer's handle, split across both windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moves back The PR said the simulator's server moved less than the formula and that past a run of 8 the streak no longer drove its moves. Both were wrong: at runs of 8, 10 and 12 it moved 5 times in 40 runs, more than the 3.8 the formula allows, and nothing but the rule can move it there. A trace of a moving seed shows why. All 24 requests from the near client (cpu1) found the server running and became flags it consumed unseen; every request that found it parked came from the far client (cpu9), so the far client alone made the run. Only a wake that finds the task parked reaches the owner with its cause (park::deliver), and the doc said the opposite. At ten times the rounds, 12 of 40 runs move the server, each once. After the move both clients' requests find it parked (71 counted from LLC 0, 63 from LLC 1 in the traced seed), and no run moves it back. A run of 4 does bounce it, 4 times in one run. The server test now holds both: at most one move per run at 240 rounds, and at most 10 moves over 40 runs at 24. Runs of 1 and 4 red the first sweep. The rule's doc now says that only parked wakes count, what that does to a server with a client in each LLC, and that a run at or past WAKER_RUN moves the task, so a task stolen out of the LLC it was woken from goes back on that LLC's next wake. The issue record says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule moved a task on the second wake in a row from another LLC, and said a server with a client in each LLC never moved. It did: only a wake that finds the server parked reaches the rule, and which client's request that is depends on timing. The simulator's own server_of_two_llcs moved it 36 times over 40 seeds, and with independent, even arrivals a run of two moves it on 1 parked wake in 6. The u16 the owner admitted now holds the LLC in ten bits and the run's count in six (WakerStreak), and the wake that makes WAKER_RUN in a row from another LLC moves the task. With independent, even arrivals a run of k moves a server on 1 parked wake in 2^(k+1) - 2, checked by direct simulation: 1 in 6 at 2, 30 at 4, 126 at 6, 510 at 8. A split pair pays k split round trips once, about 1.7 us each on the AMD bench, so k = 8 costs a pair about 14 us and keeps a server's moves to one wake in 510. Measured in the simulator with the scenarios at 24 rounds (40 seeds, server moves in all / most in one run / runs that moved; then runs where the split pair moved exactly once): k=1 407/45/34 40 k=4 17/1/17 40 k=10 5/1/5 40 k=2 60/5/27 40 k=6 10/1/10 40 k=12 5/1/5 40 k=3 31/3/24 40 k=8 5/1/5 40 Past 8 the server's 5 moves stay put, so the streak no longer drives them. The simulator's clients alternate more than independent ones would, which is why it moves less than the formula; the formula is the bound the constant cites. The server test now requires at most one move per run and at most 10 over 40 seeds; k = 4 and k = 1 both red it. The move chose among sleepers that were answering, which accepts a CPU a kick is already on its way to, and the scan always starts at the waker. Two runs ending in one drain both went to the same CPU, which then forwarded one on. The move now takes only a sleeping CPU with no kick pending (Doorbell::sleeping_unrung, as a wake forward does), and scans the waker's LLC alone (CpuHandles::idle_in_llc) rather than every ring with the other LLCs filtered out. A host test ends two runs in one drain; the old predicate reds it. post and post_boosted named the waker before post_as disabled preemption, so a migration in between named the previous CPU; post_as names it now, as post_n, revoke and post_in_place do. The issue record describes main after landing and records the server's remaining move rate. Controls, each applied and reversed in place: the old predicate reds the two-run test; k = 1 and k = 4 red the server test; no rule reds split_pair, the run test and the two-run test; ignoring the mask reds the mask test and the server test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moves back The PR said the simulator's server moved less than the formula and that past a run of 8 the streak no longer drove its moves. Both were wrong: at runs of 8, 10 and 12 it moved 5 times in 40 runs, more than the 3.8 the formula allows, and nothing but the rule can move it there. A trace of a moving seed shows why. All 24 requests from the near client (cpu1) found the server running and became flags it consumed unseen; every request that found it parked came from the far client (cpu9), so the far client alone made the run. Only a wake that finds the task parked reaches the owner with its cause (park::deliver), and the doc said the opposite. At ten times the rounds, 12 of 40 runs move the server, each once. After the move both clients' requests find it parked (71 counted from LLC 0, 63 from LLC 1 in the traced seed), and no run moves it back. A run of 4 does bounce it, 4 times in one run. The server test now holds both: at most one move per run at 240 rounds, and at most 10 moves over 40 runs at 24. Runs of 1 and 4 red the first sweep. The rule's doc now says that only parked wakes count, what that does to a server with a client in each LLC, and that a run at or past WAKER_RUN moves the task, so a task stolen out of the LLC it was woken from goes back on that LLC's next wake. The issue record says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b89f78b to
b013609
Compare
e8e9682 to
132bc2c
Compare
The rule moved a task on the second wake in a row from another LLC, and said a server with a client in each LLC never moved. It did: only a wake that finds the server parked reaches the rule, and which client's request that is depends on timing. The simulator's own server_of_two_llcs moved it 36 times over 40 seeds, and with independent, even arrivals a run of two moves it on 1 parked wake in 6. The u16 the owner admitted now holds the LLC in ten bits and the run's count in six (WakerStreak), and the wake that makes WAKER_RUN in a row from another LLC moves the task. With independent, even arrivals a run of k moves a server on 1 parked wake in 2^(k+1) - 2, checked by direct simulation: 1 in 6 at 2, 30 at 4, 126 at 6, 510 at 8. A split pair pays k split round trips once, about 1.7 us each on the AMD bench, so k = 8 costs a pair about 14 us and keeps a server's moves to one wake in 510. Measured in the simulator with the scenarios at 24 rounds (40 seeds, server moves in all / most in one run / runs that moved; then runs where the split pair moved exactly once): k=1 407/45/34 40 k=4 17/1/17 40 k=10 5/1/5 40 k=2 60/5/27 40 k=6 10/1/10 40 k=12 5/1/5 40 k=3 31/3/24 40 k=8 5/1/5 40 Past 8 the server's 5 moves stay put, so the streak no longer drives them. The simulator's clients alternate more than independent ones would, which is why it moves less than the formula; the formula is the bound the constant cites. The server test now requires at most one move per run and at most 10 over 40 seeds; k = 4 and k = 1 both red it. The move chose among sleepers that were answering, which accepts a CPU a kick is already on its way to, and the scan always starts at the waker. Two runs ending in one drain both went to the same CPU, which then forwarded one on. The move now takes only a sleeping CPU with no kick pending (Doorbell::sleeping_unrung, as a wake forward does), and scans the waker's LLC alone (CpuHandles::idle_in_llc) rather than every ring with the other LLCs filtered out. A host test ends two runs in one drain; the old predicate reds it. post and post_boosted named the waker before post_as disabled preemption, so a migration in between named the previous CPU; post_as names it now, as post_n, revoke and post_in_place do. The issue record describes main after landing and records the server's remaining move rate. Controls, each applied and reversed in place: the old predicate reds the two-run test; k = 1 and k = 4 red the server test; no rule reds split_pair, the run test and the two-run test; ignoring the mask reds the mask test and the server test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moves back The PR said the simulator's server moved less than the formula and that past a run of 8 the streak no longer drove its moves. Both were wrong: at runs of 8, 10 and 12 it moved 5 times in 40 runs, more than the 3.8 the formula allows, and nothing but the rule can move it there. A trace of a moving seed shows why. All 24 requests from the near client (cpu1) found the server running and became flags it consumed unseen; every request that found it parked came from the far client (cpu9), so the far client alone made the run. Only a wake that finds the task parked reaches the owner with its cause (park::deliver), and the doc said the opposite. At ten times the rounds, 12 of 40 runs move the server, each once. After the move both clients' requests find it parked (71 counted from LLC 0, 63 from LLC 1 in the traced seed), and no run moves it back. A run of 4 does bounce it, 4 times in one run. The server test now holds both: at most one move per run at 240 rounds, and at most 10 moves over 40 runs at 24. Runs of 1 and 4 red the first sweep. The rule's doc now says that only parked wakes count, what that does to a server with a client in each LLC, and that a run at or past WAKER_RUN moves the task, so a task stolen out of the LLC it was woken from goes back on that LLC's next wake. The issue record says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132bc2c to
e48eee9
Compare
b013609 to
9dcc24e
Compare
A wake went back to the CPU its task parked on, and spawn placement does not weigh LLCs, so a request/reply pair that started split across an LLC boundary stayed split. On the AMD bench such a pair reads about 25% slower: ipc.reqrep.8 at 8.4–8.5 µs against 6.7–6.9 µs, and fs.disk.read_4k_at at 11.6–11.7 µs against 9.7 µs. Every wake a thread posts now names the CPU it ran on (WakeCause.waker). A wake posted from a device interrupt's handler, a deadline or a retire names none. The kernel tells a handler apart by a per-CPU interrupt depth, which device_irq_entry and aarch64's irq bracket. A poll's completion runs in its poster's context, so a client's send that completes a server's inbox poll names the client's CPU. The woken task's owner CPU keeps the LLC of the task's last waker, the one u16 of per-task placement state the owner admitted on 2026-11-01. A wake from another LLC that the previous wake also came from goes to a sleeping CPU of the waker's LLC that the task's mask names, nearest the waker and quickest to wake. So a split pair shares an LLC after its second round trip, and a server woken from two LLCs in turn stays put. Real-time, stopped and killed tasks keep their own placement. The simulator's wakes name their posting CPU. - split_pair: on the AMD shape, a pair started on cpu0 and cpu8 moves exactly once in every one of 40 seeds. - server_of_two_llcs: a server answering a client pinned in each LLC moves 36 times over 40 seeds, at most 3 in one run. - Following every wake's LLC moves it about 220 times over the same seeds. Three host tests state the rule, including that the move stays inside the mask. Controls, each run and restored in place: - Dropping the rule reds split_pair and the moves-once test. - Dropping the previous-waker condition reds the server scenario and both host tests that keep a task home. - Ignoring the mask reds the mask test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule moved a task on the second wake in a row from another LLC, and said a server with a client in each LLC never moved. It did: only a wake that finds the server parked reaches the rule, and which client's request that is depends on timing. The simulator's own server_of_two_llcs moved it 36 times over 40 seeds, and with independent, even arrivals a run of two moves it on 1 parked wake in 6. The u16 the owner admitted now holds the LLC in ten bits and the run's count in six (WakerStreak), and the wake that makes WAKER_RUN in a row from another LLC moves the task. With independent, even arrivals a run of k moves a server on 1 parked wake in 2^(k+1) - 2, checked by direct simulation: 1 in 6 at 2, 30 at 4, 126 at 6, 510 at 8. A split pair pays k split round trips once, about 1.7 us each on the AMD bench, so k = 8 costs a pair about 14 us and keeps a server's moves to one wake in 510. Measured in the simulator with the scenarios at 24 rounds (40 seeds, server moves in all / most in one run / runs that moved; then runs where the split pair moved exactly once): k=1 407/45/34 40 k=4 17/1/17 40 k=10 5/1/5 40 k=2 60/5/27 40 k=6 10/1/10 40 k=12 5/1/5 40 k=3 31/3/24 40 k=8 5/1/5 40 Past 8 the server's 5 moves stay put, so the streak no longer drives them. The simulator's clients alternate more than independent ones would, which is why it moves less than the formula; the formula is the bound the constant cites. The server test now requires at most one move per run and at most 10 over 40 seeds; k = 4 and k = 1 both red it. The move chose among sleepers that were answering, which accepts a CPU a kick is already on its way to, and the scan always starts at the waker. Two runs ending in one drain both went to the same CPU, which then forwarded one on. The move now takes only a sleeping CPU with no kick pending (Doorbell::sleeping_unrung, as a wake forward does), and scans the waker's LLC alone (CpuHandles::idle_in_llc) rather than every ring with the other LLCs filtered out. A host test ends two runs in one drain; the old predicate reds it. post and post_boosted named the waker before post_as disabled preemption, so a migration in between named the previous CPU; post_as names it now, as post_n, revoke and post_in_place do. The issue record describes main after landing and records the server's remaining move rate. Controls, each applied and reversed in place: the old predicate reds the two-run test; k = 1 and k = 4 red the server test; no rule reds split_pair, the run test and the two-run test; ignoring the mask reds the mask test and the server test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moves back The PR said the simulator's server moved less than the formula and that past a run of 8 the streak no longer drove its moves. Both were wrong: at runs of 8, 10 and 12 it moved 5 times in 40 runs, more than the 3.8 the formula allows, and nothing but the rule can move it there. A trace of a moving seed shows why. All 24 requests from the near client (cpu1) found the server running and became flags it consumed unseen; every request that found it parked came from the far client (cpu9), so the far client alone made the run. Only a wake that finds the task parked reaches the owner with its cause (park::deliver), and the doc said the opposite. At ten times the rounds, 12 of 40 runs move the server, each once. After the move both clients' requests find it parked (71 counted from LLC 0, 63 from LLC 1 in the traced seed), and no run moves it back. A run of 4 does bounce it, 4 times in one run. The server test now holds both: at most one move per run at 240 rounds, and at most 10 moves over 40 runs at 24. Runs of 1 and 4 red the first sweep. The rule's doc now says that only parked wakes count, what that does to a server with a client in each LLC, and that a run at or past WAKER_RUN moves the task, so a task stolen out of the LLC it was woken from goes back on that LLC's next wake. The issue record says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ab6 on the AMD bench put every request/reply pair in one LLC, but often on one core: ipc.reqrep.8 ran on SMT siblings in 3 of 4 head boots (cpu3+cpu2, cpu11+cpu10 twice) and fs.disk.read_4k_at in 2 of 4 (cpu0+cpu1, cpu2+cpu3), against none in the base. On one core the pair reads about as slow as split across LLCs: ipc.reqrep.8 7.9-8.0 us against 6.9 on two cores of one LLC and 8.4 split, read_4k_at 11.7 us against 9.6-9.8 and 11.7. pipe.throughput.16384 read 3.7 GB/s against the base's 8.6, its writer and reader sharing one core. A request/reply waker has usually parked by the time its wake is placed, so its CPU reads asleep and busy_siblings counts its SMT sibling as a free core; the scan starts at the waker, so the sibling is first. The move now skips the waker's own core. A host test parks the waker's sibling first in the scan; without the check the task goes there (cpu5) and the test reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9dcc24e to
8a13576
Compare
e48eee9 to
c9f88d3
Compare
A wake names the CPU of the thread that posted it, and a run of WAKER_RUN such wakes from another LLC moves the woken task there. A thread on its way out is no partner to follow: a parent woken by each child's exit, its join or its teardown's closes chased children that were already gone. On the AMD bench that cost work.build_shaped.j16 about 4% at any run length (ab8), and plausibly thread.spawn_join's +3-5% in ab5. The running task now carries one lifecycle bit, set once by `thread_exit` and `process::leave` before either posts anything, and `CpuSched::waker` answers no waker while it is set. The kernel's `driver::waker` asks it, outside a device interrupt's handler, so the pure scheduler owns who names a waker. Such a wake is neither counted nor breaks the run, as an interrupt's is not. Host test `a_leaving_threads_wakes_leave_the_task_home`: seven wakes from cpu1's staying thread, then a full run from it once it leaves, and the task stays home. With `waker` ignoring the bit, the eighth wake moves it and the test reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacked on #1117 (#1111 → #1116 → #1117). Review only the last four commits (hashes after the rebase onto main
8122a4ef5):48d2f04b5(the rule),34c12a7eb(review r1),7c69daeea(review r2: comments, the issue and the simulator test only),8a1357642(ab6: the move stays off the waker's core) andcebbdedd5(a thread leaving its process names no waker). ab7 measured the same code at9dcc24ee8, before this rebase.Problem
A wake goes back to the CPU its task parked on, and spawn placement does not weigh LLCs. So a request/reply pair that starts split across an LLC boundary stays split, and every round trip moves its lines between L3s.
On the AMD bench such a pair reads about 25% slower (ab3 placement table):
ipc.reqrep.8: 8.4–8.5 µs split against 6.7–6.9 µs in one LLC.fs.disk.read_4k_at: 11.6–11.7 µs split against 9.7 µs.This is the first half of
issues/placement-has-no-migration-cost-or-wake-affinity.md's "wake-affine vs spread".Behaviour
A wake a thread posts names its CPU (
WakeCause::waker, fromCpuSched::waker). One that no thread posted names none, and neither does one a thread posts once it has begun leaving its process (thread_exit,process::leave): a dying waker is no partner. Without that, a parent woken by each child's exit, join or teardown closes followed children that were already gone, which ab8 read aswork.build_shaped.j16+4%. The running task carries this as one bit, set once and never cleared. Such a wake is neither counted nor breaks the run.The rule (
CpuSched::toward_its_waker). The woken task's owner CPU counts how many of its parked wakes in a row came from one LLC. A wake from another LLC whose run is at or pastWAKER_RUN= 8 sends the task to a CPU of that LLC:Doorbell::sleeping_unrung, the test a wake forward uses), so a burst of moves spreads;CpuHandles::idle_in_llc).What follows from it:
park::deliver). So a server with a client in each LLC follows the far client when the near client's requests keep finding it running. In a traced moving seed, all 24 of the near client's requests became flags and every counted wake came from the far one. Once the server has moved, both clients' requests find it parked (71 counted from LLC 0 and 63 from LLC 1 in that seed), and nothing moves it back.Why 8. A split pair pays k split round trips once, about 1.7 µs each on the AMD bench, so 8 costs a pair about 14 µs. Too short a run makes a server bounce: at ten times the rounds (240, 40 seeds), a run of 4 moved the server 4 times in one run, while 8 moved none more than once. Under independent, even parked wakes, k sets the rate: 1 in 6 at 2, 30 at 4, 126 at 6, 510 at 8. The simulator at 24 rounds, 40 seeds:
The pair moved exactly once in every run for every k up to 12. The 5 moves at 8 to 12 are the flag mechanism above: one client's wakes are never counted, so k does not limit them. They exceed the formula's 3.8 for independent wakes, which does not describe this scenario.
Owner ruling, 2026-11-01. One u16 of per-task placement state is admitted, kept by the CPU that owns the task. It is the single exception to the 2026-10-28 scheduling model's "no per-task state". It now holds the LLC in ten bits and the run's count in six (
WakerStreak), a choice made inside that ruling.issues/placement-has-no-migration-cost-or-wake-affinity.mdrecords both.Cost: an
Option<CpuId>in the wake message; the u16 in the task; per wake that names a waker, one topology lookup and a count; on a move, one scan of the waker's LLC.Which wakes name a waker
Every wake reaches
park::deliver. Each poster below runs on the poster's own CPU, and none is deferred.Watch::postand its boosted form: pipe write, port connect (syscall/ipc.rs,object/port.rs), event signal, console/log watches,quiesceprogressthread_exitorprocess::leaveon: its join, its process's exit notice (publish_exit), its teardown's closes and releases, its unmapsWatch::post_n: futex wake, sleeplock hand-offWatch::revoke: futex revokeIrqWatch::post_in_placefrom a thread:pmmframes returned, and the inbox, when a client's send completes a server's pollIrqWatch::post_in_placefrom a device interrupt:isarows,pcidevinterrupts and faults,countersanswers (kick IPI)consolenotifyfire_deadlines) and a retire's wakeInbox::wakeruns in the client's syscall and names the client's CPU. The file server waits on its inbox.device_irq_entryraises it beside the preempt count; it brackets the kick, ISA, DMA-fault and user-device vectors. The LAPIC timer, TLB IPI, spurious, unclaimed and NMI vectors have their own entries and leave it alone, which is harmless because none of them posts a wake. On aarch64,irqraises it around a device arm.Watchpost names its waker inside the preempt-off section, so a migration between the two cannot name the previous CPU.Checks
Evidence:
pr-1129/r7at 2a0a47fc (this round),pr-1129/r3at 3c713712 (review r2: the traces and controls), andpr-1129/r2at 7009cfc7.cargo run -- --ci hostcebbdedd5(stack top), clean treea_leaving_threads_wakes_leave_the_task_homecebbdedd5osbench_systemon the ab9 head arm (33df293ad; the capability-groups entry pointed at that group, uncommitted)33df293adthread.spawn_join's joiner took 1047 wakes and none named a waker, while pipe wakes were named (log)cargo run -- --ci host8a1357642(stack top, on main8122a4ef5), clean treecargo run -- --ci host9dcc24ee8(on main8520204a0), clean treecargo run -- --ci hostb89f78bee, clean treecargo run -- --ci host1178599c4, clean treekernel-loom/kernel-simsuites1178599c4, inside the host CI run abovea_split_pair_moves_once_into_one_llc(AMD shape, 40 seeds)1178599c4a_server_with_a_client_in_each_llc_seldom_follows_them(40 seeds)b89f78bee1178599c4The QEMU runs of
affinity_pins_hold_on_every_cpu,std_threads_count_…andlibc_childrenatb8bd0ea02predate this round and are not repeated: the round changes only which wakes move and where, which those tests do not observe.Controls, each a checked patch, applied, run and reversed in the same run:
sleeping(), a rung CPU accepted)a_move_stays_off_the_wakers_core(the task goes to cpu5, the waker's sibling)WAKER_RUN= 1 (follow every wake)WAKER_RUN= 4a_split_pair…, the run test and the two-run testCpuSched::wakerignores the leaving bita_leaving_threads_wakes_leave_the_task_home: the eighth wake moves the task (log)driver::current_leavessets nothing (QEMU, same run shape as above)The host tests are written in terms of
WAKER_RUN, so k = 1 leaves them green; the simulator pins its value.Fan-out, simulator only (uncommitted scenario): one producer on cpu0 of the AMD shape wakes 16 futex waiters for 16 rounds under the shipped balance policy. Over 20 seeds that is 213 migrations with the rule and 182 without, the same most in a run (17). That is about 1.5 more moves per run of 256 wakes, and no sustained ping-pong against the balancer.
Metal
ab5 (AMD, 4+4 benchcase boots at
b8bd0ea02, the rule before this round: run of 2, a rung CPU accepted) (judgment). All 8 osbench verdicts PASS. The two I219 reds are T14-only rows thatboot:benchcasetakes in, as the request said beforehand.ipc.reqreppair ran in one LLC in 4 of 4 head boots against 2 of 4 base.fs.disk.read_4k_at: 4 of 4 against 2 of 4. The head p50s sit in the one-LLC mode (6.85–6.93 µs, 9.76–9.93 µs), except one boot of each where the pair landed on SMT siblings (8.0 and 11.4 µs). Base has a sibling boot too.ipc.reqrep.65536(IPC phase 2: a connection carries whole messages #1121): base 31.4, 31.7, 51.1 and 52.7 µs, with the two split boots slow; head 32.6–33.1 µs and 41.4 (the sibling boot). The 2× AMD reading is consistent with a split pair.pipe.throughput.256/4096/655362.1–2.5×,wake.storm.drain.w4−10%, fourfs.memrows −1 to −2%. Worse:wake.storm.call.w1/w4/w64+6–7%,thread.spawn_join.default+5% and.stack64k+3%,mem.munmap_touched_remote.4k+8%,wake.storm.drain.w1+3%, and five rows +1–3% (wake.periodic.idle,time.sleep_late.10ms,mem.mprotect.4k,pingpong.pipe.cross_core,fs.mem.mkdir_rmdir).wake.storm.drain.w16's median is +23%, unresolved. The fair rows andwake.onto_busyare unchanged.thread.spawn_joinis a joiner following its dead children's LLCs under a run of 2.wake.storm's waiters, all woken by one producer, move into its LLC.ab6 (AMD, 4+4 at
1178599c4, before the core check) (judgment). osbench PASSes on all 8; the I219 reds are the T14-only rows.ipc.reqrep.8ran on cpu3+cpu2 and twice on cpu11+cpu10, at 7.9–8.0 µs, against 6.9 µs on two cores; base splits it at 8.4.fs.disk.read_4k_atran on cpu0+cpu1 and cpu2+cpu3, at 11.7 µs, the same as split, against 9.6–9.8 on two cores.pipe.throughput.16384read 3.7 GB/s against the base's 8.6, which fits a writer and reader sharing a core. No base pair shared a core.busy_siblingscounts only awake siblings, and the scan starts at the waker.9dcc24ee8keeps the move off the waker's core; without that check the test sends the task to the waker's sibling (cpu5).wake.storm.call.w1/w4and boththread.spawn_joinrows no longer resolve.wake.storm.call.w64+5.6%,drain.w64+2.7% andmem.munmap_touched_remote.4k+10.5% still resolve.pingpong.futex.cross_core, pinned on cpu1 and cpu2, read 491 and 4469 ns in two head boots against 160–171; not explained.tail.*, timer wakes no change here reaches, read slower on head by 0.7–1.7% in ab5, ab6 and the T14 A/B of sched: a wake its poster parks or exits behind runs where it was posted #1111. In each A/B the base boots first and the images differ, so shifts of 1–3% are not attributable in them.ab7 (AMD, 4+4 at
9dcc24ee8in balanced order, with a metal-only diagnostic on both arms) (judgment). osbench PASSes on all 8.ipc.reqrep.86.56–6.63 µs against base 7.83–8.00 (base split in 4 of 4), −16%.reqrep.65536−42%,reqrep.4096−24%,reqrep_handle.8−18%. No head pair shares a core.pipe.throughput.16384is still 0.43× (3.4–3.7 against 8.1–8.4 GB/s), but writer and reader never share a core (one_core0/7 in every boot). One head boot even places its sides as the base does. So the mechanism is unknown. ab5 at a run of 2 read this row at base speed.pingpong.futex.cross_core: both arms now read 4.5 µs, the regime where each side parks; head is +1.6%, the bias's size. ab6's 491 and 4469 ns were the row tipping from its no-park regime (about 170 ns) into the parking one.ran=shows no other process on cpu0–3 in any head boot, so it is not interference from moved work.tail.*still reads about 1.5% slower on head with balanced order, so the bias is not boot order.work.build_shaped.j16+4.2%,fs.disk.seq_readandfs.mem.seq_read−2 to −6% MB/s.ab8 (AMD, 6 boots: base, head and head with a run of 2, each with a diary-based diagnostic; judgment). osbench PASSes on all 6.
pipe.throughput.16384: head 3.6 GB/s, run of 2 8.4–8.5, base 8.2–8.4. Every arm's rounds are bimodal (about 3.5 against 8.4 GB/s). A run of 8 makes the slow mode more frequent. Each round has 0–1 migrations and 2–10 wakes. One run-of-2 boot placed reader and writer on the same CPUs as head and read 8.5. Why the slow mode is more frequent at 8 is not established.work.build_shaped.j16269–270 ms on all four rule boots against 258–260 on base. Inference: the build's parent is woken only by exiting children and follows them.ipc.reqrep.86.6–6.7 µs against 8.2–8.3 split.ab8 refit, from its own rows (fit). Each pipe round's writer is a fresh thread, and spawn placement starts it in the same rotation in every arm (cpu13, 14, 15, 0, 1, 2, 3 at 16384). The reader is osbench's main thread, wherever earlier rows left it. In all 18 ab8 pipe rows (3 sizes × 6 boots), the number of slow rounds that
n,min,maxandmeanimply equals the number of rounds whose writer ran in the other LLC (0–7 against 8–15). The p50 is slow exactly when 4 or more of the 7 rounds were split, and the rows with no split round have min ≈ max. So at 16384 head read slow because its reader sat in LLC 1, where 4 of the 7 rotation slots are across the boundary, against 3 of 7 for base's reader in LLC 0. A round has too few parked wakes (2–10) for a run of 8 to bring the reader over, while a run of 2 did. This is an inference from means; ab9 measures it per round. If it holds, the pipe rows measure the writer's spawn placement and not a cost of the rule, and choosing k on them would tune to the bench's rotation.ab9 (6 AMD boots, base, head, head, base, base, head, all
passed; basef06df08b7=c9f88d36e+ diagnostic, head33df293ad=cebbdedd5+ the same diagnostic; judged against the rule fixed before staging; judgment).work.build_shaped.j16head/base 0.976. The parent's wakes on head are still mostly named by a waker that is not leaving, with an unidentified source, so how much the leaving rule earns is not isolated.ipc.reqrep.8head/base 0.856.thread.spawn_join+11% (+2.2% without one fast base boot), with non-overlapping ranges, although the joiner's wakes are all unnamed and it never moves on head.fs.disk.seek+4.9%,mem.anon.map+6.2%,mem.munmap_touched_remote.4k+4.9%,wake.storm.call.w64+3.8% andwake.storm.drain.w4+8.6% are not explained.The T14 has one LLC, so the rule never fires there.
Risks
Not landable on ab9:
thread.spawn_joinand five rows slower by 3.8–8.6% block it. Each is a row the rule does not demonstrably reach, so their cause (image layout, or a per-post cost of the newwaker()path) is the open question. k stays 8.The leaving bit is approved as lifecycle state, not placement accounting (orchestrator, 2026-11-02). No existing state already says it.
KILLis set only on a thread someone else retires, andprocess::exitexcludes its caller.TaskState::Deadcomes only after the exit pass. In the kernel, the process table's thread mark comes afterrelease_thread's unmaps, andTaskHandle.releasesis held only during a process teardown.Spawn placement starts a new thread across the LLC from its spawner while the spawner's LLC has free cores. That is what the pipe rows measure, if ab9 confirms it. It is sched: a spawn wakes the shallowest of its equal free-core sleepers #1117's domain and is not changed here. If ab9 confirms it, it gets an issue with evidence and a small separate change.
Spreading a waker's partners for parallelism is not decided. A fan-out whose producer wakes the same workers 8 times in a row moves them into its LLC while sleeping CPUs are there. The simulator shows the balancer and the rule not fighting, but no metal row isolates it.
No metal row has a server with concurrent clients in both LLCs. That a server follows its far client once, and never back, rests on the simulator alone.
console's notify from an interrupt is left to the depth. On aarch64 only a device arm raises it, so an interrupt outside a device arm is attributed to the interrupted thread. Those arms post no watch today.🤖 Generated with Claude Code