scx_pandemoniumv5.19.0: - #3756
Conversation
"The last image was too immediate for any eye to register. It may have been a
human figure, dreaming of an early evening in each great capital luminous
enough to tell him he will never die, coming out to wish on a first star."
— Thomas Pynchon, "Gravity's Rainbow."
This release removes the behavioural classifier. It had not worked since it was
written: last_woke_at does two jobs that destroy each other, so every task scored
zero and classified BATCH after its first wake, and the four EWMAs behind the
score were correct code averaging a corrupted input. No number this project has
ever measured was a function of tier. What the classifier still did was decide.
Seven sites read its output, and one of them handed a hard preempt kick to every
task it could not call BATCH -- which, through the PF_WQ_WORKER floor, means
every kworker on the machine. A field capture from an AMD desktop caught the
consequence: 1,174,848 hard kicks against 1,174,815 requeues inside one
one-second sample, 46 real wakeups in the whole second, held flat at that rate
for 32.3 seconds. Flat is the finding. A load spike is ragged; a plateau at a
fixed ceiling is a closed loop running at its IPI throughput limit, and the CoDel
controller that exists to brake exactly this fired once in that second. The
estimator is gone. Everything that replaces it was already in the tree.
THE KICK ASKS ABOUT THE CPU AND NOT ABOUT THE TASK:
src/bpf/main.bpf.c
- TIER 1's kick flag read `tctx->tier != TIER_BATCH`, a property of the task, to
decide a question about a CPU. The seat was picked from an idle scan; whether
it is still idle a moment later is the CPU's business and the tier test could
not see it. It now reads the target's live state: an idle seat takes
SCX_KICK_IDLE, a seat that went busy takes the preempt that actually reaches
it. TIER 0's warm-stay took an unconditional preempt and books its kicks by
the flag issued rather than by is_wakeup, so a warm-stay requeue no longer
reports a hard IPI as a soft kick.
- A requeue's preempt is rate-limited to one per CPU per live CoDel target. A
task re-entering enqueue with ran_since_wake set did not wake, it was evicted,
and letting it evict someone in turn is the loop the field capture recorded:
every preempt lands a requeue, every requeue issues a preempt. TIER 0 books a
requeue soft and TIER 2/3 already gate on is_wakeup, which leaves TIER 1 as
the only site able to produce hard kicks and requeues in lockstep, which is
exactly what it produced.
- Removing that preempt outright instead of bounding it stalls the machine.
Under --no-adaptive the knob page is all zeros, so the tick preempt never
resolved to a band and TOTAL PREEMPT reads 0 on every BPF-only run in the
archive of every version -- that kick was quietly the only mechanism
dislodging a resident in that mode. Without it: runnable task stall, watchdog
at 10.001s, 11.0M dispatch/s at 0.0% idle hit rate. The loop was never the
preempt, it was the unbounded rate.
THE TICK PREEMPT HAD NEVER FIRED IN BPF-ONLY MODE:
src/bpf/main.bpf.c
- The band came from knobs->preempt_thresh_ns, which the adaptive layer writes
and --no-adaptive leaves at zero. It is codel_target_ns now: BPF-derived,
always live, and the same unit every other bound in the gate is priced in. The
band exists in both modes for the first time.
- The tier split inside it was the worst half of the classifier. The enqueue
side read tier != TIER_BATCH and kicked hardest for INTERACTIVE while this
side read the same field and braked softest for it, at twice the threshold,
with LAT_CRITICAL exempt from the sojourn bound altogether. Two controllers on
one variable pulling opposite ways. One band now, and the overflow starvation
net above it is unconditional.
- A declared RT policy answers to the starvation bound, lag_cap_ns, rather than
the service bound. This is not the classifier returning: SCHED_FIFO/RR is a
contract userspace stated, not a character this scheduler guessed, and RT is
still forced off a CPU it has camped on. The split is the audio quantum --
codel_target falls inside one PipeWire period and lag_cap does not, so a
uniform band preempted an RT thread mid-buffer every time a waiter existed.
The old blanket exemption fixed neither half: it kept an RT thread from being
kicked off while doing nothing to get it on, and let a camper hold a CPU
without limit. p->policy is read directly, so the tick also sheds a per-call
task_ctx lookup.
- Measured on the BPF-only arm of prism --dev fork-thread, thread/g24: wall
78.486s to 21.3s, +367.6% against EEVDF to +27.5%, IPC 0.277 to 0.423. That
arm was not conservative, it was inert -- 370 traced migrations because
nothing ever moved work.
SERVICE RENDERED REPLACES CHARACTER GUESSED:
src/bpf/main.bpf.c
- task_ctx carries standing_runs, a saturating count of consecutive runs that
consumed a full codel_target_ns, maintained in stopping() beside last_run_ns
and cleared by any shorter run. It is a ledger and not an estimator: it
records what was rendered and never guesses what the task is. The confirm
depth is the memory a single last_run_ns sample lacks, since a hog that blocks
once reads as drained, and it needs no threshold constant because the boundary
is one live target by construction.
- task_slice selects on it instead of on tier. The two EWMA branches it replaced
(LAT_CRITICAL at avg_runtime*1.5, INTERACTIVE at avg_runtime*2) sat behind a
score that returns BATCH for every task after its first wake, so the selector
was a constant and both branches were reachable only through the RT and
PF_WQ_WORKER overrides. Everything drew the batch quantum, roughly 5.6ms at
12C. A task that has not stood on a CPU for a full target now draws the
adaptive slice.
- TIER 3 routes to the batch or interactive overflow DSQ on the same ledger. The
split itself stays: dispatch STEP 2 already prices the crossover between the
two in codel_target_ns with codel_starve_ns as the net above it, so the
fairness half was never the classifier's. The burst-spawn carve-out that kept
ewma_age < 2 threads out of the batch DSQ becomes structural rather than a
special case, since a fresh fork has rendered no service and routes
interactive by construction.
- prism --dev scale, ADAPTIVE: deadline miss 1.6/0.6/0.3/0.2% to 0.3/0.0/0.1/0.1%
at 2/4/8/12C. The 0.0-0.4% band has never held at 2C on any release. Long-run
work spread 0.14-0.96% against EEVDF's 2.25-6.45%, absolute per-process
throughput above EEVDF at every width, wall +1.0 to +4.1%.
PLACEMENT IS A DISTANCE PRICE WITH NOTHING IN FRONT OF IT:
src/bpf/main.bpf.c
- find_idle_l2_sibling is deleted. It searched one L2 group and fell straight
into an unordered node-wide pick on a miss, so it had no gradient: either an
idle CPU shared the wakee's L2 group or placement fell off a cliff. An L2
group is two CPUs, so under any messaging load the sibling is busy and every
wakeup took the cliff. find_idle_by_affinity walks affinity_rank in R_eff
order and contains that search as its prefix, then degrades continuously.
- The three-term gate in front of it goes with it. The tier test was a dead
classifier read, and affinity_mode and PF_KTHREAD only carved exceptions out
of a wall that no longer exists.
- phi_warm_target's tier parameter is dropped. It had already been (void)tier.
TWO CONFLICTS RECORDED IN PLACE:
src/bpf/main.bpf.c
- The warm-stay occupancy bail is a rate limit on the home-pull, which is not
what its comment claimed. warm_stay_anchor returns home_cpu, and seating a
task there when it last ran elsewhere is itself a migration. Pricing the bail
as queue depth instead of gating on it admits far more wakees to the warm seat
and fires far more home-pulls: intra_wake 2161 to 57485, total migrations 3037
to 59150 against a flat wakeup count, 1.3% to 24.0% per wakeup. The comment
now says so, and says not to re-price it until the home_cpu against last_cpu
divergence beneath it is settled.
- montauk's per-cause counters put 81% of migrations on the wake path, not the
steal path (intra_wake 2161 and cross_wake 293 against intra_steal 547 and
cross_steal 36). Routing the idle pick through R_eff moved none of it, and the
hub signature a home-pull would leave is absent -- modal-CPU share per thread
reads 44.8% against EEVDF's 66.8%, lower rather than higher, which is diffuse
scatter and not a star.
HARNESS, A BENCH IS NAMED FOR THE BENCH THAT PRODUCED IT:
tests/pandemonium-tests.py
- prism-scale's --ipc, --deadline, --launch, --mixed, --longrun and --burst
modes wrote prism-scale's filenames. That was not only confusing, it was
wrong: the archive prom is named {version}-{stamp}.prom, baseline_gate globs
[0-9]*.prom and prism-golden globs {version}-*.prom, so an IPC-only run
dropped a file both of them read as that version's full scale run. Each mode
takes its own name now. The metric family stays pandemonium_scale_* -- one
schema, and renaming it would orphan every archived run.
- 93 archived IPC runs are renamed out of the version-prom namespace to match,
selected by report header rather than by guesswork.
|
I tested this particular release privately before it was published, and it fixed my AMDGPU related hangs which occurred while the scheduler was enabled almost immediately from boot. Before this release, it would hang for 5-15 seconds at a time every so often while running the desktop, or even while in a TTY. It was likely related to this: #3687 |
|
Hey I tried this version 5.19 and it seems fixed the VirtualBox issue I post days ago. That issue is deleted so I would post a comment here. Reading kode54's comment above, I feel I might meet the same issue. I'm on an AMD GPU + Intel CPU system, and start VirtualBox VM immediately after booting the system, would lead the VM to be very slow, almost not processing anything. It seems version 5.19 fixed it. Thank you. |
This release removes the behavioural classifier. It had not worked since it was written: last_woke_at does two jobs that destroy each other, so every task scored zero and classified BATCH after its first wake, and the four EWMAs behind the score were correct code averaging a corrupted input. No number this project has ever measured was a function of tier. What the classifier still did was decide. Seven sites read its output, and one of them handed a hard preempt kick to every task it could not call BATCH -- which, through the PF_WQ_WORKER floor, means every kworker on the machine. A field capture from an AMD desktop caught the consequence: 1,174,848 hard kicks against 1,174,815 requeues inside one one-second sample, 46 real wakeups in the whole second, held flat at that rate for 32.3 seconds. Flat is the finding. A load spike is ragged; a plateau at a fixed ceiling is a closed loop running at its IPI throughput limit, and the CoDel controller that exists to brake exactly this fired once in that second. The estimator is gone. Everything that replaces it was already in the tree.
THE KICK ASKS ABOUT THE CPU AND NOT ABOUT THE TASK: src/bpf/main.bpf.c
tctx->tier != TIER_BATCH, a property of the task, to decide a question about a CPU. The seat was picked from an idle scan; whether it is still idle a moment later is the CPU's business and the tier test could not see it. It now reads the target's live state: an idle seat takes SCX_KICK_IDLE, a seat that went busy takes the preempt that actually reaches it. TIER 0's warm-stay took an unconditional preempt and books its kicks by the flag issued rather than by is_wakeup, so a warm-stay requeue no longer reports a hard IPI as a soft kick.THE TICK PREEMPT HAD NEVER FIRED IN BPF-ONLY MODE: src/bpf/main.bpf.c
SERVICE RENDERED REPLACES CHARACTER GUESSED:
src/bpf/main.bpf.c
PLACEMENT IS A DISTANCE PRICE WITH NOTHING IN FRONT OF IT: src/bpf/main.bpf.c
TWO CONFLICTS RECORDED IN PLACE:
src/bpf/main.bpf.c