Repository navigation
sched: a spawned thread starts on a free core in its spawner's LLC - #1153
Conversation
Review of #1153 at
|
Spawn placement ranked CPUs by queue, sleep, busy SMT siblings and wake depth, and broke ties by a rotation over the whole machine, so a thread spawned to work with its spawner started in either LLC. On the AMD bench ab9 measured what that costs a thread streaming to its spawner: every pipe.throughput round whose writer thread the rotation put in the other LLC read about 0.42x at 16384 bytes per write and 0.48x at 65536, in both arms and without exception (issues/a-spawned-thread-starts-in-any-llc-not-its-spawners.md, #1151). `CpuHandles::place` takes the spawner's CPU as `near` and, after the queue, the sleep and the free core, prefers a CPU in its LLC; a free core elsewhere still beats a shared core near, so a burst of threads spreads over every free core before it doubles up. A thread spawned into its own process passes its CPU; a new process's first thread, a kernel thread and a relocation pass none and place as before. The simulator's `Op::Spawn` passes the spawning step's CPU, as the kernel does. Tests: `a_spawned_thread_starts_on_a_free_core_in_its_spawners_llc` (host) and `a_spawned_partner_starts_in_its_spawners_llc` (simulator, AMD shape, 40 seeds: no kick crosses the LLCs but the spawner's own move). Without the LLC term both red; with the simulator passing no spawner, the simulator test reds with 11 crossing kicks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll home
ab10 read wake.storm.drain.w16 17% slower with the near term, .w4 4%. The
storm spawns its waiters in bursts and each parks at once. A parked
thread raises no published load, so every waiter saw its spawner's LLC
as free cores, and ties broken from a rotating start wrapped onto the
LLC's first CPUs. The simulator reproduces it: a thread on cpu7 of the
AMD shape spawning 16 that park put them on cpu0-5, up to four to a CPU,
where without the near term they covered 14 CPUs. A storm then woke a
CPU's waiters one after another.
The spawn now carries `Near { spawner, homes }`. `homes` is every CPU
the process's threads call home, which `process::spawn_thread` reads
off the table it already holds, one state word per thread. Placement
ranks a CPU no thread of the process calls home ahead of the spawner's
LLC. A burst therefore fills free cores no sibling holds, near ones
first, before doubling up anywhere. A thread that works with its
spawner still starts beside it while that LLC has such a core.
Tests: `a_spawned_thread_passes_over_cpus_its_siblings_call_home`
(host) and `a_burst_of_parked_threads_is_spread_over_free_cores`
(simulator, `scenarios::spawned_burst`, 40 seeds). Each spawn's CPU is
now recorded as `Outcome::placed`. Without the homes term both red: the
burst shares cpu2 and cpu3. Without the LLC term the near tests still
red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or share it A thread spawn walked every thread of its process under PROCESS_TABLE with preemption off: up to 4096, unjoined zombies included, so growing a process to N threads cost O(N^2) state-word reads. `homes` (pure, shared by `process::spawn_thread` and the simulator's `Op::Spawn`) now reads siblings newest first, sets each home bit in place, and stops: - once the homes cover every CPU of the new thread's mask on the CPUs online, from which the homed term ranks every candidate alike and the placement cannot change; or - after HOMES_READ = 64 siblings. A process with no more threads than that places exactly as before. Past it, the oldest go unseen, so a CPU only they call home may look free: the bound trades that for a spawn's cost under the lock, at most 64 state-word reads. `CpuMask` gains `insert`, `covers` and `FromIterator<CpuId>`, and the kernel names `driver::current_cpu` and imports in place of full paths. `Near`'s doc says the LLC is preferred, not that the thread starts beside its spawner. Tests `homes_stop_once_they_cover_the_mask` and `homes_read_at_most_their_bound`; without the early stop, or without the cap, each reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
501c228 to
8c97388
Compare
Review r2 of #1153 at
|
On main (after #1117). Three commits:
d487ed227(the near term),7913b5bda(round 2: a spawn passes over the CPUs its process's threads call home) and8c9738802(review r1: the homes walk is bounded and shared). Fixes the defect #1151 described; #1151 closed unmerged, so this PR carries the record and there is no issue file to delete.Problem
Spawn placement (
CpuHandles::place) ranked CPUs by queue, sleep, busy SMT siblings and wake depth, and broke ties by a rotation over the whole machine. A thread spawned to work with its spawner therefore started in either LLC.On the AMD bench, ab9 measured
pipe.throughput, whose writer is a new thread each round. In all six boots of both arms, every round that the rotation put in the other LLC from its reader read slow, without exception: about 2.0, 3.5 and 4.8 GB/s at 4096, 16384 and 65536 bytes per write, against about 5.0, 8.4 and 10.0 in one LLC (judgment).Behaviour
CpuHandles::place(start, now, mask, near)takesNear { spawner, homes }.homesis the CPUs the process's threads call home, fromcpu::homes, whichprocess::spawn_threadand the simulator'sOp::Spawnshare. It reads siblings newest first, one state word each, and stops once the homes cover every CPU of the new thread's mask on the CPUs online, or afterHOMES_READ= 64 siblings. The key is (queue, not a sleeper with no kick coming, busy SMT siblings, a CPU a thread of the process calls home, outside the spawner's LLC, wake depth).homes: a parked thread raises no published load. With the LLC term alone, a burst of threads that park all saw the spawner's LLC as free. ab10'swake.storm.drain.w16read +17% from that, and the simulator reproduced it (mechanism): 16 parked threads packed 2–4 to a CPU on cpu0–5, where without the term they covered 14 CPUs.near(process::spawn_thread, as the CPU it runs on). A new process's first thread, a kernel thread and a mask relocation passNoneand place as before. A new process shares no address space with its spawner, andwork.build_shaped's children are meant to spread.Op::Spawnpasses the spawning step's CPU, as the kernel does, so its placement stays the kernel's (kernel/sim/src/vm.rs).Checks
Evidence:
pr-1153/r4(review r1), andpr-1153/r2(round 2: logs, controls, the storm and process-spawn mechanisms, ab11's rule and request), andpr-1153/r1at 6b42a7b7: logs, control patches, ab10's rule, request and judge.cargo run -- --ci host791d0a23b, clean treecargo run -- --ci host8c9738802(review r1, on main), clean treetestcases(orchestrator)8c9738802abuse_thread_tableexit=0 (4096 spawns, 166 ms CPU)homes_stop_once_they_cover_the_mask,homes_read_at_most_their_bound;cargo test -p kernel --lib --features sched-check,cargo test -p kernel-sim8c9738802std_threads_count_the_cpus_and_give_back_ended_threads(MAX_THREADS + 8 detached threads)8c9738802abuse_thread_table(4095 zombies, then refusals)8c9738802testcasesboot. It has no QEMU entry: a shared-boot member the harness filters only on metalcargo run -- --ci host501c228b3, clean treecargo test -p kernel --lib --features sched-check,cargo test -p kernel-sim501c228b3a_spawned_thread_starts_on_a_free_core_in_its_spawners_llc(host, two LLCs of two SMT cores)791d0a23ba_spawned_thread_passes_over_cpus_its_siblings_call_home(host) anda_burst_of_parked_threads_is_spread_over_free_cores(simulator,spawned_burst, 40 seeds;Outcome::placedrecords each spawn's CPU)501c228b3a_spawned_partner_starts_in_its_spawners_llc(simulator, AMD shape, 40 seeds: a thread moves itself to cpu8 and spawns its turn-taking partner; no kick crosses the LLCs but its own move)791d0a23bcargo test -p kernel --lib --features sched-check,cargo test -p kernel-sim791d0a23bstd_threads_count_the_cpus_and_give_back_ended_threads,affinity_pins_hold_on_every_cpu,libc_children791d0a23bThe QEMU guest has one LLC, so the new term never changes a choice there. Those runs show only that every spawn path still places and runs threads. The placement claim is the host and simulator tests' and ab10's.
Controls, each a checked patch, applied, run and reversed in the same run:
Op::Spawnpasses no spawner501c228b3)[1, 2, 3, 4, 5, 0, 2, 3, 8, 9])take(HOMES_READ)removed)homes_read_at_most_their_boundhomes_stop_once_they_cover_the_mask501c228b3)Metal
ab10 (12 AMD boots, 3 for this change, all
passed; judgment). Base isc9f88d36e+ diagnostic. The decision rule was fixed before the run.work.build_shaped.j160.996.thread.spawn_join20.4–20.5 µs in every boot, against base's 20.4, 27.2 and 27.3: the 27 µs mode is a joined thread in the other LLC.wake.storm.call.w1/w4/w16−6 to −9%.wake.storm.drain.w16+17% and.w4+4%, not explained.fs.mem.seq_read.4096+4.7%, not explained.proc.posix_spawn_wait.tiny+23% andproc.spawn_wait_loop.100+21%. These spawn processes, which this change places as before.proc.spawn_wait.tinyis bimodal on base too. Inference: they read where the parent sat.wake.storm.drainwas packing, now fixed withhomes(above).proc.*spawn*rows read each boot's share of children ending in the other LLC from their parent (1–76%), which tracksspawn_wait_loop.100. Two of three spawn boots drew a parent in LLC 1, and so did one no-move sched: a woken partner follows its waker's LLC #1129 boot (data). Those children are processes, which this change does not place.ab11 (6 AMD boots, base
520532b89againstbbf88b647=501c228b3+ diagnostic, allpassed; judgment). LAND by the rule fixed before staging:wake.storm.drain.w160.894,.w40.844.work.build_shaped.j160.974.thread.spawn_join0.777,pipe.throughput.*2.1–2.4× against base's split draws.thread.spawn_join.default+3.8% and.stack64k+4.2%, about 0.8 µs per spawn and join, cause not established.Risks
wake.stormrows are the check, and they are faster.thread.spawn_join.defaultand.stack64kread about 0.8 µs slower per spawn and join in ab11, cause not established.thread.spawn_join.defaultand.stack64kcost (+4%) is not attributed. The walk is a step or two at those rows' one or two threads, so it is not a plausible cause. More likely is where the thread lands: the homed and near terms now rank above wake depth, so a deeper sleeper in the near LLC can beat a shallower far one. That is an inference; ab11's readbacks do not record a thread spawn's CPU or its wake depth.🤖 Generated with Claude Code